14 Commits

Author SHA1 Message Date
l-jonas 5d09d011b1 Update whatsnew 2018-12-07 10:28:46 +01:00
l-jonas 71a433edf6 Release 0.3.8 2018-12-07 10:28:30 +01:00
l-jonas 974817b7a3 Update Releasing.md
Add link to build.gradle which specified the version
2018-12-07 10:27:59 +01:00
Jonas L f00760bddd Update translations 2018-12-07 10:22:32 +01:00
l-jonas 5f539c4149 Fix release builds (#120)
* Fix creating release builds
2018-12-07 10:07:59 +01:00
l-jonas 1869a49c2c Partial crashes (#119)
* Add partial exceptions

* Save last (caught) exception

* Add link to GitHub issues from the settings screen

* Ignore cancellation exceptions

* Show exception message toast longer

* Add more details to exceptions in LocalDiscoveryUtil

* Ensure that the error toast is always shown
2018-12-07 09:57:57 +01:00
l-jonas 91289b05ce Settings locking (#117)
* Add lock for configuration object changes

* Add lock for saving config file
2018-12-07 07:47:59 +01:00
l-jonas 98bc67939f Fix crash in intro activity after screen rotation (#118) 2018-12-07 07:45:07 +01:00
l-jonas fcb31ae9fa Remove connection change callback (#115) 2018-12-06 17:16:52 +01:00
l-jonas 147ad6abcc Fix crash if mime type can not be determined (#114) 2018-12-06 16:56:55 +01:00
l-jonas 4c13af3662 Remove things from the Roadmap which are done 2018-12-06 16:49:09 +01:00
l-jonas 17f9ad336c Selective folder sharing (#113)
* Remove obsolete isConnected from DeviceInfo

* Implement selective folder sharing in the backend

* Fix building

* Add ignored devices to folder info

* Add UI to ask for sync for new devices which share a folder

* Add adding new devices which share a folder to the blacklist

* Add saving the config when the user chose to (not) share a folder

* Fix sync question text + reconnect after decision

* Add UI to configure folder sharing

* Fix applying share config change
2018-12-06 16:46:02 +01:00
l-jonas 852fc0d230 Show status in ui (#110)
* Add showing connection status in the device list

* Show if trying to find device at the last step of the setup wizard
2018-12-04 14:44:03 +01:00
l-jonas 0032726e3e Update dependencies (#109)
* Update build tools and support library

* Remove specifying build tools version

* Remove obsolete comment
2018-12-04 12:52:31 +01:00
68 changed files with 1391 additions and 510 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
- 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 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
-2
View File
@@ -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>
+2 -13
View File
@@ -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 18
versionName "0.3.8"
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'
@@ -17,6 +17,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 +110,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) {
@@ -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)
}
}
}
}
}
}
@@ -103,7 +103,10 @@ class MainActivity : SyncthingActivity() {
private fun cleanCacheAndIndex() {
GlobalScope.launch (Dispatchers.Main) {
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
libraryHandler.libraryManager.withLibrary {
it.syncthingClient.clearCacheAndIndex()
}
recreate()
}
}
@@ -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.getFromUrl(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)
}
@@ -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.getFromUrl(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,15 @@ 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.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 +25,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"
@@ -55,12 +61,18 @@ class LibraryInstance (context: Context) {
clearTempStorageHook = { tempRepository.deleteAllData() }
),
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.getFromUrl(fileInfo.fileName)
)
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
add(Document.COLUMN_FLAGS, 0)
@@ -0,0 +1,17 @@
package net.syncthing.lite.utils
import android.webkit.MimeTypeMap
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 getFromUrl(url: String) = getFromExtension(
MimeTypeMap.getFileExtensionFromUrl(url)
)
}
@@ -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
}
}
}
+10 -1
View File
@@ -1 +1,10 @@
- fix crash after launch in release builds
- showing more detailed connection status in the device list
- selective folder sharing (after this update, you have to confirm each folder once)
- better error handling
- bugfixes
- fix crash if mime type of a file can not be determined
- fix crash in the intro at the second step after a screen rotation
- internal changes
- updated build tools and some dependencies
- locks for changing and saving the configuration
- removed old connection status change listening API
@@ -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>
+38 -25
View File
@@ -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>
+25 -1
View File
@@ -36,9 +36,33 @@
<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_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</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="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-Updates 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>
+2 -1
View File
@@ -49,4 +49,5 @@
<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>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -47,4 +47,5 @@
<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_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -47,4 +47,5 @@
<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>
+2 -1
View File
@@ -47,4 +47,5 @@
<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>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+1
View File
@@ -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>
+1
View File
@@ -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>
+2 -1
View File
@@ -51,4 +51,5 @@ fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
<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>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -47,4 +47,5 @@
<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>
</resources>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+2 -1
View File
@@ -46,4 +46,5 @@
<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>
+18 -8
View File
@@ -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_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,15 @@
</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>
</resources>
+9 -9
View File
@@ -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
View File
@@ -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 {
@@ -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())
@@ -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()
}
}
}
}
@@ -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()
}
@@ -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
}
@@ -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()
}
@@ -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()
)
@@ -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) }
@@ -24,6 +24,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
@@ -38,7 +39,7 @@ 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)
@@ -52,7 +53,7 @@ class IndexHandler(
onIndexRecordAcquiredEvents = onIndexRecordAcquiredEvents,
onFullIndexAcquiredEvents = onFullIndexAcquiredEvents,
onFolderStatsUpdatedEvents = onFolderStatsUpdatedEvents,
enableDetailedException = enableDetailedException
exceptionReportHandler = exceptionReportHandler
)
fun subscribeToOnFullIndexAcquiredEvents() = onFullIndexAcquiredEvents.openSubscription()
@@ -14,8 +14,7 @@ object IndexMessageProcessor {
fun doHandleIndexMessageReceivedEvent(
message: BlockExchangeProtos.IndexUpdate,
peerDeviceId: DeviceId,
transaction: IndexTransaction,
enableDetailedException: Boolean
transaction: IndexTransaction
): Result {
val folderId = message.folder
@@ -29,8 +28,7 @@ object IndexMessageProcessor {
oldRecords = oldRecords,
folder = folderId,
folderStatsUpdateCollector = folderStatsUpdateCollector,
updates = message.filesList,
enableDetailedException = enableDetailedException
updates = message.filesList
)
var sequence: Long = -1
@@ -14,19 +14,18 @@
*/
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
@@ -39,7 +38,7 @@ class IndexMessageQueueProcessor (
private val onFullIndexAcquiredEvents: BroadcastChannel<String>,
private val onFolderStatsUpdatedEvents: BroadcastChannel<FolderStats>,
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 +80,13 @@ class IndexMessageQueueProcessor (
}
init {
GlobalScope.launch(Dispatchers.IO + job) {
GlobalScope.async(Dispatchers.IO + job) {
indexUpdateProcessingQueue.consumeEach {
doHandleIndexMessageReceivedEvent(it)
}
}
}.reportExceptions("IndexMessageQueueProcessor.indexUpdateProcessingQueue", exceptionReportHandler)
GlobalScope.launch(Dispatchers.IO + job) {
GlobalScope.async(Dispatchers.IO + job) {
indexUpdateProcessStoredQueue.consumeEach { action ->
logger.debug("processing index message event from temp record {}", action.updateId)
@@ -100,12 +99,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 +122,7 @@ class IndexMessageQueueProcessor (
val indexResult = IndexMessageProcessor.doHandleIndexMessageReceivedEvent(
message = message,
peerDeviceId = peerDeviceId,
transaction = indexTransaction,
enableDetailedException = enableDetailedException
transaction = indexTransaction
)
val endTime = System.currentTimeMillis()
@@ -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())
}
}
+1
View File
@@ -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()
}
}
@@ -2,6 +2,12 @@ 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.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,6 +23,8 @@ import java.util.*
class Configuration(configFolder: File = DefaultConfigFolder) {
private val logger = LoggerFactory.getLogger(javaClass)
private val modifyLock = Mutex()
private val saveLock = Mutex()
private val configFile = File(configFolder, ConfigFileName)
val databaseFolder = File(configFolder, DatabaseFolderName)
@@ -44,7 +52,7 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
customDiscoveryServers = emptySet(),
useDefaultDiscoveryServers = true
)
persistNow()
runBlocking { persistNow() }
} else {
config = Config.parse(JsonReader(StringReader(configFile.readText())))
}
@@ -78,51 +86,64 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
val peerIds: Set<DeviceId>
get() = config.peers.map { it.deviceId }.toSet()
var localDeviceName: String
val localDeviceName: String
get() = config.localDeviceName
set(localDeviceName) {
config = config.copy(localDeviceName = localDeviceName)
isSaved = false
}
var folders: Set<FolderInfo>
val folders: Set<FolderInfo>
get() = config.folders
set(folders) {
config = config.copy(folders = folders)
isSaved = false
}
var peers: Set<DeviceInfo>
val peers: Set<DeviceInfo>
get() = config.peers
set(peers) {
config = config.copy(peers = peers)
isSaved = false
}
fun persistNow() {
suspend fun update(operation: suspend (Config) -> Config): Boolean {
modifyLock.withLock {
val oldConfig = config
val newConfig = operation(config)
if (oldConfig != newConfig) {
config = newConfig
isSaved = false
return true
} else {
return false
}
}
}
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 { config 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 === config) {
isSaved = true
}
}
}
}
@@ -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
}
}
}
}
@@ -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 = "/"
@@ -47,13 +50,25 @@ object PathUtils {
private fun assertPathValid(path: String) {
if (!isValidPath(path)) {
throw IllegalArgumentException("provided path is invalid")
throw ExceptionDetailException(
IllegalArgumentException("provided path is invalid"),
ExceptionDetails(
component = "PathUtils",
details = "processed path: $path"
)
)
}
}
private fun assertFilenameValid(filename: String) {
if (!isFilenameValid(filename)) {
throw IllegalArgumentException("provided filename is invalid")
throw ExceptionDetailException(
IllegalArgumentException("provided filename is invalid"),
ExceptionDetails(
component = "PathUtils",
details = "processed filename: $filename"
)
)
}
}
@@ -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) {
@@ -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() {
@@ -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"
)
)
}
}
}