Skip to content

Commit

Permalink
Shoving saved devices on Scanner screen
Browse files Browse the repository at this point in the history
  • Loading branch information
philips77 committed Sep 30, 2024
1 parent ddd2778 commit 9ff1d86
Show file tree
Hide file tree
Showing 33 changed files with 1,051 additions and 353 deletions.
279 changes: 36 additions & 243 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,231 +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));
((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

0 comments on commit 9ff1d86

Please sign in to comment.