Initial implementation of the Kotlin Sync Manager API
This commit is contained in:
Родитель
ca6a6a90b7
Коммит
9f6fcedc85
|
@ -57,6 +57,13 @@ projects:
|
||||||
- name: sync15
|
- name: sync15
|
||||||
type: aar
|
type: aar
|
||||||
description: Shared Sync types for Kotlin.
|
description: Shared Sync types for Kotlin.
|
||||||
|
syncmanager:
|
||||||
|
path: components/sync_manager/android
|
||||||
|
artifactId: syncmanager
|
||||||
|
publications:
|
||||||
|
- name: syncmanager
|
||||||
|
type: aar
|
||||||
|
description: Sync manager implementation
|
||||||
lockbox-megazord:
|
lockbox-megazord:
|
||||||
uploadSymbols: true
|
uploadSymbols: true
|
||||||
path: megazords/lockbox/android
|
path: megazords/lockbox/android
|
||||||
|
|
|
@ -623,6 +623,7 @@ dependencies = [
|
||||||
"places-ffi 0.1.0",
|
"places-ffi 0.1.0",
|
||||||
"push-ffi 0.1.0",
|
"push-ffi 0.1.0",
|
||||||
"rc_log_ffi 0.1.0",
|
"rc_log_ffi 0.1.0",
|
||||||
|
"sync_manager_ffi 0.1.0",
|
||||||
"viaduct 0.1.0",
|
"viaduct 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1028,6 +1029,7 @@ dependencies = [
|
||||||
"fxaclient_ffi 0.1.0",
|
"fxaclient_ffi 0.1.0",
|
||||||
"logins_ffi 0.1.0",
|
"logins_ffi 0.1.0",
|
||||||
"rc_log_ffi 0.1.0",
|
"rc_log_ffi 0.1.0",
|
||||||
|
"sync_manager_ffi 0.1.0",
|
||||||
"viaduct 0.1.0",
|
"viaduct 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1107,6 +1109,7 @@ dependencies = [
|
||||||
"places-ffi 0.1.0",
|
"places-ffi 0.1.0",
|
||||||
"push-ffi 0.1.0",
|
"push-ffi 0.1.0",
|
||||||
"rc_log_ffi 0.1.0",
|
"rc_log_ffi 0.1.0",
|
||||||
|
"sync_manager_ffi 0.1.0",
|
||||||
"viaduct 0.1.0",
|
"viaduct 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2351,6 +2354,37 @@ dependencies = [
|
||||||
"viaduct 0.1.0",
|
"viaduct 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"error-support 0.1.0",
|
||||||
|
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"ffi-support 0.3.5",
|
||||||
|
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"logins 0.1.0",
|
||||||
|
"places 0.1.0",
|
||||||
|
"prost 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"prost-build 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"prost-derive 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"sync15 0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_manager_ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ffi-support 0.3.5",
|
||||||
|
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"logins_ffi 0.1.0",
|
||||||
|
"places-ffi 0.1.0",
|
||||||
|
"prost 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"sync15 0.1.0",
|
||||||
|
"sync_manager 0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
|
|
@ -20,6 +20,8 @@ members = [
|
||||||
"components/support/rc_crypto/nss/nss_sys",
|
"components/support/rc_crypto/nss/nss_sys",
|
||||||
"components/viaduct",
|
"components/viaduct",
|
||||||
"components/sync15",
|
"components/sync15",
|
||||||
|
"components/sync_manager",
|
||||||
|
"components/sync_manager/ffi",
|
||||||
"components/rc_log",
|
"components/rc_log",
|
||||||
"megazords/fenix",
|
"megazords/fenix",
|
||||||
"megazords/full",
|
"megazords/full",
|
||||||
|
|
|
@ -29,6 +29,17 @@ class DatabaseLoginsStorage(private val dbPath: String) : AutoCloseable, LoginsS
|
||||||
return handle
|
return handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the raw handle used to reference this logins database.
|
||||||
|
*
|
||||||
|
* Generally should only be used to pass the handle into `SyncManager.setLogins`.
|
||||||
|
*
|
||||||
|
* Note: handles do not remain valid after locking / unlocking the logins database.
|
||||||
|
*/
|
||||||
|
fun getHandle(): Long {
|
||||||
|
return this.raw.get()
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(LoginsStorageException::class)
|
@Throws(LoginsStorageException::class)
|
||||||
override fun lock() {
|
override fun lock() {
|
||||||
|
|
|
@ -14,9 +14,13 @@ use ffi_support::{
|
||||||
};
|
};
|
||||||
use logins::{Login, PasswordEngine, Result};
|
use logins::{Login, PasswordEngine, Result};
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref ENGINES: ConcurrentHandleMap<PasswordEngine> = ConcurrentHandleMap::new();
|
// TODO: this is basically a RwLock<HandleMap<Mutex<Arc<Mutex<...>>>>.
|
||||||
|
// but could just be a `RwLock<HandleMap<Arc<Mutex<...>>>>`.
|
||||||
|
// Find a way to express this cleanly in ffi_support?
|
||||||
|
pub static ref ENGINES: ConcurrentHandleMap<Arc<Mutex<PasswordEngine>>> = ConcurrentHandleMap::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -26,10 +30,10 @@ pub extern "C" fn sync15_passwords_state_new(
|
||||||
error: &mut ExternError,
|
error: &mut ExternError,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
log::debug!("sync15_passwords_state_new");
|
log::debug!("sync15_passwords_state_new");
|
||||||
ENGINES.insert_with_result(error, || {
|
ENGINES.insert_with_result(error, || -> logins::Result<_> {
|
||||||
let path = db_path.as_str();
|
let path = db_path.as_str();
|
||||||
let key = encryption_key.as_str();
|
let key = encryption_key.as_str();
|
||||||
PasswordEngine::new(path, Some(key))
|
Ok(Arc::new(Mutex::new(PasswordEngine::new(path, Some(key))?)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,12 +69,15 @@ pub unsafe extern "C" fn sync15_passwords_state_new_with_hex_key(
|
||||||
error: &mut ExternError,
|
error: &mut ExternError,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
log::debug!("sync15_passwords_state_new_with_hex_key");
|
log::debug!("sync15_passwords_state_new_with_hex_key");
|
||||||
ENGINES.insert_with_result(error, || {
|
ENGINES.insert_with_result(error, || -> logins::Result<_> {
|
||||||
let path = db_path.as_str();
|
let path = db_path.as_str();
|
||||||
let key = bytes_to_key_string(encryption_key, encryption_key_len as usize);
|
let key = bytes_to_key_string(encryption_key, encryption_key_len as usize);
|
||||||
// We have a Option<String>, but need an Option<&str>...
|
// We have a Option<String>, but need an Option<&str>...
|
||||||
let opt_key_ref = key.as_ref().map(String::as_str);
|
let opt_key_ref = key.as_ref().map(String::as_str);
|
||||||
PasswordEngine::new(path, opt_key_ref)
|
Ok(Arc::new(Mutex::new(PasswordEngine::new(
|
||||||
|
path,
|
||||||
|
opt_key_ref,
|
||||||
|
)?)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +90,7 @@ fn parse_url(url: &str) -> sync15::Result<url::Url> {
|
||||||
pub extern "C" fn sync15_passwords_disable_mem_security(handle: u64, error: &mut ExternError) {
|
pub extern "C" fn sync15_passwords_disable_mem_security(handle: u64, error: &mut ExternError) {
|
||||||
log::debug!("sync15_passwords_disable_mem_security");
|
log::debug!("sync15_passwords_disable_mem_security");
|
||||||
ENGINES.call_with_result(error, handle, |state| -> Result<()> {
|
ENGINES.call_with_result(error, handle, |state| -> Result<()> {
|
||||||
state.disable_mem_security()
|
state.lock().unwrap().disable_mem_security()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +105,7 @@ pub extern "C" fn sync15_passwords_sync(
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
log::debug!("sync15_passwords_sync");
|
log::debug!("sync15_passwords_sync");
|
||||||
ENGINES.call_with_result(error, handle, |state| -> Result<_> {
|
ENGINES.call_with_result(error, handle, |state| -> Result<_> {
|
||||||
let ping = state.sync(
|
let ping = state.lock().unwrap().sync(
|
||||||
&sync15::Sync15StorageClientInit {
|
&sync15::Sync15StorageClientInit {
|
||||||
key_id: key_id.into_string(),
|
key_id: key_id.into_string(),
|
||||||
access_token: access_token.into_string(),
|
access_token: access_token.into_string(),
|
||||||
|
@ -113,7 +120,9 @@ pub extern "C" fn sync15_passwords_sync(
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn sync15_passwords_touch(handle: u64, id: FfiStr<'_>, error: &mut ExternError) {
|
pub extern "C" fn sync15_passwords_touch(handle: u64, id: FfiStr<'_>, error: &mut ExternError) {
|
||||||
log::debug!("sync15_passwords_touch");
|
log::debug!("sync15_passwords_touch");
|
||||||
ENGINES.call_with_result(error, handle, |state| state.touch(id.as_str()))
|
ENGINES.call_with_result(error, handle, |state| {
|
||||||
|
state.lock().unwrap().touch(id.as_str())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -123,25 +132,27 @@ pub extern "C" fn sync15_passwords_delete(
|
||||||
error: &mut ExternError,
|
error: &mut ExternError,
|
||||||
) -> u8 {
|
) -> u8 {
|
||||||
log::debug!("sync15_passwords_delete");
|
log::debug!("sync15_passwords_delete");
|
||||||
ENGINES.call_with_result(error, handle, |state| state.delete(id.as_str()))
|
ENGINES.call_with_result(error, handle, |state| {
|
||||||
|
state.lock().unwrap().delete(id.as_str())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn sync15_passwords_wipe(handle: u64, error: &mut ExternError) {
|
pub extern "C" fn sync15_passwords_wipe(handle: u64, error: &mut ExternError) {
|
||||||
log::debug!("sync15_passwords_wipe");
|
log::debug!("sync15_passwords_wipe");
|
||||||
ENGINES.call_with_result(error, handle, |state| state.wipe())
|
ENGINES.call_with_result(error, handle, |state| state.lock().unwrap().wipe())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn sync15_passwords_wipe_local(handle: u64, error: &mut ExternError) {
|
pub extern "C" fn sync15_passwords_wipe_local(handle: u64, error: &mut ExternError) {
|
||||||
log::debug!("sync15_passwords_wipe_local");
|
log::debug!("sync15_passwords_wipe_local");
|
||||||
ENGINES.call_with_result(error, handle, |state| state.wipe_local())
|
ENGINES.call_with_result(error, handle, |state| state.lock().unwrap().wipe_local())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn sync15_passwords_reset(handle: u64, error: &mut ExternError) {
|
pub extern "C" fn sync15_passwords_reset(handle: u64, error: &mut ExternError) {
|
||||||
log::debug!("sync15_passwords_reset");
|
log::debug!("sync15_passwords_reset");
|
||||||
ENGINES.call_with_result(error, handle, |state| state.reset())
|
ENGINES.call_with_result(error, handle, |state| state.lock().unwrap().reset())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -150,7 +161,9 @@ pub extern "C" fn sync15_passwords_new_interrupt_handle(
|
||||||
error: &mut ExternError,
|
error: &mut ExternError,
|
||||||
) -> *mut sql_support::SqlInterruptHandle {
|
) -> *mut sql_support::SqlInterruptHandle {
|
||||||
log::debug!("sync15_passwords_new_interrupt_handle");
|
log::debug!("sync15_passwords_new_interrupt_handle");
|
||||||
ENGINES.call_with_output(error, handle, |state| state.new_interrupt_handle())
|
ENGINES.call_with_output(error, handle, |state| {
|
||||||
|
state.lock().unwrap().new_interrupt_handle()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -166,7 +179,7 @@ pub extern "C" fn sync15_passwords_interrupt(
|
||||||
pub extern "C" fn sync15_passwords_get_all(handle: u64, error: &mut ExternError) -> *mut c_char {
|
pub extern "C" fn sync15_passwords_get_all(handle: u64, error: &mut ExternError) -> *mut c_char {
|
||||||
log::debug!("sync15_passwords_get_all");
|
log::debug!("sync15_passwords_get_all");
|
||||||
ENGINES.call_with_result(error, handle, |state| -> Result<String> {
|
ENGINES.call_with_result(error, handle, |state| -> Result<String> {
|
||||||
let all_passwords = state.list()?;
|
let all_passwords = state.lock().unwrap().list()?;
|
||||||
let result = serde_json::to_string(&all_passwords)?;
|
let result = serde_json::to_string(&all_passwords)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
})
|
})
|
||||||
|
@ -179,7 +192,9 @@ pub extern "C" fn sync15_passwords_get_by_id(
|
||||||
error: &mut ExternError,
|
error: &mut ExternError,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
log::debug!("sync15_passwords_get_by_id");
|
log::debug!("sync15_passwords_get_by_id");
|
||||||
ENGINES.call_with_result(error, handle, |state| state.get(id.as_str()))
|
ENGINES.call_with_result(error, handle, |state| {
|
||||||
|
state.lock().unwrap().get(id.as_str())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -196,7 +211,7 @@ pub extern "C" fn sync15_passwords_add(
|
||||||
parsed["id"] = serde_json::Value::String(String::default());
|
parsed["id"] = serde_json::Value::String(String::default());
|
||||||
}
|
}
|
||||||
let login: Login = serde_json::from_value(parsed)?;
|
let login: Login = serde_json::from_value(parsed)?;
|
||||||
state.add(login)
|
state.lock().unwrap().add(login)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +237,7 @@ pub extern "C" fn sync15_passwords_update(
|
||||||
log::debug!("sync15_passwords_update");
|
log::debug!("sync15_passwords_update");
|
||||||
ENGINES.call_with_result(error, handle, |state| {
|
ENGINES.call_with_result(error, handle, |state| {
|
||||||
let parsed: Login = serde_json::from_str(record_json.as_str())?;
|
let parsed: Login = serde_json::from_str(record_json.as_str())?;
|
||||||
state.update(parsed)
|
state.lock().unwrap().update(parsed)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,15 @@ class PlacesApi(path: String) : PlacesManager, AutoCloseable {
|
||||||
private const val READ_WRITE: Int = 2
|
private const val READ_WRITE: Int = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the raw handle used to reference this PlacesApi.
|
||||||
|
*
|
||||||
|
* Generally should only be used to pass the handle into `SyncManager.setPlaces`
|
||||||
|
*/
|
||||||
|
fun getHandle(): Long {
|
||||||
|
return this.handle.get()
|
||||||
|
}
|
||||||
|
|
||||||
override fun openReader(): PlacesReaderConnection {
|
override fun openReader(): PlacesReaderConnection {
|
||||||
val connHandle = rustCall(this) { error ->
|
val connHandle = rustCall(this) { error ->
|
||||||
LibPlacesFFI.INSTANCE.places_connection_new(handle.get(), READ_ONLY, error)
|
LibPlacesFFI.INSTANCE.places_connection_new(handle.get(), READ_ONLY, error)
|
||||||
|
|
|
@ -30,7 +30,7 @@ fn parse_url(url: &str) -> places::Result<url::Url> {
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref APIS: ConcurrentHandleMap<Arc<PlacesApi>> = ConcurrentHandleMap::new();
|
pub static ref APIS: ConcurrentHandleMap<Arc<PlacesApi>> = ConcurrentHandleMap::new();
|
||||||
static ref CONNECTIONS: ConcurrentHandleMap<PlacesDb> = ConcurrentHandleMap::new();
|
static ref CONNECTIONS: ConcurrentHandleMap<PlacesDb> = ConcurrentHandleMap::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "sync_manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["application-services <application-services@mozilla.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sync15 = { path = "../sync15" }
|
||||||
|
places = { path = "../places", optional = true }
|
||||||
|
logins = { path = "../logins", optional = true }
|
||||||
|
ffi-support = { path = "../support/ffi" }
|
||||||
|
failure = "0.1.5"
|
||||||
|
error-support = { path = "../support/error" }
|
||||||
|
prost = "0.5.0"
|
||||||
|
prost-derive = "0.5.0"
|
||||||
|
bytes = "0.4.12"
|
||||||
|
lazy_static = "1.3.0"
|
||||||
|
log = "0.4.7"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.5.0"
|
|
@ -0,0 +1,120 @@
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'com.google.protobuf'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion rootProject.ext.build.compileSdkVersion
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion rootProject.ext.build['minSdkVersion']
|
||||||
|
targetSdkVersion rootProject.ext.build['targetSdkVersion']
|
||||||
|
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
buildConfigField("String", "LIBRARY_VERSION", "\"${rootProject.ext.library.version}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
consumerProguardFiles "$rootDir/proguard-rules-consumer-jna.pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
test.resources.srcDirs += "$buildDir/rustJniLibs/desktop"
|
||||||
|
test.resources.srcDirs += "${project(':full-megazord').buildDir}/rustJniLibs/desktop"
|
||||||
|
|
||||||
|
main {
|
||||||
|
proto {
|
||||||
|
srcDir '../src'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
// There's an interaction between Gradle's resolution of dependencies with different types
|
||||||
|
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in
|
||||||
|
// JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the
|
||||||
|
// dependency from the `implementation`, which is type @aar, and therefore the JNA dependency
|
||||||
|
// doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think
|
||||||
|
// what's happening is that @aar type in `implementation` resolves to the @jar type in
|
||||||
|
// `testImplementation`, and that it wins the dependency resolution battle.
|
||||||
|
//
|
||||||
|
// A workaround is to add a new configuration which depends on the @jar type and to reference
|
||||||
|
// the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to
|
||||||
|
// the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the
|
||||||
|
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
|
||||||
|
// Success!
|
||||||
|
jnaForTest
|
||||||
|
}
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = 'com.google.protobuf:protoc:3.0.0'
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
javalite {
|
||||||
|
artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().each { task ->
|
||||||
|
task.builtins {
|
||||||
|
remove java
|
||||||
|
}
|
||||||
|
task.plugins {
|
||||||
|
javalite { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Part of the public API.
|
||||||
|
api project(':sync15')
|
||||||
|
|
||||||
|
jnaForTest "net.java.dev.jna:jna:$jna_version@jar"
|
||||||
|
implementation "net.java.dev.jna:jna:$jna_version@aar"
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
|
||||||
|
implementation 'com.google.protobuf:protobuf-lite:3.0.0'
|
||||||
|
api project(":full-megazord")
|
||||||
|
implementation project(":native-support")
|
||||||
|
|
||||||
|
// For reasons unknown, resolving the jnaForTest configuration directly
|
||||||
|
// trips a nasty issue with the Android-Gradle plugin 3.2.1, like `Cannot
|
||||||
|
// change attributes of configuration ':PROJECT:kapt' after it has been
|
||||||
|
// resolved`. I think that the configuration is being made a
|
||||||
|
// super-configuration of the testImplementation and then the `.files` is
|
||||||
|
// causing it to be resolved. Cloning first dissociates the configuration,
|
||||||
|
// avoiding other configurations from being resolved. Tricky!
|
||||||
|
testImplementation files(configurations.jnaForTest.copyRecursive().files)
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
testImplementation 'org.robolectric:robolectric:3.8'
|
||||||
|
testImplementation 'org.mockito:mockito-core:2.21.0'
|
||||||
|
|
||||||
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
// The `cargoBuild` task isn't available until after evaluation.
|
||||||
|
android.libraryVariants.all { variant ->
|
||||||
|
def productFlavor = ""
|
||||||
|
variant.productFlavors.each {
|
||||||
|
productFlavor += "${it.name.capitalize()}"
|
||||||
|
}
|
||||||
|
def buildType = "${variant.buildType.name.capitalize()}"
|
||||||
|
tasks["generate${productFlavor}${buildType}Assets"].dependsOn(project(':full-megazord').tasks["cargoBuild"])
|
||||||
|
|
||||||
|
// For unit tests.
|
||||||
|
tasks["process${productFlavor}${buildType}UnitTestJavaRes"].dependsOn(project(':full-megazord').tasks["cargoBuild"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/publish.gradle"
|
||||||
|
|
||||||
|
ext.configurePublish()
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,2 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.mozilla.appservices.syncmanager" />
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.appservices.syncmanager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for sync manager errors. Generally should not
|
||||||
|
* have concrete instances.
|
||||||
|
*/
|
||||||
|
open class SyncManagerException(msg: String) : Exception(msg)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General catch-all error, generally indicating API misuse of some sort.
|
||||||
|
*/
|
||||||
|
open class UnexpectedError(msg: String) : SyncManagerException(msg)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync manager paniced. Please report these.
|
||||||
|
*/
|
||||||
|
open class InternalPanic(msg: String) : SyncManagerException(msg)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We were asked to sync an engine which is either unknown, or which the sync
|
||||||
|
* manager was not compiled with support for (message will elaborate).
|
||||||
|
*/
|
||||||
|
open class UnsupportedEngine(msg: String) : SyncManagerException(msg)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We were asked to sync an engine but we couldn't because the connection
|
||||||
|
*
|
||||||
|
* Note: When not syncing, the manager holds a weak reference to connection
|
||||||
|
* objects, and so performing something like: `SyncManager.setLogins(handle)`,
|
||||||
|
* closing/locking the logins connection, and then trying to sync logins will
|
||||||
|
* produce this error.
|
||||||
|
*
|
||||||
|
* TODO: Should this be an error reported in SyncResult and not cause the sync
|
||||||
|
* to fail? It's probably a bug in the caller (they should call `setBlah` first)...
|
||||||
|
*/
|
||||||
|
open class ClosedEngine(msg: String) : SyncManagerException(msg)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.appservices.syncmanager
|
||||||
|
|
||||||
|
import com.sun.jna.Library
|
||||||
|
import com.sun.jna.Pointer
|
||||||
|
import com.sun.jna.PointerType
|
||||||
|
import com.sun.jna.StringArray
|
||||||
|
import mozilla.appservices.support.native.RustBuffer
|
||||||
|
import mozilla.appservices.support.native.loadIndirect
|
||||||
|
import org.mozilla.appservices.syncmanager.BuildConfig
|
||||||
|
|
||||||
|
@Suppress("FunctionNaming", "FunctionParameterNaming", "LongParameterList", "TooGenericExceptionThrown")
|
||||||
|
internal interface LibSyncManagerFFI : Library {
|
||||||
|
companion object {
|
||||||
|
internal var INSTANCE: LibSyncManagerFFI =
|
||||||
|
loadIndirect(componentName = "syncmanager", componentVersion = BuildConfig.LIBRARY_VERSION)
|
||||||
|
}
|
||||||
|
fun sync_manager_set_places(handle: PlacesApiHandle, error: RustError.ByReference)
|
||||||
|
fun sync_manager_set_logins(handle: LoginsDbHandle, error: RustError.ByReference)
|
||||||
|
fun sync_manager_disconnect(error: RustError.ByReference)
|
||||||
|
|
||||||
|
fun sync_manager_sync(data: Pointer, len: Int, error: RustError.ByReference): RustBuffer.ByValue
|
||||||
|
|
||||||
|
fun sync_manager_destroy_string(s: Pointer)
|
||||||
|
fun sync_manager_destroy_bytebuffer(bb: RustBuffer.ByValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal typealias PlacesApiHandle = Long
|
||||||
|
internal typealias LoginsDbHandle = Long
|
|
@ -0,0 +1,69 @@
|
||||||
|
/* Copyright 2018 Mozilla
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||||
|
* this file except in compliance with the License. You may obtain a copy of the
|
||||||
|
* License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed
|
||||||
|
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations under the License. */
|
||||||
|
package mozilla.appservices.syncmanager
|
||||||
|
|
||||||
|
import com.sun.jna.Pointer
|
||||||
|
import com.sun.jna.Structure
|
||||||
|
|
||||||
|
@Structure.FieldOrder("code", "message")
|
||||||
|
internal open class RustError : Structure() {
|
||||||
|
|
||||||
|
class ByReference : RustError(), Structure.ByReference
|
||||||
|
|
||||||
|
@JvmField var code: Int = 0
|
||||||
|
@JvmField var message: Pointer? = null
|
||||||
|
|
||||||
|
fun isSuccess(): Boolean {
|
||||||
|
return code == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isFailure(): Boolean {
|
||||||
|
return code != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod", "ReturnCount", "TooGenericExceptionThrown")
|
||||||
|
fun intoException(): SyncManagerException {
|
||||||
|
if (!isFailure()) {
|
||||||
|
// It's probably a bad idea to throw here! We're probably leaking something if this is
|
||||||
|
// ever hit! (But we shouldn't ever hit it?)
|
||||||
|
throw RuntimeException("[Bug] intoException called on non-failure!")
|
||||||
|
}
|
||||||
|
val message = this.consumeErrorMessage()
|
||||||
|
when (code) {
|
||||||
|
2 -> return UnsupportedEngine(message)
|
||||||
|
3 -> return ClosedEngine(message)
|
||||||
|
-1 -> return InternalPanic(message)
|
||||||
|
// Note: `1` is used as a generic catch all, but we
|
||||||
|
// might as well handle the others the same way.
|
||||||
|
else -> return UnexpectedError(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get and consume the error message, or null if there is none.
|
||||||
|
*/
|
||||||
|
fun consumeErrorMessage(): String {
|
||||||
|
val result = this.getMessage()
|
||||||
|
if (this.message != null) {
|
||||||
|
LibSyncManagerFFI.INSTANCE.sync_manager_destroy_string(this.message!!)
|
||||||
|
this.message = null
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
throw NullPointerException("consumeErrorMessage called with null message!")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the error message or null if there is none.
|
||||||
|
*/
|
||||||
|
fun getMessage(): String? {
|
||||||
|
return this.message?.getString(0, "utf8")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.appservices.syncmanager
|
||||||
|
|
||||||
|
import com.sun.jna.Native
|
||||||
|
import mozilla.appservices.support.native.toNioDirectBuffer
|
||||||
|
|
||||||
|
object SyncManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point the manager at the implementation of `PlacesApi` to use.
|
||||||
|
*
|
||||||
|
* @param placesApiHandle A value returned by `PlacesApi.getHandle()`
|
||||||
|
* @throws [UnsupportedEngine] If the manager was not compiled with places support.
|
||||||
|
*/
|
||||||
|
fun setPlaces(placesApiHandle: Long) {
|
||||||
|
rustCall { err ->
|
||||||
|
LibSyncManagerFFI.INSTANCE.sync_manager_set_places(placesApiHandle, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Point the manager at the implementation of `DatabaseLoginsStorage` to use.
|
||||||
|
*
|
||||||
|
* @param loginsDbHandle A value returned by `DatabaseLoginsStorage.getHandle()`
|
||||||
|
* @throws [UnsupportedEngine] If the manager was not compiled with logins support.
|
||||||
|
*/
|
||||||
|
fun setLogins(loginsDbHandle: Long) {
|
||||||
|
rustCall { err ->
|
||||||
|
LibSyncManagerFFI.INSTANCE.sync_manager_set_logins(loginsDbHandle, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect this device from sync. This essentially clears shared state having to do with
|
||||||
|
* sync, as well as each engines sync-specific local state
|
||||||
|
*/
|
||||||
|
fun disconnect() {
|
||||||
|
rustCall { err ->
|
||||||
|
LibSyncManagerFFI.INSTANCE.sync_manager_disconnect(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Perform a sync.
|
||||||
|
*/
|
||||||
|
fun sync(params: SyncParams): SyncResult {
|
||||||
|
val buf = params.toProtobuf()
|
||||||
|
val (nioBuf, len) = buf.toNioDirectBuffer()
|
||||||
|
val rustBuf = rustCall { err ->
|
||||||
|
val ptr = Native.getDirectBufferPointer(nioBuf)
|
||||||
|
LibSyncManagerFFI.INSTANCE.sync_manager_sync(ptr, len, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val stream = rustBuf.asCodedInputStream()
|
||||||
|
return SyncResult.fromProtobuf(MsgTypes.SyncResult.parseFrom(stream))
|
||||||
|
} finally {
|
||||||
|
LibSyncManagerFFI.INSTANCE.sync_manager_destroy_bytebuffer(rustBuf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <U> rustCall(callback: (RustError.ByReference) -> U): U {
|
||||||
|
val e = RustError.ByReference()
|
||||||
|
val ret: U = callback(e)
|
||||||
|
if (e.isFailure()) {
|
||||||
|
throw e.intoException()
|
||||||
|
} else {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.appservices.syncmanager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reason for syncing.
|
||||||
|
*/
|
||||||
|
enum class SyncReason {
|
||||||
|
/**
|
||||||
|
* This is a scheduled sync
|
||||||
|
*/
|
||||||
|
SCHEDULED,
|
||||||
|
/**
|
||||||
|
* This is a manually triggered sync invoked by the user.
|
||||||
|
*/
|
||||||
|
USER,
|
||||||
|
/**
|
||||||
|
* This is a sync that is running optimistically before
|
||||||
|
* the device goes to sleep / is backgrounded.
|
||||||
|
*/
|
||||||
|
PRE_SLEEP,
|
||||||
|
/**
|
||||||
|
* This is a sync that is run on application startup.
|
||||||
|
*/
|
||||||
|
STARTUP,
|
||||||
|
/**
|
||||||
|
* This is a sync that is being performed simply to update the
|
||||||
|
* enabled state of one or more engines.
|
||||||
|
*/
|
||||||
|
ENABLED_CHANGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for providing the auth-related information needed to sync.
|
||||||
|
*/
|
||||||
|
data class SyncAuthInfo(
|
||||||
|
val kid: String,
|
||||||
|
val fxaAccessToken: String,
|
||||||
|
val syncKey: String,
|
||||||
|
val tokenserverURL: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters to use for syncing.
|
||||||
|
*/
|
||||||
|
data class SyncParams(
|
||||||
|
/**
|
||||||
|
* The reason we're syncing.
|
||||||
|
*/
|
||||||
|
val reason: SyncReason,
|
||||||
|
/**
|
||||||
|
* The list of engines to sync.
|
||||||
|
*
|
||||||
|
* Engine names are lowercase, and refer to the server-side engine name, e.g.
|
||||||
|
* "passwords" (not "logins"!), "bookmarks", "history", etc.
|
||||||
|
*
|
||||||
|
* Requesting that we sync an unknown engine type will result in a
|
||||||
|
* [UnsupportedEngine] error.
|
||||||
|
*
|
||||||
|
* Passing `null` here is used to indicate that all known engines
|
||||||
|
* should be synced.
|
||||||
|
*/
|
||||||
|
val engines: List<String>?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of engine name to new-enabled-state. That is,
|
||||||
|
*
|
||||||
|
* - The map should be empty to indicate "no changes"
|
||||||
|
*
|
||||||
|
* - The map should have `enginename: true` if an engine named
|
||||||
|
* `enginename` should be enabled.
|
||||||
|
*
|
||||||
|
* - The map should have `enginename: false` if an engine named
|
||||||
|
* `enginename` should be disabled.
|
||||||
|
*/
|
||||||
|
val enabledChanges: Map<String, Boolean>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The information used to authenticate with the sync server.
|
||||||
|
*/
|
||||||
|
val authInfo: SyncAuthInfo,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The previously persisted sync state (from `SyncResult.persistedState`),
|
||||||
|
* if any exists.
|
||||||
|
*/
|
||||||
|
val persistedState: String?
|
||||||
|
) {
|
||||||
|
internal fun toProtobuf(): MsgTypes.SyncParams {
|
||||||
|
val builder = MsgTypes.SyncParams.newBuilder()
|
||||||
|
|
||||||
|
this.engines?.let {
|
||||||
|
builder.addAllEnginesToSync(it)
|
||||||
|
builder.syncAllEngines = false
|
||||||
|
} ?: run {
|
||||||
|
// Null `engines`, sync everything.
|
||||||
|
builder.syncAllEngines = true
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.reason = when (this.reason) {
|
||||||
|
SyncReason.SCHEDULED -> MsgTypes.SyncReason.SCHEDULED
|
||||||
|
SyncReason.USER -> MsgTypes.SyncReason.USER
|
||||||
|
SyncReason.PRE_SLEEP -> MsgTypes.SyncReason.PRE_SLEEP
|
||||||
|
SyncReason.STARTUP -> MsgTypes.SyncReason.STARTUP
|
||||||
|
SyncReason.ENABLED_CHANGE -> MsgTypes.SyncReason.ENABLED_CHANGE
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.putAllEnginesToChangeState(this.enabledChanges)
|
||||||
|
|
||||||
|
builder.acctAccessToken = this.authInfo.fxaAccessToken
|
||||||
|
builder.acctSyncKey = this.authInfo.syncKey
|
||||||
|
builder.acctKeyId = this.authInfo.kid
|
||||||
|
builder.acctTokenserverUrl = this.authInfo.tokenserverURL
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package mozilla.appservices.syncmanager
|
||||||
|
|
||||||
|
import mozilla.appservices.sync15.SyncTelemetryPing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates, at a high level whether the sync succeeded or failed.
|
||||||
|
*/
|
||||||
|
enum class SyncServiceStatus {
|
||||||
|
/**
|
||||||
|
* The sync did not fail.
|
||||||
|
*/
|
||||||
|
OK,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync failed due to network problems.
|
||||||
|
*/
|
||||||
|
NETWORK_ERROR,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sync failed due to some apparent error with the servers.
|
||||||
|
*/
|
||||||
|
SERVICE_ERROR,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth information we were provided was somehow invalid. Refreshing it
|
||||||
|
* with FxA may resolve this issue.
|
||||||
|
*/
|
||||||
|
AUTH_ERROR,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that we declined to sync because the server had requested a
|
||||||
|
* backoff which has not yet expired.
|
||||||
|
*/
|
||||||
|
BACKED_OFF,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some other error occurred.
|
||||||
|
*/
|
||||||
|
OTHER_ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a sync.
|
||||||
|
*/
|
||||||
|
data class SyncResult(
|
||||||
|
/**
|
||||||
|
* The general health.
|
||||||
|
*/
|
||||||
|
val status: SyncServiceStatus,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For engines which failed to sync, contains a string
|
||||||
|
* description of the error.
|
||||||
|
*
|
||||||
|
* The error strings are mostly provided for local debugging,
|
||||||
|
* and more robust information is present in e.g. telemetry.
|
||||||
|
*/
|
||||||
|
val failures: Map<String, String>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of engines which synced without any errors.
|
||||||
|
*/
|
||||||
|
val successful: List<String>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state string that should be persisted by the caller, and
|
||||||
|
* used as the value for `SyncParams.persistedState` in subsequent
|
||||||
|
* calls to `SyncManager.sync`.
|
||||||
|
*/
|
||||||
|
val persistedState: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of engines which have been declined by the user.
|
||||||
|
*
|
||||||
|
* Null if we didn't make it far enough to know.
|
||||||
|
*/
|
||||||
|
val declined: List<String>?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The next time we're allowed to sync, in milliseconds since
|
||||||
|
* the unix epoch.
|
||||||
|
*
|
||||||
|
* If this value is in the future, then there's some kind of back-off.
|
||||||
|
* Note that it's not necessary for the app to enforce this, but should
|
||||||
|
* probably be used as an input in the application's sync scheduling logic.
|
||||||
|
*
|
||||||
|
* Syncs before this passes will generally fail with a BACKED_OFF error,
|
||||||
|
* unless they are syncs that were manually requested by the user (that
|
||||||
|
* is, they have the reason `SyncReason.USER`).
|
||||||
|
*/
|
||||||
|
val nextSyncAllowedAt: Long?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bundle of telemetry information recorded during this sync.
|
||||||
|
*/
|
||||||
|
val telemetry: SyncTelemetryPing?
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
internal fun fromProtobuf(pb: MsgTypes.SyncResult): SyncResult {
|
||||||
|
val nextSyncAllowedAt = if (pb.hasNextSyncAllowedAt()) {
|
||||||
|
pb.nextSyncAllowedAt
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val declined = if (pb.haveDeclined) {
|
||||||
|
pb.declinedList
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val successful = pb.resultsMap.entries
|
||||||
|
.filter { it.value.isEmpty() }
|
||||||
|
.map { it.key }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val failures = pb.resultsMap.filter { it.value.isNotEmpty() }
|
||||||
|
|
||||||
|
val telemetry = if (pb.hasTelemetryJson()) {
|
||||||
|
SyncTelemetryPing.fromJSONString(pb.telemetryJson)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = when (pb.status) {
|
||||||
|
MsgTypes.ServiceStatus.OK -> SyncServiceStatus.OK
|
||||||
|
MsgTypes.ServiceStatus.NETWORK_ERROR -> SyncServiceStatus.NETWORK_ERROR
|
||||||
|
MsgTypes.ServiceStatus.SERVICE_ERROR -> SyncServiceStatus.SERVICE_ERROR
|
||||||
|
MsgTypes.ServiceStatus.AUTH_ERROR -> SyncServiceStatus.AUTH_ERROR
|
||||||
|
MsgTypes.ServiceStatus.BACKED_OFF -> SyncServiceStatus.BACKED_OFF
|
||||||
|
MsgTypes.ServiceStatus.OTHER_ERROR -> SyncServiceStatus.OTHER_ERROR
|
||||||
|
else -> SyncServiceStatus.OTHER_ERROR // impossible *sigh*
|
||||||
|
}
|
||||||
|
|
||||||
|
return SyncResult(
|
||||||
|
status = status,
|
||||||
|
failures = failures,
|
||||||
|
successful = successful,
|
||||||
|
declined = declined,
|
||||||
|
telemetry = telemetry,
|
||||||
|
nextSyncAllowedAt = nextSyncAllowedAt,
|
||||||
|
persistedState = pb.persistedState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=src/manager_msg_types.proto");
|
||||||
|
prost_build::compile_protos(&["src/manager_msg_types.proto"], &["src/"]).unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "sync_manager_ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["application-services <application-services@mozilla.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
logins = ["sync_manager/logins", "logins_ffi"]
|
||||||
|
places = ["sync_manager/places", "places-ffi"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sync_manager = { path = ".." }
|
||||||
|
sync15 = { path = "../../sync15" }
|
||||||
|
ffi-support = { path = "../../support/ffi" }
|
||||||
|
places-ffi = { path = "../../places/ffi", optional = true }
|
||||||
|
logins_ffi = { path = "../../logins/ffi", optional = true }
|
||||||
|
prost = "0.5.0"
|
||||||
|
log = "0.4.7"
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#![allow(unknown_lints)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
// Let's allow these in the FFI code, since it's usually just a coincidence if
|
||||||
|
// the closure is small.
|
||||||
|
#![allow(clippy::redundant_closure)]
|
||||||
|
|
||||||
|
use ffi_support::{ExternError, HandleError};
|
||||||
|
use sync_manager::Result as MgrResult;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn sync_manager_set_places(_places_api_handle: u64, error: &mut ExternError) {
|
||||||
|
ffi_support::call_with_result(error, || -> MgrResult<()> {
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
{
|
||||||
|
let api = places_ffi::APIS
|
||||||
|
.get_u64(_places_api_handle, |api| -> Result<_, HandleError> {
|
||||||
|
Ok(std::sync::Arc::clone(api))
|
||||||
|
})?;
|
||||||
|
sync_manager::set_places(api);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "places"))]
|
||||||
|
{
|
||||||
|
log::error!("Sync manager not compiled with places support");
|
||||||
|
Err(sync_manager::ErrorKind::UnsupportedFeature("places".to_string()).into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn sync_manager_set_logins(_logins_handle: u64, error: &mut ExternError) {
|
||||||
|
ffi_support::call_with_result(error, || -> MgrResult<()> {
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
{
|
||||||
|
let api = logins_ffi::ENGINES
|
||||||
|
.get_u64(_logins_handle, |api| -> Result<_, HandleError> {
|
||||||
|
Ok(std::sync::Arc::clone(api))
|
||||||
|
})?;
|
||||||
|
sync_manager::set_logins(api);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "logins"))]
|
||||||
|
{
|
||||||
|
log::error!("Sync manager not compiled with logins support");
|
||||||
|
Err(sync_manager::ErrorKind::UnsupportedFeature("logins".to_string()).into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn sync_manager_disconnect(error: &mut ExternError) {
|
||||||
|
ffi_support::call_with_output(error, sync_manager::disconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> &'a [u8] {
|
||||||
|
assert!(len >= 0, "Bad buffer len: {}", len);
|
||||||
|
if len == 0 {
|
||||||
|
// This will still fail, but as a bad protobuf format.
|
||||||
|
&[]
|
||||||
|
} else {
|
||||||
|
assert!(!data.is_null(), "Unexpected null data pointer");
|
||||||
|
std::slice::from_raw_parts(data, len as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn sync_manager_sync(
|
||||||
|
params_data: *const u8,
|
||||||
|
params_len: i32,
|
||||||
|
error: &mut ExternError,
|
||||||
|
) -> ffi_support::ByteBuffer {
|
||||||
|
ffi_support::call_with_result(error, || {
|
||||||
|
let buffer = get_buffer(params_data, params_len);
|
||||||
|
let params: sync_manager::msg_types::SyncParams = prost::Message::decode(buffer)?;
|
||||||
|
sync_manager::sync(params)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ffi_support::define_string_destructor!(sync_manager_destroy_string);
|
||||||
|
ffi_support::define_bytebuffer_destructor!(sync_manager_destroy_bytebuffer);
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
use failure::Fail;
|
||||||
|
|
||||||
|
#[derive(Debug, Fail)]
|
||||||
|
pub enum ErrorKind {
|
||||||
|
#[fail(display = "Unknown engine: {}", _0)]
|
||||||
|
UnknownEngine(String),
|
||||||
|
#[fail(display = "Manager was compiled without support for {:?}", _0)]
|
||||||
|
UnsupportedFeature(String),
|
||||||
|
#[fail(display = "Database connection for '{}' is not open", _0)]
|
||||||
|
ConnectionClosed(String),
|
||||||
|
#[fail(display = "Handle is invalid: {}", _0)]
|
||||||
|
InvalidHandle(#[fail(cause)] ffi_support::HandleError),
|
||||||
|
#[fail(display = "Protobuf decode error: {}", _0)]
|
||||||
|
ProtobufDecodeError(#[fail(cause)] prost::DecodeError),
|
||||||
|
}
|
||||||
|
|
||||||
|
error_support::define_error! {
|
||||||
|
ErrorKind {
|
||||||
|
(InvalidHandle, ffi_support::HandleError),
|
||||||
|
(ProtobufDecodeError, prost::DecodeError),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorKind};
|
||||||
|
use ffi_support::{ErrorCode, ExternError};
|
||||||
|
|
||||||
|
pub mod error_codes {
|
||||||
|
// Note: 0 (success) and -1 (panic) are reserved by ffi_support
|
||||||
|
pub const UNEXPECTED: i32 = 1;
|
||||||
|
|
||||||
|
/// We were asked to sync an engine, but we either don't know what it is,
|
||||||
|
/// or were compiled without support for it.
|
||||||
|
pub const UNSUPPORTED_ENGINE: i32 = 2;
|
||||||
|
|
||||||
|
/// We were asked to sync an engine which is not open (e.g. Weak::upgrade
|
||||||
|
/// returns None).
|
||||||
|
pub const ENGINE_NOT_OPEN: i32 = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_code(err: &Error) -> ErrorCode {
|
||||||
|
match err.kind() {
|
||||||
|
ErrorKind::UnknownEngine(e) => {
|
||||||
|
log::error!("Unknown engine: {}", e);
|
||||||
|
ErrorCode::new(error_codes::UNSUPPORTED_ENGINE)
|
||||||
|
}
|
||||||
|
ErrorKind::UnsupportedFeature(f) => {
|
||||||
|
log::error!("Unsupported feature: {}", f);
|
||||||
|
ErrorCode::new(error_codes::UNSUPPORTED_ENGINE)
|
||||||
|
}
|
||||||
|
ErrorKind::ConnectionClosed(e) => {
|
||||||
|
log::error!("Connection closed: {}", e);
|
||||||
|
ErrorCode::new(error_codes::ENGINE_NOT_OPEN)
|
||||||
|
}
|
||||||
|
err => {
|
||||||
|
log::error!("Unexpected error: {}", err);
|
||||||
|
ErrorCode::new(error_codes::UNEXPECTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for ExternError {
|
||||||
|
fn from(e: Error) -> ExternError {
|
||||||
|
ExternError::new_error(get_code(&e), e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ffi_support::implement_into_ffi_by_protobuf!(crate::msg_types::SyncResult);
|
||||||
|
ffi_support::implement_into_ffi_by_protobuf!(crate::msg_types::SyncParams);
|
|
@ -0,0 +1,53 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#![allow(unknown_lints)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
mod ffi;
|
||||||
|
mod manager;
|
||||||
|
|
||||||
|
pub use error::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
pub mod msg_types {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/msg_types.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
use logins::PasswordEngine;
|
||||||
|
use manager::SyncManager;
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
use places::PlacesApi;
|
||||||
|
#[cfg(any(feature = "places", feature = "logins"))]
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref MANAGER: Mutex<SyncManager> = Mutex::new(SyncManager::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
pub fn set_places(places: Arc<PlacesApi>) {
|
||||||
|
let mut manager = MANAGER.lock().unwrap();
|
||||||
|
manager.set_places(places);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
pub fn set_logins(places: Arc<Mutex<PasswordEngine>>) {
|
||||||
|
let mut manager = MANAGER.lock().unwrap();
|
||||||
|
manager.set_logins(places);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect() {
|
||||||
|
let mut manager = MANAGER.lock().unwrap();
|
||||||
|
manager.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(params: msg_types::SyncParams) -> Result<msg_types::SyncResult> {
|
||||||
|
let mut manager = MANAGER.lock().unwrap();
|
||||||
|
// TODO: translate the protobuf message into something nicer to work with in
|
||||||
|
// Rust.
|
||||||
|
manager.sync(params)
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
use places::PlacesApi;
|
||||||
|
// use sql_support::SqlInterruptHandle;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::msg_types::{SyncParams, SyncResult};
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
use logins::PasswordEngine;
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
use std::sync::Mutex;
|
||||||
|
#[cfg(any(feature = "places", feature = "logins"))]
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
|
pub struct SyncManager {
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
places: Weak<PlacesApi>,
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
logins: Weak<Mutex<PasswordEngine>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
places: Weak::new(),
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
logins: Weak::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "places")]
|
||||||
|
pub fn set_places(&mut self, places: Arc<PlacesApi>) {
|
||||||
|
self.places = Arc::downgrade(&places);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "logins")]
|
||||||
|
pub fn set_logins(&mut self, logins: Arc<Mutex<PasswordEngine>>) {
|
||||||
|
self.logins = Arc::downgrade(&logins);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(&mut self) {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(&mut self, params: SyncParams) -> Result<SyncResult> {
|
||||||
|
check_engine_list(¶ms.engines_to_sync)?;
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_engine_list(list: &[String]) -> Result<()> {
|
||||||
|
for e in list {
|
||||||
|
if e == "bookmarks" || e == "history" {
|
||||||
|
if cfg!(not(feature = "places")) {
|
||||||
|
return Err(ErrorKind::UnsupportedFeature(e.to_string()).into());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if e == "passwords" {
|
||||||
|
if cfg!(not(feature = "logins")) {
|
||||||
|
return Err(ErrorKind::UnsupportedFeature(e.to_string()).into());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Err(ErrorKind::UnknownEngine(e.to_string()).into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
// Note: this file name must be unique due to how the iOS megazord works :(
|
||||||
|
|
||||||
|
package msg_types;
|
||||||
|
|
||||||
|
option java_package = "mozilla.appservices.syncmanager";
|
||||||
|
option java_outer_classname = "MsgTypes";
|
||||||
|
|
||||||
|
enum SyncReason {
|
||||||
|
SCHEDULED = 1;
|
||||||
|
USER = 2;
|
||||||
|
PRE_SLEEP = 3;
|
||||||
|
STARTUP = 4;
|
||||||
|
ENABLED_CHANGE = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncParams {
|
||||||
|
repeated string engines_to_sync = 1;
|
||||||
|
required bool sync_all_engines = 2;
|
||||||
|
|
||||||
|
required SyncReason reason = 3;
|
||||||
|
|
||||||
|
map<string, bool> engines_to_change_state = 4;
|
||||||
|
|
||||||
|
optional string persisted_state = 5;
|
||||||
|
|
||||||
|
// These conceptually are a nested type, but exposing them as such would add
|
||||||
|
// needless complexity to the FFI.
|
||||||
|
required string acct_key_id = 6;
|
||||||
|
required string acct_access_token = 7;
|
||||||
|
required string acct_tokenserver_url = 8;
|
||||||
|
required string acct_sync_key = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ServiceStatus {
|
||||||
|
OK = 1;
|
||||||
|
NETWORK_ERROR = 2;
|
||||||
|
SERVICE_ERROR = 3;
|
||||||
|
AUTH_ERROR = 4;
|
||||||
|
BACKED_OFF = 5;
|
||||||
|
OTHER_ERROR = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncResult {
|
||||||
|
required ServiceStatus status = 1;
|
||||||
|
map<string, string> results = 2; // empty string used for 'no error'
|
||||||
|
|
||||||
|
repeated string declined = 3;
|
||||||
|
// false if we didn't manage to check declined.
|
||||||
|
required bool have_declined = 4;
|
||||||
|
|
||||||
|
optional int64 next_sync_allowed_at = 5;
|
||||||
|
required string persisted_state = 6;
|
||||||
|
optional string telemetry_json = 7;
|
||||||
|
}
|
|
@ -15,3 +15,4 @@ places-ffi = { path = "../../components/places/ffi" }
|
||||||
push-ffi = { path = "../../components/push/ffi" }
|
push-ffi = { path = "../../components/push/ffi" }
|
||||||
rc_log_ffi = { path = "../../components/rc_log" }
|
rc_log_ffi = { path = "../../components/rc_log" }
|
||||||
viaduct = { path = "../../components/viaduct", default-features = false }
|
viaduct = { path = "../../components/viaduct", default-features = false }
|
||||||
|
sync_manager_ffi = { path = "../../components/sync_manager/ffi", features = ["places"] }
|
||||||
|
|
|
@ -10,4 +10,5 @@ pub use logins_ffi;
|
||||||
pub use places_ffi;
|
pub use places_ffi;
|
||||||
pub use push_ffi;
|
pub use push_ffi;
|
||||||
pub use rc_log_ffi;
|
pub use rc_log_ffi;
|
||||||
|
pub use sync_manager_ffi;
|
||||||
pub use viaduct;
|
pub use viaduct;
|
||||||
|
|
|
@ -15,4 +15,5 @@ places-ffi = { path = "../../components/places/ffi" }
|
||||||
push-ffi = { path = "../../components/push/ffi" }
|
push-ffi = { path = "../../components/push/ffi" }
|
||||||
rc_log_ffi = { path = "../../components/rc_log" }
|
rc_log_ffi = { path = "../../components/rc_log" }
|
||||||
viaduct = { path = "../../components/viaduct", default_features = false }
|
viaduct = { path = "../../components/viaduct", default_features = false }
|
||||||
|
sync_manager_ffi = { path = "../../components/sync_manager/ffi", features = ["logins", "places"] }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
|
|
@ -13,6 +13,7 @@ pub use logins_ffi;
|
||||||
pub use places_ffi;
|
pub use places_ffi;
|
||||||
pub use push_ffi;
|
pub use push_ffi;
|
||||||
pub use rc_log_ffi;
|
pub use rc_log_ffi;
|
||||||
|
pub use sync_manager_ffi;
|
||||||
pub use viaduct;
|
pub use viaduct;
|
||||||
|
|
||||||
/// In order to support the use case of consumers who don't know about megazords
|
/// In order to support the use case of consumers who don't know about megazords
|
||||||
|
|
|
@ -13,3 +13,4 @@ fxaclient_ffi = { path = "../../components/fxa-client/ffi" }
|
||||||
logins_ffi = { path = "../../components/logins/ffi" }
|
logins_ffi = { path = "../../components/logins/ffi" }
|
||||||
rc_log_ffi = { path = "../../components/rc_log" }
|
rc_log_ffi = { path = "../../components/rc_log" }
|
||||||
viaduct = { path = "../../components/viaduct", default-features = false }
|
viaduct = { path = "../../components/viaduct", default-features = false }
|
||||||
|
sync_manager_ffi = { path = "../../components/sync_manager/ffi", features = ["logins"] }
|
||||||
|
|
|
@ -8,4 +8,5 @@
|
||||||
pub use fxaclient_ffi;
|
pub use fxaclient_ffi;
|
||||||
pub use logins_ffi;
|
pub use logins_ffi;
|
||||||
pub use rc_log_ffi;
|
pub use rc_log_ffi;
|
||||||
|
pub use sync_manager_ffi;
|
||||||
pub use viaduct;
|
pub use viaduct;
|
||||||
|
|
Загрузка…
Ссылка в новой задаче