Skip to content
This repository has been archived by the owner on Nov 6, 2023. It is now read-only.

Feat: battery info #42

Merged
merged 9 commits into from
Oct 22, 2021
35 changes: 27 additions & 8 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const UiExtension = Me.imports.ui;
const Bluetooth = Me.imports.bluetooth;
const Utils = Me.imports.utils;
const Settings = Me.imports.settings.Settings;
const BatteryProvider = Me.imports.power.UPowerBatteryProvider;


class BluetoothQuickConnect {
Expand All @@ -32,7 +33,8 @@ class BluetoothQuickConnect {
this._menu = bluetooth._item.menu;
this._proxy = bluetooth._proxy;
this._controller = new Bluetooth.BluetoothController();
this._settings = settings
this._settings = settings;
this._battery_provider = new BatteryProvider(this._logger);

this._items = {};
}
Expand All @@ -50,7 +52,7 @@ class BluetoothQuickConnect {
this._connectSignal(this._menu, 'open-state-changed', (menu, isOpen) => {
this._logger.info(`Menu toggled: ${isOpen}`);
if (isOpen)
this._disconnectIdleMonitor()
this._disconnectIdleMonitor();
else
this._connectIdleMonitor();

Expand Down Expand Up @@ -82,40 +84,53 @@ class BluetoothQuickConnect {

this._connectSignal(this._controller, 'device-inserted', (ctrl, device) => {
this._logger.info(`Device inserted event: ${device.name}`);
this._addMenuItem(device);
if (device.isPaired) {
this._addMenuItem(device);
} else {
this._logger.info(`Device ${device.name} not paired, ignoring`);
}
});

this._connectSignal(this._controller, 'device-changed', (ctrl, device) => {
this._logger.info(`Device changed event: ${device.name}`);
if (device.isDefault)
this._refresh();
else
else if (device.isPaired)
this._syncMenuItem(device);
else
this._logger.info(`Skipping change event for unpaired device ${device.name}`);
});

this._connectSignal(this._controller, 'device-deleted', () => {
this._logger.info(`Device deleted event`);
this._refresh();
});

this._connectSignal(Main.sessionMode, 'updated', () => {
this._refresh()
this._refresh();
});
}

_syncMenuItem(device) {
this._logger.info(`Synchronizing device menu item: ${device.name}`);
let item = this._items[device.mac] || this._addMenuItem(device);

item.sync(device);
}

_addMenuItem(device) {
this._logger.info(`Adding device menu item: ${device.name}`);
this._logger.info(`Adding device menu item: ${device.name} ${device.mac}`);

let menuItem = new UiExtension.PopupBluetoothDeviceMenuItem(
device,
this._battery_provider,
this._logger,
{
showRefreshButton: this._settings.isShowRefreshButtonEnabled(),
closeMenuOnAction: !this._settings.isKeepMenuOnToggleEnabled()
}
);

this._items[device.mac] = menuItem;
this._menu.addMenuItem(menuItem, 1);

Expand Down Expand Up @@ -161,12 +176,17 @@ class BluetoothQuickConnect {

_addDevicesToMenu() {
this._controller.getDevices().forEach((device) => {
this._addMenuItem(device);
if (device.isPaired) {
let item = this._addMenuItem(device);
} else {
this._logger.info(`skipping adding device ${device.name}`);
}
});
}

_removeDevicesFromMenu() {
Object.values(this._items).forEach((item) => {
item.disconnectSignals();
item.destroy();
});

Expand All @@ -184,7 +204,6 @@ class BluetoothQuickConnect {

Utils.addSignalsHelperMethods(BluetoothQuickConnect.prototype);


let bluetoothQuickConnect = null;

function init() {}
Expand Down
37 changes: 37 additions & 0 deletions power.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const ExtensionUtils = imports.misc.extensionUtils;
const UPower = imports.gi.UPowerGlib;
const Me = ExtensionUtils.getCurrentExtension();
const Utils = Me.imports.utils;

class UPowerBatteryProvider {
constructor(logger) {
this._upower_client = UPower.Client.new();
this._logger = logger;
}

locateBatteryDevice(device) {
// upower has no field in the devices that indicate that a battery is
// from a bluetooth device, so we must try and find by the provided mac.
// Problem is, the native_path field has macs in all kinds of forms ...
let _mac_addrs = [
device.mac.toUpperCase(),
device.mac.replace(/:/g, "_").toUpperCase(),
];

let _battery_devices = this._upower_client.get_devices();
let _bateries = _battery_devices.filter(bat => {
let _native_path = bat.native_path.toUpperCase();
return _mac_addrs.some(mac => _native_path.includes(mac));
});

if (_bateries.length > 1) {
this._logger.warn(`device ${device.name} matched more than one UPower device by native_path`);
_bateries = [];
}

let _battery_native_path = _bateries.map(bat => bat.native_path)[0] || "NOT-FOUND";
this._logger.info(`battery: ${_battery_native_path}`);

return _bateries;
}
}
136 changes: 127 additions & 9 deletions ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,24 @@
const Clutter = imports.gi.Clutter;
const GObject = imports.gi.GObject;
const St = imports.gi.St;
const GLib = imports.gi.GLib;
const PopupMenu = imports.ui.popupMenu;
const Config = imports.misc.config;

var PopupBluetoothDeviceMenuItem = GObject.registerClass(
class PopupSwitchWithButtonMenuItem extends PopupMenu.PopupSwitchMenuItem {
_init(device, params) {
_init(device, batteryProvider, logger, params) {
let label = device.name || '(unknown)';
super._init(label, device.isConnected, {});

this._logger = logger;

this._device = device;
this._optBatDevice = [];
this._batteryProvider = batteryProvider;
this._batteryDeviceChangeSignal = null;
this._batteryDeviceLocateTimeout = null;

this._showRefreshButton = params.showRefreshButton;
this._closeMenuOnAction = params.closeMenuOnAction;

Expand All @@ -43,27 +51,100 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
this.add(this._statusBin, { expand: false });
}

this._batteryInfo = new BatteryInfoWidget({visible: false});
this.insert_child_at_index(this._batteryInfo, this.get_n_children() - 1);

this.insert_child_at_index(this._refreshButton, this.get_n_children() - 1);
this.add_child(this._pendingLabel);

this.sync(device);
}

_tryLocateBatteryWithTimeout(count = 10) {

let device = this._device;

this._logger.info(`looking up battery info for ${device.name}`);

this._batteryDeviceLocateTimeout = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
1000,
() => {
this._logger.info(`Looking up battery info for ${device.name}`);
let optBat = this._batteryProvider.locateBatteryDevice(device);

if (optBat.length) {
this._batteryFound(optBat);
this._batteryDeviceLocateTimeout = null;
} else if (count) {
// try again
this._tryLocateBatteryWithTimeout(count - 1);
} else {
this._logger.info(`Did not find a battery for device ${device.name}`);
this._batteryDeviceLocateTimeout = null;
}
});
}

_batteryFound(optBatDevice) {
this._optBatDevice = optBatDevice;

for (const bat of this._optBatDevice) {
this._batteryInfo.show();
this._batteryInfo.setPercentage(bat.percentage);

this._batteryDeviceChangeSignal = bat.connect("notify", (_dev, pspec) => {
if (pspec.name == 'percentage') {
this._logger.info(`${_dev.native_path} notified ${pspec.name}, percentage is ${_dev.percentage}`);
this._batteryInfo.setPercentage(bat.percentage);
}
});
}
}

disconnectSignals() {
this._optBatDevice.map(bat => bat.disconnect(this._batteryDeviceChangeSignal));
this._batteryDeviceChangeSignal = null;

if (this._batteryDeviceLocateTimeout != null) {
GLib.Source.remove(this._batteryDeviceLocateTimeout);
this._batteryDeviceLocateTimeout = null;
}
}

sync(device) {
this.disconnectSignals();

this._optBatDevice = [];
this._batteryInfo.visible = false;

this._device = device;

this._syncSwitch(device);
this.visible = device.isPaired;
if (this._showRefreshButton && device.isConnected)
this._refreshButton.show();
else
this._refreshButton.hide();

this._updateDeviceInfo();

if (device.isConnected)
this._tryLocateBatteryWithTimeout();

}

_syncSwitch(device) {
if (this._isOldGnome())
return this._switch.setToggleState(device.isConnected);
if (this._isOldGnome()) {
this._switch.setToggleState(device.isConnected);
} else {
this._switch.state = device.isConnected;
}
}

this._switch.state = device.isConnected;
_updateDeviceInfo() {
this._logger.info(`updating label for ${this._device.name} ${this._optBatDevice.map(bat => bat.percentage)}`);
this.label.text = this._device.name || "unknown";;
}

_buildRefreshButton() {
Expand Down Expand Up @@ -99,7 +180,8 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
button.connect('clicked', () => {
this._enablePending();
this._device.reconnect(() => {
this._disablePending()
this._disablePending();
this._updateDeviceInfo();
});

if (this._closeMenuOnAction)
Expand All @@ -118,14 +200,16 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(

_connectToggledEvent() {
this.connect('toggled', (item, state) => {
if (state)
if (state) {
this._device.connect(() => {
this._disablePending()
this._disablePending();
this._updateDeviceInfo();
});
else
} else {
this._device.disconnect(() => {
this._disablePending()
this._disablePending();
});
}
});
}

Expand Down Expand Up @@ -169,3 +253,37 @@ var PopupBluetoothDeviceMenuItem = GObject.registerClass(
}
}
);

var BatteryInfoWidget = GObject.registerClass(
class BatteryInfoWidget extends St.BoxLayout {
_init(params) {
super._init(params);
this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
this.add_child(this._icon);
this._icon.icon_name = null;

// dirty trick: instantiate the label with text 100%, so we can set
// the natural width of the label in case monospace has no effect
this._label = new St.Label({ y_expand: false, style_class: 'monospace', text: '100%' });
this._label.natural_width = this._label.width;
this._label.text = "";

this._label.x_expand = false;
this._label.x_align = Clutter.ActorAlign.LEFT;
this.add_child(this._label);
}

setPercentage(value) {
if (value == null) {
this._label.text = '';
this._icon.icon_name = 'battery-missing-symbolic';
} else {
this._label.text = '%d%%'.format(value);

let fillLevel = 10 * Math.floor(value / 10);
let iconName = 'battery-level-%d-symbolic'.format(fillLevel);
this._icon.icon_name = iconName;
}
}
}
);
4 changes: 4 additions & 0 deletions utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ var Logger = class Logger {

log(`[bluetooth-quick-connect] ${message}`);
}

warn(message) {
log(`[bluetooth-quick-connect WARNING] ${message}`);
}
};

function addSignalsHelperMethods(prototype) {
Expand Down