Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bcc7f7af9 | |||
| 16c5b514d7 | |||
| eb89014948 | |||
| 6ea33f18e3 | |||
| 2ec151534e | |||
| 1e97e11cb7 | |||
| 681fdb9802 | |||
| 0df347377a | |||
| d6fc356bd7 | |||
| 78ad8756d5 | |||
| b96c672a8e | |||
| 2a25e9882b | |||
| cb33d8f3e4 | |||
| db8e91eafa | |||
| f9b91f6ef8 | |||
| 01fb92e2c9 | |||
| 4b519e84e3 | |||
| f3d51f0cb9 | |||
| fa30beb9d5 | |||
| 919fdc31bd | |||
| b3f2af0ee7 | |||
| f33364939b | |||
| 1a773daf24 | |||
| b115a99907 | |||
| 5d09d011b1 | |||
| 71a433edf6 | |||
| 974817b7a3 | |||
| f00760bddd | |||
| 5f539c4149 | |||
| 1869a49c2c | |||
| 91289b05ce | |||
| 98bc67939f | |||
| fcb31ae9fa | |||
| 147ad6abcc | |||
| 4c13af3662 | |||
| 17f9ad336c | |||
| 852fc0d230 | |||
| 0032726e3e |
+4
-3
@@ -1,8 +1,9 @@
|
||||
# Releasing
|
||||
|
||||
- do tests
|
||||
- update translations using ``tx pull -a -af`` (as extra merge request or branch for the case it does not build correctly)
|
||||
- update the version name and version code of the app
|
||||
- update translations using ``tx pull -af`` (as extra merge request or branch for the case it does not build correctly)
|
||||
- update the version name and version code of the app [here](https://github.com/syncthing/syncthing-lite/blob/master/app/build.gradle)
|
||||
- update the changelog at [app/src/main/play/en-GB/whatsnew](https://github.com/syncthing/syncthing-lite/blob/master/app/src/main/play/en-GB/whatsnew)
|
||||
- create a tag/ release in GitHub with an changelog; The tag name should be the version number
|
||||
- F-Droid picks up the release by the tag; additonally, the tag triggers a CI build which uploads the generated APK to Google Play
|
||||
- trigger a release at <https://build.syncthing.net/> to publish the release to google play
|
||||
- F-Droid picks up the release by the tag
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
- fixing bugs and crashs
|
||||
- just create a issue WITH a detailed crash report (not: it does not work)
|
||||
- search if there is an other issue for it before creating a new one
|
||||
- showing more details in the UI (<https://github.com/syncthing/syncthing-lite/issues/44>)
|
||||
- add option to manually select the IP address of an device (<https://github.com/syncthing/syncthing-lite/issues/25>)
|
||||
- allow custom discovery servers or disabling device discovery (<https://github.com/syncthing/syncthing-lite/issues/105>)
|
||||
- downloading all files of an folder (<https://github.com/syncthing/syncthing-lite/issues/34>)
|
||||
- selective folder sharing (<https://github.com/syncthing/syncthing-lite/issues/17>)
|
||||
- better server offline handling (<https://github.com/syncthing/syncthing-lite/issues/63>)
|
||||
- file uploading support (it currently does not work) <https://github.com/syncthing/syncthing-lite/issues/70>
|
||||
|
||||
|
||||
+3
-13
@@ -6,7 +6,6 @@ apply plugin: 'com.github.triplet.play'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "28.0.2"
|
||||
dataBinding.enabled = true
|
||||
|
||||
playAccountConfigs {
|
||||
@@ -19,8 +18,8 @@ android {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 26
|
||||
versionCode 17
|
||||
versionName "0.3.7"
|
||||
versionCode 22
|
||||
versionName "0.3.12"
|
||||
multiDexEnabled true
|
||||
playAccountConfig = playAccountConfigs.defaultAccountConfig
|
||||
}
|
||||
@@ -77,16 +76,6 @@ dependencies {
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
implementation 'android.arch.lifecycle:extensions:1.1.1'
|
||||
|
||||
/**
|
||||
* syncthing-java depends on the Apache HTTP Client
|
||||
* https://github.com/syncthing/syncthing-java/blob/dd020737ba5fc6a7c681a1d258025b8ddb2e8f67/core/build.gradle#L9
|
||||
*
|
||||
* Android itself contains an older version of this HTTP Client. Due to that, there is an
|
||||
* extra version of it which does not cause conflicts with the builtin client of Android.
|
||||
*
|
||||
* This extra implementation is included below. As this other version is used,
|
||||
* it's ignored as dependency of syncthing-java.
|
||||
*/
|
||||
implementation(project(':syncthing-client')) {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.slf4j'
|
||||
@@ -99,4 +88,5 @@ dependencies {
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.3'
|
||||
|
||||
implementation project(':syncthing-repository-android')
|
||||
implementation project(':syncthing-temp-repository-encryption')
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
@@ -17,6 +19,7 @@ import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.FolderContentsAdapter
|
||||
import net.syncthing.lite.adapters.FolderContentsListener
|
||||
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
|
||||
import net.syncthing.lite.dialogs.EnableFolderSyncForNewDeviceDialog
|
||||
import net.syncthing.lite.dialogs.FileMenuDialogFragment
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
import net.syncthing.lite.dialogs.ReconnectIssueDialogFragment
|
||||
@@ -109,6 +112,29 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
launch {
|
||||
val devicesToAskFor = libraryHandler.libraryManager.withLibrary {
|
||||
val folderInfo = it.configuration.folders.find { it.folderId == folder }
|
||||
val notIgnoredBlacklistEntries = folderInfo?.notIgnoredBlacklistEntries ?: emptySet()
|
||||
|
||||
notIgnoredBlacklistEntries.mapNotNull { deviceId ->
|
||||
it.configuration.peers.find { peer -> peer.deviceId == deviceId }
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesToAskFor.isNotEmpty()) {
|
||||
EnableFolderSyncForNewDeviceDialog.newInstance(
|
||||
folderId = folder,
|
||||
devices = devicesToAskFor,
|
||||
folderName = libraryHandler.libraryManager.withLibrary {
|
||||
it.configuration.folders.find { it.folderId == folder }?.label ?: folder
|
||||
}
|
||||
).show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -160,4 +186,26 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.folder_browser, menu)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.go_home -> {
|
||||
finish()
|
||||
|
||||
true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
if (!goUp()) {
|
||||
finish()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.github.paolorotolo.appintro.ISlidePolicy
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
@@ -40,14 +40,6 @@ class IntroActivity : AppIntro() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Disable continue button on second slide until a valid device ID is entered.
|
||||
nextButton.setOnClickListener {
|
||||
val fragment = fragments[pager.currentItem]
|
||||
if (fragment !is IntroFragmentTwo || fragment.isDeviceIdValid()) {
|
||||
pager.goToNextSlide()
|
||||
}
|
||||
}
|
||||
|
||||
addSlide(IntroFragmentOne())
|
||||
addSlide(IntroFragmentTwo())
|
||||
addSlide(IntroFragmentThree())
|
||||
@@ -72,6 +64,19 @@ class IntroActivity : AppIntro() {
|
||||
* Display some simple welcome text.
|
||||
*/
|
||||
class IntroFragmentOne : SyncthingFragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
library.configuration.update { oldConfig ->
|
||||
oldConfig.copy(localDeviceName = Util.getDeviceName())
|
||||
}
|
||||
|
||||
library.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentIntroOneBinding.inflate(inflater, container, false)
|
||||
@@ -80,21 +85,12 @@ class IntroActivity : AppIntro() {
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
|
||||
libraryHandler.configuration { config ->
|
||||
config.localDeviceName = Util.getDeviceName()
|
||||
config.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display device ID entry field and QR scanner option.
|
||||
*/
|
||||
class IntroFragmentTwo : SyncthingFragment() {
|
||||
class IntroFragmentTwo : SyncthingFragment(), ISlidePolicy {
|
||||
|
||||
private lateinit var binding: FragmentIntroTwoBinding
|
||||
|
||||
@@ -122,7 +118,7 @@ class IntroActivity : AppIntro() {
|
||||
fun isDeviceIdValid(): Boolean {
|
||||
return try {
|
||||
val deviceId = binding.enterDeviceId.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { })
|
||||
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { })
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
binding.enterDeviceId.deviceId.error = getString(R.string.invalid_device_id)
|
||||
@@ -130,6 +126,12 @@ class IntroActivity : AppIntro() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPolicyRespected() = isDeviceIdValid()
|
||||
|
||||
override fun onUserIllegallyRequestedNextPage() {
|
||||
// nothing to do, but some user feedback would be nice
|
||||
}
|
||||
|
||||
private val addedDeviceIds = HashSet<DeviceId>()
|
||||
|
||||
override fun onResume() {
|
||||
@@ -176,32 +178,31 @@ class IntroActivity : AppIntro() {
|
||||
*/
|
||||
class IntroFragmentThree : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentIntroThreeBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
|
||||
val binding = FragmentIntroThreeBinding.inflate(inflater, container, false)
|
||||
|
||||
libraryHandler.library { config, client, _ ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
client.addOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
val deviceId = config.localDeviceId.deviceId
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
launch {
|
||||
val ownDeviceId = libraryHandler.libraryManager.withLibrary { it.configuration.localDeviceId }
|
||||
|
||||
libraryHandler.subscribeToConnectionStatus().consumeEach {
|
||||
if (it.values.find { it.addresses.isNotEmpty() } != null) {
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$ownDeviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
} else {
|
||||
binding.description.text = getString(R.string.intro_page_three_searching_device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
libraryHandler.subscribeToFolderStatusList().consumeEach {
|
||||
if (it.isNotEmpty()) {
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
libraryHandler.library { config, client, _ ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
if (config.folders.isNotEmpty()) {
|
||||
client.removeOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
|
||||
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,10 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
it.syncthingClient.clearCacheAndIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ package net.syncthing.lite.adapters
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionStatus
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewDeviceBinding
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
var data: List<DeviceInfo> by Delegates.observable(listOf()) {
|
||||
var data: List<Pair<DeviceInfo, ConnectionInfo>> by Delegates.observable(listOf()) {
|
||||
_, _, _ -> notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@@ -19,7 +22,7 @@ class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
override fun getItemId(position: Int) = data[position].deviceId.deviceId.hashCode().toLong()
|
||||
override fun getItemId(position: Int) = data[position].first.deviceId.deviceId.hashCode().toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DeviceViewHolder(
|
||||
ListviewDeviceBinding.inflate(
|
||||
@@ -28,13 +31,23 @@ class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
|
||||
val deviceStats = data[position]
|
||||
val binding = holder.binding
|
||||
val context = binding.root.context
|
||||
val (deviceInfo, connectionInfo) = data[position]
|
||||
|
||||
binding.name = deviceStats.name
|
||||
binding.isConnected = deviceStats.isConnected
|
||||
binding.name = deviceInfo.name
|
||||
binding.isConnected = connectionInfo.status == ConnectionStatus.Connected
|
||||
|
||||
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceStats) ?: false }
|
||||
binding.status = when (connectionInfo.status) {
|
||||
ConnectionStatus.Connected -> context.getString(R.string.device_status_connected, connectionInfo.currentAddress?.address)
|
||||
ConnectionStatus.Connecting -> context.getString(R.string.device_status_connecting, connectionInfo.currentAddress?.address)
|
||||
ConnectionStatus.Disconnected -> if (connectionInfo.addresses.isEmpty())
|
||||
context.getString(R.string.device_status_no_address)
|
||||
else
|
||||
context.getString(R.string.device_status_disconnected, connectionInfo.addresses.size)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener { listener?.onDeviceLongClicked(deviceInfo) ?: false }
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
@@ -44,4 +57,4 @@ interface DeviceAdapterListener {
|
||||
fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean
|
||||
}
|
||||
|
||||
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
|
||||
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@@ -55,6 +55,10 @@ class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
|
||||
binding.root.setOnClickListener {
|
||||
listener?.onFolderClicked(folderInfo, folderStats)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
listener?.onFolderLongClicked(folderInfo) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +66,5 @@ class FolderListViewHolder(val binding: ListviewFolderBinding): RecyclerView.Vie
|
||||
|
||||
interface FolderListAdapterListener {
|
||||
fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats)
|
||||
fun onFolderLongClicked(folderInfo: FolderInfo): Boolean
|
||||
}
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
package net.syncthing.lite.android
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
|
||||
class Application: Application() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "Application"
|
||||
private const val PREF_ENABLE_CRASH_HANDLER = "crash_handler"
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
val mainThread = Thread.currentThread()
|
||||
|
||||
@@ -33,18 +25,10 @@ class Application: Application() {
|
||||
fun handleCrash(ex: Throwable) {
|
||||
Log.w(LOG_TAG, "app crashed", ex)
|
||||
|
||||
val enableCustomCrashHandling = defaultSharedPreferences.getBoolean(PREF_ENABLE_CRASH_HANDLER, false)
|
||||
|
||||
if (enableCustomCrashHandling) {
|
||||
clipboard.primaryClip = ClipData.newPlainText(
|
||||
"stacktrace",
|
||||
StringWriter().apply {
|
||||
append("Version: ").append(BuildConfig.VERSION_NAME).append('\n')
|
||||
append(Log.getStackTraceString(ex)).append('\n')
|
||||
ex.printStackTrace(PrintWriter(this))
|
||||
}.buffer.toString()
|
||||
)
|
||||
}
|
||||
ErrorStorage.reportError(
|
||||
this,
|
||||
Log.getStackTraceString(ex)
|
||||
)
|
||||
|
||||
if (defaultHandler != null) {
|
||||
defaultHandler.uncaughtException(mainThread, ex)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.syncthing.lite.async
|
||||
|
||||
import android.support.v4.app.DialogFragment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class CoroutineDialogFragment: DialogFragment(), CoroutineScope {
|
||||
val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
|
||||
class EnableFolderSyncForNewDeviceDialog: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val FOLDER_NAME = "folderName"
|
||||
private const val DEVICES = "devices"
|
||||
private const val STATUS_CURRENT_DEVICE_ID = "currentDeviceId"
|
||||
|
||||
private const val TAG = "EnableFolderSyncForNewDeviceDialog"
|
||||
|
||||
fun newInstance(folderId: String, folderName: String, devices: List<DeviceInfo>) = EnableFolderSyncForNewDeviceDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(FOLDER_ID, folderId)
|
||||
putString(FOLDER_NAME, folderName)
|
||||
putSerializable(DEVICES, ArrayList<DeviceInfo>(devices))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentDeviceId = 0
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val folderId = arguments!!.getString(FOLDER_ID)
|
||||
val folderName = arguments!!.getString(FOLDER_NAME)
|
||||
val devices = arguments!!.getSerializable(DEVICES) as ArrayList<DeviceInfo>
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
currentDeviceId = savedInstanceState.getInt(STATUS_CURRENT_DEVICE_ID)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.dialog_enable_folder_sync_for_new_device_title)
|
||||
.setMessage(R.string.dialog_enable_folder_sync_for_new_device_text)
|
||||
.setPositiveButton(R.string.dialog_enable_folder_sync_for_new_device_positive, null)
|
||||
.setNegativeButton(R.string.dialog_enable_folder_sync_for_new_device_negative, null)
|
||||
.create()
|
||||
|
||||
fun bindDeviceId() {
|
||||
if (currentDeviceId >= devices.size) {
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
val device = devices[currentDeviceId]
|
||||
|
||||
dialog.setMessage(getString(
|
||||
R.string.dialog_enable_folder_sync_for_new_device_text,
|
||||
folderName,
|
||||
device.name,
|
||||
device.deviceId.deviceId
|
||||
))
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
|
||||
|
||||
it.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
|
||||
oldFolderEntry.copy(
|
||||
deviceIdWhitelist = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId),
|
||||
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist - setOf(device.deviceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.syncthingClient.reconnect(device.deviceId)
|
||||
it.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
currentDeviceId++
|
||||
bindDeviceId()
|
||||
}
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
|
||||
GlobalScope.launch {
|
||||
libraryHandler.libraryManager.withLibrary {
|
||||
val oldFolderEntry = it.configuration.folders.find { it.folderId == folderId }!!
|
||||
|
||||
it.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
folders = oldConfig.folders.filter { it != oldFolderEntry }.toSet() + setOf(
|
||||
oldFolderEntry.copy(
|
||||
ignoredDeviceIdList = oldFolderEntry.deviceIdWhitelist + setOf(device.deviceId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it.syncthingClient.reconnect(device.deviceId)
|
||||
it.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
currentDeviceId++
|
||||
bindDeviceId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.setOnShowListener { bindDeviceId() }
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(STATUS_CURRENT_DEVICE_ID, currentDeviceId)
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.widget.Toast
|
||||
import net.syncthing.lite.R
|
||||
|
||||
class ErrorReportDialog: DialogFragment() {
|
||||
companion object {
|
||||
private const val REPORT = "report"
|
||||
private const val TAG = "ErrorReportDialog"
|
||||
|
||||
fun newInstance(report: String) = ErrorReportDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(REPORT, report)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val report = arguments!!.getString(REPORT)
|
||||
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
return AlertDialog.Builder(context!!)
|
||||
.setTitle(R.string.settings_last_error_title)
|
||||
.setMessage(report)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.copy_to_clipboard, null)
|
||||
.create()
|
||||
.apply {
|
||||
setOnShowListener {
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||
clipboard.primaryClip = ClipData.newPlainText(
|
||||
context!!.getString(R.string.settings_last_error_title),
|
||||
report
|
||||
)
|
||||
|
||||
Toast.makeText(context, context!!.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
@@ -8,12 +8,11 @@ import android.support.v4.app.FragmentManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.databinding.DialogFileBinding
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
|
||||
import net.syncthing.lite.dialogs.downloadfile.DownloadFileSpec
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
|
||||
class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
@@ -48,9 +47,7 @@ class FileMenuDialogFragment: BottomSheetDialogFragment() {
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
FilenameUtils.getExtension(fileSpec.fileName)
|
||||
)
|
||||
type = MimeType.getFromFilename(fileSpec.fileName)
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.AppCompatCheckBox
|
||||
import android.view.LayoutInflater
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogFolderInfoBinding
|
||||
import net.syncthing.lite.fragments.SyncthingDialogFragment
|
||||
|
||||
class FolderInfoDialog: SyncthingDialogFragment() {
|
||||
companion object {
|
||||
fun newInstance(folderId: String) = FolderInfoDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(FOLDER_ID, folderId)
|
||||
}
|
||||
}
|
||||
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val TAG = "FolderInfoDialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val folderId = arguments!!.getString(FOLDER_ID)
|
||||
val binding = DialogFolderInfoBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!)
|
||||
.setTitle(folderId)
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
launch {
|
||||
val configuration = libraryHandler.libraryManager.withLibrary { it.configuration }
|
||||
|
||||
val folderInfo = configuration.folders.find { it.folderId == folderId }
|
||||
|
||||
if (folderInfo == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return@launch
|
||||
}
|
||||
|
||||
dialog.setTitle(folderInfo.label)
|
||||
|
||||
binding.deviceCheckboxesContainer.removeAllViews()
|
||||
|
||||
val allRelatedDevices = (folderInfo.deviceIdWhitelist + folderInfo.deviceIdBlacklist).toSet()
|
||||
|
||||
allRelatedDevices.forEach { deviceId ->
|
||||
val deviceInfo = configuration.peers.find { it.deviceId == deviceId }
|
||||
|
||||
val deviceLabel = if (deviceInfo == null)
|
||||
deviceId.deviceId
|
||||
else
|
||||
context!!.getString(R.string.dialog_folder_info_device_list_item, deviceInfo.name, deviceId.deviceId)
|
||||
|
||||
binding.deviceCheckboxesContainer.addView(
|
||||
AppCompatCheckBox(context!!).apply {
|
||||
text = deviceLabel
|
||||
isChecked = folderInfo.deviceIdWhitelist.contains(deviceId)
|
||||
|
||||
setOnCheckedChangeListener { _, isShared ->
|
||||
this@FolderInfoDialog.launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
// update the config
|
||||
library.configuration.update { oldConfig ->
|
||||
val oldFolders = oldConfig.folders
|
||||
var folderToChange = oldFolders.find { it.folderId == folderId }!!
|
||||
val foldersNotToChange = oldFolders.filterNot { it.folderId == folderId }.toSet()
|
||||
|
||||
if (isShared) {
|
||||
folderToChange = folderToChange.copy(
|
||||
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdBlacklist = folderToChange.deviceIdBlacklist.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdWhitelist = folderToChange.deviceIdWhitelist + setOf(deviceId)
|
||||
)
|
||||
} else {
|
||||
folderToChange = folderToChange.copy(
|
||||
deviceIdWhitelist = folderToChange.deviceIdWhitelist.filterNot { it == deviceId }.toSet(),
|
||||
deviceIdBlacklist = folderToChange.deviceIdBlacklist + setOf(deviceId),
|
||||
ignoredDeviceIdList = folderToChange.ignoredDeviceIdList + setOf(deviceId)
|
||||
)
|
||||
}
|
||||
|
||||
oldConfig.copy(folders = foldersNotToChange + folderToChange)
|
||||
}
|
||||
library.configuration.persistLater()
|
||||
|
||||
// apply the change
|
||||
library.syncthingClient.reconnect(deviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = show(fragmentManager, TAG)
|
||||
}
|
||||
+2
-3
@@ -12,13 +12,12 @@ import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.CacheFileProviderUrl
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
import org.jetbrains.anko.newTask
|
||||
import org.jetbrains.anko.toast
|
||||
|
||||
@@ -89,7 +88,7 @@ class DownloadFileDialogFragment : DialogFragment() {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
if (outputUri == null) {
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
|
||||
val mimeType = MimeType.getFromFilename(fileSpec.fileName)
|
||||
|
||||
try {
|
||||
context!!.startActivity(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.syncthing.lite.error
|
||||
|
||||
import android.content.Context
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
object ErrorStorage {
|
||||
private const val PREF_KEY = "last_error"
|
||||
|
||||
fun reportError(context: Context, error: String) {
|
||||
// this uses commit because the App could be quit directly after that
|
||||
context.defaultSharedPreferences.edit()
|
||||
.putString(PREF_KEY, error)
|
||||
.commit()
|
||||
}
|
||||
|
||||
fun getLastErrorReport(context: Context) = context.defaultSharedPreferences.getString(PREF_KEY, "there is no saved report")
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.DeviceAdapterListener
|
||||
@@ -34,25 +34,7 @@ class DevicesFragment : SyncthingFragment() {
|
||||
savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_devices, container, false)
|
||||
binding.addDevice.setOnClickListener { showDialog() }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
libraryHandler.syncthingClient { it.addOnConnectionChangedListener { _ -> updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
libraryHandler.syncthingClient { it.removeOnConnectionChangedListener{ _ -> updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
initDeviceList()
|
||||
updateDeviceList()
|
||||
}
|
||||
|
||||
private fun initDeviceList() {
|
||||
binding.list.adapter = adapter
|
||||
|
||||
adapter.listener = object: DeviceAdapterListener {
|
||||
@@ -61,12 +43,21 @@ class DevicesFragment : SyncthingFragment() {
|
||||
.setTitle(getString(R.string.remove_device_title, deviceInfo.name))
|
||||
.setMessage(getString(R.string.remove_device_message, deviceInfo.deviceId.deviceId.substring(0, 7)))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
libraryHandler.library { config, syncthingClient, _ ->
|
||||
config.peers = config.peers.filterNot { it.deviceId == deviceInfo.deviceId }.toSet()
|
||||
config.persistLater()
|
||||
updateDeviceList()
|
||||
launch {
|
||||
libraryHandler.libraryManager.withLibrary { library ->
|
||||
library.configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers
|
||||
.filterNot { it.deviceId == deviceInfo.deviceId }
|
||||
.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
syncthingClient.disconnectFromRemovedDevices()
|
||||
library.configuration.persistLater()
|
||||
|
||||
// TODO: update the device list (should become a side effect of the call below)
|
||||
library.syncthingClient.disconnectFromRemovedDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
@@ -75,15 +66,17 @@ class DevicesFragment : SyncthingFragment() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDeviceList() {
|
||||
libraryHandler.syncthingClient { syncthingClient ->
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
adapter.data = syncthingClient.getPeerStatus()
|
||||
binding.isEmpty = adapter.data.isEmpty()
|
||||
launch {
|
||||
libraryHandler.subscribeToConnectionStatus().consumeEach { connectionInfo ->
|
||||
val devices = libraryHandler.libraryManager.withLibrary { it.configuration.peers }
|
||||
|
||||
adapter.data = devices.map { device -> device to (connectionInfo[device.deviceId] ?: ConnectionInfo.empty) }
|
||||
binding.isEmpty = devices.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
@@ -94,35 +87,38 @@ class DevicesFragment : SyncthingFragment() {
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
addDeviceDialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.view_enter_device_id, null, false)
|
||||
addDeviceDialogBinding?.let { binding ->
|
||||
binding.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
}
|
||||
binding.deviceId.post {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
val binding = ViewEnterDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
addDeviceDialogBinding = binding
|
||||
|
||||
addDeviceDialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
addDeviceDialog?.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
?.setOnClickListener {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { updateDeviceList() })
|
||||
addDeviceDialog?.dismiss()
|
||||
} catch (e: IOException) {
|
||||
binding.deviceId.error = getString(R.string.invalid_device_id)
|
||||
}
|
||||
}
|
||||
binding.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
}
|
||||
binding.deviceId.post {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
addDeviceDialog = dialog
|
||||
|
||||
fun handleAddClick() {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { /* TODO: Is updateDeviceList() still required? */ })
|
||||
dialog.dismiss()
|
||||
} catch (e: IOException) {
|
||||
binding.deviceId.error = getString(R.string.invalid_device_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)!!.setOnClickListener { handleAddClick() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
import net.syncthing.lite.adapters.FolderListAdapterListener
|
||||
import net.syncthing.lite.adapters.FoldersListAdapter
|
||||
import net.syncthing.lite.databinding.FragmentFoldersBinding
|
||||
import net.syncthing.lite.dialogs.FolderInfoDialog
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class FoldersFragment : SyncthingFragment() {
|
||||
@@ -27,6 +28,14 @@ class FoldersFragment : SyncthingFragment() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFolderLongClicked(folderInfo: FolderInfo): Boolean {
|
||||
FolderInfoDialog
|
||||
.newInstance(folderId = folderInfo.folderId)
|
||||
.show(fragmentManager!!)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
import net.syncthing.lite.dialogs.ErrorReportDialog
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
import net.syncthing.lite.library.DefaultLibraryManager
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@@ -14,25 +21,47 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
val localDeviceName = findPreference("local_device_name") as EditTextPreference
|
||||
val appVersion = findPreference("app_version")
|
||||
val forceStop = findPreference("force_stop")
|
||||
val lastCrash = findPreference("last_crash")
|
||||
val reportBug = findPreference("report_bug")
|
||||
val libraryManager = DefaultLibraryManager.with(context!!)
|
||||
|
||||
(activity as SyncthingActivity?)?.let { activity ->
|
||||
val versionName = activity.packageManager.getPackageInfo(activity.packageName, 0)?.versionName
|
||||
appVersion.summary = versionName
|
||||
|
||||
activity.libraryHandler.configuration { localDeviceName.text = it.localDeviceName }
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
activity.libraryHandler.configuration { conf ->
|
||||
conf.localDeviceName = localDeviceName.text
|
||||
conf.persistLater()
|
||||
}
|
||||
true
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryManager.withLibrary { library ->
|
||||
localDeviceName.text = library.configuration.localDeviceName
|
||||
}
|
||||
}
|
||||
|
||||
appVersion.summary = context!!.packageManager.getPackageInfo(context!!.packageName, 0)?.versionName
|
||||
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
val newDeviceName = localDeviceName.text
|
||||
|
||||
GlobalScope.launch {
|
||||
libraryManager.withLibrary { library ->
|
||||
library.configuration.update { it.copy(localDeviceName = newDeviceName) }
|
||||
library.configuration.persistLater()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
forceStop.setOnPreferenceClickListener {
|
||||
System.exit(0)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
lastCrash.setOnPreferenceClickListener {
|
||||
ErrorReportDialog.newInstance(ErrorStorage.getLastErrorReport(context!!)).show(fragmentManager!!)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
reportBug.setOnPreferenceClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/syncthing/syncthing-lite/issues")))
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.support.v4.app.DialogFragment
|
||||
import net.syncthing.lite.async.CoroutineDialogFragment
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
|
||||
abstract class SyncthingDialogFragment : DialogFragment() {
|
||||
abstract class SyncthingDialogFragment : CoroutineDialogFragment() {
|
||||
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
|
||||
context = context!!
|
||||
)}
|
||||
|
||||
@@ -4,8 +4,10 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.error.ErrorStorage
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
|
||||
object DefaultLibraryManager {
|
||||
@@ -39,7 +41,16 @@ object DefaultLibraryManager {
|
||||
}
|
||||
|
||||
instance = LibraryManager(
|
||||
synchronousInstanceCreator = { LibraryInstance(context) },
|
||||
synchronousInstanceCreator = {
|
||||
LibraryInstance(context) { ex ->
|
||||
// this delay ensures that the toast is shown even if the UI thread is busy
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, R.string.toast_error, Toast.LENGTH_LONG).show()
|
||||
}, 100L)
|
||||
|
||||
ErrorStorage.reportError(context, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
|
||||
}
|
||||
},
|
||||
userCounterListener = {
|
||||
newUserCounter ->
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.bep.folder.FolderBrowser
|
||||
import net.syncthing.java.bep.folder.FolderStatus
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
@@ -37,6 +39,7 @@ class LibraryHandler(context: Context) {
|
||||
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { postValue(false) }
|
||||
private val indexUpdateCompleteMessages = BroadcastChannel<String>(capacity = 16)
|
||||
private val folderStatusList = BroadcastChannel<List<FolderStatus>>(capacity = Channel.CONFLATED)
|
||||
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>()
|
||||
private var job: Job = Job()
|
||||
|
||||
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
|
||||
@@ -78,6 +81,12 @@ class LibraryHandler(context: Context) {
|
||||
folderStatusList.send(it)
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
libraryInstance.syncthingClient.subscribeToConnectionStatus().consumeEach {
|
||||
connectionStatus.send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +148,5 @@ class LibraryHandler(context: Context) {
|
||||
|
||||
fun subscribeToOnFullIndexAcquiredEvents() = indexUpdateCompleteMessages.openSubscription()
|
||||
fun subscribeToFolderStatusList() = folderStatusList.openSubscription()
|
||||
fun subscribeToConnectionStatus() = connectionStatus.openSubscription()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.repository.EncryptedTempRepository
|
||||
import net.syncthing.repository.android.SqliteIndexRepository
|
||||
import net.syncthing.repository.android.TempDirectoryLocalRepository
|
||||
import net.syncthing.repository.android.database.RepositoryDatabase
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import java.io.File
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
@@ -22,7 +26,10 @@ import java.net.SocketException
|
||||
*
|
||||
* The creation and the shutdown are synchronous, so keep them out of the UI Thread
|
||||
*/
|
||||
class LibraryInstance (context: Context) {
|
||||
class LibraryInstance (
|
||||
context: Context,
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "LibraryInstance"
|
||||
|
||||
@@ -43,7 +50,11 @@ class LibraryInstance (context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val tempRepository = TempDirectoryLocalRepository(File(context.filesDir, "temp_repository"))
|
||||
private val tempRepository = EncryptedTempRepository(
|
||||
TempDirectoryLocalRepository(
|
||||
File(context.filesDir, "temp_repository")
|
||||
)
|
||||
)
|
||||
|
||||
val isListeningPortTaken = checkIsListeningPortTaken() // this must come first to work correctly
|
||||
val configuration = Configuration(configFolder = context.filesDir)
|
||||
@@ -52,15 +63,21 @@ class LibraryInstance (context: Context) {
|
||||
repository = SqliteIndexRepository(
|
||||
database = RepositoryDatabase.with(context),
|
||||
closeDatabaseOnClose = false,
|
||||
clearTempStorageHook = { tempRepository.deleteAllData() }
|
||||
clearTempStorageHook = { tempRepository.deleteAllTempData() }
|
||||
),
|
||||
tempRepository = tempRepository,
|
||||
enableDetailedException = context.defaultSharedPreferences.getBoolean("detailed_exception", false)
|
||||
exceptionReportHandler = { ex ->
|
||||
Log.w(LOG_TAG, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
exceptionReportHandler(ex)
|
||||
}
|
||||
}
|
||||
)
|
||||
val folderBrowser = syncthingClient.indexHandler.folderBrowser
|
||||
val indexBrowser = syncthingClient.indexHandler.indexBrowser
|
||||
|
||||
fun shutdown() {
|
||||
suspend fun shutdown() {
|
||||
syncthingClient.close()
|
||||
configuration.persistNow()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
@@ -93,7 +94,7 @@ class LibraryManager (
|
||||
fun shutdownIfThereAreZeroUsers(listener: (wasShutdownPerformed: Boolean) -> Unit = {}) {
|
||||
startStopExecutor.submit {
|
||||
if (userCounter == 0) {
|
||||
instanceStream.value?.shutdown()
|
||||
runBlocking { instanceStream.value?.shutdown() }
|
||||
instanceStream.offer(null)
|
||||
|
||||
handler.post { isRunningListener(false) }
|
||||
|
||||
@@ -15,8 +15,8 @@ import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.utils.MimeType
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URLConnection
|
||||
|
||||
class SyncthingProvider : DocumentsProvider() {
|
||||
|
||||
@@ -159,7 +159,7 @@ class SyncthingProvider : DocumentsProvider() {
|
||||
if (fileInfo.isDirectory())
|
||||
Document.MIME_TYPE_DIR
|
||||
else
|
||||
URLConnection.guessContentTypeFromName(fileInfo.fileName)
|
||||
MimeType.getFromFilename(fileInfo.fileName)
|
||||
)
|
||||
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
|
||||
add(Document.COLUMN_FLAGS, 0)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
|
||||
object MimeType {
|
||||
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
|
||||
|
||||
private fun getFromExtension(extension: String): String {
|
||||
val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
|
||||
return mimeType ?: DEFAULT_MIME_TYPE
|
||||
}
|
||||
|
||||
fun getFromFilename(path: String) = getFromExtension(
|
||||
PathUtils.getFileExtensionFromFilename(path).toLowerCase()
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import net.syncthing.lite.library.LibraryManager
|
||||
import org.apache.commons.lang3.StringUtils.capitalize
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.IOException
|
||||
@@ -23,11 +23,11 @@ object Util {
|
||||
val manufacturer = Build.MANUFACTURER ?: ""
|
||||
val model = Build.MODEL ?: ""
|
||||
val deviceName =
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
} else {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
} else {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
return deviceName ?: "android"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,34 @@ object Util {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun importDeviceId(libraryHandler: LibraryHandler?, context: Context?, deviceId: String,
|
||||
onComplete: () -> Unit) {
|
||||
val deviceId2 = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
libraryHandler?.library { configuration, syncthingClient, _ ->
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
syncthingClient.connectToNewlyAddedDevices()
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
|
||||
fun importDeviceId(libraryManager: LibraryManager, context: Context, deviceId: String, onComplete: () -> Unit) {
|
||||
val newDeviceId = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
libraryManager.withLibrary { library ->
|
||||
val didAddDevice = library.configuration.update { oldConfig ->
|
||||
if (oldConfig.peers.find { it.deviceId == newDeviceId } != null) {
|
||||
// already known
|
||||
|
||||
oldConfig
|
||||
} else {
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers + DeviceInfo(newDeviceId, newDeviceId.shortId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (didAddDevice) {
|
||||
library.configuration.persistLater()
|
||||
library.syncthingClient.connectToNewlyAddedDevices()
|
||||
|
||||
context.toast(context.getString(R.string.device_import_success, newDeviceId.shortId))
|
||||
onComplete()
|
||||
} else {
|
||||
context.toast(context.getString(R.string.device_already_known, newDeviceId.shortId))
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch (Dispatchers.Main) {
|
||||
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
- fix crash after launch in release builds
|
||||
- update translations
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/dialog_folder_info_device_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/device_checkboxes_container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<!--
|
||||
<CheckBox
|
||||
android:text="Test device 1 (the very very very very very very very very very very very long id)"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<CheckBox
|
||||
android:text="Test device 2 (the very very very very very very very very very very very long id)"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -7,46 +7,59 @@
|
||||
name="name"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="status"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="isConnected"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:background="?selectableItemBackground"
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="12dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
tools:src="@drawable/ic_laptop_green_24dp"
|
||||
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"/>
|
||||
android:src="@{safeUnbox(isConnected) ? @drawable/ic_laptop_green_24dp : @drawable/ic_laptop_red_24dp}" />
|
||||
|
||||
<TextView
|
||||
tools:text="Computer"
|
||||
android:text="@{name}"
|
||||
android:id="@+id/device_name"
|
||||
<View
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_alignParentTop="true"
|
||||
android:gravity="top"
|
||||
android:textAlignment="gravity"
|
||||
android:paddingStart="40dp"
|
||||
android:paddingEnd="40dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
tools:text="Computer"
|
||||
android:text="@{name}"
|
||||
android:id="@+id/device_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="gravity"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:text="@{status}"
|
||||
tools:text="Trying to connect to 127.0.0.1 ..."
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
app:showAsAction="always"
|
||||
android:icon="@drawable/ic_home_white_24dp"
|
||||
android:id="@+id/go_home"
|
||||
android:title="@string/folder_browser_home" />
|
||||
</menu>
|
||||
@@ -0,0 +1,60 @@
|
||||
<resources>
|
||||
<string name="folder_list_empty_message">Няма папки</string>
|
||||
<string name="clear_local_cache_index_label">Изчисти кеша/индекса</string>
|
||||
<string name="devices_list_view_empty_message">Няма устройства</string>
|
||||
<string name="invalid_device_id">Грешка: Невалиден идентификатор</string>
|
||||
<string name="dialog_downloading_file">Сваляне на файла %1$s</string>
|
||||
<string name="toast_file_download_failed">Свалянето на файла се провали</string>
|
||||
<string name="toast_open_file_failed">Не са намерени съвместими приложения</string>
|
||||
<string name="toast_file_upload_failed">Качването на файла се провали</string>
|
||||
<string name="toast_upload_complete">Файлът е качен</string>
|
||||
<string name="dialog_uploading_file">Качване на файла %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Изчистване на локалния кеш/индекс?</string>
|
||||
<string name="clear_cache_and_index_body">Да бъдат ли изтрити всички локално кеширани данни и индекси?</string>
|
||||
<string name="loading_config_starting_syncthing_client">Зареждане на настройките, стартиране на Syncthing клиента...</string>
|
||||
<string name="last_modified_time">Последна промяна: %1$s</string>
|
||||
<string name="remove_device_title">Изтриване на устройството %1$s\?</string>
|
||||
<string name="remove_device_message">Да бъде ли премахнато устройството %1$s от списъка с познати устройства?</string>
|
||||
<string name="device_import_success">Устройството %1$s е внесено успешно</string>
|
||||
<string name="device_already_known">Устройството %1$s вече съществува</string>
|
||||
<string name="folders_label">Папки</string>
|
||||
<string name="devices_label">Устройства</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d файла, %3$d директории</string>
|
||||
<string name="file_info">%1$s, последна промяна %2$s</string>
|
||||
<string name="show_device_id">Идентификатор на устройството</string>
|
||||
<string name="device_id">Идентификатор на устройството</string>
|
||||
<string name="device_id_copied">Идентификатора на устройството е копиран</string>
|
||||
<string name="share_device_id_chooser">Споделяне на идентификатор чрез</string>
|
||||
<string name="other_syncthing_instance_title">Syncthing вече е стартиран</string>
|
||||
<string name="other_syncthing_instance_message">Локалното откриване няма да работи. За да сработи е необходимо да спрете друго стартирано Syncthing приложение.</string>
|
||||
<string name="intro_page_one_title">Добре дошли в Syncthing Lite</string>
|
||||
<string name="intro_page_two_title">Добавяне на устройство</string>
|
||||
<string name="intro_page_three_title">Споделете папки</string>
|
||||
<string name="intro_page_two_description">Въведете Syncthing идентификатор на устройство или го сканирайте от QR код</string>
|
||||
<string name="intro_page_three_searching_device">Търсене за други устройства. Може да отнеме известно време.</string>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="settings_app_version_title">Версия на приложението</string>
|
||||
<string name="settings_local_device_name">Име на устройството</string>
|
||||
<string name="settings_local_device_summary">Въведеното име ще бъде ползвано за представяне пред други устройства</string>
|
||||
<string name="settings_force_stop">Принудително спиране на приложението</string>
|
||||
<string name="settings_last_error_title">Последна грешка</string>
|
||||
<string name="settings_last_error_summary">Прегледайте подробностите за последната грешка</string>
|
||||
<string name="copy_to_clipboard">Копирай</string>
|
||||
<string name="copied_to_clipboard">Копирано в клипборда</string>
|
||||
<string name="device_id_dialog_title">Въведете идентификатор на устройство</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 секунди</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 секунди</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 минута</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 минути</string>
|
||||
<string name="dialog_file_save_as">Запиши като</string>
|
||||
<string name="device_status_connecting">Свързване с %s</string>
|
||||
<string name="device_status_connected">Осъществена връзка с %s</string>
|
||||
<string name="device_status_disconnected">Скоро ще бъде направен опит за свързване - %d известни адреса</string>
|
||||
<string name="device_status_no_address">Устройството няма известен адрес</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Желаете ли да синхронизирате %1$s с %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Синхронизирай</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Не синхронизирай</string>
|
||||
<string name="dialog_folder_info_device_list">Споделяне на папката с:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
@@ -28,7 +28,7 @@
|
||||
<string name="device_id">Geräte ID</string>
|
||||
<string name="device_id_copied">Geräte ID in den Zwischenspeicher kopiert</string>
|
||||
<string name="share_device_id_chooser">Teile Geräte ID mit</string>
|
||||
<string name="other_syncthing_instance_title">Eine andere Syncthing Instanz läuft bereits</string>
|
||||
<string name="other_syncthing_instance_title">Eine andere Syncthing-Instanz läuft bereits</string>
|
||||
<string name="other_syncthing_instance_message">Lokale Auffindung wird nicht funktionieren. Stoppen Sie die andere Syncthing Instanz, um die lokale Auffindung zu ermöglichen.</string>
|
||||
<string name="intro_page_one_title">Willkommen zu Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing ersetzt proprietäre Sync- und Cloud-Services durch etwas Offenes, Vertrauenswürdiges und Dezentrales. Ihre Daten sind allein Ihre Daten, und Sie verdienen es zu wählen, wo sie gespeichert werden, ob sie an Dritte weitergegeben werden und wie sie über das Internet übertragen werden.</string>
|
||||
@@ -36,9 +36,38 @@
|
||||
<string name="intro_page_three_title">Ordner teilen</string>
|
||||
<string name="intro_page_two_description">Eine Syncthing Geräte ID eingeben oder QR Code einer Geräte ID scannen.</string>
|
||||
<string name="intro_page_three_description">Akzeptieren Sie nun das Gerät mit der ID %1$s und geben Sie einen Ordner mit ihm frei. Es kann einige Minuten dauern, bis sich die Geräte verbinden.</string>
|
||||
<string name="intro_page_three_searching_device">Versuche das andere Gerät zu finden. Dies kann einen Moment dauern.</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="settings_app_version_title">App Version</string>
|
||||
<string name="settings_local_device_name">Lokaler Geräte Namen</string>
|
||||
<string name="settings_app_version_title">App-Version</string>
|
||||
<string name="settings_local_device_name">Lokaler Gerätenamen</string>
|
||||
<string name="settings_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</string>
|
||||
<string name="settings_shutdown_delay_title">Ausschaltverzögerung</string>
|
||||
<string name="settings_force_stop">Beenden dieser App erzwingen</string>
|
||||
<string name="settings_last_error_title">Letzter Fehler</string>
|
||||
<string name="settings_last_error_summary">Details des letzten Fehlers anzeigen</string>
|
||||
<string name="settings_report_bug_title">Einen Fehler melden</string>
|
||||
<string name="settings_report_bug_summary">Den Bugtracker bei GitHub für diese App öffnen</string>
|
||||
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
|
||||
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
</resources>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 Sekunden</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 Sekunden</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 Minute</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 Minuten</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Aufgrund des Verhaltens dieser App und des Verhaltens des Syncthing-Servers können Sie sich für einige Minuten nicht verbinden, wenn die App erzwungen beendet wurde (durch das Entfernen aus der Liste der aktiven Apps) oder die Verbindung unterbrochen wurde.
|
||||
Dies gilt nicht für Verbindungen, die per lokaler Gerätesuche hergestellt wurden.</string>
|
||||
<string name="dialog_file_save_as">Speichern unter</string>
|
||||
<string name="pending_index_updates">%d Index-Aktualisierungen verbleibend</string>
|
||||
<string name="device_status_connecting">Verbinden mit %s</string>
|
||||
<string name="device_status_connected">Mit %s verbunden</string>
|
||||
<string name="device_status_disconnected">Verbinden wird bald erneut versucht - es sind %d Adressen bekannt</string>
|
||||
<string name="device_status_no_address">Keine bekannte Adresse für dieses Gerät</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Ordnersynchronisation für neues Gerät aktivieren</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Möchten Sie den Ordner %1$s mit %2$s (%3$s) synchronisieren?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Synchronisieren</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Nicht synchronisieren</string>
|
||||
<string name="dialog_folder_info_device_list">Ordner teilen mit:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Etwas in Syncthing Lite hat nicht funktioniert. Sie können die Details in den Einstellungen von Syncthing Lite anzeigen.</string>
|
||||
</resources>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<string name="clear_local_cache_index_label">Limpia caché/índice local</string>
|
||||
<string name="devices_list_view_empty_message">Dispositivos no disponibles</string>
|
||||
<string name="invalid_device_id">Error: identificador de dispositivo inválido</string>
|
||||
<string name="dialog_downloading_file">Descargando archivo%1$s</string>
|
||||
<string name="dialog_downloading_file">Descargando archivo %1$s</string>
|
||||
<string name="toast_file_download_failed">Falló al descargar el archivo</string>
|
||||
<string name="toast_open_file_failed">No se ha encontrado una app compatible</string>
|
||||
<string name="toast_file_upload_failed">Error en la carga de archivos
|
||||
@@ -38,15 +38,42 @@
|
||||
<string name="intro_page_three_title">Compartir tus carpetas</string>
|
||||
<string name="intro_page_two_description">Introduce un ID de dispositivo de Syncthing o escanea un ID de dispositivo desde un código QR</string>
|
||||
<string name="intro_page_three_description">Acepta ahora el dispositivo con ID %1$s, y comparte una carpeta con él. Pueden pasar unos minutos hasta que los dispositivos se conecten.</string>
|
||||
<string name="intro_page_three_searching_device">Intentando encontrar el otro dispositivo. Puede llevar un rato</string>
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="settings_app_version_title">Versión de la aplicación</string>
|
||||
<string name="settings_local_device_name">Nombre del dispositivo local</string>
|
||||
<string name="settings_local_device_summary">El nombre que otros dispositivos verán para este dispositivo</string>
|
||||
<string name="settings_shutdown_delay_title">Retardo en el apagado</string>
|
||||
<string name="settings_shutdown_delay_summary">Tiempo antes de apagar el cliente Syncthing después de su último uso</string>
|
||||
<string name="settings_shutdown_delay_summary">Tiempo antes de que el cliente de Syncthing se cierre después de su último uso</string>
|
||||
<string name="settings_force_stop">Fuerza a parar esta aplicación</string>
|
||||
<string name="settings_last_error_title">Último error</string>
|
||||
<string name="settings_last_error_summary">Ver los detalles del último error</string>
|
||||
<string name="settings_report_bug_title">Informar de un error</string>
|
||||
<string name="settings_report_bug_summary">Abrir las incidencias de esta aplicación en GitHub</string>
|
||||
<string name="copy_to_clipboard">Copiar al portapapeles</string>
|
||||
<string name="copied_to_clipboard">Copiado al portapapeles</string>
|
||||
<string name="device_id_dialog_title">Introducir la ID del dispositivo</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 segundos</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 segundos</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutos</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Debido al comportamiento de esta aplicación y al comportamiento del servidor de Syncthing,
|
||||
no puedes volver a conectar durante unos minutos si la aplicación fue cancelada (debido a la eliminación de la lista de aplicaciones recientes)
|
||||
O la conexión fue interrumpida.
|
||||
Esto no aplica a las conexiones de descubrimiento local.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Guardar como</string>
|
||||
<string name="pending_index_updates">%d actualizaciones de índice pendientes</string>
|
||||
<string name="device_status_connecting">Conectando a %s</string>
|
||||
<string name="device_status_connected">Conectado a %s</string>
|
||||
<string name="device_status_disconnected">Volverá a conectar pronto - hay %d direcciones conocidas</string>
|
||||
<string name="device_status_no_address">Sin dirección conocida para el dispositivo</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Activar sincronización para el nuevo dispositivo</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">¿Quieres sincronizar %1$s con %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Sincronizar</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">No sincronización</string>
|
||||
<string name="dialog_folder_info_device_list">Compartir la carpeta con:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Algo salió mal en Syncthing Lite. Puedes ver los detalles en la configuración de Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,15 +36,26 @@
|
||||
<string name="intro_page_three_title">Partager vos dossiers</string>
|
||||
<string name="intro_page_two_description">Entrer l\'ID Syncthing de l\'appareil, ou scanner le QR code de l\'ID d\'un appareil.</string>
|
||||
<string name="intro_page_three_description">Maintenant, acceptez l\'appareil avec l\'ID %1$s, et partagez un dossier avec lui. Cela peut prendre quelques minutes avant que les appareils ne se connectent.</string>
|
||||
<string name="intro_page_three_searching_device">Essayer de trouver l\'autre appareil. Cela peut prendre un moment.</string>
|
||||
<string name="settings">Réglages</string>
|
||||
<string name="settings_app_version_title">Version d\'application</string>
|
||||
<string name="settings_local_device_name">Nom local de l\'appareil</string>
|
||||
<string name="settings_local_device_summary">Le nom que les autres appareils verront pour cet appareil</string>
|
||||
<string name="settings_shutdown_delay_title">Délai d\'arrêt</string>
|
||||
<string name="settings_shutdown_delay_summary">Délai avant d\'arrêter le client Syncthing après sa dernière utilisation</string>
|
||||
<string name="settings_force_stop">Forcer l\'arrêt de cette application</string>
|
||||
<string name="settings_last_error_title">Dernière erreur</string>
|
||||
<string name="settings_last_error_summary">Voir les détails de la dernière erreur</string>
|
||||
<string name="settings_report_bug_title">Rapporter un bug</string>
|
||||
<string name="settings_report_bug_summary">Ouvrir un incident pour cette application sur GitHub</string>
|
||||
<string name="copy_to_clipboard">Copier dans le presse-papiers</string>
|
||||
<string name="copied_to_clipboard">Copié dans le presse-papiers</string>
|
||||
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secondes</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secondes</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minute</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
|
||||
</resources>
|
||||
<string name="dialog_file_save_as">Enregistrer sous</string>
|
||||
<string name="pending_index_updates">%d mises à jour d\'index en attente</string>
|
||||
<string name="device_status_connecting">Connexion à %s</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
<string name="settings_local_device_name">Helyi eszköz neve</string>
|
||||
<string name="settings_local_device_summary">Név amit a többi eszköz fog látni</string>
|
||||
<string name="settings_shutdown_delay_title">Leállítás késleltetés</string>
|
||||
<string name="settings_shutdown_delay_summary">A Syncthing leállítása ennyi idő elteltével a kliens utolsó csatlakozása után</string>
|
||||
<string name="device_id_dialog_title">Eszközazonosító megadása</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 másodperc</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 perc</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,15 +36,42 @@
|
||||
<string name="intro_page_three_title">Condividi le tue cartelle</string>
|
||||
<string name="intro_page_two_description">Immetti un ID dispositivo Syncthing o esegui la scansione di un ID dispositivo da un codice QR</string>
|
||||
<string name="intro_page_three_description">Ora accetta il dispositivo con ID %1$s, e condividi una cartella con esso. Potrebbero essere necessari alcuni minuti prima che i dispositivi si connettano.</string>
|
||||
<string name="intro_page_three_searching_device">In attesa di trovare altri dispositivi. Questo potrebbe richiedere un momento.</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
<string name="settings_app_version_title">Versione dell\'app</string>
|
||||
<string name="settings_local_device_name">Nome del dispositivo locale</string>
|
||||
<string name="settings_local_device_summary">Il nome che altri dispositivi vedranno per questo dispositivo</string>
|
||||
<string name="settings_shutdown_delay_title">Ritardo di chiusura</string>
|
||||
<string name="settings_shutdown_delay_summary">Tempo prima della chiusura del client Syncthing dopo l\'ultimo utilizzo</string>
|
||||
<string name="settings_force_stop">Forza l\'arresto dell\'App</string>
|
||||
<string name="settings_last_error_title">Ultimo errore</string>
|
||||
<string name="settings_last_error_summary">Visualizza i dettagli dell\'ultimo errore</string>
|
||||
<string name="settings_report_bug_title">Segnala un bug</string>
|
||||
<string name="settings_report_bug_summary">Apri segnalazioni per quest\'App su GitHub</string>
|
||||
<string name="copy_to_clipboard">Copia negli appunti</string>
|
||||
<string name="copied_to_clipboard">Copiato negli appunti</string>
|
||||
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secondi</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secondi</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minuti</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
A causa del comportamento di quest\'App e del comportamento del Server Syncthing,
|
||||
non è possibile riconnettersi per alcuni minuti se l\'app è stata chiusa (a causa della rimozione dall\'elenco app recenti)
|
||||
o la connessione è stata interrotta.
|
||||
Questo non si applica alle connessioni di individuazione locale
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Salva come</string>
|
||||
<string name="pending_index_updates">%d aggiornamenti degli indici in sospeso</string>
|
||||
<string name="device_status_connecting">Connessione a %s</string>
|
||||
<string name="device_status_connected">Connesso a %s</string>
|
||||
<string name="device_status_disconnected">Nuovo tentativo di connessione a breve: ci sono %d indirizzi noti</string>
|
||||
<string name="device_status_no_address">Nessun indirizzo conosciuto per il dispositivo</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Abilita sincronizzazione cartella per il nuovo dispositivo</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Vuoi sincronizzare %1$s con %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Sincronizzare</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Non sincronizzare</string>
|
||||
<string name="dialog_folder_info_device_list">Condividi la cartella con:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Qualcosa non ha funzionato in Syncthing Lite. È possibile visualizzare i dettagli dalle impostazioni di Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
<string name="settings_local_device_name">ローカルのデバイス名</string>
|
||||
<string name="settings_local_device_summary">他のデバイスがこのデバイスを表示する名前</string>
|
||||
<string name="device_id_dialog_title">デバイス ID を入力</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
|
||||
<string name="settings_local_device_summary">De naam die dat andere apparaten voor dit apparaat gaan zien</string>
|
||||
<string name="device_id_dialog_title">Voert nen apparaats-ID in</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
<string name="settings_local_device_name">Naam van lokaal apparaat</string>
|
||||
<string name="settings_local_device_summary">De naam die andere apparaten voor dit apparaat zullen zien</string>
|
||||
<string name="device_id_dialog_title">Voer een apparaats-ID in</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -40,15 +40,42 @@ fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
|
||||
<string name="intro_page_three_title">Partajați-vă directoarele</string>
|
||||
<string name="intro_page_two_description">Introduceți ID-ul Syncthing al unui dispozitiv sau scanați ID-ul unui dispozitiv dintr-un cod QR</string>
|
||||
<string name="intro_page_three_description">Acceptați acum dispozitivul cu ID-ul %1$s, și partajați un director cu el. S-ar putea să dureze câteva minute până când dispozitivele se vor conecta.</string>
|
||||
<string name="intro_page_three_searching_device">Se încearcă găsirea celuilalt dispozitiv. Această operație poate dura un moment.</string>
|
||||
<string name="settings">Setări</string>
|
||||
<string name="settings_app_version_title">Versiune aplicație</string>
|
||||
<string name="settings_local_device_name">Nume local dispozitiv</string>
|
||||
<string name="settings_local_device_summary">Numele pe care celălalt dispozitiv îl va vedea pentru acest dispozitiv</string>
|
||||
<string name="settings_shutdown_delay_title">Temporizare oprire</string>
|
||||
<string name="settings_shutdown_delay_summary">După cât timp se va închide clientul Syncthing în funcție de ultima utilizare</string>
|
||||
<string name="settings_force_stop">Forțează oprirea acestei aplicații</string>
|
||||
<string name="settings_last_error_title">Ultima eroare</string>
|
||||
<string name="settings_last_error_summary">Arată detaliile ultimei erori</string>
|
||||
<string name="settings_report_bug_title">Raportează o eroare</string>
|
||||
<string name="settings_report_bug_summary">Deschideți un raport de eroare pentru această aplicație pe GitHub</string>
|
||||
<string name="copy_to_clipboard">Copiază în memorie</string>
|
||||
<string name="copied_to_clipboard">Copiat în memorie</string>
|
||||
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 secunde</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 secunde</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minut</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minute</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
Datorită modului în care această aplicație și serverul Syncthing funcționează,
|
||||
nu se poate face reconectarea timp de câteva minute după ce aplicația a fost oprită (ștearsă din lista de aplicații care rulează)
|
||||
sau conexiunea a fost întreruptă.
|
||||
Aceasta limitare nu se aplica la conexiunile descoperite local.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Salvează ca</string>
|
||||
<string name="pending_index_updates">%d actualizări de index în așteptare</string>
|
||||
<string name="device_status_connecting">Conectare la %s</string>
|
||||
<string name="device_status_connected">Conectat la %s</string>
|
||||
<string name="device_status_disconnected">Se va încerca conectarea în curând - există %d adrese cunoscute</string>
|
||||
<string name="device_status_no_address">Nici o adresă cunoscută pentru acest dispozitiv</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Activați sincronizarea directorului pentru un dispozitiv nou</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Doriți să sincronizați %1$s cu %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Se sincronizează</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Nu se sincronizează</string>
|
||||
<string name="dialog_folder_info_device_list">Partajează directorul cu:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">O eroare s-a produs in Syncthing Lite. Puteți vedea detalii în setările Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,15 +36,42 @@
|
||||
<string name="intro_page_three_title">Dela dina mappar</string>
|
||||
<string name="intro_page_two_description">Ange ett Syncthing enhets-ID, eller skanna ett enhets-ID-nummer från en QR-kod</string>
|
||||
<string name="intro_page_three_description">Acceptera nu enheten med ID %1$s och dela en mapp med den. Det kan ta några minuter tills enheterna ansluter.</string>
|
||||
<string name="intro_page_three_searching_device">Försöker hitta den andra enheten. Det kan ta ett ögonblick.</string>
|
||||
<string name="settings">Inställningar</string>
|
||||
<string name="settings_app_version_title">Appversion</string>
|
||||
<string name="settings_local_device_name">Lokala enhetens namn</string>
|
||||
<string name="settings_local_device_summary">Namnet som andra enheter kommer att se för den här enheten</string>
|
||||
<string name="settings_shutdown_delay_title">Avstängningsfördröjning</string>
|
||||
<string name="settings_shutdown_delay_summary">Tid innan du stänger av Syncthing-klienten efter den senaste användningen</string>
|
||||
<string name="settings_force_stop">Tvinga stoppa denna App</string>
|
||||
<string name="settings_last_error_title">Senaste felet</string>
|
||||
<string name="settings_last_error_summary">Visa detaljerna för det senaste felet</string>
|
||||
<string name="settings_report_bug_title">Rapportera ett fel</string>
|
||||
<string name="settings_report_bug_summary">Öppna problemen för den här appen på GitHub</string>
|
||||
<string name="copy_to_clipboard">Kopiera till urklipp</string>
|
||||
<string name="copied_to_clipboard">Kopierad till urklipp</string>
|
||||
<string name="device_id_dialog_title">Ange enhets-ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 sekunder</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 sekunder</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 minut</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 minuter</string>
|
||||
<string name="dialog_warning_reconnect_problem">
|
||||
På grund av beteendet hos denna App och beteendet hos Syncthing-servern,
|
||||
du kan inte återansluta i några minuter om appen dödades (på grund av att du tog bort från den senaste applistan)
|
||||
eller anslutningen avbröts.
|
||||
Detta gäller inte lokala upptäcktsanslutningar.
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Spara som</string>
|
||||
<string name="pending_index_updates">%d indexuppdateringar som väntar</string>
|
||||
<string name="device_status_connecting">Ansluter till %s</string>
|
||||
<string name="device_status_connected">Ansluten till %s</string>
|
||||
<string name="device_status_disconnected">Kommer att försöka ansluta snart - det finns%d kända adresser</string>
|
||||
<string name="device_status_no_address">Ingen känd adress för enheten</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Aktivera mappsynkronisering för ny enhet</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Vill du synkronisera %1$s med %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Synkronisera</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Synkronisera inte</string>
|
||||
<string name="dialog_folder_info_device_list">Dela mapp med:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Något gick fel i Syncthing Lite. Du kan visa detaljerna från inställningarna för Syncthing Lite.</string>
|
||||
</resources>
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<string name="settings_local_device_name">本地设备名称</string>
|
||||
<string name="settings_local_device_summary">此设备将被其他设备看到的名称</string>
|
||||
<string name="settings_shutdown_delay_title">关闭延迟</string>
|
||||
<string name="settings_shutdown_delay_summary">关闭 Syncthing 客户端与其最后使用之间的时间</string>
|
||||
<string name="device_id_dialog_title">输入设备 ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 秒</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 秒</string>
|
||||
<string name="settings_shutdown_delay_1_minute">1 分钟</string>
|
||||
<string name="settings_shutdown_delay_5_minutes">5 分钟</string>
|
||||
</resources>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s(%2$s)</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,21 +36,20 @@
|
||||
<string name="intro_page_three_title">Share your folders</string>
|
||||
<string name="intro_page_two_description">Enter a Syncthing device ID, or scan a device ID from a QR code</string>
|
||||
<string name="intro_page_three_description">Now accept the device with ID %1$s, and share a folder with it. It may take a few minutes until the devices connect.</string>
|
||||
<string name="intro_page_three_searching_device">Trying to find the other device. This may take a moment.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_app_version_title">App version</string>
|
||||
<string name="settings_local_device_name">Local device name</string>
|
||||
<string name="settings_local_device_summary">The name that other devices will see for this device</string>
|
||||
<string name="settings_shutdown_delay_title">Shutdown delay</string>
|
||||
<string name="settings_shutdown_delay_summary">Time before shuting down the Syncthing client after its last usage</string>
|
||||
<string name="settings_crash_handler_title">Custom Crash-Handler</string>
|
||||
<string name="settings_crash_handler_summary">Copy the error message to the clipboard when the App crashes</string>
|
||||
<string name="settings_detailed_exception_title">Enable more detailed crash reports</string>
|
||||
<string name="settings_detailed_exception_summary">
|
||||
This could leak private data.
|
||||
You should only use it with the custom crash handler and should not send a crash report without review.
|
||||
Changes of this need an App restart (use force stop to be safe).
|
||||
</string>
|
||||
<string name="settings_shutdown_delay_summary">Time before shutting down the Syncthing client after its last usage</string>
|
||||
<string name="settings_force_stop">Force stop this App</string>
|
||||
<string name="settings_last_error_title">Last error</string>
|
||||
<string name="settings_last_error_summary">View the details of the last error</string>
|
||||
<string name="settings_report_bug_title">Report a bug</string>
|
||||
<string name="settings_report_bug_summary">Open the issues for this App at GitHub</string>
|
||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||
<string name="copied_to_clipboard">Copied to the clipboard</string>
|
||||
<string name="device_id_dialog_title">Enter Device ID</string>
|
||||
<string name="settings_shutdown_delay_10_seconds">10 seconds</string>
|
||||
<string name="settings_shutdown_delay_30_seconds">30 seconds</string>
|
||||
@@ -64,4 +63,16 @@
|
||||
</string>
|
||||
<string name="dialog_file_save_as">Save as</string>
|
||||
<string name="pending_index_updates">%d index updates pending</string>
|
||||
<string name="device_status_connecting">Connecting to %s</string>
|
||||
<string name="device_status_connected">Connected to %s</string>
|
||||
<string name="device_status_disconnected">Will retry connecting soon - there are %d known addresses</string>
|
||||
<string name="device_status_no_address">No known address for the device</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_title">Enable folder sync for new device</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_text">Do you want to sync %1$s with %2$s (%3$s)?</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_positive">Sync</string>
|
||||
<string name="dialog_enable_folder_sync_for_new_device_negative">Do not sync</string>
|
||||
<string name="dialog_folder_info_device_list">Share folder with:</string>
|
||||
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
|
||||
<string name="toast_error">Something went wrong in Syncthing Lite. You can view the details from the settings of Syncthing Lite.</string>
|
||||
<string name="folder_browser_home">Home</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,20 +24,20 @@
|
||||
|
||||
-->
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="crash_handler"
|
||||
android:title="@string/settings_crash_handler_title"
|
||||
android:summary="@string/settings_crash_handler_summary" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="detailed_exception"
|
||||
android:title="@string/settings_detailed_exception_title"
|
||||
android:summary="@string/settings_detailed_exception_summary" />
|
||||
<Preference
|
||||
android:key="last_crash"
|
||||
android:title="@string/settings_last_error_title"
|
||||
android:summary="@string/settings_last_error_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="app_version"
|
||||
android:title="@string/settings_app_version_title"/>
|
||||
|
||||
<Preference
|
||||
android:key="report_bug"
|
||||
android:title="@string/settings_report_bug_title"
|
||||
android:summary="@string/settings_report_bug_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="force_stop"
|
||||
android:title="@string/settings_force_stop" />
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.0'
|
||||
ext.support_version = '27.0.2'
|
||||
ext.build_tools_version = '3.2.0'
|
||||
ext.support_version = '27.1.1'
|
||||
ext.build_tools_version = '3.2.1'
|
||||
ext.anko_version = '0.10.8'
|
||||
ext.protobuf_lite_version = '3.0.1'
|
||||
repositories {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli'
|
||||
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli', ':syncthing-temp-repository-encryption'
|
||||
|
||||
@@ -22,10 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.syncthing.java.bep.BlockExchangeProtos.Vector
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.bep.index.FolderStatsUpdateCollector
|
||||
import net.syncthing.java.bep.index.IndexElementProcessor
|
||||
import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.bep.index.IndexMessageProcessor
|
||||
import net.syncthing.java.bep.index.*
|
||||
import net.syncthing.java.core.beans.BlockInfo
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FileInfo.Version
|
||||
@@ -108,16 +105,20 @@ class BlockPusher(private val localDeviceId: DeviceId,
|
||||
}
|
||||
|
||||
logger.debug("send index update for file = {}", targetPath)
|
||||
val indexListenerStream = indexHandler.subscribeToOnIndexRecordAcquiredEvents()
|
||||
val indexListenerStream = indexHandler.subscribeToOnIndexUpdateEvents()
|
||||
GlobalScope.launch {
|
||||
indexListenerStream.consumeEach { (indexFolderId, newRecords, _) ->
|
||||
if (indexFolderId == folderId) {
|
||||
for (fileInfo2 in newRecords) {
|
||||
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
|
||||
// sentBlocks.addAll(dataSource.getHashes());
|
||||
isCompleted.set(true)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
indexListenerStream.consumeEach { event ->
|
||||
if (event is IndexRecordAcquiredEvent) {
|
||||
val (indexFolderId, newRecords, _) = event
|
||||
|
||||
if (indexFolderId == folderId) {
|
||||
for (fileInfo2 in newRecords) {
|
||||
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
|
||||
// sentBlocks.addAll(dataSource.getHashes());
|
||||
isCompleted.set(true)
|
||||
synchronized(updateLock) {
|
||||
updateLock.notifyAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
-61
@@ -33,42 +33,44 @@ object ClusterConfigHandler {
|
||||
val builder = BlockExchangeProtos.ClusterConfig.newBuilder()
|
||||
|
||||
indexHandler.indexRepository.runInTransaction { indexTransaction ->
|
||||
for (folder in configuration.folders) {
|
||||
val folderBuilder = BlockExchangeProtos.Folder.newBuilder()
|
||||
.setId(folder.folderId)
|
||||
.setLabel(folder.label)
|
||||
configuration.folders
|
||||
.filter { it.deviceIdWhitelist.contains(deviceId) }
|
||||
.forEach { folder ->
|
||||
val folderBuilder = BlockExchangeProtos.Folder.newBuilder()
|
||||
.setId(folder.folderId)
|
||||
.setLabel(folder.label)
|
||||
|
||||
// add this device
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
|
||||
.setIndexId(indexTransaction.getSequencer().indexId())
|
||||
.setMaxSequence(indexTransaction.getSequencer().currentSequence())
|
||||
)
|
||||
// add this device
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
|
||||
.setIndexId(indexTransaction.getSequencer().indexId())
|
||||
.setMaxSequence(indexTransaction.getSequencer().currentSequence())
|
||||
)
|
||||
|
||||
// add other device
|
||||
val indexSequenceInfo = indexTransaction.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
|
||||
// add other device
|
||||
val indexSequenceInfo = indexTransaction.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
|
||||
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(deviceId.toHashData()))
|
||||
.apply {
|
||||
indexSequenceInfo?.let {
|
||||
setIndexId(indexSequenceInfo.indexId)
|
||||
setMaxSequence(indexSequenceInfo.localSequence)
|
||||
folderBuilder.addDevices(
|
||||
BlockExchangeProtos.Device.newBuilder()
|
||||
.setId(ByteString.copyFrom(deviceId.toHashData()))
|
||||
.apply {
|
||||
indexSequenceInfo?.let {
|
||||
indexId = indexSequenceInfo.indexId
|
||||
maxSequence = indexSequenceInfo.localSequence
|
||||
|
||||
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
|
||||
indexSequenceInfo.deviceId,
|
||||
indexSequenceInfo.indexId,
|
||||
indexSequenceInfo.localSequence)
|
||||
}
|
||||
}
|
||||
)
|
||||
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
|
||||
indexSequenceInfo.deviceId,
|
||||
indexSequenceInfo.indexId,
|
||||
indexSequenceInfo.localSequence)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
builder.addFolders(folderBuilder)
|
||||
builder.addFolders(folderBuilder)
|
||||
|
||||
// TODO: add the other devices to the cluster config
|
||||
}
|
||||
// TODO: add the other devices to the cluster config
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
@@ -84,40 +86,69 @@ object ClusterConfigHandler {
|
||||
val folderInfoList = mutableListOf<ClusterConfigFolderInfo>()
|
||||
val newSharedFolders = mutableListOf<FolderInfo>()
|
||||
|
||||
for (folder in clusterConfig.foldersList ?: emptyList()) {
|
||||
var folderInfo = ClusterConfigFolderInfo(folder.id, folder.label)
|
||||
val devicesById = (folder.devicesList ?: emptyList())
|
||||
.associateBy { input ->
|
||||
DeviceId.fromHashData(input.id!!.toByteArray())
|
||||
}
|
||||
val otherDevice = devicesById[otherDeviceId]
|
||||
val ourDevice = devicesById[configuration.localDeviceId]
|
||||
if (otherDevice != null) {
|
||||
folderInfo = folderInfo.copy(isAnnounced = true)
|
||||
}
|
||||
if (ourDevice != null) {
|
||||
folderInfo = folderInfo.copy(isShared = true)
|
||||
logger.info("folder shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
configuration.update { oldConfig ->
|
||||
val configFolders = oldConfig.folders.toMutableSet()
|
||||
|
||||
val newFolderInfo = FolderInfo(folderInfo.folderId, folderInfo.label)
|
||||
|
||||
val oldFolderEntry = configuration.folders.find { it.folderId == folderInfo.folderId }
|
||||
|
||||
if (oldFolderEntry == null) {
|
||||
configuration.folders = configuration.folders + newFolderInfo
|
||||
newSharedFolders.add(newFolderInfo)
|
||||
logger.info("new folder shared = {}", folderInfo)
|
||||
} else {
|
||||
if (oldFolderEntry != newFolderInfo) {
|
||||
configuration.folders = configuration.folders.filter { it != oldFolderEntry }.toSet() + setOf(newFolderInfo)
|
||||
}
|
||||
for (folder in clusterConfig.foldersList ?: emptyList()) {
|
||||
var folderInfo = ClusterConfigFolderInfo(folder.id, folder.label, isDeviceInSharedFolderWhitelist = false)
|
||||
val devicesById = (folder.devicesList ?: emptyList())
|
||||
.associateBy { input ->
|
||||
DeviceId.fromHashData(input.id!!.toByteArray())
|
||||
}
|
||||
val otherDevice = devicesById[otherDeviceId]
|
||||
val ourDevice = devicesById[configuration.localDeviceId]
|
||||
if (otherDevice != null) {
|
||||
folderInfo = folderInfo.copy(isAnnounced = true)
|
||||
}
|
||||
} else {
|
||||
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
if (ourDevice != null) {
|
||||
folderInfo = folderInfo.copy(isShared = true)
|
||||
logger.info("folder shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
|
||||
val oldFolderEntry = configFolders.find { it.folderId == folderInfo.folderId }
|
||||
|
||||
if (oldFolderEntry == null) {
|
||||
folderInfo = folderInfo.copy(isDeviceInSharedFolderWhitelist = true)
|
||||
|
||||
val newFolderInfo = FolderInfo(
|
||||
folderId = folderInfo.folderId,
|
||||
label = folderInfo.label,
|
||||
deviceIdWhitelist = setOf(otherDeviceId),
|
||||
deviceIdBlacklist = emptySet(),
|
||||
ignoredDeviceIdList = emptySet()
|
||||
)
|
||||
|
||||
configFolders.add(newFolderInfo)
|
||||
newSharedFolders.add(newFolderInfo)
|
||||
logger.info("new folder shared = {}", folderInfo)
|
||||
} else {
|
||||
if (oldFolderEntry.deviceIdWhitelist.contains(otherDeviceId)) {
|
||||
folderInfo = folderInfo.copy(isDeviceInSharedFolderWhitelist = true)
|
||||
|
||||
if (oldFolderEntry.label != folderInfo.label) {
|
||||
configFolders.remove(oldFolderEntry)
|
||||
configFolders.add(oldFolderEntry.copy(label = folderInfo.label))
|
||||
}
|
||||
} else {
|
||||
if (!oldFolderEntry.deviceIdBlacklist.contains(otherDeviceId)) {
|
||||
configFolders.remove(oldFolderEntry)
|
||||
configFolders.add(
|
||||
oldFolderEntry.copy(
|
||||
deviceIdBlacklist = oldFolderEntry.deviceIdBlacklist + setOf(otherDeviceId)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("folder not shared from device = {} folder = {}", otherDeviceId, folderInfo)
|
||||
}
|
||||
|
||||
folderInfoList.add(folderInfo)
|
||||
}
|
||||
|
||||
folderInfoList.add(folderInfo)
|
||||
oldConfig.copy(folders = configFolders)
|
||||
}
|
||||
|
||||
configuration.persistLater()
|
||||
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
|
||||
|
||||
@@ -132,7 +163,7 @@ class ClusterConfigInfo (val folderInfo: List<ClusterConfigFolderInfo>, val newS
|
||||
|
||||
val folderInfoById = folderInfo.associateBy { it.folderId }
|
||||
val sharedFolderIds: Set<String> by lazy {
|
||||
folderInfo.filter { it.isShared }.map { it.folderId }.toSet()
|
||||
folderInfo.filter { it.isShared && it.isDeviceInSharedFolderWhitelist }.map { it.folderId }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +171,8 @@ data class ClusterConfigFolderInfo(
|
||||
val folderId: String,
|
||||
val label: String = folderId,
|
||||
val isAnnounced: Boolean = false,
|
||||
val isShared: Boolean = false
|
||||
val isShared: Boolean = false,
|
||||
val isDeviceInSharedFolderWhitelist: Boolean
|
||||
) {
|
||||
init {
|
||||
assert(folderId.isNotEmpty())
|
||||
|
||||
+79
-27
@@ -23,6 +23,11 @@ import net.syncthing.java.core.configuration.Configuration
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
|
||||
data class Connection (
|
||||
val actor: SendChannel<ConnectionAction>,
|
||||
val clusterConfigInfo: ClusterConfigInfo
|
||||
)
|
||||
|
||||
object ConnectionActorGenerator {
|
||||
private val closed = Channel<ConnectionAction>().apply { cancel() }
|
||||
private val logger = LoggerFactory.getLogger(ConnectionActorGenerator::class.java)
|
||||
@@ -89,18 +94,42 @@ object ConnectionActorGenerator {
|
||||
configuration: Configuration,
|
||||
indexHandler: IndexHandler,
|
||||
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
|
||||
) = GlobalScope.produce<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>> {
|
||||
) = GlobalScope.produce<Pair<Connection, ConnectionInfo>> {
|
||||
var currentActor: SendChannel<ConnectionAction> = closed
|
||||
var currentClusterConfig = ClusterConfigInfo.dummy
|
||||
var currentDeviceAddress: DeviceAddress? = null
|
||||
var currentStatus = ConnectionInfo.empty
|
||||
|
||||
suspend fun dispatchStatus() {
|
||||
send(Connection(currentActor, currentClusterConfig) to currentStatus)
|
||||
}
|
||||
|
||||
suspend fun closeCurrent() {
|
||||
if (currentActor != closed) {
|
||||
currentActor.close()
|
||||
currentActor = closed
|
||||
send(currentActor to ClusterConfigInfo.dummy)
|
||||
currentClusterConfig = ClusterConfigInfo.dummy
|
||||
|
||||
if (currentStatus.status != ConnectionStatus.Disconnected) {
|
||||
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
|
||||
}
|
||||
|
||||
dispatchStatus()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun dispatchConnection(
|
||||
connection: SendChannel<ConnectionAction>,
|
||||
clusterConfig: ClusterConfigInfo,
|
||||
deviceAddress: DeviceAddress
|
||||
) {
|
||||
currentActor = connection
|
||||
currentDeviceAddress = deviceAddress
|
||||
currentClusterConfig = clusterConfig
|
||||
|
||||
dispatchStatus()
|
||||
}
|
||||
|
||||
suspend fun tryConnectingToAddressHandleBaseErrors(deviceAddress: DeviceAddress) = try {
|
||||
val newActor = ConnectionActor.createInstance(deviceAddress, configuration, indexHandler, requestHandler)
|
||||
val clusterConfig = ConnectionActorUtil.waitUntilConnected(newActor)
|
||||
@@ -118,31 +147,37 @@ object ConnectionActorGenerator {
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun dispatchConnection(
|
||||
connection: SendChannel<ConnectionAction>,
|
||||
clusterConfig: ClusterConfigInfo,
|
||||
deviceAddress: DeviceAddress
|
||||
) {
|
||||
currentActor = connection
|
||||
currentDeviceAddress = deviceAddress
|
||||
|
||||
send(connection to clusterConfig)
|
||||
}
|
||||
|
||||
suspend fun tryConnectingToAddress(deviceAddress: DeviceAddress): Boolean {
|
||||
closeCurrent()
|
||||
|
||||
var connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return false
|
||||
suspend fun handleCancel() {
|
||||
currentStatus = currentStatus.copy(
|
||||
status = ConnectionStatus.Disconnected
|
||||
)
|
||||
dispatchStatus()
|
||||
}
|
||||
|
||||
currentStatus = currentStatus.copy(
|
||||
status = ConnectionStatus.Connecting,
|
||||
currentAddress = deviceAddress
|
||||
)
|
||||
dispatchStatus()
|
||||
|
||||
var connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return run {handleCancel(); false}
|
||||
|
||||
if (connection.second.newSharedFolders.isNotEmpty()) {
|
||||
logger.debug("connected to $deviceAddress with new folders -> reconnect")
|
||||
// reconnect to send new cluster config
|
||||
connection.first.close()
|
||||
connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return false
|
||||
connection = tryConnectingToAddressHandleBaseErrors(deviceAddress) ?: return run {handleCancel(); false}
|
||||
}
|
||||
|
||||
logger.debug("connected to $deviceAddress")
|
||||
|
||||
currentStatus = currentStatus.copy(
|
||||
status = ConnectionStatus.Connected,
|
||||
currentAddress = deviceAddress
|
||||
)
|
||||
dispatchConnection(connection.first, connection.second, deviceAddress)
|
||||
|
||||
return true
|
||||
@@ -157,18 +192,26 @@ object ConnectionActorGenerator {
|
||||
val reconnectTicker = ticker(delayMillis = 30 * 1000, initialDelayMillis = 0)
|
||||
|
||||
deviceAddressSource.consume {
|
||||
var lastDeviceAddressList: List<DeviceAddress> = emptyList()
|
||||
|
||||
while (true) {
|
||||
if (isConnected()) {
|
||||
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
|
||||
run {
|
||||
// get the new list version if there is any
|
||||
val newDeviceAddressList = deviceAddressSource.poll()
|
||||
|
||||
if (lastDeviceAddressList.isNotEmpty()) {
|
||||
if (newDeviceAddressList != null) {
|
||||
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
|
||||
dispatchStatus()
|
||||
}
|
||||
}
|
||||
|
||||
if (isConnected()) {
|
||||
val deviceAddressList = currentStatus.addresses
|
||||
|
||||
if (deviceAddressList.isNotEmpty()) {
|
||||
if (reconnectTicker.poll() != null) {
|
||||
if (currentDeviceAddress != lastDeviceAddressList.first()) {
|
||||
if (currentDeviceAddress != deviceAddressList.first()) {
|
||||
val oldDeviceAddress = currentDeviceAddress!!
|
||||
|
||||
if (!tryConnectingToAddress(lastDeviceAddressList.first())) {
|
||||
if (!tryConnectingToAddress(deviceAddressList.first())) {
|
||||
tryConnectingToAddress(oldDeviceAddress)
|
||||
}
|
||||
}
|
||||
@@ -179,11 +222,15 @@ object ConnectionActorGenerator {
|
||||
|
||||
delay(500) // don't take too much CPU
|
||||
} else /* is not connected */ {
|
||||
// get the new list version if there is any
|
||||
lastDeviceAddressList = deviceAddressSource.poll() ?: lastDeviceAddressList
|
||||
if (currentStatus.status == ConnectionStatus.Connected) {
|
||||
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
|
||||
dispatchStatus()
|
||||
}
|
||||
|
||||
val deviceAddressList = currentStatus.addresses
|
||||
|
||||
// try all addresses
|
||||
for (address in lastDeviceAddressList) {
|
||||
for (address in deviceAddressList) {
|
||||
if (tryConnectingToAddress(address)) {
|
||||
break
|
||||
}
|
||||
@@ -194,9 +241,14 @@ object ConnectionActorGenerator {
|
||||
reconnectTicker.poll()
|
||||
|
||||
// wait for new device address list but not more than 15 seconds before the next iteration
|
||||
lastDeviceAddressList = withTimeoutOrNull(15 * 1000) {
|
||||
val newDeviceAddressList = withTimeoutOrNull(15 * 1000) {
|
||||
deviceAddressSource.receive()
|
||||
} ?: lastDeviceAddressList
|
||||
}
|
||||
|
||||
if (newDeviceAddressList != null) {
|
||||
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
|
||||
dispatchStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -16,6 +16,7 @@ package net.syncthing.java.bep.connectionactor
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import java.io.IOException
|
||||
|
||||
object ConnectionActorUtil {
|
||||
suspend fun waitUntilConnected(actor: SendChannel<ConnectionAction>): ClusterConfigInfo {
|
||||
@@ -28,22 +29,34 @@ object ConnectionActorUtil {
|
||||
}
|
||||
|
||||
suspend fun sendRequest(request: BlockExchangeProtos.Request, actor: SendChannel<ConnectionAction>): BlockExchangeProtos.Response {
|
||||
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
|
||||
try {
|
||||
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
|
||||
|
||||
actor.send(SendRequestConnectionAction(request, deferred))
|
||||
actor.send(SendRequestConnectionAction(request, deferred))
|
||||
|
||||
return deferred.await()
|
||||
return deferred.await()
|
||||
} catch (ex: Exception) {
|
||||
throw IOException("not connected", ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate, actor: SendChannel<ConnectionAction>) {
|
||||
val deferred = CompletableDeferred<Unit?>()
|
||||
try {
|
||||
val deferred = CompletableDeferred<Unit?>()
|
||||
|
||||
actor.send(SendIndexUpdateAction(update, deferred))
|
||||
actor.send(SendIndexUpdateAction(update, deferred))
|
||||
|
||||
deferred.await()
|
||||
deferred.await()
|
||||
} catch (ex: Exception) {
|
||||
throw IOException("not connected", ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disconnect(actor: SendChannel<ConnectionAction>) {
|
||||
actor.send(CloseConnectionAction)
|
||||
try {
|
||||
actor.send(CloseConnectionAction)
|
||||
} catch (ex: Exception) {
|
||||
// ignore if the channel is closed already
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-34
@@ -14,74 +14,60 @@
|
||||
package net.syncthing.java.bep.connectionactor
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.exception.reportExceptions
|
||||
import java.io.IOException
|
||||
|
||||
class ConnectionActorWrapper (
|
||||
private val source: ReceiveChannel<Pair<SendChannel<ConnectionAction>, ClusterConfigInfo>>,
|
||||
private val source: ReceiveChannel<Pair<Connection, ConnectionInfo>>,
|
||||
val deviceId: DeviceId,
|
||||
val connectivityChangeListener: () -> Unit
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
private val job = Job()
|
||||
|
||||
private var currentConnectionActor: SendChannel<ConnectionAction>? = null
|
||||
private var clusterConfigInfo: ClusterConfigInfo? = null
|
||||
private var connection: Connection? = null
|
||||
private val connectionInfo = ConflatedBroadcastChannel<ConnectionInfo>(ConnectionInfo.empty)
|
||||
|
||||
var isConnected = false
|
||||
get() = currentConnectionActor?.isClosedForSend == false
|
||||
val isConnected
|
||||
get() = connectionInfo.valueOrNull?.status == ConnectionStatus.Connected
|
||||
|
||||
init {
|
||||
GlobalScope.launch (job) {
|
||||
source.consumeEach { (connectionActor, clusterConfig) ->
|
||||
currentConnectionActor = connectionActor
|
||||
clusterConfigInfo = clusterConfig
|
||||
GlobalScope.async (job) {
|
||||
source.consumeEach { (connection, connectionInfo) ->
|
||||
this@ConnectionActorWrapper.connection = connection
|
||||
this@ConnectionActorWrapper.connectionInfo.send(connectionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// this is a very simple solution but it does its job
|
||||
GlobalScope.launch (job) {
|
||||
var previousConnected = false
|
||||
|
||||
while (isActive) {
|
||||
val nowConnected = isConnected
|
||||
|
||||
if (previousConnected != nowConnected) {
|
||||
previousConnected = nowConnected
|
||||
|
||||
connectivityChangeListener()
|
||||
}
|
||||
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
}.reportExceptions("ConnectionActorWrapper(${deviceId.deviceId})", exceptionReportHandler)
|
||||
}
|
||||
|
||||
suspend fun sendRequest(request: BlockExchangeProtos.Request) = ConnectionActorUtil.sendRequest(
|
||||
request,
|
||||
currentConnectionActor ?: throw IOException("not connected")
|
||||
connection?.actor ?: throw IOException("not connected")
|
||||
)
|
||||
|
||||
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate) = ConnectionActorUtil.sendIndexUpdate(
|
||||
update,
|
||||
currentConnectionActor ?: throw IOException("not connected")
|
||||
connection?.actor ?: throw IOException("not connected")
|
||||
)
|
||||
|
||||
fun hasFolder(folderId: String) = clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
|
||||
fun hasFolder(folderId: String) = connection?.clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
|
||||
|
||||
fun getClusterConfig() = clusterConfigInfo ?: throw IOException("not connected")
|
||||
fun getClusterConfig() = connection?.clusterConfigInfo ?: throw IOException("not connected")
|
||||
|
||||
fun shutdown() {
|
||||
job.cancel()
|
||||
connectionInfo.close()
|
||||
}
|
||||
|
||||
// this triggers a disconnection
|
||||
// the ConnectionActorGenerator will reconnect soon
|
||||
fun reconnect() {
|
||||
val actor = currentConnectionActor
|
||||
val actor = connection?.actor
|
||||
|
||||
GlobalScope.launch {
|
||||
if (actor != null) {
|
||||
@@ -89,4 +75,6 @@ class ConnectionActorWrapper (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToConnectionInfo() = connectionInfo.openSubscription()
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package net.syncthing.java.bep.connectionactor
|
||||
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
|
||||
data class ConnectionInfo (
|
||||
val addresses: List<DeviceAddress>,
|
||||
val currentAddress: DeviceAddress?,
|
||||
val status: ConnectionStatus
|
||||
) {
|
||||
companion object {
|
||||
val empty = ConnectionInfo(
|
||||
addresses = emptyList(),
|
||||
currentAddress = null,
|
||||
status = ConnectionStatus.Disconnected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ConnectionStatus {
|
||||
Disconnected, Connecting, Connected
|
||||
}
|
||||
+12
-9
@@ -73,7 +73,7 @@ object HelloMessageHandler {
|
||||
)
|
||||
}
|
||||
|
||||
fun processHelloMessage(
|
||||
suspend fun processHelloMessage(
|
||||
hello: BlockExchangeProtos.Hello,
|
||||
configuration: Configuration,
|
||||
deviceId: DeviceId
|
||||
@@ -81,14 +81,17 @@ object HelloMessageHandler {
|
||||
logger.info("Received hello message, deviceName=${hello.deviceName}, clientName=${hello.clientName}, clientVersion=${hello.clientVersion}")
|
||||
|
||||
// update the local device name
|
||||
// TODO: this could need some locking
|
||||
configuration.peers = configuration.peers.map { peer ->
|
||||
if (peer.deviceId == deviceId) {
|
||||
DeviceInfo(deviceId, hello.deviceName)
|
||||
} else {
|
||||
peer
|
||||
}
|
||||
}.toSet()
|
||||
configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = oldConfig.peers.map { peer ->
|
||||
if (peer.deviceId == deviceId) {
|
||||
DeviceInfo(deviceId, hello.deviceName)
|
||||
} else {
|
||||
peer
|
||||
}
|
||||
}.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
configuration.persistLater()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import kotlinx.coroutines.channels.first
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.bep.index.*
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import java.io.Closeable
|
||||
@@ -35,7 +35,7 @@ class FolderBrowser internal constructor(private val indexHandler: IndexHandler,
|
||||
// get initial status
|
||||
val currentFolderStats = mutableMapOf<String, FolderStats>()
|
||||
|
||||
var currentIndexInfo = withContext(Dispatchers.IO) {
|
||||
val currentIndexInfo = withContext(Dispatchers.IO) {
|
||||
indexHandler.indexRepository.runInTransaction { indexTransaction ->
|
||||
configuration.folders.map { it.folderId }.forEach { folderId ->
|
||||
currentFolderStats[folderId] = indexTransaction.findFolderStats(folderId) ?: FolderStats.createDummy(folderId)
|
||||
@@ -64,9 +64,12 @@ class FolderBrowser internal constructor(private val indexHandler: IndexHandler,
|
||||
val updateLock = Mutex()
|
||||
|
||||
async {
|
||||
indexHandler.subscribeFolderStatsUpdatedEvents().consumeEach { folderStats ->
|
||||
indexHandler.subscribeFolderStatsUpdatedEvents().consumeEach { event ->
|
||||
updateLock.withLock {
|
||||
currentFolderStats[folderStats.folderId] = folderStats
|
||||
when (event) {
|
||||
is FolderStatsUpdatedEvent -> currentFolderStats[event.folderStats.folderId] = event.folderStats
|
||||
FolderStatsResetEvent -> currentFolderStats.clear()
|
||||
}.let { /* require that all cases are handled */ }
|
||||
|
||||
dispatch()
|
||||
}
|
||||
@@ -74,16 +77,28 @@ class FolderBrowser internal constructor(private val indexHandler: IndexHandler,
|
||||
}
|
||||
|
||||
async {
|
||||
indexHandler.subscribeToOnIndexRecordAcquiredEvents().consumeEach { event ->
|
||||
indexHandler.subscribeToOnIndexUpdateEvents().consumeEach { event ->
|
||||
updateLock.withLock {
|
||||
val oldList = currentIndexInfo[event.folderId] ?: emptyList()
|
||||
val newList = oldList.filter { it.deviceId != event.indexInfo.deviceId } + event.indexInfo
|
||||
currentIndexInfo[event.folderId] = newList
|
||||
when (event) {
|
||||
is IndexRecordAcquiredEvent -> {
|
||||
val oldList = currentIndexInfo[event.folderId] ?: emptyList()
|
||||
val newList = oldList.filter { it.deviceId != event.indexInfo.deviceId } + event.indexInfo
|
||||
|
||||
currentIndexInfo[event.folderId] = newList
|
||||
}
|
||||
IndexInfoClearedEvent -> currentIndexInfo.clear()
|
||||
}.let { /* require that all cases are handled */ }
|
||||
|
||||
dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async {
|
||||
configuration.subscribe().consumeEach {
|
||||
dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ data class FolderStatus(
|
||||
) {
|
||||
companion object {
|
||||
fun createDummy(folder: String) = FolderStatus(
|
||||
info = FolderInfo(folder, folder),
|
||||
info = FolderInfo(
|
||||
folder,
|
||||
folder,
|
||||
deviceIdBlacklist = emptySet(),
|
||||
deviceIdWhitelist = emptySet(),
|
||||
ignoredDeviceIdList = emptySet()
|
||||
),
|
||||
stats = FolderStats.createDummy(folder),
|
||||
indexInfo = emptyList()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
|
||||
sealed class FolderStatsChangedEvent
|
||||
data class FolderStatsUpdatedEvent(val folderStats: FolderStats): FolderStatsChangedEvent()
|
||||
object FolderStatsResetEvent: FolderStatsChangedEvent()
|
||||
+4
-13
@@ -4,6 +4,8 @@ import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.core.beans.BlockInfo
|
||||
import net.syncthing.java.core.beans.FileBlocks
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.exception.ExceptionDetailException
|
||||
import net.syncthing.java.core.exception.ExceptionDetails
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import org.bouncycastle.util.encoders.Hex
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -18,8 +20,7 @@ object IndexElementProcessor {
|
||||
folder: String,
|
||||
updates: List<BlockExchangeProtos.FileInfo>,
|
||||
oldRecords: Map<String, FileInfo>,
|
||||
folderStatsUpdateCollector: FolderStatsUpdateCollector,
|
||||
enableDetailedException: Boolean
|
||||
folderStatsUpdateCollector: FolderStatsUpdateCollector
|
||||
): List<FileInfo> {
|
||||
// this always keeps the last version per path
|
||||
val filesToProcess = updates
|
||||
@@ -28,17 +29,7 @@ object IndexElementProcessor {
|
||||
.distinctBy { it.name /* this is the whole path */ }
|
||||
.reversed()
|
||||
|
||||
val preparedUpdates = filesToProcess.mapNotNull {
|
||||
try {
|
||||
prepareUpdate(folder, it)
|
||||
} catch (ex: Exception) {
|
||||
if (enableDetailedException) {
|
||||
throw IOException("error processing index update: ${it.name}", ex)
|
||||
} else {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
val preparedUpdates = filesToProcess.mapNotNull { prepareUpdate(folder, it) }
|
||||
|
||||
val updatesToApply = preparedUpdates.filter { shouldUpdateRecord(oldRecords[it.first.path], it.first) }
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
*/
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.consume
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
|
||||
@@ -24,6 +26,7 @@ import net.syncthing.java.bep.folder.FolderBrowser
|
||||
import net.syncthing.java.bep.index.browser.IndexBrowser
|
||||
import net.syncthing.java.core.beans.*
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
@@ -32,37 +35,40 @@ import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
|
||||
data class IndexRecordAcquiredEvent(val folderId: String, val files: List<FileInfo>, val indexInfo: IndexInfo)
|
||||
|
||||
class IndexHandler(
|
||||
configuration: Configuration,
|
||||
val indexRepository: IndexRepository,
|
||||
tempRepository: TempRepository,
|
||||
enableDetailedException: Boolean
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) : Closeable {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val onIndexRecordAcquiredEvents = BroadcastChannel<IndexRecordAcquiredEvent>(capacity = 16)
|
||||
private val indexInfoUpdateEvents = BroadcastChannel<IndexInfoUpdateEvent>(capacity = 16)
|
||||
private val onFullIndexAcquiredEvents = BroadcastChannel<String>(capacity = 16)
|
||||
private val onFolderStatsUpdatedEvents = BroadcastChannel<FolderStats>(capacity = 16)
|
||||
private val onFolderStatsUpdatedEvents = BroadcastChannel<FolderStatsChangedEvent>(capacity = 16)
|
||||
|
||||
private val indexMessageProcessor = IndexMessageQueueProcessor(
|
||||
indexRepository = indexRepository,
|
||||
tempRepository = tempRepository,
|
||||
isRemoteIndexAcquired = ::isRemoteIndexAcquired,
|
||||
onIndexRecordAcquiredEvents = onIndexRecordAcquiredEvents,
|
||||
onIndexRecordAcquiredEvents = indexInfoUpdateEvents,
|
||||
onFullIndexAcquiredEvents = onFullIndexAcquiredEvents,
|
||||
onFolderStatsUpdatedEvents = onFolderStatsUpdatedEvents,
|
||||
enableDetailedException = enableDetailedException
|
||||
exceptionReportHandler = exceptionReportHandler
|
||||
)
|
||||
|
||||
fun subscribeToOnFullIndexAcquiredEvents() = onFullIndexAcquiredEvents.openSubscription()
|
||||
fun subscribeToOnIndexRecordAcquiredEvents() = onIndexRecordAcquiredEvents.openSubscription()
|
||||
fun subscribeToOnIndexUpdateEvents() = indexInfoUpdateEvents.openSubscription()
|
||||
fun subscribeFolderStatsUpdatedEvents() = onFolderStatsUpdatedEvents.openSubscription()
|
||||
|
||||
fun getNextSequenceNumber() = indexRepository.runInTransaction { it.getSequencer().nextSequence() }
|
||||
|
||||
fun clearIndex() {
|
||||
indexRepository.runInTransaction { it.clearIndex() }
|
||||
suspend fun clearIndex() {
|
||||
withContext(Dispatchers.IO) {
|
||||
indexRepository.runInTransaction { it.clearIndex() }
|
||||
}
|
||||
|
||||
onFolderStatsUpdatedEvents.send(FolderStatsResetEvent)
|
||||
indexInfoUpdateEvents.send(IndexInfoClearedEvent)
|
||||
}
|
||||
|
||||
private fun isRemoteIndexAcquiredWithoutTransaction(clusterConfigInfo: ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
|
||||
@@ -123,7 +129,7 @@ class IndexHandler(
|
||||
for (deviceRecord in folderRecord.devicesList) {
|
||||
val deviceId = DeviceId.fromHashData(deviceRecord.id.toByteArray())
|
||||
if (deviceRecord.hasIndexId() && deviceRecord.hasMaxSequence()) {
|
||||
val folderIndexInfo = UpdateIndexInfo.updateIndexInfo(transaction, folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence, null)
|
||||
val folderIndexInfo = UpdateIndexInfo.updateIndexInfoFromClusterConfig(transaction, folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence)
|
||||
logger.debug("acquired folder index info from cluster config = {}", folderIndexInfo)
|
||||
updatedIndexInfos.add(folderIndexInfo)
|
||||
}
|
||||
@@ -134,7 +140,7 @@ class IndexHandler(
|
||||
}
|
||||
|
||||
updatedIndexInfos.forEach {
|
||||
onIndexRecordAcquiredEvents.send(
|
||||
indexInfoUpdateEvents.send(
|
||||
IndexRecordAcquiredEvent(
|
||||
folderId = it.folderId,
|
||||
indexInfo = it,
|
||||
@@ -175,11 +181,11 @@ class IndexHandler(
|
||||
val indexBrowser = IndexBrowser(indexRepository, this)
|
||||
|
||||
suspend fun sendFolderStatsUpdate(event: FolderStats) {
|
||||
onFolderStatsUpdatedEvents.send(event)
|
||||
onFolderStatsUpdatedEvents.send(FolderStatsUpdatedEvent(event))
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
onIndexRecordAcquiredEvents.close()
|
||||
indexInfoUpdateEvents.close()
|
||||
onFullIndexAcquiredEvents.close()
|
||||
indexMessageProcessor.stop()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.IndexInfo
|
||||
|
||||
sealed class IndexInfoUpdateEvent
|
||||
data class IndexRecordAcquiredEvent(val folderId: String, val files: List<FileInfo>, val indexInfo: IndexInfo): IndexInfoUpdateEvent()
|
||||
object IndexInfoClearedEvent: IndexInfoUpdateEvent()
|
||||
+17
-11
@@ -7,6 +7,7 @@ import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.java.core.beans.IndexInfo
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.RuntimeException
|
||||
|
||||
object IndexMessageProcessor {
|
||||
private val logger = LoggerFactory.getLogger(IndexMessageProcessor::class.java)
|
||||
@@ -14,10 +15,11 @@ object IndexMessageProcessor {
|
||||
fun doHandleIndexMessageReceivedEvent(
|
||||
message: BlockExchangeProtos.IndexUpdate,
|
||||
peerDeviceId: DeviceId,
|
||||
transaction: IndexTransaction,
|
||||
enableDetailedException: Boolean
|
||||
transaction: IndexTransaction
|
||||
): Result {
|
||||
val folderId = message.folder
|
||||
val oldIndexInfo = transaction.findIndexInfoByDeviceAndFolder(peerDeviceId, folderId)
|
||||
?: throw IndexInfoNotFoundException()
|
||||
|
||||
logger.debug("processing {} index records for folder {}", message.filesList.size, folderId)
|
||||
|
||||
@@ -29,20 +31,23 @@ object IndexMessageProcessor {
|
||||
oldRecords = oldRecords,
|
||||
folder = folderId,
|
||||
folderStatsUpdateCollector = folderStatsUpdateCollector,
|
||||
updates = message.filesList,
|
||||
enableDetailedException = enableDetailedException
|
||||
updates = message.filesList
|
||||
)
|
||||
|
||||
var sequence: Long = -1
|
||||
val newIndexInfo = if (message.filesList.isEmpty()) {
|
||||
oldIndexInfo
|
||||
} else {
|
||||
var sequence: Long = -1
|
||||
|
||||
for (newRecord in message.filesList) {
|
||||
sequence = Math.max(newRecord.sequence, sequence)
|
||||
for (newRecord in message.filesList) {
|
||||
sequence = Math.max(newRecord.sequence, sequence)
|
||||
}
|
||||
|
||||
handleFolderStatsUpdate(transaction, folderStatsUpdateCollector)
|
||||
|
||||
UpdateIndexInfo.updateIndexInfoFromIndexElementProcessor(transaction, oldIndexInfo, sequence)
|
||||
}
|
||||
|
||||
handleFolderStatsUpdate(transaction, folderStatsUpdateCollector)
|
||||
|
||||
val newIndexInfo = UpdateIndexInfo.updateIndexInfo(transaction, folderId, peerDeviceId, null, null, sequence)
|
||||
|
||||
return Result(newIndexInfo, newRecords.toList(), transaction.findFolderStats(folderId) ?: FolderStats.createDummy(folderId))
|
||||
}
|
||||
|
||||
@@ -61,4 +66,5 @@ object IndexMessageProcessor {
|
||||
}
|
||||
|
||||
data class Result(val newIndexInfo: IndexInfo, val updatedFiles: List<FileInfo>, val newFolderStats: FolderStats)
|
||||
class IndexInfoNotFoundException: RuntimeException()
|
||||
}
|
||||
|
||||
+28
-17
@@ -14,19 +14,17 @@
|
||||
*/
|
||||
package net.syncthing.java.bep.index
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.bep.BlockExchangeProtos
|
||||
import net.syncthing.java.bep.connectionactor.ClusterConfigInfo
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.exception.reportExceptions
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
@@ -35,11 +33,11 @@ import org.slf4j.LoggerFactory
|
||||
class IndexMessageQueueProcessor (
|
||||
private val indexRepository: IndexRepository,
|
||||
private val tempRepository: TempRepository,
|
||||
private val onIndexRecordAcquiredEvents: BroadcastChannel<IndexRecordAcquiredEvent>,
|
||||
private val onIndexRecordAcquiredEvents: BroadcastChannel<IndexInfoUpdateEvent>,
|
||||
private val onFullIndexAcquiredEvents: BroadcastChannel<String>,
|
||||
private val onFolderStatsUpdatedEvents: BroadcastChannel<FolderStats>,
|
||||
private val onFolderStatsUpdatedEvents: BroadcastChannel<FolderStatsChangedEvent>,
|
||||
private val isRemoteIndexAcquired: (ClusterConfigInfo, DeviceId, IndexTransaction) -> Boolean,
|
||||
private val enableDetailedException: Boolean
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) {
|
||||
private data class IndexUpdateAction(val update: BlockExchangeProtos.IndexUpdate, val clusterConfigInfo: ClusterConfigInfo, val peerDeviceId: DeviceId)
|
||||
private data class StoredIndexUpdateAction(val updateId: String, val clusterConfigInfo: ClusterConfigInfo, val peerDeviceId: DeviceId)
|
||||
@@ -81,13 +79,20 @@ class IndexMessageQueueProcessor (
|
||||
}
|
||||
|
||||
init {
|
||||
GlobalScope.launch(Dispatchers.IO + job) {
|
||||
GlobalScope.async(Dispatchers.IO + job) {
|
||||
indexUpdateProcessingQueue.consumeEach {
|
||||
doHandleIndexMessageReceivedEvent(it)
|
||||
}
|
||||
}
|
||||
try {
|
||||
doHandleIndexMessageReceivedEvent(it)
|
||||
} catch (ex: IndexMessageProcessor.IndexInfoNotFoundException) {
|
||||
// ignored
|
||||
// this is expected when the data is deleted but some index updates are still in the queue
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO + job) {
|
||||
logger.warn("could not find index info for index update")
|
||||
}
|
||||
}
|
||||
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessingQueue", exceptionReportHandler)
|
||||
|
||||
GlobalScope.async(Dispatchers.IO + job) {
|
||||
indexUpdateProcessStoredQueue.consumeEach { action ->
|
||||
logger.debug("processing index message event from temp record {}", action.updateId)
|
||||
|
||||
@@ -100,12 +105,19 @@ class IndexMessageQueueProcessor (
|
||||
action.peerDeviceId
|
||||
))
|
||||
}
|
||||
}
|
||||
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessStoredQueue", exceptionReportHandler)
|
||||
}
|
||||
|
||||
private suspend fun doHandleIndexMessageReceivedEvent(action: IndexUpdateAction) {
|
||||
val (message, clusterConfigInfo, peerDeviceId) = action
|
||||
|
||||
val folderInfo = clusterConfigInfo.folderInfoById[message.folder]
|
||||
?: throw IllegalStateException("got folder info for folder without known folder info")
|
||||
|
||||
if (!folderInfo.isDeviceInSharedFolderWhitelist) {
|
||||
throw IllegalStateException("received index update for folder which is not shared")
|
||||
}
|
||||
|
||||
logger.info("processing index message with {} records", message.filesCount)
|
||||
|
||||
val (indexResult, wasIndexAcquired) = indexRepository.runInTransaction { indexTransaction ->
|
||||
@@ -116,8 +128,7 @@ class IndexMessageQueueProcessor (
|
||||
val indexResult = IndexMessageProcessor.doHandleIndexMessageReceivedEvent(
|
||||
message = message,
|
||||
peerDeviceId = peerDeviceId,
|
||||
transaction = indexTransaction,
|
||||
enableDetailedException = enableDetailedException
|
||||
transaction = indexTransaction
|
||||
)
|
||||
|
||||
val endTime = System.currentTimeMillis()
|
||||
@@ -133,7 +144,7 @@ class IndexMessageQueueProcessor (
|
||||
onIndexRecordAcquiredEvents.send(IndexRecordAcquiredEvent(message.folder, indexResult.updatedFiles, indexResult.newIndexInfo))
|
||||
}
|
||||
|
||||
onFolderStatsUpdatedEvents.send(indexResult.newFolderStats)
|
||||
onFolderStatsUpdatedEvents.send(FolderStatsUpdatedEvent(indexResult.newFolderStats))
|
||||
|
||||
if (wasIndexAcquired) {
|
||||
logger.debug("index acquired")
|
||||
|
||||
@@ -5,46 +5,53 @@ import net.syncthing.java.core.beans.IndexInfo
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
|
||||
object UpdateIndexInfo {
|
||||
fun updateIndexInfo(
|
||||
fun updateIndexInfoFromClusterConfig(
|
||||
transaction: IndexTransaction,
|
||||
folder: String,
|
||||
deviceId: DeviceId,
|
||||
indexId: Long?,
|
||||
maxSequence: Long?,
|
||||
localSequence: Long?
|
||||
indexId: Long,
|
||||
maxSequence: Long
|
||||
): IndexInfo {
|
||||
val oldIndexSequenceInfo = transaction.findIndexInfoByDeviceAndFolder(deviceId, folder)
|
||||
|
||||
var newIndexSequenceInfo = oldIndexSequenceInfo ?: kotlin.run {
|
||||
assert(indexId != null) {
|
||||
"index sequence info not found, and supplied null index id (folder = $folder, device = $deviceId)"
|
||||
}
|
||||
var newIndexSequenceInfo = oldIndexSequenceInfo ?: IndexInfo(
|
||||
folderId = folder,
|
||||
deviceId = deviceId.deviceId,
|
||||
indexId = indexId,
|
||||
localSequence = 0,
|
||||
maxSequence = -1
|
||||
)
|
||||
|
||||
IndexInfo(
|
||||
folderId = folder,
|
||||
deviceId = deviceId.deviceId,
|
||||
indexId = indexId!!,
|
||||
localSequence = 0,
|
||||
maxSequence = -1
|
||||
)
|
||||
}
|
||||
|
||||
if (indexId != null && indexId != newIndexSequenceInfo.indexId) {
|
||||
if (indexId != newIndexSequenceInfo.indexId) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(indexId = indexId)
|
||||
}
|
||||
|
||||
if (maxSequence != null && maxSequence > newIndexSequenceInfo.maxSequence) {
|
||||
if (maxSequence > newIndexSequenceInfo.maxSequence) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(maxSequence = maxSequence)
|
||||
}
|
||||
|
||||
if (localSequence != null && localSequence > newIndexSequenceInfo.localSequence) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(localSequence = localSequence)
|
||||
}
|
||||
|
||||
if (oldIndexSequenceInfo != newIndexSequenceInfo) {
|
||||
transaction.updateIndexInfo(newIndexSequenceInfo)
|
||||
}
|
||||
|
||||
return newIndexSequenceInfo
|
||||
}
|
||||
|
||||
fun updateIndexInfoFromIndexElementProcessor(
|
||||
transaction: IndexTransaction,
|
||||
oldIndexInfo: IndexInfo,
|
||||
localSequence: Long?
|
||||
): IndexInfo {
|
||||
var newIndexSequenceInfo = oldIndexInfo
|
||||
|
||||
if (localSequence != null && localSequence > newIndexSequenceInfo.localSequence) {
|
||||
newIndexSequenceInfo = newIndexSequenceInfo.copy(localSequence = localSequence)
|
||||
}
|
||||
|
||||
if (oldIndexInfo != newIndexSequenceInfo) {
|
||||
transaction.updateIndexInfo(newIndexSequenceInfo)
|
||||
}
|
||||
|
||||
return newIndexSequenceInfo
|
||||
}
|
||||
}
|
||||
|
||||
+30
-25
@@ -19,7 +19,10 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.consume
|
||||
import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.syncthing.java.bep.index.FolderStatsUpdatedEvent
|
||||
import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.bep.index.IndexInfoUpdateEvent
|
||||
import net.syncthing.java.bep.index.IndexRecordAcquiredEvent
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.IndexTransaction
|
||||
@@ -64,7 +67,7 @@ class IndexBrowser internal constructor(
|
||||
}
|
||||
|
||||
fun streamDirectoryListing(folder: String, path: String) = GlobalScope.produce {
|
||||
indexHandler.subscribeToOnIndexRecordAcquiredEvents().consume {
|
||||
indexHandler.subscribeToOnIndexUpdateEvents().consume {
|
||||
val directoryName = PathUtils.getFileName(path)
|
||||
val parentPath = if (PathUtils.isRoot(path)) null else PathUtils.getParentPath(path)
|
||||
val parentDirectoryName = if (parentPath != null) PathUtils.getFileName(parentPath) else null
|
||||
@@ -107,37 +110,39 @@ class IndexBrowser internal constructor(
|
||||
|
||||
// handle updates
|
||||
for (event in this) {
|
||||
var hadChanges = false
|
||||
if (event is IndexRecordAcquiredEvent) {
|
||||
var hadChanges = false
|
||||
|
||||
if (event.folderId == folder) {
|
||||
event.files.forEach { fileUpdate ->
|
||||
// entry change
|
||||
if (fileUpdate.parent == path) {
|
||||
hadChanges = true
|
||||
if (event.folderId == folder) {
|
||||
event.files.forEach { fileUpdate ->
|
||||
// entry change
|
||||
if (fileUpdate.parent == path) {
|
||||
hadChanges = true
|
||||
|
||||
entries = entries.filter { it.fileName != fileUpdate.fileName }
|
||||
entries = entries.filter { it.fileName != fileUpdate.fileName }
|
||||
|
||||
if (!fileUpdate.isDeleted) {
|
||||
entries += listOf(fileUpdate)
|
||||
if (!fileUpdate.isDeleted) {
|
||||
entries += listOf(fileUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
// handle directory info changes
|
||||
if (fileUpdate.parent == parentPath && fileUpdate.fileName == directoryName) {
|
||||
directoryInfo = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
|
||||
// handle parent directory info changes
|
||||
if (fileUpdate.parent == parentParentPath && fileUpdate.fileName == parentDirectoryName) {
|
||||
parentEntry = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
// handle directory info changes
|
||||
if (fileUpdate.parent == parentPath && fileUpdate.fileName == directoryName) {
|
||||
directoryInfo = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
|
||||
// handle parent directory info changes
|
||||
if (fileUpdate.parent == parentParentPath && fileUpdate.fileName == parentDirectoryName) {
|
||||
parentEntry = if (fileUpdate.isDeleted) null else fileUpdate
|
||||
hadChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hadChanges) {
|
||||
dispatch()
|
||||
if (hadChanges) {
|
||||
dispatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,13 @@ class Main(private val commandLine: CommandLine) {
|
||||
|
||||
val repository = SqlRepository(configuration.databaseFolder)
|
||||
|
||||
SyncthingClient(configuration, repository, repository).use { syncthingClient ->
|
||||
SyncthingClient(
|
||||
configuration,
|
||||
repository,
|
||||
repository
|
||||
) { ex ->
|
||||
throw ex.exception
|
||||
}.use { syncthingClient ->
|
||||
val main = Main(cmd)
|
||||
cmd.options.forEach { main.handleOption(it, configuration, syncthingClient) }
|
||||
}
|
||||
@@ -86,8 +92,16 @@ class Main(private val commandLine: CommandLine) {
|
||||
.map { DeviceId(it.trim()) }
|
||||
.toList()
|
||||
System.out.println("set peers = $peers")
|
||||
configuration.peers = peers.map { DeviceInfo(it, null) }.toSet()
|
||||
configuration.persistNow()
|
||||
|
||||
runBlocking {
|
||||
configuration.update { oldConfig ->
|
||||
oldConfig.copy(
|
||||
peers = peers.map { DeviceInfo(it, it.shortId) }.toSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking { configuration.persistNow() }
|
||||
}
|
||||
"p" -> {
|
||||
val folderAndPath = option.value
|
||||
|
||||
@@ -13,14 +13,25 @@
|
||||
*/
|
||||
package net.syncthing.java.client
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
|
||||
import net.syncthing.java.bep.connectionactor.ConnectionInfo
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
|
||||
class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
private val map = mutableMapOf<DeviceId, ConnectionActorWrapper>()
|
||||
private val connectionStatus = ConflatedBroadcastChannel<Map<DeviceId, ConnectionInfo>>(emptyMap())
|
||||
private val connectionStatusLock = Mutex()
|
||||
private val job = Job()
|
||||
|
||||
fun getByDeviceId(deviceId: DeviceId): ConnectionActorWrapper {
|
||||
return synchronized(map) {
|
||||
synchronized(map) {
|
||||
val oldEntry = map[deviceId]
|
||||
|
||||
if (oldEntry != null) {
|
||||
@@ -30,6 +41,17 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
|
||||
map[deviceId] = newEntry
|
||||
|
||||
GlobalScope.launch (job) {
|
||||
newEntry.subscribeToConnectionInfo().consumeEach { status ->
|
||||
connectionStatusLock.withLock {
|
||||
connectionStatus.send(
|
||||
connectionStatus.value +
|
||||
mapOf(deviceId to status)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newEntry
|
||||
}
|
||||
}
|
||||
@@ -39,6 +61,8 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
synchronized(map) {
|
||||
map.values.forEach { it.shutdown() }
|
||||
}
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
fun reconnectAllConnections() {
|
||||
@@ -46,4 +70,12 @@ class Connections (val generate: (DeviceId) -> ConnectionActorWrapper) {
|
||||
map.values.forEach { it.reconnect() }
|
||||
}
|
||||
}
|
||||
|
||||
fun reconnect(deviceId: DeviceId) {
|
||||
synchronized(map) {
|
||||
map[deviceId]?.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToConnectionStatusMap() = connectionStatus.openSubscription()
|
||||
}
|
||||
|
||||
@@ -26,22 +26,21 @@ import net.syncthing.java.bep.index.IndexHandler
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.interfaces.IndexRepository
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
import net.syncthing.java.discovery.DiscoveryHandler
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
class SyncthingClient(
|
||||
private val configuration: Configuration,
|
||||
private val repository: IndexRepository,
|
||||
private val tempRepository: TempRepository,
|
||||
enableDetailedException: Boolean = false
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) : Closeable {
|
||||
val indexHandler = IndexHandler(configuration, repository, tempRepository, enableDetailedException)
|
||||
val discoveryHandler = DiscoveryHandler(configuration)
|
||||
private val onConnectionChangedListeners = Collections.synchronizedList(mutableListOf<(DeviceId) -> Unit>())
|
||||
val indexHandler = IndexHandler(configuration, repository, tempRepository, exceptionReportHandler)
|
||||
val discoveryHandler = DiscoveryHandler(configuration, exceptionReportHandler)
|
||||
|
||||
private val requestHandlerRegistry = RequestHandlerRegistry()
|
||||
private val connections = Connections(
|
||||
@@ -61,31 +60,20 @@ class SyncthingClient(
|
||||
configuration = configuration
|
||||
),
|
||||
deviceId = deviceId,
|
||||
connectivityChangeListener = {
|
||||
synchronized(onConnectionChangedListeners) {
|
||||
onConnectionChangedListeners.forEach { it(deviceId) }
|
||||
}
|
||||
}
|
||||
exceptionReportHandler = exceptionReportHandler
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fun clearCacheAndIndex() {
|
||||
suspend fun clearCacheAndIndex() {
|
||||
indexHandler.clearIndex()
|
||||
configuration.folders = emptySet()
|
||||
configuration.update {
|
||||
it.copy(folders = emptySet())
|
||||
}
|
||||
configuration.persistLater()
|
||||
connections.reconnectAllConnections()
|
||||
}
|
||||
|
||||
fun addOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
|
||||
onConnectionChangedListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeOnConnectionChangedListener(listener: (DeviceId) -> Unit) {
|
||||
assert(onConnectionChangedListeners.contains(listener))
|
||||
onConnectionChangedListeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun getConnections() = configuration.peerIds.map { connections.getByDeviceId(it) }
|
||||
|
||||
init {
|
||||
@@ -93,6 +81,10 @@ class SyncthingClient(
|
||||
getConnections()
|
||||
}
|
||||
|
||||
fun reconnect(deviceId: DeviceId) {
|
||||
connections.reconnect(deviceId)
|
||||
}
|
||||
|
||||
fun connectToNewlyAddedDevices() {
|
||||
getConnections()
|
||||
}
|
||||
@@ -129,11 +121,7 @@ class SyncthingClient(
|
||||
)
|
||||
}
|
||||
|
||||
fun getPeerStatus() = configuration.peers.map { device ->
|
||||
device.copy(
|
||||
isConnected = connections.getByDeviceId(device.deviceId).isConnected
|
||||
)
|
||||
}
|
||||
fun subscribeToConnectionStatus() = connections.subscribeToConnectionStatusMap()
|
||||
|
||||
override fun close() {
|
||||
discoveryHandler.close()
|
||||
@@ -141,6 +129,5 @@ class SyncthingClient(
|
||||
repository.close()
|
||||
tempRepository.close()
|
||||
connections.shutdown()
|
||||
assert(onConnectionChangedListeners.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ dependencies {
|
||||
compile "com.google.code.gson:gson:2.8.2"
|
||||
compile "org.bouncycastle:bcmail-jdk15on:1.59"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import com.google.gson.stream.JsonWriter
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.apache.commons.codec.binary.Base32
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
|
||||
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) {
|
||||
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String): Serializable {
|
||||
|
||||
init {
|
||||
val withoutDashes = this.deviceId.replace("-", "")
|
||||
|
||||
@@ -16,8 +16,9 @@ package net.syncthing.java.core.beans
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.Serializable
|
||||
|
||||
data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected: Boolean? = null) {
|
||||
data class DeviceInfo(val deviceId: DeviceId, val name: String): Serializable {
|
||||
|
||||
companion object {
|
||||
private const val DEVICE_ID = "deviceId"
|
||||
@@ -44,9 +45,6 @@ data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected:
|
||||
}
|
||||
}
|
||||
|
||||
constructor(deviceId: DeviceId, name: String?) :
|
||||
this(deviceId, if (name != null && !name.isBlank()) name else deviceId.shortId, null)
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
@@ -17,20 +17,72 @@ package net.syncthing.java.core.beans
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
|
||||
data class FolderInfo(val folderId: String, val label: String) {
|
||||
// the whitelist are device ids with which the folder should be shared
|
||||
|
||||
// the blacklist are device ids with which the folder should not be shared
|
||||
|
||||
// the ignored device ids are devices for which the user confirmed the blacklist entry so that
|
||||
// there should not be any question
|
||||
data class FolderInfo(
|
||||
val folderId: String,
|
||||
val label: String,
|
||||
val deviceIdWhitelist: Set<DeviceId>,
|
||||
val deviceIdBlacklist: Set<DeviceId>,
|
||||
val ignoredDeviceIdList: Set<DeviceId>
|
||||
) {
|
||||
companion object {
|
||||
private const val FOLDER_ID = "folderId"
|
||||
private const val LABEL = "label"
|
||||
private const val DEVICE_ID_WHITELIST = "deviceWhitelist"
|
||||
private const val DEVICE_ID_BLACKLIST = "deviceBlacklist"
|
||||
private const val IGNORED_DEVICE_ID_LIST = "ignoredDeviceIdList"
|
||||
|
||||
fun parse(reader: JsonReader): FolderInfo {
|
||||
var folderId: String? = null
|
||||
var label: String? = null
|
||||
// the following fields were added later and thus have got a default value
|
||||
var deviceIdWhitelist = emptySet<DeviceId>()
|
||||
var deviceIdBlacklist = emptySet<DeviceId>()
|
||||
var ignoredDeviceIdList = emptySet<DeviceId>()
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
FOLDER_ID -> folderId = reader.nextString()
|
||||
LABEL -> label = reader.nextString()
|
||||
DEVICE_ID_WHITELIST -> {
|
||||
reader.beginArray()
|
||||
|
||||
deviceIdWhitelist = mutableSetOf<DeviceId>().apply {
|
||||
while (reader.hasNext()) {
|
||||
add(DeviceId(reader.nextString()))
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
DEVICE_ID_BLACKLIST -> {
|
||||
reader.beginArray()
|
||||
|
||||
deviceIdBlacklist = mutableSetOf<DeviceId>().apply {
|
||||
while (reader.hasNext()) {
|
||||
add(DeviceId(reader.nextString()))
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
IGNORED_DEVICE_ID_LIST -> {
|
||||
reader.beginArray()
|
||||
|
||||
ignoredDeviceIdList = mutableSetOf<DeviceId>().apply {
|
||||
while (reader.hasNext()) {
|
||||
add(DeviceId(reader.nextString()))
|
||||
}
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
@@ -38,13 +90,23 @@ data class FolderInfo(val folderId: String, val label: String) {
|
||||
|
||||
return FolderInfo(
|
||||
folderId = folderId!!,
|
||||
label = label!!
|
||||
label = label!!,
|
||||
deviceIdBlacklist = deviceIdBlacklist,
|
||||
deviceIdWhitelist = deviceIdWhitelist,
|
||||
ignoredDeviceIdList = ignoredDeviceIdList
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
assert(!folderId.isEmpty())
|
||||
assert(deviceIdWhitelist.find { deviceIdBlacklist.contains(it) } == null)
|
||||
}
|
||||
|
||||
val notIgnoredBlacklistEntries: Set<DeviceId> by lazy {
|
||||
deviceIdBlacklist
|
||||
.filterNot { ignoredDeviceIdList.contains(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -57,7 +119,18 @@ data class FolderInfo(val folderId: String, val label: String) {
|
||||
writer.name(FOLDER_ID).value(folderId)
|
||||
writer.name(LABEL).value(label)
|
||||
|
||||
writer.name(DEVICE_ID_WHITELIST).beginArray()
|
||||
deviceIdWhitelist.forEach { writer.value(it.deviceId) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name(DEVICE_ID_BLACKLIST).beginArray()
|
||||
deviceIdBlacklist.forEach { writer.value(it.deviceId) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name(IGNORED_DEVICE_ID_LIST).beginArray()
|
||||
ignoredDeviceIdList.forEach { writer.value(it.deviceId) }
|
||||
writer.endArray()
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+74
-43
@@ -2,6 +2,14 @@ package net.syncthing.java.core.configuration
|
||||
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
@@ -17,12 +25,14 @@ import java.util.*
|
||||
class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val modifyLock = Mutex()
|
||||
private val saveLock = Mutex()
|
||||
private val configChannel = ConflatedBroadcastChannel<Config>()
|
||||
|
||||
private val configFile = File(configFolder, ConfigFileName)
|
||||
val databaseFolder = File(configFolder, DatabaseFolderName)
|
||||
|
||||
private var isSaved = true
|
||||
private var config: Config
|
||||
|
||||
init {
|
||||
configFolder.mkdirs()
|
||||
@@ -36,19 +46,23 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
}
|
||||
val keystoreData = KeystoreHandler.Loader().generateKeystore()
|
||||
isSaved = false
|
||||
config = Config(peers = setOf(), folders = setOf(),
|
||||
localDeviceName = localDeviceName,
|
||||
localDeviceId = keystoreData.first.deviceId,
|
||||
keystoreData = Base64.toBase64String(keystoreData.second),
|
||||
keystoreAlgorithm = keystoreData.third,
|
||||
customDiscoveryServers = emptySet(),
|
||||
useDefaultDiscoveryServers = true
|
||||
configChannel.sendBlocking(
|
||||
Config(peers = setOf(), folders = setOf(),
|
||||
localDeviceName = localDeviceName,
|
||||
localDeviceId = keystoreData.first.deviceId,
|
||||
keystoreData = Base64.toBase64String(keystoreData.second),
|
||||
keystoreAlgorithm = keystoreData.third,
|
||||
customDiscoveryServers = emptySet(),
|
||||
useDefaultDiscoveryServers = true
|
||||
)
|
||||
)
|
||||
persistNow()
|
||||
runBlocking { persistNow() }
|
||||
} else {
|
||||
config = Config.parse(JsonReader(StringReader(configFile.readText())))
|
||||
configChannel.sendBlocking(
|
||||
Config.parse(JsonReader(StringReader(configFile.readText())))
|
||||
)
|
||||
}
|
||||
logger.debug("Loaded config = $config")
|
||||
logger.debug("Loaded config = ${configChannel.value}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -60,72 +74,89 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
|
||||
val instanceId = Math.abs(Random().nextLong())
|
||||
|
||||
val localDeviceId: DeviceId
|
||||
get() = DeviceId(config.localDeviceId)
|
||||
get() = DeviceId(configChannel.value.localDeviceId)
|
||||
|
||||
val discoveryServers: Set<DiscoveryServer>
|
||||
get() = config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
|
||||
get() = configChannel.value.let { config ->
|
||||
config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
|
||||
}
|
||||
|
||||
val keystoreData: ByteArray
|
||||
get() = Base64.decode(config.keystoreData)
|
||||
get() = Base64.decode(configChannel.value.keystoreData)
|
||||
|
||||
val keystoreAlgorithm: String
|
||||
get() = config.keystoreAlgorithm
|
||||
get() = configChannel.value.keystoreAlgorithm
|
||||
|
||||
val clientName = "syncthing-java"
|
||||
|
||||
val clientVersion = javaClass.`package`.implementationVersion ?: "0.0.0"
|
||||
|
||||
val peerIds: Set<DeviceId>
|
||||
get() = config.peers.map { it.deviceId }.toSet()
|
||||
get() = configChannel.value.peers.map { it.deviceId }.toSet()
|
||||
|
||||
var localDeviceName: String
|
||||
get() = config.localDeviceName
|
||||
set(localDeviceName) {
|
||||
config = config.copy(localDeviceName = localDeviceName)
|
||||
isSaved = false
|
||||
val localDeviceName: String
|
||||
get() = configChannel.value.localDeviceName
|
||||
|
||||
val folders: Set<FolderInfo>
|
||||
get() = configChannel.value.folders
|
||||
|
||||
val peers: Set<DeviceInfo>
|
||||
get() = configChannel.value.peers
|
||||
|
||||
suspend fun update(operation: suspend (Config) -> Config): Boolean {
|
||||
modifyLock.withLock {
|
||||
val oldConfig = configChannel.value
|
||||
val newConfig = operation(oldConfig)
|
||||
|
||||
if (oldConfig != newConfig) {
|
||||
configChannel.send(newConfig)
|
||||
isSaved = false
|
||||
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var folders: Set<FolderInfo>
|
||||
get() = config.folders
|
||||
set(folders) {
|
||||
config = config.copy(folders = folders)
|
||||
isSaved = false
|
||||
}
|
||||
|
||||
var peers: Set<DeviceInfo>
|
||||
get() = config.peers
|
||||
set(peers) {
|
||||
config = config.copy(peers = peers)
|
||||
isSaved = false
|
||||
}
|
||||
|
||||
fun persistNow() {
|
||||
suspend fun persistNow() {
|
||||
persist()
|
||||
}
|
||||
|
||||
fun persistLater() {
|
||||
Thread { persist() }.start()
|
||||
GlobalScope.launch (Dispatchers.IO) { persist() }
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
if (isSaved)
|
||||
return
|
||||
private suspend fun persist() {
|
||||
saveLock.withLock {
|
||||
val (config1, isConfig1Saved) = modifyLock.withLock { configChannel.value to isSaved }
|
||||
|
||||
if (isConfig1Saved) {
|
||||
return
|
||||
}
|
||||
|
||||
config.let {
|
||||
System.out.println("writing config to $configFile")
|
||||
|
||||
configFile.writeText(
|
||||
StringWriter().apply {
|
||||
JsonWriter(this).apply {
|
||||
setIndent(" ")
|
||||
|
||||
config.serialize(this)
|
||||
config1.serialize(this)
|
||||
}
|
||||
}.toString()
|
||||
)
|
||||
isSaved = true
|
||||
|
||||
modifyLock.withLock {
|
||||
if (config1 === configChannel.value) {
|
||||
isSaved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribe() = configChannel.openSubscription()
|
||||
|
||||
override fun toString() = "Configuration(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " +
|
||||
"localDeviceId=${localDeviceId.deviceId}, discoveryServers=$discoveryServers, instanceId=$instanceId, " +
|
||||
"configFile=$configFile, databaseFolder=$databaseFolder)"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2018 Jonas Lochmann
|
||||
*
|
||||
* 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/.
|
||||
*
|
||||
* 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 net.syncthing.java.core.exception
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.Exception
|
||||
|
||||
data class ExceptionReport(
|
||||
val component: String,
|
||||
val exception: Exception,
|
||||
val details: List<ExceptionDetails>
|
||||
) {
|
||||
companion object {
|
||||
fun fromException(exception: Exception, component: String) = ExceptionReport(
|
||||
component,
|
||||
exception,
|
||||
ExceptionDetailException.getExceptionReportDetails(exception)
|
||||
)
|
||||
}
|
||||
|
||||
val detailsReadableString: String by lazy {
|
||||
details.map { it.readableString }.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
data class ExceptionDetails(
|
||||
val component: String,
|
||||
val details: String
|
||||
) {
|
||||
val readableString: String by lazy { component + "\n" + details + "\n" }
|
||||
}
|
||||
|
||||
class ExceptionDetailException(
|
||||
cause: Throwable,
|
||||
val details: ExceptionDetails
|
||||
): Exception(cause) {
|
||||
companion object {
|
||||
fun getExceptionReportDetails(exception: Exception): List<ExceptionDetails> {
|
||||
val result = mutableListOf<ExceptionDetails>()
|
||||
|
||||
var ex: Throwable? = exception
|
||||
|
||||
while (ex != null) {
|
||||
if (ex is ExceptionDetailException) {
|
||||
result.add(ex.details)
|
||||
}
|
||||
|
||||
ex = ex.cause
|
||||
}
|
||||
|
||||
return result.reversed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Job.reportExceptions(component: String, exceptionReportHandler: (ExceptionReport) -> Unit) {
|
||||
invokeOnCompletion {
|
||||
if (it != null) {
|
||||
if (it is Exception) {
|
||||
if (it is CancellationException) {
|
||||
// ignore
|
||||
} else {
|
||||
exceptionReportHandler(ExceptionReport.fromException(it, component))
|
||||
}
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -22,4 +23,6 @@ interface TempRepository: Closeable {
|
||||
fun popTempData(key: String): ByteArray
|
||||
|
||||
fun deleteTempData(keys: List<String>)
|
||||
|
||||
fun deleteAllTempData()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
*/
|
||||
package net.syncthing.java.core.utils
|
||||
|
||||
import net.syncthing.java.core.exception.ExceptionDetailException
|
||||
import net.syncthing.java.core.exception.ExceptionDetails
|
||||
|
||||
object PathUtils {
|
||||
const val ROOT_PATH = ""
|
||||
const val PATH_SEPARATOR = "/"
|
||||
@@ -31,29 +34,56 @@ object PathUtils {
|
||||
return pathSegments.contains(PARENT_PATH) or pathSegments.contains(CURRENT_PATH)
|
||||
}
|
||||
|
||||
private fun isTrimmed(value: String) = value.trim() == value
|
||||
private fun containsWindowsPathSeparator(path: String) = path.contains(PATH_SEPARATOR_WIN)
|
||||
private fun startsWithPathSeperator(path: String) = path.startsWith(PATH_SEPARATOR)
|
||||
private fun isValidPath(path: String) = (!containsRelativeElements(path)) and
|
||||
(!containsWindowsPathSeparator(path)) and
|
||||
path.isNotEmpty() and
|
||||
(!startsWithPathSeperator(path)) and
|
||||
isTrimmed(path)
|
||||
private fun startsWithPathSeparator(path: String) = path.startsWith(PATH_SEPARATOR)
|
||||
|
||||
private fun containsPathSeparator(file: String) = file.contains(PATH_SEPARATOR) or file.contains(PATH_SEPARATOR_WIN)
|
||||
private fun isFilenameValid(file: String) = file.isNotBlank() and
|
||||
(!containsPathSeparator(file)) and
|
||||
isTrimmed(file)
|
||||
|
||||
private fun assertPathValid(path: String) {
|
||||
if (!isValidPath(path)) {
|
||||
throw IllegalArgumentException("provided path is invalid")
|
||||
fun throwException(reason: String) {
|
||||
throw ExceptionDetailException(
|
||||
IllegalArgumentException("provided path is invalid because it $reason"),
|
||||
ExceptionDetails(
|
||||
component = "PathUtils",
|
||||
details = "processed path: \"$path\""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (containsRelativeElements(path)) {
|
||||
throwException("contains relative path elements")
|
||||
}
|
||||
|
||||
if (containsWindowsPathSeparator(path)) {
|
||||
throwException("contains windows path separators")
|
||||
}
|
||||
|
||||
if (path.isEmpty()) {
|
||||
throwException("is empty")
|
||||
}
|
||||
|
||||
if (startsWithPathSeparator(path)) {
|
||||
throwException("starts with a path separator")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertFilenameValid(filename: String) {
|
||||
if (!isFilenameValid(filename)) {
|
||||
throw IllegalArgumentException("provided filename is invalid")
|
||||
fun throwException(reason: String) {
|
||||
throw ExceptionDetailException(
|
||||
IllegalArgumentException("provided filename is invalid because the filename $reason"),
|
||||
ExceptionDetails(
|
||||
component = "PathUtils",
|
||||
details = "processed filename: \"$filename\""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (filename.isBlank()) {
|
||||
throwException("is blank")
|
||||
}
|
||||
|
||||
if (containsPathSeparator(filename)) {
|
||||
throwException("contains a path separator")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +130,12 @@ object PathUtils {
|
||||
|
||||
return dir.removeSuffix(PATH_SEPARATOR) + file
|
||||
}
|
||||
|
||||
fun getFileExtensionFromFilename(filename: String): String {
|
||||
assertFilenameValid(filename)
|
||||
|
||||
val dotIndex = filename.lastIndexOf(".")
|
||||
|
||||
return if (dotIndex != 0) filename.substring(dotIndex + 1) else ""
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler
|
||||
import net.syncthing.java.discovery.utils.AddressRanker
|
||||
@@ -27,19 +28,27 @@ import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
|
||||
class DiscoveryHandler(private val configuration: Configuration) : Closeable {
|
||||
class DiscoveryHandler(
|
||||
private val configuration: Configuration,
|
||||
exceptionReportHandler: (ExceptionReport) -> Unit
|
||||
) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val globalDiscoveryHandler = GlobalDiscoveryHandler(configuration)
|
||||
private val localDiscoveryHandler = LocalDiscoveryHandler(configuration, { message ->
|
||||
logger.info("received device address list from local discovery")
|
||||
private val localDiscoveryHandler = LocalDiscoveryHandler(
|
||||
configuration,
|
||||
exceptionReportHandler,
|
||||
{ message ->
|
||||
logger.info("received device address list from local discovery")
|
||||
|
||||
GlobalScope.launch {
|
||||
processDeviceAddressBg(message.addresses)
|
||||
}
|
||||
}, { deviceId ->
|
||||
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
|
||||
})
|
||||
GlobalScope.launch {
|
||||
processDeviceAddressBg(message.addresses)
|
||||
}
|
||||
},
|
||||
{ deviceId ->
|
||||
onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
|
||||
}
|
||||
)
|
||||
val devicesAddressesManager = DevicesAddressesManager()
|
||||
private var isClosed = false
|
||||
private val onMessageFromUnknownDeviceListeners = Collections.synchronizedSet(HashSet<(DeviceId) -> Unit>())
|
||||
|
||||
@@ -83,14 +83,20 @@ class Main {
|
||||
private fun queryLocalDiscovery(configuration: Configuration, deviceId: DeviceId): Collection<DeviceAddress> {
|
||||
val lock = Object()
|
||||
val discoveredAddresses = mutableListOf<DeviceAddress>()
|
||||
val handler = LocalDiscoveryHandler(configuration, { message ->
|
||||
synchronized(lock) {
|
||||
if (message.deviceId == deviceId) {
|
||||
discoveredAddresses.addAll(message.addresses)
|
||||
lock.notify()
|
||||
val handler = LocalDiscoveryHandler(
|
||||
configuration,
|
||||
{
|
||||
throw it.exception
|
||||
},
|
||||
{ message ->
|
||||
synchronized(lock) {
|
||||
if (message.deviceId == deviceId) {
|
||||
discoveredAddresses.addAll(message.addresses)
|
||||
lock.notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
handler.startListener()
|
||||
handler.sendAnnounceMessage()
|
||||
synchronized(lock) {
|
||||
|
||||
+13
-11
@@ -14,35 +14,37 @@
|
||||
*/
|
||||
package net.syncthing.java.discovery.protocol
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import net.syncthing.java.core.exception.ExceptionReport
|
||||
import net.syncthing.java.core.exception.reportExceptions
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
|
||||
internal class LocalDiscoveryHandler(private val configuration: Configuration,
|
||||
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
|
||||
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}) : Closeable {
|
||||
internal class LocalDiscoveryHandler(
|
||||
private val configuration: Configuration,
|
||||
private val exceptionReportHandler: (ExceptionReport) -> Unit,
|
||||
private val onMessageReceivedListener: (LocalDiscoveryMessage) -> Unit,
|
||||
private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}
|
||||
) : Closeable {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val job = Job()
|
||||
|
||||
fun sendAnnounceMessage() {
|
||||
GlobalScope.launch (Dispatchers.IO) {
|
||||
GlobalScope.async (Dispatchers.IO) {
|
||||
LocalDiscoveryUtil.sendAnnounceMessage(
|
||||
ownDeviceId = configuration.localDeviceId,
|
||||
instanceId = configuration.instanceId
|
||||
)
|
||||
}
|
||||
}.reportExceptions("LocalDiscoveryHandler.sendAnnounceMessage", exceptionReportHandler)
|
||||
}
|
||||
|
||||
fun startListener() {
|
||||
GlobalScope.launch (job) {
|
||||
GlobalScope.async (job) {
|
||||
try {
|
||||
LocalDiscoveryUtil.listenForAnnounceMessages().consumeEach { message ->
|
||||
if (message.deviceId == configuration.localDeviceId) {
|
||||
@@ -60,7 +62,7 @@ internal class LocalDiscoveryHandler(private val configuration: Configuration,
|
||||
} catch (ex: IOException) {
|
||||
logger.warn("Failed to listen for announcement messages", ex)
|
||||
}
|
||||
}
|
||||
}.reportExceptions("LocalDiscoveryHandler.startListener", exceptionReportHandler)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
+20
-7
@@ -22,11 +22,14 @@ import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.syncthing.java.core.beans.DeviceAddress
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.java.core.exception.ExceptionDetailException
|
||||
import net.syncthing.java.core.exception.ExceptionDetails
|
||||
import net.syncthing.java.core.utils.NetworkUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.net.*
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@@ -117,14 +120,24 @@ object LocalDiscoveryUtil {
|
||||
if (broadcastAddress != null) {
|
||||
logger.debug("sending broadcast announce on {}", broadcastAddress)
|
||||
|
||||
DatagramSocket().use { broadcastSocket ->
|
||||
broadcastSocket.broadcast = true
|
||||
try {
|
||||
DatagramSocket().use { broadcastSocket ->
|
||||
broadcastSocket.broadcast = true
|
||||
|
||||
broadcastSocket.send(DatagramPacket(
|
||||
discoveryMessage,
|
||||
discoveryMessage.size,
|
||||
broadcastAddress,
|
||||
LISTENING_PORT))
|
||||
broadcastSocket.send(DatagramPacket(
|
||||
discoveryMessage,
|
||||
discoveryMessage.size,
|
||||
broadcastAddress,
|
||||
LISTENING_PORT))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
throw ExceptionDetailException(
|
||||
ex,
|
||||
ExceptionDetails(
|
||||
component = "LocalDiscoveryUtil.sendAnnounceMessage",
|
||||
details = "interface: $networkInterface\naddress: $interfaceAddress\nbroadcast address: $broadcastAddress"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -1,3 +1,16 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file 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/.
|
||||
*
|
||||
* 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 net.syncthing.repository.android
|
||||
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
@@ -14,7 +27,7 @@ class TempDirectoryLocalRepository(private val directory: File): TempRepository
|
||||
directory.mkdirs()
|
||||
|
||||
// there could be garbage from the previous session which we don't need anymore
|
||||
deleteAllData()
|
||||
deleteAllTempData()
|
||||
}
|
||||
|
||||
override fun pushTempData(data: ByteArray): String {
|
||||
@@ -59,10 +72,10 @@ class TempDirectoryLocalRepository(private val directory: File): TempRepository
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
deleteAllData()
|
||||
deleteAllTempData()
|
||||
}
|
||||
|
||||
fun deleteAllData() {
|
||||
override fun deleteAllTempData() {
|
||||
directory.listFiles().forEach { file ->
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Davide Imbriaco
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -227,6 +228,14 @@ class SqlRepository(databaseFolder: File) : Closeable, IndexRepository, TempRepo
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAllTempData() {
|
||||
getConnection().use { connection ->
|
||||
connection.prepareStatement("DELETE FROM temporary_data").use { statement ->
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VERSION = 13
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
implementation (project(':syncthing-core')) {
|
||||
exclude group: 'commons-logging', module:'commons-logging'
|
||||
exclude group: 'org.slf4j'
|
||||
exclude group: 'ch.qos.logback'
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file 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/.
|
||||
*
|
||||
* 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 net.syncthing.java.repository
|
||||
|
||||
import net.syncthing.java.core.interfaces.TempRepository
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class EncryptedTempRepository(private val otherRepository: TempRepository): TempRepository {
|
||||
companion object {
|
||||
private val secureRandom = SecureRandom()
|
||||
}
|
||||
|
||||
private val keyStorage = Collections.synchronizedMap(mutableMapOf<String, EncryptedTempRepositoryItem>())
|
||||
|
||||
override fun pushTempData(data: ByteArray): String {
|
||||
val (encrypted, config) = encrypt(data)
|
||||
val key = otherRepository.pushTempData(encrypted)
|
||||
|
||||
keyStorage[key] = config
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
override fun popTempData(key: String) = decrypt(otherRepository.popTempData(key), keyStorage.remove(key)!!)
|
||||
|
||||
override fun deleteTempData(keys: List<String>) {
|
||||
keys.forEach { keyStorage.remove(it) }
|
||||
otherRepository.deleteTempData(keys)
|
||||
}
|
||||
|
||||
override fun deleteAllTempData() {
|
||||
keyStorage.clear()
|
||||
otherRepository.deleteAllTempData()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
keyStorage.clear()
|
||||
otherRepository.close()
|
||||
}
|
||||
|
||||
private fun encrypt(input: ByteArray): Pair<ByteArray, EncryptedTempRepositoryItem> {
|
||||
val cryptoSpec = EncryptedTempRepositoryItem(
|
||||
key = ByteArray(16).apply { secureRandom.nextBytes(this) },
|
||||
iv = ByteArray(16).apply { secureRandom.nextBytes(this) },
|
||||
sha512 = sha512(input)
|
||||
)
|
||||
|
||||
val output = Cipher.getInstance("AES/GCM/NoPadding").apply {
|
||||
init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
SecretKeySpec(cryptoSpec.key, "AES"),
|
||||
GCMParameterSpec(128, cryptoSpec.iv)
|
||||
)
|
||||
}.doFinal(input)
|
||||
|
||||
return output to cryptoSpec
|
||||
}
|
||||
|
||||
private fun decrypt(input: ByteArray, config: EncryptedTempRepositoryItem): ByteArray {
|
||||
val output = Cipher.getInstance("AES/GCM/NoPadding").apply {
|
||||
init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(config.key, "AES"),
|
||||
GCMParameterSpec(128, config.iv)
|
||||
)
|
||||
}.doFinal(input)
|
||||
|
||||
if (!sha512(output).contentEquals(config.sha512)) {
|
||||
throw IOException("temporarily file was modified")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
private fun sha512(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-512").digest(data)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Jonas Lochmann
|
||||
*
|
||||
* This Java file 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/.
|
||||
*
|
||||
* 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 net.syncthing.java.repository
|
||||
|
||||
internal class EncryptedTempRepositoryItem(
|
||||
val iv: ByteArray,
|
||||
val key: ByteArray,
|
||||
val sha512: ByteArray
|
||||
)
|
||||
Reference in New Issue
Block a user