Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a new Bluetooth indicator #459

Merged
merged 81 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
ba0d60b
Implement the private portion of the Bluetooth API
EbonJaeger Sep 26, 2022
29f9614
Add the public-facing Bluetooth API
EbonJaeger Jan 17, 2023
891dea9
Fix last TODO item
EbonJaeger Jan 17, 2023
820d716
Fix warnings
EbonJaeger Jan 17, 2023
5f7d4d1
WIP: Redo Bluetooth indicator and popover
EbonJaeger Jan 17, 2023
672501a
Fix Bluetooth setup
EbonJaeger Jan 18, 2023
c4f9d82
Greatly simplify the BluetoothClient API
EbonJaeger Jan 18, 2023
588dbab
Follow Vala naming conventions
EbonJaeger Jan 19, 2023
72b4082
Add new Bluetooth popover layout
EbonJaeger Jan 20, 2023
ae5f353
Add widgets for Bluetooth devices
EbonJaeger Jan 21, 2023
1d0a8fe
Pack popover header like the new network popover header
EbonJaeger Jan 21, 2023
a098229
Remove device wrapper class
EbonJaeger Jan 21, 2023
64ec428
Make sure connection button is clickable if the device is now paired
EbonJaeger Jan 21, 2023
91fe9cd
Implement pairing and forgetting Bluetooth devices
EbonJaeger Jan 22, 2023
669b4b8
Set a filter for Bluetooth discovery so we only get actual discoverab…
EbonJaeger Jan 22, 2023
f2d74e6
Start/stop discovery on all adapters instead of the first one found
EbonJaeger Jan 22, 2023
fb3da60
Ensure that the correct height is given to the listbox
EbonJaeger Jan 22, 2023
f71bace
Remove the Meson option for Bluetooth
EbonJaeger Jan 22, 2023
b3322ea
Use better wording for pairing button
EbonJaeger Jan 22, 2023
9427cfb
Reset revealer state when DBus operations are successful
EbonJaeger Jan 24, 2023
e314ff1
Always set adapters to be discovering
EbonJaeger Jan 25, 2023
ab12231
Forget about Bluetooth pairing; leave it to the Settings
EbonJaeger Feb 7, 2023
4501673
Remove forget device button because dialogs from the panel are Bad TM
EbonJaeger Feb 7, 2023
f5fcd91
Make Bluetooth row widget subclass ListBoxRow
EbonJaeger Feb 7, 2023
7f107a8
Add slightly more spacing around separators
EbonJaeger Feb 7, 2023
b3a0704
Minor style enhancements
EbonJaeger Feb 7, 2023
df858fa
Filter out unpaired Bluetooth devices
EbonJaeger Feb 7, 2023
fa24672
Rename row widget to be shorter and add consistent style classes
EbonJaeger Feb 7, 2023
c48641b
Add an expander indicator to Bluetooth device rows
EbonJaeger Feb 8, 2023
7d01de3
Show connected devices, paired or not
EbonJaeger Feb 8, 2023
c6dfedc
Implement power display for Bluetooth devices
EbonJaeger Feb 9, 2023
6b3af29
Use correct icon name for generic bluetooth items
EbonJaeger Feb 10, 2023
6c6f1fb
Add styling to Bluetooth applet
EbonJaeger Feb 15, 2023
d79ac2d
Redesign of Bluetooth device rows
EbonJaeger Feb 18, 2023
90911d9
Update the name of a Bluetooth device if it changes
EbonJaeger Feb 18, 2023
b63b435
Add a placeholder widget if there are no Bluetooth devices
EbonJaeger Feb 19, 2023
408168e
Make setting a new UPower device more robust
EbonJaeger Feb 19, 2023
9ea5e6c
Updating the battery state when the UPower device is null closes the …
EbonJaeger Feb 19, 2023
07cd3af
Don't rely on theme to pad revealer
EbonJaeger Feb 19, 2023
9a422a2
Refine dis/connection code flow
EbonJaeger Feb 19, 2023
ac0dded
Show or hide the panel widget based on if a Bluetooth adapter is present
EbonJaeger Feb 21, 2023
2dbadbd
Remove extra separator in Bluetooth popover
EbonJaeger Feb 21, 2023
869564d
Format style change
EbonJaeger Feb 21, 2023
cb4bb9a
Ensure that state hinging on connection status is always updated
EbonJaeger Feb 22, 2023
467ec47
Make things less embiggened
EbonJaeger Feb 22, 2023
e821bb9
Refine battery display for new device row layout
EbonJaeger Feb 27, 2023
22e0097
Update the tray icon when Bluetooth is enabled or disabled
EbonJaeger Feb 27, 2023
b1a4ee8
bluetooth-indicator: Invalidate the device filter whenever we invalid…
EbonJaeger Mar 7, 2023
c9ab2a7
bluetooth-indicator: Final (I hope) design edit to the device row layout
EbonJaeger Mar 7, 2023
8bfae77
bluetooth-indicator: Use an icon button for the disconnect button
EbonJaeger Mar 11, 2023
b1baa1a
[WIP] bluetooth-indicator: Implement support for file sending
EbonJaeger Mar 14, 2023
f249729
bluetooth-indicator: Use rfkill to enable/disable Bluetooth
EbonJaeger Mar 15, 2023
b175a06
bluetooth-indicator: Use label attributes to achieve consistent styli…
EbonJaeger Mar 16, 2023
4b08e87
build: Reintroduce option to compile without Bluetooth
EbonJaeger Mar 16, 2023
f92036e
WIP - sendto implementation
EbonJaeger May 13, 2023
e58bd4b
[WIP] bluetooth: Implement bluetooth sendto functionality
EbonJaeger Aug 12, 2023
3a20022
sendto: Fix command line arguments
EbonJaeger Aug 14, 2023
28819c0
sendto: Better description for files arg
EbonJaeger Aug 14, 2023
6d71f89
sendto: Remember to remove comment
EbonJaeger Aug 14, 2023
92de442
sendto: Properly exit if the file picker or scan dialog is closed
EbonJaeger Aug 14, 2023
9f73a8d
bluetooth-indicator: Hook up file send button in the header
EbonJaeger Aug 14, 2023
2788220
sendto: Implement file receiving, which manages to break everything
EbonJaeger Aug 19, 2023
1f51606
bluetooth/sendto: Fix issues with no transfer dialog windows being shown
EbonJaeger Aug 19, 2023
c65652d
sendto: Correct attribution in file headers
EbonJaeger Aug 19, 2023
181545e
sendto: Add spacing in dialogs
EbonJaeger Aug 24, 2023
0aef339
sendto: Cancel the transfer on cancel/reject
EbonJaeger Aug 24, 2023
8f0a34a
bluetooth-indicator: Use symbolic icons for the tray item
EbonJaeger Aug 25, 2023
419cae4
sendto: Filter devices in the device chooser by connected state
EbonJaeger Aug 26, 2023
54cddb8
sendto: Let's actually use the grid we're creating for the header
EbonJaeger Aug 26, 2023
7c40c6e
sendto: Somehow these files were using spaces instead of tabs
EbonJaeger Aug 26, 2023
b9a67b5
sendto: Add titles and margins to send and receive dialogs
EbonJaeger Aug 26, 2023
d022e6b
sendto: Cleanup
EbonJaeger Sep 14, 2023
c3a682d
sendto: Use the correct property when looking for changes
EbonJaeger Sep 15, 2023
05e95d2
sendto: Refactor format_time function
EbonJaeger Jan 10, 2024
1a55093
Apply suggestions from code review
EbonJaeger Jan 10, 2024
bac7e64
sendto: Add new base dialog class
EbonJaeger Jan 12, 2024
81a2bfc
bluetooth-indicator: Address feedback from fossfreedom
EbonJaeger Jan 22, 2024
32cd3ab
sendto, bluetooth-indicator: Support sending files to a specific device
EbonJaeger Jan 22, 2024
4a16257
bluetooth-indicator: Fix button relief and device class checks
EbonJaeger Jan 23, 2024
1a46306
bluetooth-indicator: Hide the device's battery status when disconnected
EbonJaeger Jan 23, 2024
891839e
bluetooth-indicator: Remove debugging message
EbonJaeger Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading