Managing Device State and Preferences
Before we dive into how Bluetooth communication with BLEService, we need to build two important helper components:
ConnectedDeviceManager.java
This class will help us track whether a device is connected, store the name of that device, and handle connection-related status.PreferencesManager.java
This class wraps Android’s SharedPreferences system, allowing us to save and retrieve vales like the last connected device or user settings between sessions.
Together, these two classes will form the foundation of how your app remembers devices and interacts with them intelligently.
1. ConnectedDeviceManager
This class helps you:
Track the current device name.
Check whether a device is connected or not.
Avoid passing around device names or connection flags manually.
We’ll implement ConnectedDeviceManager as a singleton - meaning only one instance exists for the whole app. This way, you can access and modify connection state from any activity or class without passing it around explicitly.
1.1. ConnectedDeviceManager.java
public class ConnectedDeviceManager {
// Singleton instance
private static ConnectedDeviceManager instance;
// Device connection state
private String deviceName = null;
private String deviceAddress = null;
private boolean isConnected = false;
// Private constructor to enforce singleton pattern
private ConnectedDeviceManager() {}
// Access the singleton instance
public static ConnectedDeviceManager getInstance() {
if (instance == null) {
instance = new ConnectedDeviceManager();
}
return instance;
}
// Connection state
public boolean isConnected() {
return isConnected;
}
public void setConnected(boolean connected) {
this.isConnected = connected;
}
// Device name
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String name) {
this.deviceName = name;
}
// Device MAC address
public String getDeviceAddress() {
return deviceAddress;
}
public void setDeviceAddress(String address) {
this.deviceAddress = address;
}
}
1.2. Explanation
This is a singleton utility class that stores and shares information about the currently connected BLE device throughout the app. It helps keep your code clean by avoiding the need to pass device name or address between activities or services manually.
getInstance()
Returns the one and ony instance of the manager.setDeviceName(name)
Stores the bluetooth device name (e.g. “Exampl-01-BLE”).getDeviceName()
Returns the last saved device name.setDeviceAddress(address)
Stores the MAC address (e.g. “C4:7F:51:2B:9A:01”).getDeviceAddress()
Returns the last saved address - can be used for reconnecting.setConnected(true/false)
Updates whether a device is currently connected.isConnected()
Returns whether a device is connected.
2. PreferencesManager
This class provides a simple and reusable way to save and load small pieces of data like:
The last connected device address
Custom user settings or flags
Other lightweight values you want to persist between app launches
Android offers this via a system called SharedPreferences, and PreferencesManager wraps that into, a neet centralized utility.
We’ll implement PreferencesManager as a singleton so you can access it from anywhere without repeating boilerplate code.
2.1. PreferencesManager.java
public class PreferencesManager {
private static final String TAG = "PreferencesManager";
private static final String PREF_NAME = "VortexPrefs";
private static final String DEVICES_KEY = "savedDevicesArray";
private static PreferencesManager instance;
private final SharedPreferences sharedPreferences;
private PreferencesManager(Context context) {
sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public static PreferencesManager getInstance(Context context) {
if (instance == null) {
instance = new PreferencesManager(context.getApplicationContext());
}
return instance;
}
// Save or update a device
public void saveDevice(String address, String name, boolean hasCharacteristic, String status) {
try {
JSONArray devicesArray = new JSONArray(sharedPreferences.getString(DEVICES_KEY, "[]"));
for (int i = 0; i < devicesArray.length(); i++) {
JSONObject device = devicesArray.getJSONObject(i);
if (device.getString("address").equals(address)) {
device.put("name", name);
device.put("hasCharacteristic", hasCharacteristic);
device.put("status", status);
devicesArray.put(i, device);
saveDevicesArray(devicesArray);
return;
}
}
JSONObject newDevice = new JSONObject();
newDevice.put("address", address);
newDevice.put("name", name);
newDevice.put("hasCharacteristic", hasCharacteristic);
newDevice.put("status", status);
devicesArray.put(newDevice);
saveDevicesArray(devicesArray);
} catch (JSONException e) {
Log.e(TAG, "Error saving device: " + e.getMessage());
}
}
// Get a device's name by address
public String getDeviceName(String address) {
try {
JSONArray devicesArray = new JSONArray(sharedPreferences.getString(DEVICES_KEY, "[]"));
for (int i = 0; i < devicesArray.length(); i++) {
JSONObject device = devicesArray.getJSONObject(i);
if (device.getString("address").equals(address)) {
return device.optString("name", "Unknown");
}
}
} catch (JSONException e) {
Log.e(TAG, "Error getting device name: " + e.getMessage());
}
return "Unknown";
}
// Get device status
public String getDeviceStatus(String address) {
try {
JSONArray devicesArray = new JSONArray(sharedPreferences.getString(DEVICES_KEY, "[]"));
for (int i = 0; i < devicesArray.length(); i++) {
JSONObject device = devicesArray.getJSONObject(i);
if (device.getString("address").equals(address)) {
return device.optString("status", "Disconnected");
}
}
} catch (JSONException e) {
Log.e(TAG, "Error getting device status: " + e.getMessage());
}
return "Disconnected";
}
// Update device status
public void setDeviceStatus(String address, String newStatus) {
try {
JSONArray devicesArray = new JSONArray(sharedPreferences.getString(DEVICES_KEY, "[]"));
for (int i = 0; i < devicesArray.length(); i++) {
JSONObject device = devicesArray.getJSONObject(i);
if (device.getString("address").equals(address)) {
device.put("status", newStatus);
devicesArray.put(i, device);
saveDevicesArray(devicesArray);
Log.d(TAG, "Device status updated: " + newStatus);
return;
}
}
} catch (JSONException e) {
Log.e(TAG, "Error updating status: " + e.getMessage());
}
}
// Get device characteristic flag
public boolean getDeviceCharFlag(String address) {
try {
JSONArray devicesArray = new JSONArray(sharedPreferences.getString(DEVICES_KEY, "[]"));
for (int i = 0; i < devicesArray.length(); i++) {
JSONObject device = devicesArray.getJSONObject(i);
if (device.getString("address").equals(address)) {
return device.optBoolean("hasCharacteristic", false);
}
}
} catch (JSONException e) {
Log.e(TAG, "Error getting char flag: " + e.getMessage());
}
return false;
}
// Remove a device
public void removeDevice(String address) {
try {
JSONArray original = new JSONArray(sharedPreferences.getString(DEVICES_KEY, "[]"));
JSONArray filtered = new JSONArray();
for (int i = 0; i < original.length(); i++) {
JSONObject device = original.getJSONObject(i);
if (!device.getString("address").equals(address)) {
filtered.put(device);
}
}
saveDevicesArray(filtered);
} catch (JSONException e) {
Log.e(TAG, "Error removing device: " + e.getMessage());
}
}
// Clear all devices
public void clearDevices() {
sharedPreferences.edit().remove(DEVICES_KEY).apply();
}
// Save JSON array back to SharedPreferences
private void saveDevicesArray(JSONArray array) {
sharedPreferences.edit().putString(DEVICES_KEY, array.toString()).apply();
}
}
2.2. Explanation
The PreferencesManager is a singleton utility class that wraps arroud Android’s SharedPreferences system. It’s used to save and manage multiple known Bluetooth devices, including:
The device name
MAC address
Status (e.g. “Connected”, or “Disconnected”)
Whether the device supports required BLE characteristisc
It stores everything in a JSON array under a single key in persistent storage.
getInstance(Context)
Returns the ona shared instance. This ensures all parts of the app use the same data source. Must be initialized with a Context.saveDevice(…)
Adds a new device or updates an existing one based on its address. Stores metadata like name, status, and whether it has the needed characteristic.getDeviceName(address)
Returns the name of the device with the given address, or “Unknown” if not found.getDeviceStatus(address)
Returns the status of the device (like “Conneted” or “Disconnected”0 or “Disconnected” if no status is found.setDeviceStatus(address,newStatus)
Updates the status string of an existing device in the list.getDeviceCharFlag(address)
Returns true or false based on whether a device was marked as having the required BLE characteristic.removeDevice(address)
Removes a specific device entry based on irs MAC address.clearDevices()
Clears the entire saved list of devices form storage.saveDevicesArray(JSONArray)
Private healper that writes the full device list back into SharedPreferences. Used internally by all modification methods.
3. Updating MainActivity with Device State and Preferences
Now that we’ve implemented ConnectedDeviceManager and PreferencesManager, it’s time to connect them to MainActivity.
We’ll use them to:
Track if a device is connected (isConnected)
Retrieve and display the name of the connected device (deviceName)
Update the UI’s status text whenever the activity resumes
Control user access to the SeekBar based on connection status.
Add Class-Level Variables
Place these at the top of your MainActivity class (outside any methods), so you can use them throughout the activity:
private boolean isConnected;
private String deviceName;
private TextView statusText;
Connect the statusText View io onCreate()
Make sure this line is added inside your oonCreate() method:
statusText = findViewById(R.id.statusText);
Implement onResune() Connection Handling
This logic will:
Check if a device is marked as connected
Try to retrieve its name from PreferencesManager
Update the on-screen status label
@Override
protected void onResume() {
super.onResume();
// Attempt to load device name from preferences first
deviceName = PreferencesManager.getInstance(this)
.getDeviceName(ConnectedDeviceManager.getInstance().getDeviceAddress());
if (deviceName == null) {
deviceName = ConnectedDeviceManager.getInstance().getDeviceName();
}
isConnected = ConnectedDeviceManager.getInstance().isConnected();
Log.d(TAG, "ConnectionStateManager isConnected: " + isConnected);
if (isConnected) {
statusText.setText("Connected to: " + deviceName);
} else {
statusText.setText(R.string.default_statusText);
}
// Keep the navigation bar synced
bottomNavigationView.setSelectedItemId(R.id.home);
}
Use isConnected Throughout the App
You can now use the isConnected variable inside any method to prevent sending commands or allowing interactions when no device is connected.
Example - SeekBar logic to implement:
brightnessSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (isConnected) {
Log.d(TAG, "Progress changed:");
if (Math.abs(progress - lastSentCommand) >= SKIP_THRESHOLD) {
Log.d("onProgressChanged", "Command sent");
sendBrightness(progress);
lastSentCommand = progress;
}
} else {
seekBar.setProgress(lastSentCommand);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (!isConnected) {
lastSentCommand = seekBar.getProgress();
Toast.makeText(MainActivity.this, "Connect to a device first", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (!isConnected) {
seekBar.setProgress(lastSentCommand);
} else {
Log.d(TAG, "Progress stopped");
new Handler().postDelayed(() -> sendBrightness(lastSentCommand), 200);
new Handler().postDelayed(() -> sendBrightness(14), 400);
}
}
});
The new connection checks ensure that the SeekBar only works when a BLE device is actually connected. This prevents users from trying to send brightness commands when no device is available, which could otherwise cause errors or unexpected behavior.
Summary
Congratulations! You’ve just finished building the core logic for the main screen of your app. This was one of the most complex and important pieces of the app - and you now have a powerful, connected UI that’s ready to control your BLE LED devices.
We’re now ready to implement BLEService - the background component responsible for all Bluetooth communication.
It will:
Process the commands like sendBrightness().
Manage connections and disconnections.
Communicate with the device driver or characteristic
Serve as the app’s communication hub for all BLE functionality.
Updated MainActivity code
▶ Updated MainActivity
public class MainActivity extends AppCompatActivity {
private boolean isConnected;
private String deviceName;
private TextView statusText;
private SeekBar brightnessSeekBar;
private Button openAnimationsButton, staticColorButton;
private FrameLayout selectDeviceFrame;
private BottomNavigationView bottomNavigationView;
private int lastSentCommand = -1;
private final int SKIP_THRESHOLD = 5;
private final int minSeekBarValue = 48;
private final int maxSeekBarValue = 60;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Permission check
if (!checkPermissions()) {
requestPermissions();
}
// View binding
statusText = findViewById(R.id.statusText);
brightnessSeekBar = findViewById(R.id.brightnessSeekBar);
openAnimationsButton = findViewById(R.id.animationsBtn);
staticColorButton = findViewById(R.id.staticColorBtn);
selectDeviceFrame = findViewById(R.id.selectDeviceFrame);
bottomNavigationView = findViewById(R.id.bottomNavigation);
brightnessSeekBar.setMax(maxSeekBarValue - minSeekBarValue);
// SeekBar interaction
brightnessSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (isConnected) {
Log.d(TAG, "Progress changed:");
if (Math.abs(progress - lastSentCommand) >= SKIP_THRESHOLD) {
Log.d("onProgressChanged", "Command sent");
sendBrightness(progress);
lastSentCommand = progress;
}
} else {
seekBar.setProgress(lastSentCommand);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (!isConnected) {
lastSentCommand = seekBar.getProgress();
Toast.makeText(MainActivity.this, "Connect to a device first", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (!isConnected) {
seekBar.setProgress(lastSentCommand);
} else {
Log.d(TAG, "Progress stopped");
new Handler().postDelayed(() -> sendBrightness(lastSentCommand), 200);
new Handler().postDelayed(() -> sendBrightness(14), 400);
}
}
});
// Button listeners
openAnimationsButton.setOnClickListener(v -> {
Intent intent = new Intent(this, Animations.class);
startActivity(intent);
});
staticColorButton.setOnClickListener(v -> {
Intent intent = new Intent(this, StaticColorActivity.class);
startActivity(intent);
});
selectDeviceFrame.setOnClickListener(v -> {
Intent intent = new Intent(this, SelectDeviceActivity.class);
startActivity(intent);
});
// Bottom navigation
bottomNavigationView.setOnItemSelectedListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.home) return true;
else if (itemId == R.id.animations) {
startActivity(new Intent(this, Animations.class));
return true;
} else if (itemId == R.id.static_color) {
startActivity(new Intent(this, StaticColorActivity.class));
return true;
} else if (itemId == R.id.settings) {
startActivity(new Intent(this, TestingNewActivities.class));
return false;
}
return false;
});
}
@Override
protected void onResume() {
super.onResume();
deviceName = PreferencesManager.getInstance(this)
.getDeviceName(ConnectedDeviceManager.getInstance().getDeviceAddress());
if (deviceName == null) {
deviceName = ConnectedDeviceManager.getInstance().getDeviceName();
}
isConnected = ConnectedDeviceManager.getInstance().isConnected();
Log.d(TAG, "ConnectionStateManager isConnected: " + isConnected);
if (isConnected) {
statusText.setText("Connected to: " + deviceName);
} else {
statusText.setText(R.string.default_statusText);
}
bottomNavigationView.setSelectedItemId(R.id.home);
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
private boolean checkPermissions() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
private void requestPermissions() {
ActivityCompat.requestPermissions(this, new String[]{
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.ACCESS_FINE_LOCATION
}, 101);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 101) {
if (checkPermissions()) {
Toast.makeText(this, "Permissions granted.", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Permissions are required to use Bluetooth.", Toast.LENGTH_LONG).show();
}
}
}
private void sendBrightness(int brightnessValue) {
if (!isConnected) return;
Intent sendInitiatorValue = new Intent(this, BLEService.class);
sendInitiatorValue.putExtra("management_BLEService", "DRIVER_COMMAND");
sendInitiatorValue.putExtra("command", 12);
startService(sendInitiatorValue);
new Handler().postDelayed(() -> {
int finalProgress = brightnessValue + minSeekBarValue;
Intent intent = new Intent(this, BLEService.class);
intent.putExtra("management_BLEService", "DRIVER_COMMAND");
intent.putExtra("command", finalProgress);
startService(intent);
Log.d(TAG, "Brightness sent: " + finalProgress);
}, 100);
}
}