Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Showing saved (bonded) devices on the scanner screen #187

Merged
merged 3 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 36 additions & 246 deletions sample/src/main/java/io/runtime/mcumgr/sample/ScannerActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,29 @@

package io.runtime.mcumgr.sample;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.splashscreen.SplashScreen;
import androidx.lifecycle.ViewModelProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;

import com.google.android.material.color.MaterialColors;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.bottomnavigation.BottomNavigationView;

import javax.inject.Inject;

import io.runtime.mcumgr.sample.adapter.DevicesAdapter;
import io.runtime.mcumgr.sample.databinding.ActivityScannerBinding;
import io.runtime.mcumgr.sample.di.Injectable;
import io.runtime.mcumgr.sample.utils.Utils;
import io.runtime.mcumgr.sample.viewmodel.ScannerStateLiveData;
import io.runtime.mcumgr.sample.viewmodel.ScannerViewModel;
import io.runtime.mcumgr.sample.viewmodel.ViewModelFactory;
import io.runtime.mcumgr.sample.fragment.scanner.SavedDevicesFragment;
import io.runtime.mcumgr.sample.fragment.scanner.ScannerFragment;

public class ScannerActivity extends AppCompatActivity
implements Injectable, DevicesAdapter.OnItemClickListener {
public class ScannerActivity extends AppCompatActivity implements Injectable {
// This flag is false when the app is first started (cold start).
// In this case, the animation will be fully shown (1 sec).
// Subsequent launches will display it only briefly.
Expand All @@ -58,12 +37,8 @@ public class ScannerActivity extends AppCompatActivity

private static final String PREF_INTRO = "introShown";

@Inject
ViewModelFactory viewModelFactory;

private ActivityScannerBinding binding;

private ScannerViewModel scannerViewModel;
private Fragment scannerFragment;
private Fragment savedFragment;

@SuppressWarnings("ConstantConditions")
@Override
Expand Down Expand Up @@ -106,7 +81,7 @@ protected void onCreate(@Nullable final Bundle savedInstanceState) {
}
}

binding = ActivityScannerBinding.inflate(getLayoutInflater());
final ActivityScannerBinding binding = ActivityScannerBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Display Intro just once.
Expand All @@ -120,234 +95,49 @@ protected void onCreate(@Nullable final Bundle savedInstanceState) {
setSupportActionBar(binding.toolbar);
getSupportActionBar().setTitle(R.string.app_name);

// Create view model containing utility methods for scanning
scannerViewModel = new ViewModelProvider(this, viewModelFactory)
.get(ScannerViewModel.class);
scannerViewModel.getScannerState().observe(this, this::startScan);

// Configure the recycler view
final RecyclerView recyclerView = binding.recyclerViewBleDevices;
recyclerView.setLayoutManager(new LinearLayoutManager(this));
final DividerItemDecoration dividerItemDecoration =
new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL);
recyclerView.addItemDecoration(dividerItemDecoration);
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
final DevicesAdapter adapter =
new DevicesAdapter(this, scannerViewModel.getDevices());
adapter.setOnItemClickListener(this);
recyclerView.setAdapter(adapter);

// Set up permission request launcher
final ActivityResultLauncher<String> requestPermission =
registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> scannerViewModel.refresh()
);
final ActivityResultLauncher<String[]> requestPermissions =
registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> scannerViewModel.refresh()
);

// Configure views
binding.refreshLayout.setOnRefreshListener(() -> {
scannerViewModel.clear();
binding.refreshLayout.setRefreshing(false);
});
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();
final BottomNavigationView navMenu = binding.navMenu;
navMenu.setSelectedItemId(R.id.nav_scanner);
navMenu.setOnItemSelectedListener(item -> {
final int id = item.getItemId();
final FragmentTransaction t = getSupportFragmentManager().beginTransaction();
if (id == R.id.nav_scanner) t.show(scannerFragment); else t.hide(scannerFragment);
if (id == R.id.nav_bonded) t.show(savedFragment); else t.hide(savedFragment);
t.runOnCommit(this::invalidateMenu);
t.commit();
return true;
});
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
protected void onStart() {
super.onStart();
startScan();
}

@Override
protected void onStop() {
super.onStop();
stopScan();
// Initialize fragments.
if (savedInstanceState == null) {
scannerFragment = new ScannerFragment();
savedFragment = new SavedDevicesFragment();

getSupportFragmentManager().beginTransaction()
.add(R.id.container, scannerFragment, "scanner")
.add(R.id.container, savedFragment, "saved")
// Initially, show the Scanner fragment and hide others.
.hide(savedFragment)
.commit();
} else {
scannerFragment = getSupportFragmentManager().findFragmentByTag("scanner");
savedFragment = getSupportFragmentManager().findFragmentByTag("saved");
}
}

@Override
public boolean onCreateOptionsMenu(@NonNull final Menu menu) {
getMenuInflater().inflate(R.menu.filter, menu);
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.about, menu);
menu.findItem(R.id.filter_uuid).setChecked(scannerViewModel.isUuidFilterEnabled());
menu.findItem(R.id.filter_nearby).setChecked(scannerViewModel.isNearbyFilterEnabled());
return true;
}

@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.filter_uuid) {
item.setChecked(!item.isChecked());
scannerViewModel.filterByUuid(item.isChecked());
return true;
}
if (itemId == R.id.filter_nearby) {
item.setChecked(!item.isChecked());
scannerViewModel.filterByDistance(item.isChecked());
return true;
}
if (itemId == R.id.menu_about) {
final Intent launchIntro = new Intent(this, IntroActivity.class);
startActivity(launchIntro);
return true;
}
return super.onOptionsItemSelected(item);
}

@Override
public void onItemClick(@NonNull final BluetoothDevice device) {
final Intent intent = new Intent(this, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_DEVICE, device);
startActivity(intent);
}

/**
* Start scanning for Bluetooth devices or displays a message based on the scanner state.
*/
private void startScan(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()) {
binding.bluetoothOff.getRoot().setVisibility(View.GONE);

// We are now OK to start scanning
scannerViewModel.startScan();
binding.progressBar.setVisibility(View.VISIBLE);

if (!state.hasRecords()) {
binding.noDevices.getRoot().setVisibility(View.VISIBLE);

if (!Utils.isLocationRequired(this) ||
Utils.isLocationEnabled(this)) {
binding.noDevices.noLocation.setVisibility(View.INVISIBLE);
} else {
binding.noDevices.noLocation.setVisibility(View.VISIBLE);
}
} else {
binding.noDevices.getRoot().setVisibility(View.GONE);
}
} else {
binding.bluetoothOff.getRoot().setVisibility(View.VISIBLE);
binding.progressBar.setVisibility(View.INVISIBLE);
binding.noDevices.getRoot().setVisibility(View.GONE);
binding.noBluetoothPermission.getRoot().setVisibility(View.GONE);

scannerViewModel.clear();
}
} else {
binding.noBluetoothPermission.getRoot().setVisibility(View.VISIBLE);
binding.bluetoothOff.getRoot().setVisibility(View.GONE);
binding.progressBar.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 {
binding.noLocationPermission.getRoot().setVisibility(View.VISIBLE);
binding.noBluetoothPermission.getRoot().setVisibility(View.GONE);
binding.bluetoothOff.getRoot().setVisibility(View.GONE);
binding.progressBar.setVisibility(View.INVISIBLE);
binding.noDevices.getRoot().setVisibility(View.GONE);

final boolean deniedForever = Utils.isLocationPermissionDeniedForever(this);
binding.noLocationPermission.actionGrantLocationPermission.setVisibility(deniedForever ? View.GONE : View.VISIBLE);
binding.noLocationPermission.actionPermissionSettings.setVisibility(deniedForever ? View.VISIBLE : View.GONE);
}
}

/**
* Starts scanning for Bluetooth LE devices.
*/
private void startScan() {
startScan(scannerViewModel.getScannerState());
}

/**
* Stops scanning for Bluetooth LE devices.
*/
private void stopScan() {
scannerViewModel.stopScan();
}

/**
* 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}.
*/
@SuppressLint("MissingPermission")
private void requestBluetoothEnabled() {
if (Utils.isBluetoothConnectPermissionGranted(this)) {
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivity(enableIntent);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@
import android.bluetooth.BluetoothDevice;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;
import java.util.Locale;

import io.runtime.mcumgr.sample.R;
import io.runtime.mcumgr.sample.ScannerActivity;
import io.runtime.mcumgr.sample.databinding.DeviceItemBinding;
import io.runtime.mcumgr.sample.viewmodel.DevicesLiveData;
import io.runtime.mcumgr.sample.viewmodel.scanner.DevicesLiveData;

@SuppressWarnings("unused")
public class DevicesAdapter extends RecyclerView.Adapter<DevicesAdapter.ViewHolder> {
Expand All @@ -47,9 +48,9 @@ public void setOnItemClickListener(final OnItemClickListener listener) {
onItemClickListener = listener;
}

public DevicesAdapter(final ScannerActivity activity, final DevicesLiveData devicesLiveData) {
public DevicesAdapter(final LifecycleOwner owner, final DevicesLiveData devicesLiveData) {
setHasStableIds(true);
devicesLiveData.observe(activity, devices -> {
devicesLiveData.observe(owner, devices -> {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(
new DeviceDiffCallback(this.devices, devices), false);
this.devices = devices;
Expand Down Expand Up @@ -88,6 +89,7 @@ else if (deviceName.toLowerCase(Locale.US).contains("nimble"))
holder.binding.deviceAddress.setText(device.getAddress());
final int rssiPercent = (int) (100.0f * (127.0f + device.getRssi()) / (127.0f + 20.0f));
holder.binding.rssi.setImageLevel(rssiPercent);
holder.binding.rssi.setVisibility(device.getRssi() != -128 ? View.VISIBLE : View.GONE);
}

@Override
Expand Down
Loading