Managing Device State and Preferences

Before we dive into how Bluetooth communication with BLEService, we need to build two important helper components:

  1. ConnectedDeviceManager.java
    This class will help us track whether a device is connected, store the name of that device, and handle connection-related status.

  2. 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:

  1. Track the current device name.

  2. Check whether a device is connected or not.

  3. 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.

  1. getInstance()
    Returns the one and ony instance of the manager.

  2. setDeviceName(name)
    Stores the bluetooth device name (e.g. “Exampl-01-BLE”).

  3. getDeviceName()
    Returns the last saved device name.

  4. setDeviceAddress(address)
    Stores the MAC address (e.g. “C4:7F:51:2B:9A:01”).

  5. getDeviceAddress()
    Returns the last saved address - can be used for reconnecting.

  6. setConnected(true/false)
    Updates whether a device is currently connected.

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

  1. 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.

  2. 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.

  3. getDeviceName(address)
    Returns the name of the device with the given address, or “Unknown” if not found.

  4. getDeviceStatus(address)
    Returns the status of the device (like “Conneted” or “Disconnected”0 or “Disconnected” if no status is found.

  5. setDeviceStatus(address,newStatus)
    Updates the status string of an existing device in the list.

  6. getDeviceCharFlag(address)
    Returns true or false based on whether a device was marked as having the required BLE characteristic.

  7. removeDevice(address)
    Removes a specific device entry based on irs MAC address.

  8. clearDevices()
    Clears the entire saved list of devices form storage.

  9. 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);
        }
    }
  
Previous
Previous

Connecting Layout with Java (MainActivity.java)

Next
Next

BLEService: Powering Bluetooth Communication