Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ src/Dialog/BtReceiver.vala
src/Dialog/BtScan.vala
src/Dialog/SenderDialog.vala
src/Dialog/DeviceRow.vala
src/Dialog/Pair.vala
src/Services/Adapter.vala
src/Services/Agent.vala
src/Services/Device.vala
src/Services/Manager.vala
src/Services/ObexAgent.vala
Expand Down
114 changes: 114 additions & 0 deletions src/Dialog/Pair.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2018-2025 elementary, Inc. (https://elementary.io)
*/

public class PairDialog : Granite.MessageDialog {
public enum AuthType {
REQUEST_CONFIRMATION,
REQUEST_AUTHORIZATION,
DISPLAY_PASSKEY,
DISPLAY_PIN_CODE
}

public ObjectPath object_path { get; construct; }
public AuthType auth_type { get; construct; }
public string passkey { get; construct; }
public bool cancelled { get; set; }

// Un-used default constructor
private PairDialog () {
Object (
buttons: Gtk.ButtonsType.CANCEL
);
}

public PairDialog.request_authorization (ObjectPath object_path) {
Object (
auth_type: AuthType.REQUEST_AUTHORIZATION,
buttons: Gtk.ButtonsType.CANCEL,
object_path: object_path,
primary_text: _("Confirm Bluetooth Pairing")
);
}

public PairDialog.display_passkey (ObjectPath object_path, uint32 passkey, uint16 entered) {
Object (
auth_type: AuthType.DISPLAY_PASSKEY,
buttons: Gtk.ButtonsType.CANCEL,
object_path: object_path,
passkey: "%u".printf (passkey),
primary_text: _("Confirm Bluetooth Passkey")
);
}

public PairDialog.request_confirmation (ObjectPath object_path, uint32 passkey) {
Object (
auth_type: AuthType.REQUEST_CONFIRMATION,
buttons: Gtk.ButtonsType.CANCEL,
object_path: object_path,
passkey: "%u".printf (passkey),
primary_text: _("Confirm Bluetooth Passkey")
);
}

public PairDialog.display_pin_code (ObjectPath object_path, string pincode) {
Object (
auth_type: AuthType.DISPLAY_PIN_CODE,
buttons: Gtk.ButtonsType.CANCEL,
object_path: object_path,
passkey: pincode,
primary_text: _("Enter Bluetooth PIN")
);
}

construct {
Bluetooth.Device device;
string device_name = _("Unknown Bluetooth Device");
try {
device = Bus.get_proxy_sync (BusType.SYSTEM, "org.bluez", object_path, DBusProxyFlags.GET_INVALIDATED_PROPERTIES);
image_icon = new ThemedIcon (device.icon ?? "bluetooth");
device_name = device.name ?? device.address;
} catch (IOError e) {
image_icon = new ThemedIcon ("bluetooth");
critical (e.message);
}

switch (auth_type) {
case AuthType.REQUEST_CONFIRMATION:
badge_icon = new ThemedIcon ("dialog-password");
secondary_text = _("Make sure the code displayed on “%s” matches the one below.").printf (device_name);

var confirm_button = add_button (_("Pair"), Gtk.ResponseType.ACCEPT);
confirm_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
break;
case AuthType.DISPLAY_PASSKEY:
badge_icon = new ThemedIcon ("dialog-password");
secondary_text = _("“%s” would like to pair with this device. Make sure the code displayed on “%s” matches the one below.").printf (device_name, device_name);

var confirm_button = add_button (_("Pair"), Gtk.ResponseType.ACCEPT);
confirm_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
break;
case AuthType.DISPLAY_PIN_CODE:
badge_icon = new ThemedIcon ("dialog-password");
secondary_text = _("Type the code displayed below on “%s”, followed by Enter.").printf (device_name);
break;
case AuthType.REQUEST_AUTHORIZATION:
badge_icon = new ThemedIcon ("dialog-question");
secondary_text = _("“%s” would like to pair with this device.").printf (device_name);

var confirm_button = add_button (_("Pair"), Gtk.ResponseType.ACCEPT);
confirm_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
break;
}

if (passkey != null && passkey != "") {
var passkey_label = new Gtk.Label (passkey);
passkey_label.get_style_context ().add_class (Granite.STYLE_CLASS_H1_LABEL);

custom_bin.add (passkey_label);
}

modal = true;
}
}
151 changes: 151 additions & 0 deletions src/Services/Agent.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2018-2025 elementary, Inc. (https://elementary.io)
*
* Authored by: Corentin Noël <corentin@elementary.io>
*/

[DBus (name = "org.bluez.Error")]
public errordomain BluezError {
REJECTED,
CANCELED
}

[DBus (name = "org.bluez.Agent1")]
public class Bluetooth.Agent : Object {
private const string PATH = "/org/bluez/agent/elementary";

private PairDialog pair_dialog;

[DBus (visible=false)]
public Agent () {
Bus.own_name (BusType.SYSTEM, "org.bluez.AgentManager1", BusNameOwnerFlags.NONE,
(connection, name) => {
try {
connection.register_object (PATH, this);
ready = true;
} catch (Error e) {
critical (e.message);
}
},
(connection, name) => {},
(connection, name) => {}
);
}

[DBus (visible=false)]
public bool ready { get; private set; }

[DBus (visible=false)]
public signal void unregistered ();

[DBus (visible=false)]
public GLib.ObjectPath get_path () {
return new GLib.ObjectPath (PATH);
}

public void release () throws Error {
unregistered ();
}

public async string request_pin_code (ObjectPath device) throws Error, BluezError {
throw new BluezError.REJECTED ("Pairing method not supported");
}

// Called to display a pin code on-screen that needs to be entered on the other device. Can return
// instantly
public async void display_pin_code (ObjectPath device, string pincode) throws Error, BluezError {
pair_dialog = new PairDialog.display_pin_code (device, pincode);
pair_dialog.present ();
}

public async uint32 request_passkey (ObjectPath device) throws Error, BluezError {
throw new BluezError.REJECTED ("Pairing method not supported");
}

// Called to display a passkey on-screen that needs to be entered on the other device. Can return
// instantly
public async void display_passkey (ObjectPath device, uint32 passkey, uint16 entered) throws Error {
pair_dialog = new PairDialog.display_passkey (device, passkey, entered);
pair_dialog.present ();
}

// Called to request confirmation from the user that they want to pair with the given device and that
// the passkey matches. **MUST** throw BluezError if pairing is to be rejected. This is handled in
// `check_pairing_response`. If the method returns without an error, pairing is authorized
public async void request_confirmation (ObjectPath device, uint32 passkey) throws Error, BluezError {
pair_dialog = new PairDialog.request_confirmation (device, passkey);
yield check_pairing_response (pair_dialog);
}

// Called to request confirmation from the user that they want to pair with the given device
// **MUST** throw BluezError if pairing is to be rejected. This is handled in `check_pairing_response`
// If the method returns without an error, pairing is authorized
public async void request_authorization (ObjectPath device) throws Error, BluezError {
pair_dialog = new PairDialog.request_authorization (device);
yield check_pairing_response (pair_dialog);
}

// Called to authorize the use of a specific service (Audio/HID/etc), so we restrict this to paired
// devices only
public void authorize_service (ObjectPath device_path, string uuid) throws Error, BluezError {
var device = get_device_from_object_path (device_path);

bool paired = device.paired;
bool trusted = device.trusted;

// Shouldn't really happen as trusted devices should be automatically authorized, but lets handle it anyway
if (paired && trusted) {
// allow
return;
}

// A device that has been paired, but not yet trusted, trust it and allow it to access
// services
if (paired && !trusted) {
device.trusted = true;
// allow
return;
}

// Reject everything else
throw new BluezError.REJECTED ("Rejecting service auth, not paired or trusted");
}

public void cancel () throws Error {
if (pair_dialog != null) {
pair_dialog.cancelled = true;
pair_dialog.destroy ();
}
}

private async void check_pairing_response (PairDialog dialog) throws BluezError {
SourceFunc callback = check_pairing_response.callback;
BluezError? error = null;

dialog.response.connect ((response) => {
if (response != Gtk.ResponseType.ACCEPT || dialog.cancelled) {
if (dialog.cancelled) {
error = new BluezError.CANCELED ("Pairing cancelled");
} else {
error = new BluezError.REJECTED ("Pairing rejected");
}
}

Idle.add ((owned)callback);
dialog.destroy ();
});

dialog.present ();

yield;

if (error != null) {
throw error;
}
}

private Device get_device_from_object_path (ObjectPath object_path) throws GLib.Error {
return Bus.get_proxy_sync<Device> (BusType.SYSTEM, "org.bluez", object_path, DBusProxyFlags.GET_INVALIDATED_PROPERTIES);
}
}
Loading