diff --git a/.github/workflows/deploy-to-play-store.yml b/.github/workflows/deploy-to-play-store.yml new file mode 100644 index 00000000..5e196b92 --- /dev/null +++ b/.github/workflows/deploy-to-play-store.yml @@ -0,0 +1,24 @@ +name: Deploy to Play Store Internal +on: + workflow_dispatch: +jobs: + deployAabToGooglePlayInternal: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - shell: bash + env: + # The following env variables are used by signing configuration in sample/build.gradle + KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }} + KEYSTORE_KEY_PSWD: ${{ secrets.KEYSTORE_KEY_PSWD }} + KEYSTORE_PSWD: ${{ secrets.KEYSTORE_PSWD }} + # The script decodes keystore (required by sample/build.gradle) and fastlane-api.json + # needed by fastlane (see fastlane/Appfile). + run: | + echo "${{ secrets.KEYSTORE_FILE }}" > keystore.asc + gpg -d --passphrase "${{ secrets.KEYSTORE_FILE_PSWD }}" --batch keystore.asc > keystore + echo "${{ secrets.API_KEY_FILE }}" > fastlane-api.json.asc + gpg -d --passphrase "${{ secrets.API_KEY_FILE_PSWD }}" --batch fastlane-api.json.asc > fastlane-api.json + fastlane deployInternal diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..7a118b49 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/app/build.gradle b/app/build.gradle index 9862d020..c6f826dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,20 +1,33 @@ apply plugin: 'com.android.application' +apply from: rootProject.file("gradle/git-tag-version.gradle") android { - compileSdkVersion 29 - buildToolsVersion '29.0.3' + compileSdkVersion 31 defaultConfig { applicationId "no.nordicsemi.android.nrfblinky" minSdkVersion 18 - targetSdkVersion 29 - versionCode 14 - versionName "2.5.1" + targetSdkVersion 31 + versionCode getVersionCodeFromTags() + versionName getVersionNameFromTags() resConfigs "en" vectorDrawables.useSupportLibrary = true } + signingConfigs { + release { + storeFile file('../keystore') + storePassword System.env.KEYSTORE_PSWD + keyAlias System.env.KEYSTORE_ALIAS + keyPassword System.env.KEYSTORE_KEY_PSWD + } + } + + buildFeatures { + viewBinding true + } + buildTypes { debug { minifyEnabled false @@ -23,35 +36,26 @@ android { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release } } - - compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 - sourceCompatibility JavaVersion.VERSION_1_8 - } } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.android.material:material:1.2.0-alpha06' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.activity:activity:1.3.1' + implementation 'androidx.fragment:fragment:1.3.6' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6' - // Lifecycle extensions - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - // Butter Knife - implementation 'com.jakewharton:butterknife:10.2.1' - annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.1' + implementation 'com.google.android.material:material:1.4.0' + // Brings the new BluetoothLeScanner API to older platforms - implementation 'no.nordicsemi.android.support.v18:scanner:1.4.3' - //implementation project(":scanner") + implementation 'no.nordicsemi.android.support.v18:scanner:1.6.0' + // Log Bluetooth LE events in nRF Logger - implementation 'no.nordicsemi.android:log:2.2.0' + implementation 'no.nordicsemi.android:log:2.3.0' + // BLE library - implementation 'no.nordicsemi.android:ble-livedata:2.2.0' - // To add BLE Library as a module, replace the above dependency with the following - // and uncomment 2 lines in settings.gradle file. - // implementation project(":ble-livedata") + implementation 'no.nordicsemi.android:ble-livedata:2.3.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e2712933..954d61dd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,15 +25,59 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - - - - + + + + + + + + + + + + + + + + + android:launchMode="singleTop" + android:exported="true"> @@ -57,7 +102,8 @@ android:name=".ScannerActivity" android:icon="@drawable/ic_blinky_feature" android:label="@string/feature_name" - android:launchMode="singleTop"> + android:launchMode="singleTop" + android:exported="true"> diff --git a/app/src/main/java/no/nordicsemi/android/blinky/BlinkyActivity.java b/app/src/main/java/no/nordicsemi/android/blinky/BlinkyActivity.java index 6d3a00ca..cc9b7937 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/BlinkyActivity.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/BlinkyActivity.java @@ -25,43 +25,35 @@ import android.content.Intent; import android.os.Bundle; import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.switchmaterial.SwitchMaterial; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; import no.nordicsemi.android.ble.livedata.state.ConnectionState; +import no.nordicsemi.android.ble.observer.ConnectionObserver; import no.nordicsemi.android.blinky.adapter.DiscoveredBluetoothDevice; +import no.nordicsemi.android.blinky.databinding.ActivityBlinkyBinding; import no.nordicsemi.android.blinky.viewmodels.BlinkyViewModel; -@SuppressWarnings("ConstantConditions") public class BlinkyActivity extends AppCompatActivity { public static final String EXTRA_DEVICE = "no.nordicsemi.android.blinky.EXTRA_DEVICE"; private BlinkyViewModel viewModel; - - @BindView(R.id.led_switch) SwitchMaterial led; - @BindView(R.id.button_state) TextView buttonState; + private ActivityBlinkyBinding binding; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_blinky); - ButterKnife.bind(this); + binding = ActivityBlinkyBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); final Intent intent = getIntent(); final DiscoveredBluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); final String deviceName = device.getName(); final String deviceAddress = device.getAddress(); - final MaterialToolbar toolbar = findViewById(R.id.toolbar); + final MaterialToolbar toolbar = binding.toolbar; toolbar.setTitle(deviceName != null ? deviceName : getString(R.string.unknown_device)); toolbar.setSubtitle(deviceAddress); setSupportActionBar(toolbar); @@ -72,34 +64,35 @@ protected void onCreate(final Bundle savedInstanceState) { viewModel.connect(device); // Set up views. - final TextView ledState = findViewById(R.id.led_state); - final LinearLayout progressContainer = findViewById(R.id.progress_container); - final TextView connectionState = findViewById(R.id.connection_state); - final View content = findViewById(R.id.device_container); - final View notSupported = findViewById(R.id.not_supported); + binding.ledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> viewModel.setLedState(isChecked)); + binding.infoNotSupported.actionRetry.setOnClickListener(v -> viewModel.reconnect()); + binding.infoTimeout.actionRetry.setOnClickListener(v -> viewModel.reconnect()); - led.setOnCheckedChangeListener((buttonView, isChecked) -> viewModel.setLedState(isChecked)); viewModel.getConnectionState().observe(this, state -> { switch (state.getState()) { case CONNECTING: - progressContainer.setVisibility(View.VISIBLE); - notSupported.setVisibility(View.GONE); - connectionState.setText(R.string.state_connecting); + binding.progressContainer.setVisibility(View.VISIBLE); + binding.infoNotSupported.container.setVisibility(View.GONE); + binding.infoTimeout.container.setVisibility(View.GONE); + binding.connectionState.setText(R.string.state_connecting); break; case INITIALIZING: - connectionState.setText(R.string.state_initializing); + binding.connectionState.setText(R.string.state_initializing); break; case READY: - progressContainer.setVisibility(View.GONE); - content.setVisibility(View.VISIBLE); + binding.progressContainer.setVisibility(View.GONE); + binding.deviceContainer.setVisibility(View.VISIBLE); onConnectionStateChanged(true); break; case DISCONNECTED: if (state instanceof ConnectionState.Disconnected) { + binding.deviceContainer.setVisibility(View.GONE); + binding.progressContainer.setVisibility(View.GONE); final ConnectionState.Disconnected stateWithReason = (ConnectionState.Disconnected) state; - if (stateWithReason.isNotSupported()) { - progressContainer.setVisibility(View.GONE); - notSupported.setVisibility(View.VISIBLE); + if (stateWithReason.getReason() == ConnectionObserver.REASON_NOT_SUPPORTED) { + binding.infoNotSupported.container.setVisibility(View.VISIBLE); + } else { + binding.infoTimeout.container.setVisibility(View.VISIBLE); } } // fallthrough @@ -109,24 +102,19 @@ protected void onCreate(final Bundle savedInstanceState) { } }); viewModel.getLedState().observe(this, isOn -> { - ledState.setText(isOn ? R.string.turn_on : R.string.turn_off); - led.setChecked(isOn); + binding.ledState.setText(isOn ? R.string.turn_on : R.string.turn_off); + binding.ledSwitch.setChecked(isOn); }); viewModel.getButtonState().observe(this, - pressed -> buttonState.setText(pressed ? + pressed -> binding.buttonState.setText(pressed ? R.string.button_pressed : R.string.button_released)); } - @OnClick(R.id.action_clear_cache) - public void onTryAgainClicked() { - viewModel.reconnect(); - } - private void onConnectionStateChanged(final boolean connected) { - led.setEnabled(connected); + binding.ledSwitch.setEnabled(connected); if (!connected) { - led.setChecked(false); - buttonState.setText(R.string.button_unknown); + binding.ledSwitch.setChecked(false); + binding.buttonState.setText(R.string.button_unknown); } } } diff --git a/app/src/main/java/no/nordicsemi/android/blinky/ScannerActivity.java b/app/src/main/java/no/nordicsemi/android/blinky/ScannerActivity.java index 989917c2..c808b4e5 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/ScannerActivity.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/ScannerActivity.java @@ -31,8 +31,11 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Button; +import com.google.android.material.appbar.MaterialToolbar; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -42,38 +45,24 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; - -import com.google.android.material.appbar.MaterialToolbar; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import no.nordicsemi.android.blinky.adapter.DevicesAdapter; import no.nordicsemi.android.blinky.adapter.DiscoveredBluetoothDevice; +import no.nordicsemi.android.blinky.databinding.ActivityScannerBinding; import no.nordicsemi.android.blinky.utils.Utils; import no.nordicsemi.android.blinky.viewmodels.ScannerStateLiveData; import no.nordicsemi.android.blinky.viewmodels.ScannerViewModel; public class ScannerActivity extends AppCompatActivity implements DevicesAdapter.OnItemClickListener { - private static final int REQUEST_ACCESS_FINE_LOCATION = 1022; // random number - private ScannerViewModel scannerViewModel; - - @BindView(R.id.state_scanning) View scanningView; - @BindView(R.id.no_devices) View emptyView; - @BindView(R.id.no_location_permission) View noLocationPermissionView; - @BindView(R.id.action_grant_location_permission) Button grantPermissionButton; - @BindView(R.id.action_permission_settings) Button permissionSettingsButton; - @BindView(R.id.no_location) View noLocationView; - @BindView(R.id.bluetooth_off) View noBluetoothView; + private ActivityScannerBinding binding; @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_scanner); - ButterKnife.bind(this); + binding = ActivityScannerBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); - final MaterialToolbar toolbar = findViewById(R.id.toolbar); + final MaterialToolbar toolbar = binding.toolbar; toolbar.setTitle(R.string.app_name); setSupportActionBar(toolbar); @@ -82,7 +71,7 @@ protected void onCreate(@Nullable final Bundle savedInstanceState) { scannerViewModel.getScannerState().observe(this, this::startScan); // Configure the recycler view - final RecyclerView recyclerView = findViewById(R.id.recycler_view_ble_devices); + final RecyclerView recyclerView = binding.recyclerViewBleDevices; recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); final RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); @@ -92,6 +81,47 @@ protected void onCreate(@Nullable final Bundle savedInstanceState) { final DevicesAdapter adapter = new DevicesAdapter(this, scannerViewModel.getDevices()); adapter.setOnItemClickListener(this); recyclerView.setAdapter(adapter); + + // Set up permission request launcher + final ActivityResultLauncher requestPermission = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), + result -> scannerViewModel.refresh() + ); + final ActivityResultLauncher requestPermissions = + registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), + result -> scannerViewModel.refresh() + ); + + // Configure views + binding.noDevices.actionEnableLocation.setOnClickListener(v -> openLocationSettings()); + binding.bluetoothOff.actionEnableBluetooth.setOnClickListener(v -> requestBluetoothEnabled()); + binding.noLocationPermission.actionGrantLocationPermission.setOnClickListener(v -> { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.ACCESS_FINE_LOCATION)) + Utils.markLocationPermissionRequested(this); + requestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION); + }); + binding.noLocationPermission.actionPermissionSettings.setOnClickListener(v -> { + Utils.clearLocationPermissionRequested(this); + openPermissionSettings(); + }); + + if (Utils.isSorAbove()) { + binding.noBluetoothPermission.actionGrantBluetoothPermission.setOnClickListener(v -> { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.BLUETOOTH_SCAN)) { + Utils.markBluetoothScanPermissionRequested(this); + } + requestPermissions.launch(new String[] { + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + }); + }); + binding.noBluetoothPermission.actionPermissionSettings.setOnClickListener(v -> { + Utils.clearBluetoothPermissionRequested(this); + openPermissionSettings(); + }); + } } @Override @@ -107,7 +137,7 @@ protected void onStop() { } @Override - public boolean onCreateOptionsMenu(final Menu menu) { + public boolean onCreateOptionsMenu(@NonNull final Menu menu) { getMenuInflater().inflate(R.menu.filter, menu); menu.findItem(R.id.filter_uuid).setChecked(scannerViewModel.isUuidFilterEnabled()); menu.findItem(R.id.filter_nearby).setChecked(scannerViewModel.isNearbyFilterEnabled()); @@ -115,16 +145,16 @@ public boolean onCreateOptionsMenu(final Menu menu) { } @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.filter_uuid: - item.setChecked(!item.isChecked()); - scannerViewModel.filterByUuid(item.isChecked()); - return true; - case R.id.filter_nearby: - item.setChecked(!item.isChecked()); - scannerViewModel.filterByDistance(item.isChecked()); - return true; + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.filter_uuid) { + item.setChecked(!item.isChecked()); + scannerViewModel.filterByUuid(item.isChecked()); + return true; + } else if (itemId == R.id.filter_nearby) { + item.setChecked(!item.isChecked()); + scannerViewModel.filterByDistance(item.isChecked()); + return true; } return super.onOptionsItemSelected(item); } @@ -136,92 +166,78 @@ public void onItemClick(@NonNull final DiscoveredBluetoothDevice device) { startActivity(controlBlinkIntent); } - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == REQUEST_ACCESS_FINE_LOCATION) { - scannerViewModel.refresh(); - } - } - - @OnClick(R.id.action_enable_location) - public void onEnableLocationClicked() { - final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); - startActivity(intent); - } - - @OnClick(R.id.action_enable_bluetooth) - public void onEnableBluetoothClicked() { - final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivity(enableIntent); - } - - @OnClick(R.id.action_grant_location_permission) - public void onGrantLocationPermissionClicked() { - Utils.markLocationPermissionRequested(this); - ActivityCompat.requestPermissions( - this, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_ACCESS_FINE_LOCATION); - } - - @OnClick(R.id.action_permission_settings) - public void onPermissionSettingsClicked() { - final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.fromParts("package", getPackageName(), null)); - startActivity(intent); - } - /** - * Start scanning for Bluetooth devices or displays a message based on the scanner state. + * Starts scanning for Bluetooth LE devices or displays a message based on the scanner state. */ - private void startScan(final ScannerStateLiveData state) { - // First, check the Location permission. This is required on Marshmallow onwards in order - // to scan for Bluetooth LE devices. - if (Utils.isLocationPermissionsGranted(this)) { - noLocationPermissionView.setVisibility(View.GONE); + private void startScan(@NonNull final ScannerStateLiveData state) { + // First, check the Location permission. + // This is required since Marshmallow up until Android 11 in order to scan for Bluetooth LE + // devices. + if (!Utils.isLocationPermissionRequired() || + Utils.isLocationPermissionGranted(this)) { + binding.noLocationPermission.getRoot().setVisibility(View.GONE); + + // On Android 12+ a new BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions need to be + // requested. + // + // Note: This has to be done before asking user to enable Bluetooth, as + // sending BluetoothAdapter.ACTION_REQUEST_ENABLE intent requires + // BLUETOOTH_CONNECT permission. + if (!Utils.isSorAbove() || Utils.isBluetoothScanPermissionGranted(this)) { + binding.noBluetoothPermission.getRoot().setVisibility(View.GONE); - // Bluetooth must be enabled. - if (state.isBluetoothEnabled()) { - noBluetoothView.setVisibility(View.GONE); + // Bluetooth must be enabled + if (state.isBluetoothEnabled()) { + binding.bluetoothOff.getRoot().setVisibility(View.GONE); - // We are now OK to start scanning. - scannerViewModel.startScan(); - scanningView.setVisibility(View.VISIBLE); + // We are now OK to start scanning + scannerViewModel.startScan(); + binding.stateScanning.setVisibility(View.VISIBLE); - if (!state.hasRecords()) { - emptyView.setVisibility(View.VISIBLE); + if (!state.hasRecords()) { + binding.noDevices.getRoot().setVisibility(View.VISIBLE); - if (!Utils.isLocationRequired(this) || Utils.isLocationEnabled(this)) { - noLocationView.setVisibility(View.INVISIBLE); + if (!Utils.isLocationRequired(this) || + Utils.isLocationEnabled(this)) { + binding.noDevices.noLocation.setVisibility(View.INVISIBLE); + } else { + binding.noDevices.noLocation.setVisibility(View.VISIBLE); + } } else { - noLocationView.setVisibility(View.VISIBLE); + binding.noDevices.getRoot().setVisibility(View.GONE); } } else { - emptyView.setVisibility(View.GONE); + binding.bluetoothOff.getRoot().setVisibility(View.VISIBLE); + binding.stateScanning.setVisibility(View.INVISIBLE); + binding.noDevices.getRoot().setVisibility(View.GONE); + binding.noBluetoothPermission.getRoot().setVisibility(View.GONE); + clear(); } } else { - noBluetoothView.setVisibility(View.VISIBLE); - scanningView.setVisibility(View.INVISIBLE); - emptyView.setVisibility(View.GONE); - clear(); + binding.noBluetoothPermission.getRoot().setVisibility(View.VISIBLE); + binding.bluetoothOff.getRoot().setVisibility(View.GONE); + binding.stateScanning.setVisibility(View.INVISIBLE); + binding.noDevices.getRoot().setVisibility(View.GONE); + + final boolean deniedForever = Utils.isBluetoothScanPermissionDeniedForever(this); + binding.noBluetoothPermission.actionGrantBluetoothPermission.setVisibility(deniedForever ? View.GONE : View.VISIBLE); + binding.noBluetoothPermission.actionPermissionSettings.setVisibility(deniedForever ? View.VISIBLE : View.GONE); } } else { - noLocationPermissionView.setVisibility(View.VISIBLE); - noBluetoothView.setVisibility(View.GONE); - scanningView.setVisibility(View.INVISIBLE); - emptyView.setVisibility(View.GONE); + binding.noLocationPermission.getRoot().setVisibility(View.VISIBLE); + binding.noBluetoothPermission.getRoot().setVisibility(View.GONE); + binding.bluetoothOff.getRoot().setVisibility(View.GONE); + binding.stateScanning.setVisibility(View.INVISIBLE); + binding.noDevices.getRoot().setVisibility(View.GONE); final boolean deniedForever = Utils.isLocationPermissionDeniedForever(this); - grantPermissionButton.setVisibility(deniedForever ? View.GONE : View.VISIBLE); - permissionSettingsButton.setVisibility(deniedForever ? View.VISIBLE : View.GONE); + binding.noLocationPermission.actionGrantLocationPermission.setVisibility(deniedForever ? View.GONE : View.VISIBLE); + binding.noLocationPermission.actionPermissionSettings.setVisibility(deniedForever ? View.VISIBLE : View.GONE); } } /** - * stop scanning for bluetooth devices. + * Stops scanning for Bluetooth LE devices. */ private void stopScan() { scannerViewModel.stopScan(); @@ -234,4 +250,37 @@ private void clear() { scannerViewModel.getDevices().clear(); scannerViewModel.getScannerState().clearRecords(); } + + /** + * Opens application settings in Android Settings app. + */ + private void openPermissionSettings() { + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getPackageName(), null)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + /** + * Opens Location settings. + */ + private void openLocationSettings() { + final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + /** + * Shows a prompt to the user to enable Bluetooth on the device. + * + * @implSpec On Android 12+ BLUETOOTH_CONNECT permission needs to be granted before calling + * this method. Otherwise, the app would crash with {@link SecurityException}. + * @see BluetoothAdapter#ACTION_REQUEST_ENABLE + */ + private void requestBluetoothEnabled() { + if (Utils.isBluetoothConnectPermissionGranted(this)) { + final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivity(enableIntent); + } + } } diff --git a/app/src/main/java/no/nordicsemi/android/blinky/adapter/DevicesAdapter.java b/app/src/main/java/no/nordicsemi/android/blinky/adapter/DevicesAdapter.java index 71c3ccde..ad3bf3cd 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/adapter/DevicesAdapter.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/adapter/DevicesAdapter.java @@ -26,22 +26,18 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; + +import java.util.List; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; - -import java.util.List; - -import butterknife.BindView; -import butterknife.ButterKnife; import no.nordicsemi.android.blinky.R; import no.nordicsemi.android.blinky.ScannerActivity; +import no.nordicsemi.android.blinky.databinding.DeviceItemBinding; import no.nordicsemi.android.blinky.viewmodels.DevicesLiveData; -@SuppressWarnings("unused") public class DevicesAdapter extends RecyclerView.Adapter { private List devices; private OnItemClickListener onItemClickListener; @@ -51,7 +47,7 @@ public interface OnItemClickListener { void onItemClick(@NonNull final DiscoveredBluetoothDevice device); } - public void setOnItemClickListener(final OnItemClickListener listener) { + public void setOnItemClickListener(@Nullable final OnItemClickListener listener) { onItemClickListener = listener; } @@ -80,12 +76,12 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio final String deviceName = device.getName(); if (!TextUtils.isEmpty(deviceName)) - holder.deviceName.setText(deviceName); + holder.binding.deviceName.setText(deviceName); else - holder.deviceName.setText(R.string.unknown_device); - holder.deviceAddress.setText(device.getAddress()); + holder.binding.deviceName.setText(R.string.unknown_device); + holder.binding.deviceAddress.setText(device.getAddress()); final int rssiPercent = (int) (100.0f * (127.0f + device.getRssi()) / (127.0f + 20.0f)); - holder.rssi.setImageLevel(rssiPercent); + holder.binding.rssi.setImageLevel(rssiPercent); } @Override @@ -98,22 +94,15 @@ public int getItemCount() { return devices != null ? devices.size() : 0; } - public boolean isEmpty() { - return getItemCount() == 0; - } - final class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.device_address) TextView deviceAddress; - @BindView(R.id.device_name) TextView deviceName; - @BindView(R.id.rssi) ImageView rssi; + private final DeviceItemBinding binding; private ViewHolder(@NonNull final View view) { super(view); - ButterKnife.bind(this, view); - - view.findViewById(R.id.device_container).setOnClickListener(v -> { + binding = DeviceItemBinding.bind(view); + binding.deviceContainer.setOnClickListener(v -> { if (onItemClickListener != null) { - onItemClickListener.onItemClick(devices.get(getAdapterPosition())); + onItemClickListener.onItemClick(devices.get(getBindingAdapterPosition())); } }); } diff --git a/app/src/main/java/no/nordicsemi/android/blinky/profile/BlinkyManager.java b/app/src/main/java/no/nordicsemi/android/blinky/profile/BlinkyManager.java index b1a1e91f..7926c4c4 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/profile/BlinkyManager.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/profile/BlinkyManager.java @@ -38,6 +38,7 @@ import no.nordicsemi.android.ble.data.Data; import no.nordicsemi.android.ble.livedata.ObservableBleManager; +import no.nordicsemi.android.blinky.BuildConfig; import no.nordicsemi.android.blinky.profile.callback.BlinkyButtonDataCallback; import no.nordicsemi.android.blinky.profile.callback.BlinkyLedDataCallback; import no.nordicsemi.android.blinky.profile.data.BlinkyLED; @@ -89,6 +90,9 @@ public void setLogger(@Nullable final LogSession session) { @Override public void log(final int priority, @NonNull final String message) { + if (BuildConfig.DEBUG) { + Log.println(priority, "BlinkyManager", message); + } // The priority is a Log.X constant, while the Logger accepts it's log levels. Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message); } @@ -172,8 +176,8 @@ public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { boolean writeRequest = false; if (ledCharacteristic != null) { - final int rxProperties = ledCharacteristic.getProperties(); - writeRequest = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0; + final int ledProperties = ledCharacteristic.getProperties(); + writeRequest = (ledProperties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0; } supported = buttonCharacteristic != null && ledCharacteristic != null && writeRequest; @@ -181,7 +185,7 @@ public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { } @Override - protected void onDeviceDisconnected() { + protected void onServicesInvalidated() { buttonCharacteristic = null; ledCharacteristic = null; } @@ -202,8 +206,10 @@ public void turnLed(final boolean on) { return; log(Log.VERBOSE, "Turning LED " + (on ? "ON" : "OFF") + "..."); - writeCharacteristic(ledCharacteristic, - on ? BlinkyLED.turnOn() : BlinkyLED.turnOff()) - .with(ledCallback).enqueue(); + writeCharacteristic( + ledCharacteristic, + BlinkyLED.turn(on), + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + ).with(ledCallback).enqueue(); } } diff --git a/app/src/main/java/no/nordicsemi/android/blinky/profile/data/BlinkyLED.java b/app/src/main/java/no/nordicsemi/android/blinky/profile/data/BlinkyLED.java index 94ed5ea2..473689d3 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/profile/data/BlinkyLED.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/profile/data/BlinkyLED.java @@ -30,6 +30,11 @@ public final class BlinkyLED { private static final byte STATE_OFF = 0x00; private static final byte STATE_ON = 0x01; + @NonNull + public static Data turn(final boolean on) { + return on ? turnOn() : turnOff(); + } + @NonNull public static Data turnOn() { return Data.opCode(STATE_ON); diff --git a/app/src/main/java/no/nordicsemi/android/blinky/utils/Utils.java b/app/src/main/java/no/nordicsemi/android/blinky/utils/Utils.java index e76fd620..33e6790a 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/utils/Utils.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/utils/Utils.java @@ -28,17 +28,20 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.location.LocationManager; import android.os.Build; import android.preference.PreferenceManager; -import android.provider.Settings; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.location.LocationManagerCompat; public class Utils { - private static final String PREFS_LOCATION_NOT_REQUIRED = "location_not_required"; + private static final String PREFS_LOCATION_REQUIRED = "location_required"; private static final String PREFS_PERMISSION_REQUESTED = "permission_requested"; + private static final String PREFS_BLUETOOTH_PERMISSION_REQUESTED = "bluetooth_permission_requested"; /** * Checks whether Bluetooth is enabled. @@ -50,16 +53,70 @@ public static boolean isBleEnabled() { return adapter != null && adapter.isEnabled(); } + /** + * Returns whether Bluetooth Scan permission has been granted. + * + * @param context the context. + * @return Whether Bluetooth Scan permission has been granted. + */ + public static boolean isBluetoothScanPermissionGranted(@NonNull final Context context) { + if (!isSorAbove()) + return true; + return ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Returns whether Bluetooth Connect permission has been granted. + * + * @param context the context. + * @return Whether Bluetooth Connect permission has been granted. + */ + public static boolean isBluetoothConnectPermissionGranted(@NonNull final Context context) { + if (!isSorAbove()) + return true; + return ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Returns whether location permission and service is required in order to scan + * for Bluetooth LE devices. This app does not need beacons and other location-intended + * devices, and requests BLUETOOTH_SCAN permission with "never for location" flag. + * + * @return Whether the location permission and service running are required. + */ + public static boolean isLocationPermissionRequired() { + // Location is required only for Android 6-11. + return isMarshmallowOrAbove() && !isSorAbove(); + } + /** * Checks for required permissions. * * @return True if permissions are already granted, false otherwise. */ - public static boolean isLocationPermissionsGranted(@NonNull final Context context) { + public static boolean isLocationPermissionGranted(@NonNull final Context context) { return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; } + /** + * Returns true if Bluetooth Scan permission has been requested at least twice and + * user denied it, and checked 'Don't ask again'. + * + * @param activity the activity. + * @return True if permission has been denied and the popup will not come up any more, + * false otherwise. + */ + public static boolean isBluetoothScanPermissionDeniedForever(@NonNull final Activity activity) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + + return !isLocationPermissionGranted(activity) // Location permission must be denied + && preferences.getBoolean(PREFS_BLUETOOTH_PERMISSION_REQUESTED, false) // Permission must have been requested before + && !ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION); // This method should return false + } + /** * Returns true if location permission has been requested at least twice and * user denied it, and checked 'Don't ask again'. @@ -71,7 +128,7 @@ public static boolean isLocationPermissionsGranted(@NonNull final Context contex public static boolean isLocationPermissionDeniedForever(@NonNull final Activity activity) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - return !isLocationPermissionsGranted(activity) // Location permission must be denied + return !isLocationPermissionGranted(activity) // Location permission must be denied && preferences.getBoolean(PREFS_PERMISSION_REQUESTED, false) // Permission must have been requested before && !ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION); // This method should return false } @@ -86,28 +143,23 @@ public static boolean isLocationPermissionDeniedForever(@NonNull final Activity */ public static boolean isLocationEnabled(@NonNull final Context context) { if (isMarshmallowOrAbove()) { - int locationMode = Settings.Secure.LOCATION_MODE_OFF; - try { - locationMode = Settings.Secure.getInt(context.getContentResolver(), - Settings.Secure.LOCATION_MODE); - } catch (final Settings.SettingNotFoundException e) { - // do nothing - } - return locationMode != Settings.Secure.LOCATION_MODE_OFF; + LocationManager lm = context.getSystemService(LocationManager.class); + return LocationManagerCompat.isLocationEnabled(lm); } return true; } /** - * Location enabled is required on some phones running Android Marshmallow or newer - * (for example on Nexus and Pixel devices). + * Location enabled is required on some phones running Android 6 - 11 + * (for example on Nexus and Pixel devices). Initially, Samsung phones didn't require it, + * but that has been fixed for those phones in Android 9. * * @param context the context. * @return False if it is known that location is not required, true otherwise. */ public static boolean isLocationRequired(@NonNull final Context context) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - return preferences.getBoolean(PREFS_LOCATION_NOT_REQUIRED, isMarshmallowOrAbove()); + return preferences.getBoolean(PREFS_LOCATION_REQUIRED, isMarshmallowOrAbove() && !isSorAbove()); } /** @@ -119,7 +171,25 @@ public static boolean isLocationRequired(@NonNull final Context context) { */ public static void markLocationNotRequired(@NonNull final Context context) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - preferences.edit().putBoolean(PREFS_LOCATION_NOT_REQUIRED, false).apply(); + preferences.edit().putBoolean(PREFS_LOCATION_REQUIRED, false).apply(); + } + + /** + * The first time an app requests a permission there is no 'Don't ask again' checkbox and + * {@link ActivityCompat#shouldShowRequestPermissionRationale(Activity, String)} returns false. + * This situation is similar to a permission being denied forever, so to distinguish both cases + * a flag needs to be saved. + * + * @param context the context. + */ + public static void markBluetoothScanPermissionRequested(@NonNull final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit().putBoolean(PREFS_BLUETOOTH_PERMISSION_REQUESTED, true).apply(); + } + + public static void clearBluetoothPermissionRequested(@NonNull final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit().putBoolean(PREFS_BLUETOOTH_PERMISSION_REQUESTED, false).apply(); } /** @@ -135,7 +205,16 @@ public static void markLocationPermissionRequested(@NonNull final Context contex preferences.edit().putBoolean(PREFS_PERMISSION_REQUESTED, true).apply(); } + public static void clearLocationPermissionRequested(@NonNull final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit().putBoolean(PREFS_PERMISSION_REQUESTED, false).apply(); + } + public static boolean isMarshmallowOrAbove() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } + + public static boolean isSorAbove() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + } } diff --git a/app/src/main/java/no/nordicsemi/android/blinky/viewmodels/BlinkyViewModel.java b/app/src/main/java/no/nordicsemi/android/blinky/viewmodels/BlinkyViewModel.java index 6156ff14..63f18e05 100644 --- a/app/src/main/java/no/nordicsemi/android/blinky/viewmodels/BlinkyViewModel.java +++ b/app/src/main/java/no/nordicsemi/android/blinky/viewmodels/BlinkyViewModel.java @@ -26,9 +26,11 @@ import android.bluetooth.BluetoothDevice; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import no.nordicsemi.android.ble.ConnectRequest; import no.nordicsemi.android.ble.livedata.state.ConnectionState; import no.nordicsemi.android.blinky.adapter.DiscoveredBluetoothDevice; import no.nordicsemi.android.blinky.profile.BlinkyManager; @@ -38,6 +40,8 @@ public class BlinkyViewModel extends AndroidViewModel { private final BlinkyManager blinkyManager; private BluetoothDevice device; + @Nullable + private ConnectRequest connectRequest; public BlinkyViewModel(@NonNull final Application application) { super(application); @@ -47,7 +51,7 @@ public BlinkyViewModel(@NonNull final Application application) { } public LiveData getConnectionState() { - return blinkyManager.getState(); + return blinkyManager.state; } public LiveData getButtonState() { @@ -81,10 +85,11 @@ public void connect(@NonNull final DiscoveredBluetoothDevice target) { */ public void reconnect() { if (device != null) { - blinkyManager.connect(device) + connectRequest = blinkyManager.connect(device) .retry(3, 100) .useAutoConnect(false) - .enqueue(); + .then(d -> connectRequest = null); + connectRequest.enqueue(); } } @@ -93,7 +98,11 @@ public void reconnect() { */ private void disconnect() { device = null; - blinkyManager.disconnect().enqueue(); + if (connectRequest != null) { + connectRequest.cancelPendingConnection(); + } else if (blinkyManager.isConnected()) { + blinkyManager.disconnect().enqueue(); + } } /** @@ -108,8 +117,6 @@ public void setLedState(final boolean on) { @Override protected void onCleared() { super.onCleared(); - if (blinkyManager.isConnected()) { - disconnect(); - } + disconnect(); } } diff --git a/app/src/main/res/drawable/ic_timeout.xml b/app/src/main/res/drawable/ic_timeout.xml new file mode 100644 index 00000000..4a9d4a5a --- /dev/null +++ b/app/src/main/res/drawable/ic_timeout.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/app/src/main/res/layout/activity_blinky.xml b/app/src/main/res/layout/activity_blinky.xml index 68c80d58..2066a83a 100644 --- a/app/src/main/res/layout/activity_blinky.xml +++ b/app/src/main/res/layout/activity_blinky.xml @@ -191,11 +191,20 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_scanner.xml b/app/src/main/res/layout/activity_scanner.xml index 1bb49155..48ddcf7f 100644 --- a/app/src/main/res/layout/activity_scanner.xml +++ b/app/src/main/res/layout/activity_scanner.xml @@ -75,6 +75,7 @@ app:layout_constraintTop_toBottomOf="@id/appbar_layout" tools:listitem="@layout/device_item" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_splash_screen.xml b/app/src/main/res/layout/activity_splash_screen.xml index dddca5ca..bf6be5d3 100644 --- a/app/src/main/res/layout/activity_splash_screen.xml +++ b/app/src/main/res/layout/activity_splash_screen.xml @@ -33,7 +33,8 @@ android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context=".SplashScreenActivity" - tools:ignore="ContentDescription"> + tools:ignore="ContentDescription" + tools:viewBindingIgnore="true"> + android:text="@string/action_enable"/> \ No newline at end of file diff --git a/app/src/main/res/layout/info_no_bluetooth_permission.xml b/app/src/main/res/layout/info_no_bluetooth_permission.xml new file mode 100644 index 00000000..60d81aa2 --- /dev/null +++ b/app/src/main/res/layout/info_no_bluetooth_permission.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/info_no_devices.xml b/app/src/main/res/layout/info_no_devices.xml index d89d524f..a5451040 100644 --- a/app/src/main/res/layout/info_no_devices.xml +++ b/app/src/main/res/layout/info_no_devices.xml @@ -25,6 +25,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" @@ -73,6 +74,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:text="@string/blinky_guide_location_action"/> + android:text="@string/action_enable"/> \ No newline at end of file diff --git a/app/src/main/res/layout/info_no_permission.xml b/app/src/main/res/layout/info_no_location_permission.xml similarity index 96% rename from app/src/main/res/layout/info_no_permission.xml rename to app/src/main/res/layout/info_no_location_permission.xml index 2ce7f403..5543332b 100644 --- a/app/src/main/res/layout/info_no_permission.xml +++ b/app/src/main/res/layout/info_no_location_permission.xml @@ -25,6 +25,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" @@ -60,7 +61,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:text="@string/location_permission_action"/> + android:text="@string/action_grant_permission"/> \ No newline at end of file diff --git a/app/src/main/res/layout/info_not_supported.xml b/app/src/main/res/layout/info_not_supported.xml index 655aae84..f7e9d318 100644 --- a/app/src/main/res/layout/info_not_supported.xml +++ b/app/src/main/res/layout/info_not_supported.xml @@ -25,6 +25,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" @@ -42,9 +43,9 @@ + android:text="@string/action_not_supported"/> \ No newline at end of file diff --git a/app/src/main/res/layout/info_timeout.xml b/app/src/main/res/layout/info_timeout.xml new file mode 100644 index 00000000..ba7039d1 --- /dev/null +++ b/app/src/main/res/layout/info_timeout.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/filter.xml b/app/src/main/res/menu/filter.xml index ba73e0b3..552921a3 100644 --- a/app/src/main/res/menu/filter.xml +++ b/app/src/main/res/menu/filter.xml @@ -1,8 +1,24 @@ nRF Blinky BLINKY - - Filter - Only devices advertising LBS UUID - Only nearby devices - - Unknown Device - On - Off - LED - Button - State - Pressed - Released - Unknown - Toggle the switch to turn the LED 3 on or off. - Press Button 1 on the dev kit. - - LOCATION PERMISSION REQUIRED - From Android 6.0 Marshmallow onwards the application requires Location permission in order to scan for Bluetooth Low Energy devices. - \n\nThis is because Bluetooth LE beacons, for example iBeacons or Eddystone, may be used to determine the phone\'s and user\'s location. nRF Blinky will not use this information in any way. - Grant permission - Settings - - BLUETOOTH DISABLED - The Bluetooth adapter is turned off. Click the button below to enable it. - Enable - - CAN\'T SEE YOUR BLINKY? - 1. Make sure the DK is turned on and is connected to a power source using a micro USB cable or a coin cell is plugged in. - \n\n2. Make sure the ble_app_blinky firmware and SoftDevice are flashed. - 3. Location is turned off. Some Android phones require it enabled in order to scan for Bluetooth LE devices. - If you are sure your Blinky is advertising and it doesn\'t show up here, click the button below to enable Location. - Enable - - DEVICE NOT SUPPORTED - 1. The Blinky service hasn\'t been found on the device\'s attribute table. - \n\n2. Make sure the DK is flashed with the correct firmware. - \n\n3. If you used the DK with another firmware before, the old services might have been cached by the system. - Fix and try again - - Connecting… - Initializing… diff --git a/app/src/main/res/values/strings_lbs.xml b/app/src/main/res/values/strings_lbs.xml new file mode 100644 index 00000000..02fc1aff --- /dev/null +++ b/app/src/main/res/values/strings_lbs.xml @@ -0,0 +1,31 @@ + + + Connecting… + Initializing… + + Fix and try again + Retry + + On + Off + + LED + Button + + State + Pressed + Released + Unknown + + Toggle the switch to turn the LED 3 on or off. + Press Button 1 on the dev kit. + + DEVICE NOT SUPPORTED + 1. The Blinky service hasn\'t been found on the device\'s + attribute table.\n\n2. Make sure the DK is flashed with the correct firmware.\n\n3. If you + used the DK with another firmware before, the old services might have been cached by + the system. + + CONNECTION TIMED OUT + Make sure your device is powered ON and in range. + \ No newline at end of file diff --git a/app/src/main/res/values/strings_scanner.xml b/app/src/main/res/values/strings_scanner.xml new file mode 100644 index 00000000..44557354 --- /dev/null +++ b/app/src/main/res/values/strings_scanner.xml @@ -0,0 +1,37 @@ + + + Filter + Only devices advertising LBS UUID + Only nearby devices + + Grant permission + Settings + Enable + + Unknown Device + + LOCATION PERMISSION REQUIRED + From Android 6.0 Marshmallow until Android 11 the application + requires Location permission in order to scan for Bluetooth Low Energy devices. This + is because Bluetooth LE beacons, for example iBeacons or Eddystone, may be used to + determine the phone\'s and user\'s location.\n\nnRF Connect Device Manager will not use + this information in any way.\n\nOn Android 12 and above, a new Bluetooth Scan permission + could be requested with a flag that disables scanning for location, but on this system + version no scan results are returned without location enabled. + + BLUETOOTH PERMISSIONS REQUIRED + The app needs to scan and connect to\nBluetooth LE devices. + + BLUETOOTH DISABLED + The Bluetooth adapter is turned off.\nClick the button + below to enable it. + + CAN\'T SEE YOUR BLINKY? + 1. Make sure the DK is turned on and is connected to + a power source using a micro USB cable or a coin cell is plugged in.\n\n2. Make sure the + ble_app_blinky firmware and SoftDevice are flashed. + 3. Location is turned off. Most Android phones + require it in order to scan for Bluetooth LE devices. If you are sure your + device is advertising and it doesn\'t show up here, click the button below to + enable Location. + \ No newline at end of file diff --git a/build.gradle b/build.gradle index b27b75ba..9a46c970 100644 --- a/build.gradle +++ b/build.gradle @@ -2,11 +2,11 @@ buildscript { repositories { - jcenter() + mavenCentral() google() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:7.0.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,7 +15,7 @@ buildscript { allprojects { repositories { - jcenter() + mavenCentral() google() } } diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 00000000..83890b1c --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file("fastlane-api.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name("no.nordicsemi.android.nrfblinky") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 00000000..212b72dd --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,29 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:android) + +platform :android do + + desc "Deploy build to Internal channel." + lane :deployInternal do + gradle(task: "clean bundleRelease") + upload_to_play_store( + track: 'internal', + aab: 'sample/build/outputs/bundle/release/app-release.aab' + ) + end + +end diff --git a/gradle.properties b/gradle.properties index 915f0e66..ccd5dda1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,5 +16,4 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.enableJetifier=true android.useAndroidX=true \ No newline at end of file diff --git a/gradle/git-tag-version.gradle b/gradle/git-tag-version.gradle new file mode 100644 index 00000000..72631006 --- /dev/null +++ b/gradle/git-tag-version.gradle @@ -0,0 +1,27 @@ +ext.getVersionCodeFromTags = { -> + try { + def code = new ByteArrayOutputStream() + exec { + commandLine 'git', 'tag', '--list' + standardOutput = code + } + return 2 + code.toString().split("\n").size() + } + catch (ignored) { + return -1 + } +} + +ext.getVersionNameFromTags = { -> + try { + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'git', 'describe', '--tags', '--abbrev=0' + standardOutput = stdout + } + return stdout.toString().trim().split("%")[0] + } + catch (ignored) { + return null + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bc03abba..4fd89d52 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed May 06 10:16:52 CEST 2020 +#Wed Apr 21 12:43:50 CEST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/settings.gradle b/settings.gradle index 1c82643b..2310655d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,14 @@ include ':app' -// To include BLE Library project as a module, clone it to Android-BLE-Library folder and -// uncomment the following lines. Also, uncomment the dependency in gradle.build file. +// To include Android Scanner Compat Library project as a module, clone it to +// Android-Scanner-Compat-Library folder. -// include ':ble', ':ble-livedata' -// project(':ble').projectDir = file('../Android-BLE-Library/ble') -// project(':ble-livedata').projectDir = file('../Android-BLE-Library/ble-livedata') \ No newline at end of file +if (file('../Android-Scanner-Compat-Library').exists()) { + includeBuild('../Android-Scanner-Compat-Library') +} + +// To include BLE Library project as a module, clone it to Android-BLE-Library folder. + +if (file('../Android-BLE-Library').exists()) { + includeBuild('../Android-BLE-Library') +} \ No newline at end of file