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 @@