Commit 0c4d0683 authored by isherman@chromium.org's avatar isherman@chromium.org

[EasyUnlock] Add a private API for establishing an insecure Bluetooth connection.

This is needed to avoid a pairing request prompt when connecting to a service on
a system that tries to upgrade the connection from general bonding to general
bonding with MITM protection, which according to Android and ChromeOS Bluetooth
experts is the correct behavior.

BUG=403069
TEST=chrome.easyUnlockPrivate.connectToBluetoothServiceInsecurely() should work
     just like chrome.bluetoothSocket.connect(), but not show a pairing request
     prompt when pairing with Android L.
R=tengs@chromium.org, keybuk@chromium.org

Review URL: https://codereview.chromium.org/491753002

Cr-Commit-Position: refs/heads/master@{#291353}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@291353 0039d316-1c4b-4281-b951-d872f2087c98
parent ad5b7565
...@@ -436,11 +436,13 @@ void BluetoothSocketListenUsingL2capFunction::CreateResults() { ...@@ -436,11 +436,13 @@ void BluetoothSocketListenUsingL2capFunction::CreateResults() {
results_ = bluetooth_socket::ListenUsingL2cap::Results::Create(); results_ = bluetooth_socket::ListenUsingL2cap::Results::Create();
} }
BluetoothSocketConnectFunction::BluetoothSocketConnectFunction() {} BluetoothSocketAbstractConnectFunction::
BluetoothSocketAbstractConnectFunction() {}
BluetoothSocketConnectFunction::~BluetoothSocketConnectFunction() {} BluetoothSocketAbstractConnectFunction::
~BluetoothSocketAbstractConnectFunction() {}
bool BluetoothSocketConnectFunction::Prepare() { bool BluetoothSocketAbstractConnectFunction::Prepare() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
params_ = bluetooth_socket::Connect::Params::Create(*args_); params_ = bluetooth_socket::Connect::Params::Create(*args_);
EXTENSION_FUNCTION_VALIDATE(params_.get()); EXTENSION_FUNCTION_VALIDATE(params_.get());
...@@ -449,13 +451,13 @@ bool BluetoothSocketConnectFunction::Prepare() { ...@@ -449,13 +451,13 @@ bool BluetoothSocketConnectFunction::Prepare() {
return socket_event_dispatcher_ != NULL; return socket_event_dispatcher_ != NULL;
} }
void BluetoothSocketConnectFunction::AsyncWorkStart() { void BluetoothSocketAbstractConnectFunction::AsyncWorkStart() {
DCHECK(BrowserThread::CurrentlyOn(work_thread_id())); DCHECK(BrowserThread::CurrentlyOn(work_thread_id()));
device::BluetoothAdapterFactory::GetAdapter( device::BluetoothAdapterFactory::GetAdapter(
base::Bind(&BluetoothSocketConnectFunction::OnGetAdapter, this)); base::Bind(&BluetoothSocketAbstractConnectFunction::OnGetAdapter, this));
} }
void BluetoothSocketConnectFunction::OnGetAdapter( void BluetoothSocketAbstractConnectFunction::OnGetAdapter(
scoped_refptr<device::BluetoothAdapter> adapter) { scoped_refptr<device::BluetoothAdapter> adapter) {
DCHECK(BrowserThread::CurrentlyOn(work_thread_id())); DCHECK(BrowserThread::CurrentlyOn(work_thread_id()));
BluetoothApiSocket* socket = GetSocket(params_->socket_id); BluetoothApiSocket* socket = GetSocket(params_->socket_id);
...@@ -486,13 +488,10 @@ void BluetoothSocketConnectFunction::OnGetAdapter( ...@@ -486,13 +488,10 @@ void BluetoothSocketConnectFunction::OnGetAdapter(
return; return;
} }
device->ConnectToService( ConnectToService(device, uuid);
uuid,
base::Bind(&BluetoothSocketConnectFunction::OnConnect, this),
base::Bind(&BluetoothSocketConnectFunction::OnConnectError, this));
} }
void BluetoothSocketConnectFunction::OnConnect( void BluetoothSocketAbstractConnectFunction::OnConnect(
scoped_refptr<device::BluetoothSocket> socket) { scoped_refptr<device::BluetoothSocket> socket) {
DCHECK(BrowserThread::CurrentlyOn(work_thread_id())); DCHECK(BrowserThread::CurrentlyOn(work_thread_id()));
...@@ -516,13 +515,26 @@ void BluetoothSocketConnectFunction::OnConnect( ...@@ -516,13 +515,26 @@ void BluetoothSocketConnectFunction::OnConnect(
AsyncWorkCompleted(); AsyncWorkCompleted();
} }
void BluetoothSocketConnectFunction::OnConnectError( void BluetoothSocketAbstractConnectFunction::OnConnectError(
const std::string& message) { const std::string& message) {
DCHECK(BrowserThread::CurrentlyOn(work_thread_id())); DCHECK(BrowserThread::CurrentlyOn(work_thread_id()));
error_ = message; error_ = message;
AsyncWorkCompleted(); AsyncWorkCompleted();
} }
BluetoothSocketConnectFunction::BluetoothSocketConnectFunction() {}
BluetoothSocketConnectFunction::~BluetoothSocketConnectFunction() {}
void BluetoothSocketConnectFunction::ConnectToService(
device::BluetoothDevice* device,
const device::BluetoothUUID& uuid) {
device->ConnectToService(
uuid,
base::Bind(&BluetoothSocketConnectFunction::OnConnect, this),
base::Bind(&BluetoothSocketConnectFunction::OnConnectError, this));
}
BluetoothSocketDisconnectFunction::BluetoothSocketDisconnectFunction() {} BluetoothSocketDisconnectFunction::BluetoothSocketDisconnectFunction() {}
BluetoothSocketDisconnectFunction::~BluetoothSocketDisconnectFunction() {} BluetoothSocketDisconnectFunction::~BluetoothSocketDisconnectFunction() {}
......
...@@ -208,29 +208,49 @@ class BluetoothSocketListenUsingL2capFunction ...@@ -208,29 +208,49 @@ class BluetoothSocketListenUsingL2capFunction
scoped_ptr<bluetooth_socket::ListenUsingL2cap::Params> params_; scoped_ptr<bluetooth_socket::ListenUsingL2cap::Params> params_;
}; };
class BluetoothSocketConnectFunction : public BluetoothSocketAsyncApiFunction { class BluetoothSocketAbstractConnectFunction :
public BluetoothSocketAsyncApiFunction {
public: public:
DECLARE_EXTENSION_FUNCTION("bluetoothSocket.connect", BluetoothSocketAbstractConnectFunction();
BLUETOOTHSOCKET_CONNECT);
BluetoothSocketConnectFunction();
protected: protected:
virtual ~BluetoothSocketConnectFunction(); virtual ~BluetoothSocketAbstractConnectFunction();
// BluetoothSocketAsyncApiFunction: // BluetoothSocketAsyncApiFunction:
virtual bool Prepare() OVERRIDE; virtual bool Prepare() OVERRIDE;
virtual void AsyncWorkStart() OVERRIDE; virtual void AsyncWorkStart() OVERRIDE;
private: // Subclasses should implement this method to connect to the service
virtual void OnGetAdapter(scoped_refptr<device::BluetoothAdapter> adapter); // registered with |uuid| on the |device|.
virtual void ConnectToService(device::BluetoothDevice* device,
const device::BluetoothUUID& uuid) = 0;
virtual void OnConnect(scoped_refptr<device::BluetoothSocket> socket); virtual void OnConnect(scoped_refptr<device::BluetoothSocket> socket);
virtual void OnConnectError(const std::string& message); virtual void OnConnectError(const std::string& message);
private:
virtual void OnGetAdapter(scoped_refptr<device::BluetoothAdapter> adapter);
scoped_ptr<bluetooth_socket::Connect::Params> params_; scoped_ptr<bluetooth_socket::Connect::Params> params_;
BluetoothSocketEventDispatcher* socket_event_dispatcher_; BluetoothSocketEventDispatcher* socket_event_dispatcher_;
}; };
class BluetoothSocketConnectFunction :
public BluetoothSocketAbstractConnectFunction {
public:
DECLARE_EXTENSION_FUNCTION("bluetoothSocket.connect",
BLUETOOTHSOCKET_CONNECT);
BluetoothSocketConnectFunction();
protected:
virtual ~BluetoothSocketConnectFunction();
// BluetoothSocketAbstractConnectFunction:
virtual void ConnectToService(device::BluetoothDevice* device,
const device::BluetoothUUID& uuid) OVERRIDE;
};
class BluetoothSocketDisconnectFunction class BluetoothSocketDisconnectFunction
: public BluetoothSocketAsyncApiFunction { : public BluetoothSocketAsyncApiFunction {
public: public:
......
...@@ -403,6 +403,26 @@ void EasyUnlockPrivateSeekBluetoothDeviceByAddressFunction::OnSeekCompleted( ...@@ -403,6 +403,26 @@ void EasyUnlockPrivateSeekBluetoothDeviceByAddressFunction::OnSeekCompleted(
} }
} }
EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction::
EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction() {}
EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction::
~EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction() {}
void EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction::
ConnectToService(device::BluetoothDevice* device,
const device::BluetoothUUID& uuid) {
easy_unlock::ConnectToBluetoothServiceInsecurely(
device,
uuid,
base::Bind(&EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction::
OnConnect,
this),
base::Bind(&EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction::
OnConnectError,
this));
}
EasyUnlockPrivateUpdateScreenlockStateFunction:: EasyUnlockPrivateUpdateScreenlockStateFunction::
EasyUnlockPrivateUpdateScreenlockStateFunction() {} EasyUnlockPrivateUpdateScreenlockStateFunction() {}
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "base/basictypes.h" #include "base/basictypes.h"
#include "base/memory/scoped_ptr.h" #include "base/memory/scoped_ptr.h"
#include "chrome/browser/extensions/api/bluetooth_socket/bluetooth_socket_api.h"
#include "extensions/browser/browser_context_keyed_api_factory.h" #include "extensions/browser/browser_context_keyed_api_factory.h"
#include "extensions/browser/extension_function.h" #include "extensions/browser/extension_function.h"
...@@ -164,6 +165,25 @@ class EasyUnlockPrivateSeekBluetoothDeviceByAddressFunction ...@@ -164,6 +165,25 @@ class EasyUnlockPrivateSeekBluetoothDeviceByAddressFunction
EasyUnlockPrivateSeekBluetoothDeviceByAddressFunction); EasyUnlockPrivateSeekBluetoothDeviceByAddressFunction);
}; };
class EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction
: public BluetoothSocketAbstractConnectFunction {
public:
DECLARE_EXTENSION_FUNCTION(
"easyUnlockPrivate.connectToBluetoothServiceInsecurely",
EASYUNLOCKPRIVATE_CONNECTTOBLUETOOTHSERVICEINSECURELY)
EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction();
private:
virtual ~EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction();
// BluetoothSocketAbstractConnectFunction:
virtual void ConnectToService(device::BluetoothDevice* device,
const device::BluetoothUUID& uuid) OVERRIDE;
DISALLOW_COPY_AND_ASSIGN(
EasyUnlockPrivateConnectToBluetoothServiceInsecurelyFunction);
};
class EasyUnlockPrivateUpdateScreenlockStateFunction class EasyUnlockPrivateUpdateScreenlockStateFunction
: public SyncExtensionFunction { : public SyncExtensionFunction {
public: public:
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
#include "base/callback.h" #include "base/callback.h"
using device::BluetoothDevice;
namespace extensions { namespace extensions {
namespace api { namespace api {
namespace easy_unlock { namespace easy_unlock {
...@@ -24,6 +26,14 @@ void SeekBluetoothDeviceByAddress(const std::string& device_address, ...@@ -24,6 +26,14 @@ void SeekBluetoothDeviceByAddress(const std::string& device_address,
result.error_message = kApiUnavailable; result.error_message = kApiUnavailable;
callback.Run(result); callback.Run(result);
} }
void ConnectToBluetoothServiceInsecurely(
device::BluetoothDevice* device,
const device::BluetoothUUID& uuid,
const BluetoothDevice::ConnectToServiceCallback& callback,
const BluetoothDevice::ConnectToServiceErrorCallback& error_callback) {
error_callback.Run(kApiUnavailable);
}
#endif // !defined(OS_CHROMEOS) #endif // !defined(OS_CHROMEOS)
} // namespace easy_unlock } // namespace easy_unlock
......
...@@ -8,6 +8,11 @@ ...@@ -8,6 +8,11 @@
#include <string> #include <string>
#include "base/callback_forward.h" #include "base/callback_forward.h"
#include "device/bluetooth/bluetooth_device.h"
namespace device {
class BluetoothUUID;
}
namespace extensions { namespace extensions {
namespace api { namespace api {
...@@ -28,6 +33,13 @@ typedef base::Callback<void(const SeekDeviceResult& result)> SeekDeviceCallback; ...@@ -28,6 +33,13 @@ typedef base::Callback<void(const SeekDeviceResult& result)> SeekDeviceCallback;
void SeekBluetoothDeviceByAddress(const std::string& device_address, void SeekBluetoothDeviceByAddress(const std::string& device_address,
const SeekDeviceCallback& callback); const SeekDeviceCallback& callback);
void ConnectToBluetoothServiceInsecurely(
device::BluetoothDevice* device,
const device::BluetoothUUID& uuid,
const device::BluetoothDevice::ConnectToServiceCallback& callback,
const device::BluetoothDevice::ConnectToServiceErrorCallback&
error_callback);
} // namespace easy_unlock } // namespace easy_unlock
} // namespace api } // namespace api
} // namespace extensions } // namespace extensions
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include "base/time/time.h" #include "base/time/time.h"
#include "content/public/browser/browser_thread.h" #include "content/public/browser/browser_thread.h"
#include "device/bluetooth/bluetooth_device.h" #include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/bluetooth_device_chromeos.h"
#include "net/socket/socket_descriptor.h" #include "net/socket/socket_descriptor.h"
// The bluez headers are (intentionally) not available within the Chromium // The bluez headers are (intentionally) not available within the Chromium
...@@ -137,6 +138,15 @@ void SeekBluetoothDeviceByAddress(const std::string& device_address, ...@@ -137,6 +138,15 @@ void SeekBluetoothDeviceByAddress(const std::string& device_address,
callback); callback);
} }
void ConnectToBluetoothServiceInsecurely(
device::BluetoothDevice* device,
const device::BluetoothUUID& uuid,
const BluetoothDevice::ConnectToServiceCallback& callback,
const BluetoothDevice::ConnectToServiceErrorCallback& error_callback) {
static_cast<chromeos::BluetoothDeviceChromeOS*>(device)
->ConnectToServiceInsecurely(uuid, callback, error_callback);
}
} // namespace easy_unlock } // namespace easy_unlock
} // namespace api } // namespace api
} // namespace extensions } // namespace extensions
...@@ -217,6 +217,20 @@ ...@@ -217,6 +217,20 @@
static void seekBluetoothDeviceByAddress(DOMString deviceAddress, static void seekBluetoothDeviceByAddress(DOMString deviceAddress,
optional EmptyCallback callback); optional EmptyCallback callback);
// Connects the socket to a remote Bluetooth device over an insecure
// connection, i.e. a connection that requests no bonding and no
// man-in-the-middle protection. Other than the reduced security setting,
// behaves identically to the chrome.bluetoothSocket.connect() function.
// |socketId|: The socket identifier, as issued by the
// chrome.bluetoothSocket API.
// |deviceAddress|: The Bluetooth address of the device to connect to.
// |uuid|: The UUID of the service to connect to.
// |callback|: Called when the connect attempt is complete.
static void connectToBluetoothServiceInsecurely(long socketId,
DOMString deviceAddress,
DOMString uuid,
EmptyCallback callback);
// Updates the screenlock state to reflect the Easy Unlock app state. // Updates the screenlock state to reflect the Easy Unlock app state.
static void updateScreenlockState(State state, static void updateScreenlockState(State state,
optional EmptyCallback callback); optional EmptyCallback callback);
......
...@@ -432,7 +432,21 @@ void BluetoothDeviceChromeOS::ConnectToService( ...@@ -432,7 +432,21 @@ void BluetoothDeviceChromeOS::ConnectToService(
scoped_refptr<BluetoothSocketChromeOS> socket = scoped_refptr<BluetoothSocketChromeOS> socket =
BluetoothSocketChromeOS::CreateBluetoothSocket( BluetoothSocketChromeOS::CreateBluetoothSocket(
ui_task_runner_, socket_thread_); ui_task_runner_, socket_thread_);
socket->Connect(this, uuid, base::Bind(callback, socket), error_callback); socket->Connect(this, uuid, BluetoothSocketChromeOS::SECURITY_LEVEL_MEDIUM,
base::Bind(callback, socket), error_callback);
}
void BluetoothDeviceChromeOS::ConnectToServiceInsecurely(
const BluetoothUUID& uuid,
const ConnectToServiceCallback& callback,
const ConnectToServiceErrorCallback& error_callback) {
VLOG(1) << object_path_.value() << ": Connecting insecurely to service: "
<< uuid.canonical_value();
scoped_refptr<BluetoothSocketChromeOS> socket =
BluetoothSocketChromeOS::CreateBluetoothSocket(
ui_task_runner_, socket_thread_);
socket->Connect(this, uuid, BluetoothSocketChromeOS::SECURITY_LEVEL_LOW,
base::Bind(callback, socket), error_callback);
} }
void BluetoothDeviceChromeOS::CreateGattConnection( void BluetoothDeviceChromeOS::CreateGattConnection(
......
...@@ -73,6 +73,19 @@ class BluetoothDeviceChromeOS ...@@ -73,6 +73,19 @@ class BluetoothDeviceChromeOS
const base::Closure& callback, const base::Closure& callback,
const ErrorCallback& error_callback) OVERRIDE; const ErrorCallback& error_callback) OVERRIDE;
// Attempts to initiate an insecure outgoing L2CAP or RFCOMM connection to the
// advertised service on this device matching |uuid|, performing an SDP lookup
// if necessary to determine the correct protocol and channel for the
// connection. Unlike ConnectToService, the outgoing connection will request
// no bonding rather than general bonding. |callback| will be called on a
// successful connection with a BluetoothSocket instance that is to be owned
// by the receiver. |error_callback| will be called on failure with a message
// indicating the cause.
void ConnectToServiceInsecurely(
const device::BluetoothUUID& uuid,
const ConnectToServiceCallback& callback,
const ConnectToServiceErrorCallback& error_callback);
// Creates a pairing object with the given delegate |pairing_delegate| and // Creates a pairing object with the given delegate |pairing_delegate| and
// establishes it as the pairing context for this device. All pairing-related // establishes it as the pairing context for this device. All pairing-related
// method calls will be forwarded to this object until it is released. // method calls will be forwarded to this object until it is released.
......
...@@ -92,6 +92,7 @@ BluetoothSocketChromeOS::~BluetoothSocketChromeOS() { ...@@ -92,6 +92,7 @@ BluetoothSocketChromeOS::~BluetoothSocketChromeOS() {
void BluetoothSocketChromeOS::Connect( void BluetoothSocketChromeOS::Connect(
const BluetoothDeviceChromeOS* device, const BluetoothDeviceChromeOS* device,
const BluetoothUUID& uuid, const BluetoothUUID& uuid,
SecurityLevel security_level,
const base::Closure& success_callback, const base::Closure& success_callback,
const ErrorCompletionCallback& error_callback) { const ErrorCompletionCallback& error_callback) {
DCHECK(ui_task_runner()->RunsTasksOnCurrentThread()); DCHECK(ui_task_runner()->RunsTasksOnCurrentThread());
...@@ -107,6 +108,8 @@ void BluetoothSocketChromeOS::Connect( ...@@ -107,6 +108,8 @@ void BluetoothSocketChromeOS::Connect(
device_path_ = device->object_path(); device_path_ = device->object_path();
uuid_ = uuid; uuid_ = uuid;
options_.reset(new BluetoothProfileManagerClient::Options()); options_.reset(new BluetoothProfileManagerClient::Options());
if (security_level == SECURITY_LEVEL_LOW)
options_->require_authentication.reset(new bool(false));
RegisterProfile(success_callback, error_callback); RegisterProfile(success_callback, error_callback);
} }
......
...@@ -34,6 +34,11 @@ class CHROMEOS_EXPORT BluetoothSocketChromeOS ...@@ -34,6 +34,11 @@ class CHROMEOS_EXPORT BluetoothSocketChromeOS
public device::BluetoothAdapter::Observer, public device::BluetoothAdapter::Observer,
public BluetoothProfileServiceProvider::Delegate { public BluetoothProfileServiceProvider::Delegate {
public: public:
enum SecurityLevel {
SECURITY_LEVEL_LOW,
SECURITY_LEVEL_MEDIUM
};
static scoped_refptr<BluetoothSocketChromeOS> CreateBluetoothSocket( static scoped_refptr<BluetoothSocketChromeOS> CreateBluetoothSocket(
scoped_refptr<base::SequencedTaskRunner> ui_task_runner, scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
scoped_refptr<device::BluetoothSocketThread> socket_thread); scoped_refptr<device::BluetoothSocketThread> socket_thread);
...@@ -45,6 +50,7 @@ class CHROMEOS_EXPORT BluetoothSocketChromeOS ...@@ -45,6 +50,7 @@ class CHROMEOS_EXPORT BluetoothSocketChromeOS
// with a message explaining the cause of the failure. // with a message explaining the cause of the failure.
virtual void Connect(const BluetoothDeviceChromeOS* device, virtual void Connect(const BluetoothDeviceChromeOS* device,
const device::BluetoothUUID& uuid, const device::BluetoothUUID& uuid,
SecurityLevel security_level,
const base::Closure& success_callback, const base::Closure& success_callback,
const ErrorCompletionCallback& error_callback); const ErrorCompletionCallback& error_callback);
......
...@@ -942,6 +942,7 @@ enum HistogramValue { ...@@ -942,6 +942,7 @@ enum HistogramValue {
EASYUNLOCKPRIVATE_SETREMOTEDEVICES, EASYUNLOCKPRIVATE_SETREMOTEDEVICES,
EASYUNLOCKPRIVATE_GETREMOTEDEVICES, EASYUNLOCKPRIVATE_GETREMOTEDEVICES,
FILESYSTEMPROVIDER_GETALL, FILESYSTEMPROVIDER_GETALL,
EASYUNLOCKPRIVATE_CONNECTTOBLUETOOTHSERVICEINSECURELY,
// Last entry: Add new entries above and ensure to update // Last entry: Add new entries above and ensure to update
// tools/metrics/histograms/histograms.xml. // tools/metrics/histograms/histograms.xml.
ENUM_BOUNDARY ENUM_BOUNDARY
......
...@@ -40909,6 +40909,8 @@ Therefore, the affected-histogram name has to have at least one dot in it. ...@@ -40909,6 +40909,8 @@ Therefore, the affected-histogram name has to have at least one dot in it.
<int value="881" label="EASYUNLOCKPRIVATE_SETREMOTEDEVICES"/> <int value="881" label="EASYUNLOCKPRIVATE_SETREMOTEDEVICES"/>
<int value="882" label="EASYUNLOCKPRIVATE_GETREMOTEDEVICES"/> <int value="882" label="EASYUNLOCKPRIVATE_GETREMOTEDEVICES"/>
<int value="883" label="FILESYSTEMPROVIDER_GETALL"/> <int value="883" label="FILESYSTEMPROVIDER_GETALL"/>
<int value="884"
label="EASYUNLOCKPRIVATE_CONNECTTOBLUETOOTHSERVICEINSECURELY"/>
</enum> </enum>
<enum name="ExtensionInstallCause" type="int"> <enum name="ExtensionInstallCause" type="int">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment