Skip to content

Commit

Permalink
Implement a new Bluetooth indicator (#459)
Browse files Browse the repository at this point in the history
* Implement the private portion of the Bluetooth API

(I hope... The organization is a mess.)

Signed-off-by: Evan Maddock <[email protected]>

* Add the public-facing Bluetooth API

Signed-off-by: Evan Maddock <[email protected]>

* Fix last TODO item

Signed-off-by: Evan Maddock <[email protected]>

* Fix warnings

Signed-off-by: Evan Maddock <[email protected]>

* WIP: Redo Bluetooth indicator and popover

Signed-off-by: Evan Maddock <[email protected]>

* Fix Bluetooth setup

Signed-off-by: Evan Maddock <[email protected]>

* Greatly simplify the BluetoothClient API

At this time, I have no idea how to integrate the UPower aspect with the Bluetooth devices. Moreover, I can't test if it's working even if I did, because none of my Bluetooth devices trigger the UPower signals.

Signed-off-by: Evan Maddock <[email protected]>

* Follow Vala naming conventions

It'll automagically figure it out for DBus.

Signed-off-by: Evan Maddock <[email protected]>

* Add new Bluetooth popover layout

Signed-off-by: Evan Maddock <[email protected]>

* Add widgets for Bluetooth devices

Signed-off-by: Evan Maddock <[email protected]>

* Pack popover header like the new network popover header

Signed-off-by: Evan Maddock <[email protected]>

* Remove device wrapper class

Signed-off-by: Evan Maddock <[email protected]>

* Make sure connection button is clickable if the device is now paired

Signed-off-by: Evan Maddock <[email protected]>

* Implement pairing and forgetting Bluetooth devices

Signed-off-by: Evan Maddock <[email protected]>

* Set a filter for Bluetooth discovery so we only get actual discoverable devices

Signed-off-by: Evan Maddock <[email protected]>

* Start/stop discovery on all adapters instead of the first one found

Signed-off-by: Evan Maddock <[email protected]>

* Ensure that the correct height is given to the listbox

Signed-off-by: Evan Maddock <[email protected]>

* Remove the Meson option for Bluetooth

The reason for the option's addition no longer applies; the new Bluetooth indicator uses Bluez directly instead of gnome-bluetooth.

Signed-off-by: Evan Maddock <[email protected]>

* Use better wording for pairing button

Signed-off-by: Evan Maddock <[email protected]>

* Reset revealer state when DBus operations are successful

Signed-off-by: Evan Maddock <[email protected]>

* Always set adapters to be discovering

While not ideal, I suspect this is the only way to reliably pair devices.

Signed-off-by: Evan Maddock <[email protected]>

* Forget about Bluetooth pairing; leave it to the Settings

Signed-off-by: Evan Maddock <[email protected]>

* Remove forget device button because dialogs from the panel are Bad TM

Signed-off-by: Evan Maddock <[email protected]>

* Make Bluetooth row widget subclass ListBoxRow

Signed-off-by: Evan Maddock <[email protected]>

* Add slightly more spacing around separators

Signed-off-by: Evan Maddock <[email protected]>

* Minor style enhancements

Signed-off-by: Evan Maddock <[email protected]>

* Filter out unpaired Bluetooth devices

Signed-off-by: Evan Maddock <[email protected]>

* Rename row widget to be shorter and add consistent style classes

Signed-off-by: Evan Maddock <[email protected]>

* Add an expander indicator to Bluetooth device rows

Signed-off-by: Evan Maddock <[email protected]>

* Show connected devices, paired or not

Signed-off-by: Evan Maddock <[email protected]>

* Implement power display for Bluetooth devices

Signed-off-by: Evan Maddock <[email protected]>

* Use correct icon name for generic bluetooth items

At least so far as there is a correct icon...

Signed-off-by: Evan Maddock <[email protected]>

* Add styling to Bluetooth applet

Signed-off-by: Evan Maddock <[email protected]>

* Redesign of Bluetooth device rows

Signed-off-by: Evan Maddock <[email protected]>

* Update the name of a Bluetooth device if it changes

Signed-off-by: Evan Maddock <[email protected]>

* Add a placeholder widget if there are no Bluetooth devices

Signed-off-by: Evan Maddock <[email protected]>

* Make setting a new UPower device more robust

Signed-off-by: Evan Maddock <[email protected]>

* Updating the battery state when the UPower device is null closes the revealer

We want to do this, so put it above the null check.

Signed-off-by: Evan Maddock <[email protected]>

* Don't rely on theme to pad revealer

Signed-off-by: Evan Maddock <[email protected]>

* Refine dis/connection code flow

Signed-off-by: Evan Maddock <[email protected]>

* Show or hide the panel widget based on if a Bluetooth adapter is present

Signed-off-by: Evan Maddock <[email protected]>

* Remove extra separator in Bluetooth popover

Signed-off-by: Evan Maddock <[email protected]>

* Format style change

Signed-off-by: Evan Maddock <[email protected]>

* Ensure that state hinging on connection status is always updated

Signed-off-by: Evan Maddock <[email protected]>

* Make things less embiggened

Signed-off-by: Evan Maddock <[email protected]>

* Refine battery display for new device row layout

Signed-off-by: Evan Maddock <[email protected]>

* Update the tray icon when Bluetooth is enabled or disabled

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Invalidate the device filter whenever we invalidate the sorting

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Final (I hope) design edit to the device row layout

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Use an icon button for the disconnect button

Also fixes some spacing issues when not using built-in theme.

Signed-off-by: Evan Maddock <[email protected]>

* [WIP] bluetooth-indicator: Implement support for file sending

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Use rfkill to enable/disable Bluetooth

Yes, the DBus interface for it is provided by Gnome Settings Daemon, but that's always going to be present anyways and this makes life way simpler.

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Use label attributes to achieve consistent styling across all themes

Signed-off-by: Evan Maddock <[email protected]>

* build: Reintroduce option to compile without Bluetooth

Signed-off-by: Evan Maddock <[email protected]>

* WIP - sendto implementation

Signed-off-by: Evan Maddock <[email protected]>

* [WIP] bluetooth: Implement bluetooth sendto functionality

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Fix command line arguments

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Better description for files arg

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Remember to remove comment

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Properly exit if the file picker or scan dialog is closed

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Hook up file send button in the header

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Implement file receiving, which manages to break everything

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth/sendto: Fix issues with no transfer dialog windows being shown

Also fixes saving received files to the Downloads folder.

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Correct attribution in file headers

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Add spacing in dialogs

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Cancel the transfer on cancel/reject

This means that the sending device will no longer still send the file, even if the transfer was rejected.

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Use symbolic icons for the tray item

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Filter devices in the device chooser by connected state

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Let's actually use the grid we're creating for the header

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Somehow these files were using spaces instead of tabs

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Add titles and margins to send and receive dialogs

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Cleanup

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Use the correct property when looking for changes

Signed-off-by: Evan Maddock <[email protected]>

* sendto: Refactor format_time function

Signed-off-by: Evan Maddock <[email protected]>

* Apply suggestions from code review

Co-authored-by: Joshua Strobl <[email protected]>

* sendto: Add new base dialog class

Both the send and receive dialogs are almost the same, the main difference being in the background implementation and wording of strings. This causes some duplicated logic, especially around the formatting of times remaining.

This adds a new base class that both dialogs can derive from while still having their own implementations for doing the work, leading to less duplicated code.

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Address feedback from fossfreedom

Signed-off-by: Evan Maddock <[email protected]>

* sendto, bluetooth-indicator: Support sending files to a specific device

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Fix button relief and device class checks

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Hide the device's battery status when disconnected

Signed-off-by: Evan Maddock <[email protected]>

* bluetooth-indicator: Remove debugging message

Signed-off-by: Evan Maddock <[email protected]>

---------

Signed-off-by: Evan Maddock <[email protected]>
Co-authored-by: Joshua Strobl <[email protected]>
  • Loading branch information
EbonJaeger and JoshStrobl authored Jan 27, 2024
1 parent 39e9f08 commit 37d4f28
Show file tree
Hide file tree
Showing 31 changed files with 3,250 additions and 374 deletions.
13 changes: 8 additions & 5 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,6 @@ if prefix == '/usr' or prefix == '/usr/local'
cdata.set_quoted('RAVEN_PLUGIN_DATADIR_SECONDARY', join_paths(secondary_datadir_root, 'raven-plugins'))
endif

with_bluetooth = get_option('with-bluetooth')
if with_bluetooth == true
add_project_arguments('-D', 'with_bluetooth', language: 'vala')
endif

with_hibernate = get_option('with-hibernate')
if with_hibernate == true
add_project_arguments('-D', 'WITH_HIBERNATE', language: 'vala')
Expand All @@ -161,6 +156,13 @@ if xdg_appdir == ''
endif
endif

# Bluetooth option. BSD systems have no Bluetooth stack, so this allows
# BSD systems to compile and run Budgie.
with_bluetooth = get_option('with-bluetooth')
if with_bluetooth == true
add_project_arguments('-D', 'WITH_BLUETOOTH', language: 'vala')
endif

# GVC rpath. it's evil, but gvc will bomb out glib2 due to static linking weirdness now,
# so we have to use a shared library to prevent multiple registration of the same types..
rpath_libdir = join_paths(libdir, meson.project_name())
Expand Down Expand Up @@ -221,6 +223,7 @@ report = [
'',
' gtk-doc: @0@'.format(with_gtk_doc),
' stateless: @0@'.format(with_stateless),
' bluetooth: @0@'.format(with_bluetooth),
]


Expand Down
4 changes: 4 additions & 0 deletions src/dialogs/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ if with_polkit == true
subdir('polkit')
endif

if with_bluetooth == true
subdir('sendto')
endif

subdir('power')
subdir('run')
354 changes: 354 additions & 0 deletions src/dialogs/sendto/Application.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
/*
* This file is part of budgie-desktop
*
* Copyright Budgie Desktop Developers, elementary LLC
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*/

public class SendtoApplication : Gtk.Application {
private const OptionEntry[] OPTIONS = {
{ "daemon", 'd', 0, OptionArg.NONE, out silent, "Run the application in the background", null },
{ "send", 'f', 0, OptionArg.NONE, out send, "Send a file via Bluetooth", null },
{ "device", 'a', 0, OptionArg.STRING, out device_addr, "Bluetooth device to send files to", null },
{ "", 0, 0, OptionArg.STRING_ARRAY, out arg_files, "Files to send via Bluetooth", null },
{ null },
};

private static bool silent = true;
private static bool send = false;
private static bool active_once;
private static string? device_addr = null;
[CCode (array_length = false, array_null_terminated = true)]
private static string[]? arg_files = {};

private Bluetooth.ObjectManager manager;
private Bluetooth.Obex.Agent agent;
private Bluetooth.Obex.Transfer transfer;

private FileReceiver file_receiver;
private FileSender file_sender;
private List<FileReceiver> file_receivers;
private List<FileSender> file_senders;
private ScanDialog scan_dialog;

construct {
application_id = "org.buddiesofbudgie.Sendto";
flags |= ApplicationFlags.HANDLES_COMMAND_LINE;
}

public override int command_line(ApplicationCommandLine command) {
var args = command.get_arguments();
var context = new OptionContext(null);
context.add_main_entries(OPTIONS, null);

// Try to parse the command args
try {
context.parse_strv(ref args);
} catch (Error e) {
warning("Unable to parse command args: %s", e.message);
return 1;
}

activate();

// Exit early if no files to send
if (!send) return 0;

File[] files = {};
foreach (unowned var arg_file in arg_files) {
var file = command.create_file_for_arg(arg_file);

if (file.query_exists()) {
files += file;
} else {
warning("File not found: %s", file.get_path());
}
}

// If we weren't given any files, open a file picker dialog
if (files.length == 0) {
var picker = new Gtk.FileChooserDialog(
_("Files to send"),
null,
Gtk.FileChooserAction.OPEN,
_("_Cancel"), Gtk.ResponseType.CANCEL,
_("_Open"), Gtk.ResponseType.ACCEPT
) {
select_multiple = true,
};

if (picker.run() != Gtk.ResponseType.ACCEPT) {
picker.destroy();
return 0;
}

var picked_files = picker.get_files();
picked_files.foreach((file) => {
files += file;
});

picker.destroy();
}

// Still no files, exit
if (files.length == 0) return 0;

// Start and show the device scanner if we weren't given a
// Bluetooth device address
if (device_addr == null || device_addr == "") {
// Make sure we have a dialog object
if (scan_dialog == null) {
scan_dialog = new ScanDialog(this, manager);

// Wait for asyncronous initialization before showing the dialog
Idle.add(() => {
scan_dialog.show_all();
return Source.REMOVE;
});
} else {
// Dialog already exists, present it
scan_dialog.present();
}

// Clear our pointer when the scan dialog is destroyed
scan_dialog.destroy.connect(() => {
scan_dialog = null;
});

// Send the files when a device has been selected
scan_dialog.send_file.connect((device) => {
device_addr = device.address;
});
}

var device = manager.get_device(device_addr);

// Send the file to the device
if (!insert_sender(files, device)) {
file_sender = new FileSender(this);
file_sender.add_files(files, device);
file_senders.append(file_sender);
file_sender.show_all();
file_sender.destroy.connect(() => {
file_senders.remove_link(file_senders.find(file_sender));
});
}

// Cleanup
arg_files = {};
send = false;

return 0;
}

protected override void activate() {
if (silent) {
if (active_once) {
release(); // Allow normal exit if `activate()` has already been called once
}
hold(); // Prevent normal application exit if silent
silent = false;
}

if (manager != null) return;

file_receivers = new List<FileReceiver>();
file_senders = new List<FileSender>();

manager = new Bluetooth.ObjectManager();
manager.notify["has-object"].connect(() => {
var build_path = Path.build_filename(Environment.get_home_dir(), ".local", "share", "contractor");
var file = File.new_for_path(
Path.build_filename(
build_path,
Environment.get_application_name() + ".contract"
)
);
var file_exists = file.query_exists();

// Create the parent directory for the contract file if it doesn't exist
if (!File.new_for_path(build_path).query_exists()) {
DirUtils.create(build_path, 0700);
}

// If we have Bluetooth devices, create our Obex Agent and contract file
if (manager.has_object) {
// Create our Obex Agent if we haven't been activated yet
if (!active_once) {
agent = new Bluetooth.Obex.Agent();
agent.transfer_view.connect(dialog_active);
agent.response_accepted.connect(response_accepted);
agent.response_canceled.connect(response_canceled);
agent.response_notify.connect(response_notify);
active_once = true;
}

// Create and write to our Obex contract file if it doesn't exist
if (!file_exists) {
var keyfile = new KeyFile();
keyfile.set_string("Contractor Entry", "Name", _("Send Files via Bluetooth"));
keyfile.set_string("Contractor Entry", "Icon", "bluetooth-active");
keyfile.set_string("Contractor Entry", "Description", _("Send files to device…"));
keyfile.set_string("Contractor Entry", "Exec", "org.buddiesofbudgie.sendto -f %F");
keyfile.set_string("Contractor Entry", "MimeType", "!inode;");

try {
keyfile.save_to_file(file.get_path());
} catch (Error e) {
critical("Error saving contract file: %s", e.message);
}
}
} else {
// Delete the contract file if it exists
if (file_exists) {
try {
file.delete();
} catch (Error e) {
critical("Error deleting old contract file: %s", e.message);
}
}
}
});
}

private void dialog_active(string session_path) {
// Show any file receiver dialogs if there is a transfer session for the
// given path
file_receivers.foreach((receiver) => {
if (receiver.transfer.session == session_path) {
receiver.show_all();
}
});

// Show any file sender dialogs if there is a transfer session for the
// given path
file_senders.foreach((sender) => {
if (sender.transfer.session == session_path) {
sender.show_all();
}
});
}

private bool insert_sender(File[] files, Bluetooth.Device device) {
bool exists = false;

// Pass the files to send to the correct sender
file_senders.foreach((sender) => {
if (sender.device == device) {
sender.add_files(files, device);
sender.present();
exists = true;
}
});

return exists;
}

private void response_accepted(string address, ObjectPath path) {
try {
transfer = Bus.get_proxy_sync<Bluetooth.Obex.Transfer>(BusType.SESSION, "org.bluez.obex", path);
} catch (Error e) {
warning("Error getting transfer proxy: %s", e.message);
}

if (transfer.name == null) return;

file_receiver = new FileReceiver(this);
file_receivers.append(file_receiver);

file_receiver.destroy.connect(() => {
file_receivers.remove_link(file_receivers.find(file_receiver));
});

Bluetooth.Device device = manager.get_device(address);
file_receiver.set_transfer(device, path);
}

private void response_canceled(ObjectPath? path = null) {
try {
Bluetooth.Obex.Transfer? transfer = null;

if (path == null) {
var last_receiver = file_receivers.first().data as FileReceiver;
transfer = last_receiver.transfer;
} else {
transfer = Bus.get_proxy_sync<Bluetooth.Obex.Transfer>(BusType.SESSION, "org.bluez.obex", path);
}

transfer.cancel();
} catch (Error e) {
warning("Error cancelling file transfer: %s", e.message);
}
}

private void response_notify(string address, ObjectPath object_path) {
Bluetooth.Device device = manager.get_device(address);

try {
transfer = Bus.get_proxy_sync<Bluetooth.Obex.Transfer>(BusType.SESSION, "org.bluez.obex", object_path);
} catch (Error e) {
warning("Error getting transfer proxy: %s", e.message);
}

var notification = new Notification("Bluetooth");
notification.set_icon(new ThemedIcon(device.icon));

if (reject_if_exists(transfer.name, transfer.size)) {
notification.set_title(_("Rejected file"));
notification.set_body(_("File already exists: %s").printf(transfer.name));
send_notification("org.buddiesofbudgie.bluetooth", notification);
Idle.add(() => {
activate_action("btcancel", new Variant.string("Cancel"));
return Source.REMOVE;
});

return;
}

// Create a notification prompting the user what to do
notification.set_priority(NotificationPriority.URGENT);
notification.set_title(_("Receiving file"));
notification.set_body(_("Device '%s' wants to send a file: %s %s").printf(device.alias, transfer.name, format_size(transfer.size)));
notification.add_button(
_("Accept"),
Action.print_detailed_name("app.btaccept", new Variant.string("Accept"))
);
notification.add_button(
_("Reject"),
Action.print_detailed_name("app.btcancel", new Variant.string("Cancel"))
);

send_notification("org.buddiesofbudgie.bluetooth", notification);
}

private bool reject_if_exists(string name, uint64 size) {
var input_path = Path.build_filename(Environment.get_user_special_dir(UserDirectory.DOWNLOAD), name);
var input_file = File.new_for_path(input_path);
uint64 file_size = 0;

if (input_file.query_exists()) {
try {
var file_info = input_file.query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE, null);
file_size = file_info.get_size();
} catch (Error e) {
warning("Error getting file size: %s", e.message);
}
}

return size == file_size && input_file.query_exists();
}
}

public static int main(string[] args) {
Intl.setlocale(LocaleCategory.ALL, "");
Intl.bindtextdomain(Budgie.GETTEXT_PACKAGE, Budgie.LOCALEDIR);
Intl.bind_textdomain_codeset(Budgie.GETTEXT_PACKAGE, "UTF-8");
Intl.textdomain(Budgie.GETTEXT_PACKAGE);

var app = new SendtoApplication();
return app.run(args);
}
Loading

0 comments on commit 37d4f28

Please sign in to comment.