80 Commits

Author SHA1 Message Date
l-jonas 2ec151534e Update whatsnew 2019-01-01 08:55:06 +01:00
l-jonas 1e97e11cb7 Release 0.3.11 2019-01-01 08:54:10 +01:00
Jonas L 681fdb9802 Update translations 2019-01-01 08:50:58 +01:00
l-jonas 0df347377a Change up button handling (#135) 2019-01-01 08:49:07 +01:00
Jonas L d6fc356bd7 Fix typo 2018-12-27 15:53:40 +01:00
Jonas L 78ad8756d5 Update translations 2018-12-27 15:52:14 +01:00
l-jonas b96c672a8e Update whatsnew 2018-12-16 07:43:50 +01:00
l-jonas 2a25e9882b Release 0.3.10 2018-12-16 07:43:24 +01:00
Jonas L cb33d8f3e4 Update translations 2018-12-15 15:56:41 +01:00
l-jonas db8e91eafa Fix handling file names with spaces for mime type detection (#134)
This fixes https://github.com/syncthing/syncthing-lite/issues/133
2018-12-13 19:44:44 +01:00
l-jonas f9b91f6ef8 Update whatsnew 2018-12-13 10:34:10 +01:00
l-jonas 01fb92e2c9 Release 0.3.9 2018-12-13 10:33:21 +01:00
l-jonas 4b519e84e3 Update Releasing.md 2018-12-13 10:32:42 +01:00
l-jonas f3d51f0cb9 Add temp repository encryption (#131) 2018-12-13 10:27:07 +01:00
Jonas L fa30beb9d5 Update translations 2018-12-13 10:15:21 +01:00
l-jonas 919fdc31bd Fix crash when accessing closed connections (#129)
* Fix crash when accessing closed connections
2018-12-12 17:43:42 +01:00
l-jonas b3f2af0ee7 Add option to convert file extension to lower case for mime type (#127)
* Add option to convert file extension to lower case for mime type

* Always convert file extension to lower case
2018-12-12 17:20:55 +01:00
l-jonas f33364939b Ignore index updates without matching index info (#125) 2018-12-09 15:39:41 +01:00
l-jonas 1a773daf24 Clear cache handling (#124)
* Remove folder stats from cache when the data is deleted

* Move database deletion off the UI thread

* Remove index info from cache when data is deleted

* Allow observing configuration for changes

* Update folder list on config changes
2018-12-09 15:32:40 +01:00
l-jonas b115a99907 Path validation (#122)
* Remove isTrimmed path validation rule

* Include exact reason in path validation exceptions
2018-12-09 12:00:40 +01:00
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
l-jonas c70211bc24 Update whatsnew 2018-12-03 17:21:45 +01:00
l-jonas a7f80fa45c Release 0.3.7 2018-12-03 17:20:39 +01:00
l-jonas 461d64950b Fix detecting the main dispatcher
This closes https://github.com/syncthing/syncthing-lite/issues/106
2018-12-03 17:19:51 +01:00
l-jonas c37832d084 Create Roadmap.md
This closes https://github.com/syncthing/syncthing-lite/issues/85
2018-12-03 09:39:27 +01:00
l-jonas f336a2932f Update whatsnew 2018-12-02 10:49:49 +01:00
l-jonas 0b3e2bf914 Release 0.3.6 2018-12-02 10:49:08 +01:00
l-jonas 6d9009daff Refactor index handler (#104)
* Add base for new database API

* Implement new database API for the android db implementation

* Implement new API for the default database implementation

* Fix compilation errors

* Move IndexHandler to own package

* Move some code out of the IndexHandler

* Fix compilation errors

* Make IndexInfo a data class

* Make FolderStats a data class

* Make FolderInfo a data class

* Move code out of IndexHandler

* Use one transaction per index message

* Start replacing callbacks by BroadcastChannels

* Fix compilation errors

* Replace callbacks by BroadcastChannels

* Remove IndexHandler.folderInfoByFolder

* Move code out of IndexHandler

* Use index update events to notify IndexBrowsers

* Remove preloading from the IndexBrowser

* Use channels to handle index update messages

* Remove the old ExecutorUtils

* Remove functions from the IndexHandler

* Refactor FolderBrowser

* Remove writeAccessLock

* Remove the indexWaitLock

* Send index change events after the transaction

* Remove markActive from the IndexHandler

* Refactor the IndexBrowser

* Fix showing folder content

* Fix document provider integration

* Fix index sequence handling

* Use a LinearLayout as base for folder entries

* Add theoretically showing of the missing index updates

* Move folder stats update events out of the database

* Send index update events when receiving a cluster config

* Fix counting missing index updates

* Send events after the db transaction at handle cluster config

* Handle index updates in batches

* Add logging of time for index processing

* Deduplicate index updates

* Read old records in bulk

* Update folder stats in bulk

* Fix typo

* Modularize IndexElementProcessor

* Prepare bulk FileInfo updates

* Update FileInfo in bulk

* Make logger private

* Use IO dispatcher

* Reconnect better

* Fix detecting new folders

* Dispatch crashes from background threads to the main thread

* Fix random crash on library shutdown

* Add option for more detailed crash reports

* Fix transaction running check and remove old code at DB abstraction

* Sort directory listings
2018-12-02 10:47:09 +01:00
l-jonas e2a246220e Update whatsnew 2018-11-26 08:29:37 +01:00
l-jonas 98d6656683 Update whatsnew 2018-11-26 08:27:02 +01:00
l-jonas c307953fce Update Releasing.md 2018-11-26 08:26:46 +01:00
l-jonas 68f541f00b Release 0.3.5 2018-11-26 08:24:15 +01:00
l-jonas 29c71f1ca9 Add crash handler (#103)
* Add crash handler
2018-11-25 19:21:45 +01:00
l-jonas 76ddbdd3b4 New connection handling (#71) 2018-11-25 19:10:05 +01:00
l-jonas cae1026f35 Bugfixes (#100)
* Fix loading subdirectories on the main thread (which caused a crash)
* Fix LibraryHandler creation in the background (ContentProvider)
2018-11-25 18:01:31 +01:00
l-jonas d07c934ea7 Catch index updates after shutdown (#96)
* Catch index updates after shutdown
* Re-add wrongly removed line
2018-11-21 14:52:01 +01:00
l-jonas d829c18e76 Fix some warnings (#97) 2018-11-21 14:50:48 +01:00
l-jonas e41ed80d05 Document release process (#69)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Add documentation about the releasing process

* Remove release scripts

* Remove library from the release process

* Update Releasing.md

* Update Releasing.md

* Update Releasing.md

* Update Releasing.md

* Update Releasing.md
2018-11-20 13:42:28 +01:00
l-jonas 3e691b61c0 Fix handling of paths with tilde (#91)
* Refactor PathUtils
2018-11-18 08:26:51 +01:00
l-jonas 0fb7a9e93d Release 0.3.4 2018-11-13 07:33:03 +01:00
l-jonas 1b4205b04a Update library version to fix proguard warnings 2018-11-13 07:30:36 +01:00
l-jonas 8e00c8b4a0 Release 0.3.3 2018-11-12 17:48:42 +01:00
l-jonas f3ca98be80 Update changelog 2018-11-12 17:48:10 +01:00
l-jonas 96fc8bfc7b Bugfixes (#92)
* Fix loading subdirectories on the main thread (which caused a crash)
* Fix LibraryHandler creation in the background (ContentProvider)
2018-11-12 17:42:19 +01:00
l-jonas 58098aae0f Provide real file names to apps (#79)
* Send correct file names when files are opened from the app UI

This fixes https://github.com/syncthing/syncthing-lite/issues/76
2018-11-12 17:39:38 +01:00
l-jonas c4ad797905 Discovery server validation (#82)
* Add possibility to verify discovery server certificates
* Add new discovery server storage model
* Use new discovery server config
2018-11-12 16:53:32 +01:00
l-jonas a61d8c5c4f Fix file deletion (#87)
Fix applying deleted files
2018-11-12 16:46:49 +01:00
l-jonas af579f8311 Save as dialog (#88)
Add save as support
2018-11-12 16:46:02 +01:00
Jonas L fbdcdbf7ec Update translations 2018-11-11 19:48:48 +01:00
Felix Ableitner e6870a08d6 Update Google Play listing from gradle (fixes #83) (#84) 2018-11-10 14:34:33 +01:00
l-jonas fbee0ca0e8 Remove obsolete link to syncthing-java 2018-11-09 16:42:05 +01:00
l-jonas 65b42475a6 Add note about 0% sync progress 2018-11-09 16:41:33 +01:00
l-jonas af09b763a6 Adaptive icon (#80)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Steal adaptive icon from syncthing-android

https://github.com/syncthing/syncthing-android/commit/c43ee663a26ebd3579b2553783ab8ab2108fa350

* Replace background layer
2018-11-08 17:45:49 +01:00
l-jonas 5680c6c554 Use stable coroutines (#72)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Update Kotlin and use stable coroutines

* Optimize imports

* Optimize imports again

* Remove some imports manually
2018-11-08 16:27:27 +01:00
l-jonas 2caaebfc33 Document handling of requests for new languages
This closes https://github.com/syncthing/syncthing-lite/issues/77
2018-11-08 13:06:33 +01:00
l-jonas 460d421a79 Release 0.3.2 2018-11-07 07:41:38 +01:00
Jonas L 96edbaa240 Update translations 2018-11-07 07:00:37 +01:00
l-jonas de1566915b Finish spanish translation (#73)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Finish spanish translation

This is a import from Transifex because I got a automatic notification that the spanish translation was finished
2018-11-06 21:02:32 +01:00
Felix Ableitner a294d6f06c Add gradle play publisher for automated releases to Google Play 2018-11-06 17:11:07 +01:00
l-jonas 7ce017f48c Update translations (#68)
* Move google to the top of allrepositories.google

This should fix build issues according to https://gitlab.com/fdroid/fdroiddata/issues/1423

* Merge translations

* Import fixed localization of the remove device dialog title

* Add new translations

* Update strings with wrong escaping in Transifex and import them

* Fix escaping at some other strings

* Update translations
2018-11-06 13:46:57 +01:00
l-jonas 55edc592a0 Remove obsolete build instructions from the readme 2018-11-06 12:57:35 +01:00
l-jonas eb5dfcbd46 Refactor discovery code (#64)
* Move discovery server query out of GlobalDiscoveryHandler

* Refactor GlobalDiscoveryHandler

* Run global discovery in bulk (only pick discovery servers once)

* Readd ignoring failed global discovery

* Add new code for local discovery

* Use new code for local discovery

* Fix compiling the discovery CLI

* Fix code style

* Add Copyright headers to all files of syncthing-discovery

* Refactor the AddressRanker

* Fix timeout at the AddressRanker

* Refactor the DiscoveryHandler

* Use HttpUrlConnection in GlobalDiscoveryUtil

* Disable https verification in GlobalDiscoveryUtil

* Get the changes working

* Add newline

* De-hardcode announce server query url generation

* Only ignore specific global discovery exceptions

* Only catch IOException at LocalDiscoveryHandler.startListener()

* Only catch IOException at LocalDiscoveryUtil when processing the package
2018-11-06 12:13:44 +01:00
l-jonas 2b55bd9e76 Fix typo in the App which was at the Readme too 2018-10-29 07:45:14 +01:00
Licaon_Kter eb8360f276 Readme typos (#65) 2018-10-29 07:43:39 +01:00
l-jonas 48f880ee4e Release version 0.3.1 2018-10-28 17:56:02 +01:00
l-jonas 9f5072ed3a Move google() to the top of allprojects.repositories
According to https://forum.f-droid.org/t/syncthing-lite/4394 this is required to make this App build correctly for F-Droid
2018-10-28 17:55:08 +01:00
l-jonas b1743db5af Update wording of the Readme to the one of the dialog in the App 2018-10-27 15:23:02 +02:00
172 changed files with 7109 additions and 4280 deletions
+14 -16
View File
@@ -4,19 +4,21 @@
[![MPLv2 License](https://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](https://www.mozilla.org/MPL/2.0/)
This project is an Android app, that works as a client for a [Syncthing][1] share (accessing
Syncthing devices in the same way a client-server file sharing app access its proprietary server).
Syncthing devices in the same way a client-server file sharing app accesses its proprietary server).
This is a client-oriented implementation, designed to work online by downloading and
uploading files from an active device on the network (instead of synchronizing a local copy of
the entire repository). This is quite different from the way the [syncthing-android][2] works,
and its useful from those devices that cannot or wish not to download the entire repository (for
the entire repository).
Due to that, you will see a sync progress of 0% at other devices (and this is expected).
This is quite different from the way the [syncthing-android][2] works,
and it's useful for those devices that cannot or do not wish to download the entire repository (for
example, mobile devices with limited storage available, wishing to access a syncthing share).
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
This project is based on syncthing-java (which is in this repository too), a java implementation of Syncthing protocols.
Due to the behaviour of this App and the [behaviour of the Syncthing Server](https://github.com/syncthing/syncthing/issues/5224),
you can't reconnct for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
This does not apply to connections over an WiFi, but to connections over the internet.
you can't reconnect for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
This does not apply to local discovery connections.
[<img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80">](https://f-droid.org/packages/net.syncthing.lite/)
[<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80">](https://play.google.com/store/apps/details?id=net.syncthing.lite)
@@ -24,21 +26,17 @@ This does not apply to connections over an WiFi, but to connections over the int
## Translations
The project is translated on [Transifex](https://www.transifex.com/syncthing/syncthing-lite/).
Requests for new languages are always accepted (but this happens manually because there is no option to accept it automatically).
## Building
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
you install [Android Studio][4] and import the project.
To compile with a development version of the [syncthing-java][3] library, you have to install it to
the local maven repository. To do this, clone the repo and run `gradle install` in the
syncthing-java project folder.
The project uses a standard Android build, and requires the Android SDK. The easiest option is to
install [Android Studio][3] and import the project.
## License
All code is licensed under the [MPLv2 License][5].
All code is licensed under the [MPLv2 License][4].
[1]: https://syncthing.net/
[2]: https://github.com/syncthing/syncthing-android
[3]: https://github.com/Nutomic/syncthing-java
[4]: https://developer.android.com/studio/index.html
[5]: LICENSE
[3]: https://developer.android.com/studio/index.html
[4]: LICENSE
+9
View File
@@ -0,0 +1,9 @@
# Releasing
- do tests
- update translations using ``tx pull -af`` (as extra merge request or branch for the case it does not build correctly)
- update the version name and version code of the app [here](https://github.com/syncthing/syncthing-lite/blob/master/app/build.gradle)
- update the changelog at [app/src/main/play/en-GB/whatsnew](https://github.com/syncthing/syncthing-lite/blob/master/app/src/main/play/en-GB/whatsnew)
- create a tag/ release in GitHub with an changelog; The tag name should be the version number
- trigger a release at <https://build.syncthing.net/> to publish the release to google play
- F-Droid picks up the release by the tag
+20
View File
@@ -0,0 +1,20 @@
# Roadmap
## What should happen
- 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
- 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>)
- 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>
## What could happen
- thumbnails (<https://github.com/syncthing/syncthing-lite/issues/37>)
## What will not happen
- additional encryption within the App (see <https://github.com/syncthing/syncthing-lite/issues/85>)
+20 -16
View File
@@ -2,18 +2,26 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.triplet.play'
android {
compileSdkVersion 27
buildToolsVersion "28.0.2"
dataBinding.enabled = true
playAccountConfigs {
defaultAccountConfig {
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
}
}
defaultConfig {
applicationId "net.syncthing.lite"
minSdkVersion 21
targetSdkVersion 26
versionCode 10
versionName "0.3.0"
versionCode 21
versionName "0.3.11"
multiDexEnabled true
playAccountConfig = playAccountConfigs.defaultAccountConfig
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
@@ -51,33 +59,28 @@ android {
}
}
play {
jsonFile = file(System.getenv("SYNCTHING_RELEASE_PLAY_ACCOUNT_CONFIG_FILE") ?: 'keys.json')
uploadImages = true
track = 'production'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.anko:anko-commons:$anko_version"
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
implementation "com.android.support:design:$support_version"
implementation "com.android.support:preference-v14:$support_version"
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.apache.httpcomponents', module: 'httpclient'
exclude group: 'org.slf4j'
exclude group: 'ch.qos.logback'
}
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
implementation 'sk.baka.slf4j:slf4j-handroid:1.7.26'
implementation 'com.google.zxing:android-integration:3.3.0'
@@ -85,4 +88,5 @@ dependencies {
implementation 'com.github.apl-devs:appintro:v4.2.3'
implementation project(':syncthing-repository-android')
implementation project(':syncthing-temp-repository-encryption')
}
+3
View File
@@ -28,6 +28,9 @@
volatile <fields>;
}
# fix detecting the main dispatcher
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
# disable warnings
-dontwarn com.google.protobuf.UnsafeUtil
-dontwarn com.google.protobuf.UnsafeUtil$1
+3 -6
View File
@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".android.Application"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -22,14 +23,10 @@
<activity android:name=".activities.FolderBrowserActivity"
android:parentActivityName=".activities.MainActivity"/>
<provider
android:name="android.support.v4.content.FileProvider"
android:name=".library.CacheFileProvider"
android:authorities="net.syncthing.lite.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
android:exported="false" />
<provider
android:name=".library.SyncthingProvider"
android:authorities="net.syncthing.lite.documents"
@@ -4,150 +4,208 @@ import android.app.Activity
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.IndexBrowser
import android.view.Menu
import android.view.MenuItem
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.consumeEach
import net.syncthing.java.bep.index.browser.DirectoryContentListing
import net.syncthing.java.bep.index.browser.DirectoryListing
import net.syncthing.java.bep.index.browser.DirectoryNotFoundListing
import net.syncthing.java.bep.index.browser.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.BuildConfig
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
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
import org.jetbrains.anko.custom.async
class FolderBrowserActivity : SyncthingActivity() {
companion object {
private const val TAG = "FolderBrowserActivity"
private const val REQUEST_SELECT_UPLOAD_FILE = 171
private const val STATUS_PATH = "path"
const val EXTRA_FOLDER_NAME = "folder_name"
}
private lateinit var binding: ActivityFolderBrowserBinding
private lateinit var indexBrowser: IndexBrowser
private val adapter = FolderContentsAdapter()
private lateinit var folder: String
private val path = ConflatedBroadcastChannel<String>()
private val listing = ConflatedBroadcastChannel<DirectoryListing?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
val binding: ActivityFolderBrowserBinding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
val adapter = FolderContentsAdapter()
binding.listView.adapter = adapter
binding.mainListViewUploadHereButton.setOnClickListener {
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
},
REQUEST_SELECT_UPLOAD_FILE
)
}
adapter.listener = object: FolderContentsListener {
override fun onItemClicked(fileInfo: FileInfo) {
navigateToFolder(fileInfo)
if (fileInfo.isDirectory()) {
path.offer(fileInfo.path)
} else {
DownloadFileDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
}
}
override fun onItemLongClicked(fileInfo: FileInfo): Boolean {
return if (fileInfo.type == FileInfo.FileType.FILE) {
FileMenuDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
true
} else {
false
}
}
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
libraryHandler?.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
ReconnectIssueDialogFragment.showIfNeeded(this)
folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
path.offer(if (savedInstanceState == null) IndexBrowser.ROOT_PATH else savedInstanceState.getString(STATUS_PATH))
launch {
var job = Job()
path.consumeEach { path ->
job.cancel()
job = Job()
binding.listView.scrollToPosition(0)
listing.send(null)
async(job) {
libraryHandler.libraryManager.streamDirectoryListing(folder, path).consumeEach {
listing.send(it)
}
}
}
}
launch {
listing.openSubscription().consumeEach { listing ->
if (listing == null) {
binding.isLoading = true
} else {
supportActionBar?.title = if (PathUtils.isRoot(listing.path)) folder else PathUtils.getFileName(listing.path)
binding.isLoading = false
adapter.data = if (listing is DirectoryContentListing)
listing.entries.sortedWith(IndexBrowser.sortAlphabeticallyDirectoriesFirst)
else
emptyList()
}
}
}
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 onDestroy() {
super.onDestroy()
Thread {
indexBrowser.setOnFolderChangedListener(null)
indexBrowser.close()
}.start()
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(STATUS_PATH, path.value)
}
private fun goUp(): Boolean {
val currentListing = listing.value
val parentPath = when (currentListing) {
is DirectoryContentListing -> currentListing.parentEntry?.path
is DirectoryNotFoundListing -> currentListing.theoreticalParentPath
else -> null
}
return if (parentPath == null) {
false
} else {
path.offer(parentPath)
true
}
}
override fun onBackPressed() {
//click item '0', ie '..' (go to parent)
navigateToFolder(adapter.data[0])
if (!goUp()) {
super.onBackPressed()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
libraryHandler?.syncthingClient { syncthingClient ->
async (UI) {
libraryHandler.syncthingClient { syncthingClient ->
GlobalScope.launch (Dispatchers.Main) {
// FIXME: it would be better if the dialog would use the library handler
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
{ showFolderListView(indexBrowser.currentPath) }).show()
FileUploadDialog(
this@FolderBrowserActivity,
syncthingClient,
intent!!.data,
folder,
path.value,
{ /* nothing to do on success */ }
).show()
}
}
}
}
private fun showFolderListView(path: String) {
indexBrowser.navigateToNearestPath(path)
navigateToFolder(indexBrowser.currentPathInfo())
}
private fun navigateToFolder(fileInfo: FileInfo) {
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
finish()
} else {
if (fileInfo.isDirectory()) {
async {
indexBrowser.navigateTo(fileInfo)
}
Log.d(TAG, "load folder cache bg")
binding.isLoading = true
} else {
if (BuildConfig.DEBUG) {
Log.i(TAG, "pulling file = " + fileInfo)
}
DownloadFileDialogFragment.newInstance(fileInfo).show(supportFragmentManager)
}
super.onActivityResult(requestCode, resultCode, intent)
}
}
private fun onFolderChanged() {
runOnUiThread {
binding.isLoading = false
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.folder_browser, menu)
async {
val list = indexBrowser.listFiles()
return true
}
async (UI) {
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
assert(!list.isEmpty())//list must contain at least the 'parent' path
adapter.data = list
binding.listView.scrollToPosition(0)
if (indexBrowser.isRoot())
libraryHandler?.folderBrowser {
val title = it.getFolderInfo(indexBrowser.folder)?.label
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.go_home -> {
finish()
async(UI) {
supportActionBar?.title = title
}
}
else
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
}
}
true
}
}
android.R.id.home -> {
if (!goUp()) {
finish()
}
private fun updateFolderListView() {
showFolderListView(indexBrowser.currentPath)
}
private fun showUploadHereDialog() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
updateFolderListView()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -4,8 +4,6 @@ import android.arch.lifecycle.Observer
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.text.Html
@@ -14,9 +12,10 @@ 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.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.lite.R
import net.syncthing.lite.databinding.FragmentIntroOneBinding
@@ -41,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())
@@ -73,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)
@@ -81,38 +85,29 @@ 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
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_two, container, false)
binding.enterDeviceId!!.scanQrCode.setOnClickListener {
binding.enterDeviceId.scanQrCode.setOnClickListener {
FragmentIntentIntegrator(this@IntroFragmentTwo).initiateScan()
}
binding.enterDeviceId!!.scanQrCode.setImageResource(R.drawable.ic_qr_code_white_24dp)
binding.enterDeviceId.scanQrCode.setImageResource(R.drawable.ic_qr_code_white_24dp)
return binding.root
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
binding.enterDeviceId!!.deviceId.setText(scanResult.contents)
binding.enterDeviceId!!.deviceIdHolder.isErrorEnabled = false
binding.enterDeviceId.deviceId.setText(scanResult.contents)
binding.enterDeviceId.deviceIdHolder.isErrorEnabled = false
}
}
@@ -122,15 +117,21 @@ class IntroActivity : AppIntro() {
*/
fun isDeviceIdValid(): Boolean {
return try {
val deviceId = binding.enterDeviceId!!.deviceId.text.toString()
Util.importDeviceId(libraryHandler, context, deviceId, { })
val deviceId = binding.enterDeviceId.deviceId.text.toString()
Util.importDeviceId(libraryHandler.libraryManager, context!!, deviceId, { })
true
} catch (e: IOException) {
binding.enterDeviceId!!.deviceId.error = getString(R.string.invalid_device_id)
binding.enterDeviceId.deviceId.error = getString(R.string.invalid_device_id)
false
}
}
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() {
@@ -177,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, _ ->
async(UI) {
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, _ ->
async(UI) {
if (config.folders.isNotEmpty()) {
client.removeOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
(activity as IntroActivity?)?.onDonePressed(this@IntroFragmentThree)
}
}
}
}
}
}
@@ -8,8 +8,9 @@ import android.support.v4.app.Fragment
import android.support.v7.app.ActionBarDrawerToggle
import android.view.Gravity
import android.view.MenuItem
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ActivityMainBinding
import net.syncthing.lite.dialogs.DeviceIdDialogFragment
@@ -101,9 +102,10 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
async(UI) {
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
recreate()
launch {
libraryHandler.libraryManager.withLibrary {
it.syncthingClient.clearCacheAndIndex()
}
}
}
}
@@ -1,26 +1,21 @@
package net.syncthing.lite.activities
import android.app.AlertDialog
import android.content.Context
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.async.CoroutineActivity
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.library.LibraryHandler
import org.jetbrains.anko.contentView
import org.slf4j.impl.HandroidLoggerAdapter
abstract class SyncthingActivity : AppCompatActivity() {
abstract class SyncthingActivity : CoroutineActivity() {
val libraryHandler: LibraryHandler by lazy {
LibraryHandler(
context = this@SyncthingActivity,
onIndexUpdateProgressListener = this::onIndexUpdateProgress,
onIndexUpdateCompleteListener = this::onIndexUpdateComplete
context = this@SyncthingActivity
)
}
private var loadingDialog: AlertDialog? = null
@@ -59,19 +54,6 @@ abstract class SyncthingActivity : AppCompatActivity() {
loadingDialog?.dismiss()
}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {
val message = getString(R.string.index_update_progress_label, folderInfo.label, percentage)
snackBar?.setText(message) ?: run {
snackBar = Snackbar.make(contentView!!, message, Snackbar.LENGTH_INDEFINITE)
snackBar?.show()
}
}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {
snackBar?.dismiss()
snackBar = null
}
open fun onLibraryLoaded() {
// nothing to do
}
@@ -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)
@@ -50,6 +50,10 @@ class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
listener?.onItemClicked(fileInfo)
}
binding.root.setOnLongClickListener {
listener?.onItemLongClicked(fileInfo) ?: false
}
binding.executePendingBindings()
}
@@ -59,6 +63,7 @@ class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
interface FolderContentsListener {
fun onItemClicked(fileInfo: FileInfo)
fun onItemLongClicked(fileInfo: FileInfo): Boolean
}
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
@@ -2,8 +2,10 @@ package net.syncthing.lite.adapters
import android.support.v7.widget.RecyclerView
import android.text.format.DateUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import net.syncthing.java.bep.folder.FolderStatus
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
@@ -11,7 +13,7 @@ import net.syncthing.lite.databinding.ListviewFolderBinding
import kotlin.properties.Delegates
class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
var data: List<Pair<FolderInfo, FolderStats>> by Delegates.observable(listOf()) {
var data: List<FolderStatus> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
}
@@ -22,7 +24,7 @@ class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
}
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].first.folderId.hashCode().toLong()
override fun getItemId(position: Int) = data[position].info.folderId.hashCode().toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderListViewHolder (
ListviewFolderBinding.inflate(
@@ -32,19 +34,31 @@ class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
override fun onBindViewHolder(holder: FolderListViewHolder, position: Int) {
val binding = holder.binding
val (folderInfo, folderStats) = data[position]
val item = data[position]
val (folderInfo, folderStats) = item
val context = holder.itemView.context
Log.d("FolderListAdapter", "$item")
binding.folderName = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
binding.lastModification = context.getString(R.string.last_modified_time,
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.info = context.getString(R.string.folder_content_info, folderStats.describeSize(), folderStats.fileCount, folderStats.dirCount)
binding.info = context.getString(R.string.folder_content_info, folderStats.sizeDescription, folderStats.fileCount, folderStats.dirCount)
binding.info2 = if (item.missingIndexUpdates == 0L)
null
else
context.getString(R.string.pending_index_updates, item.missingIndexUpdates)
binding.root.setOnClickListener {
listener?.onFolderClicked(folderInfo, folderStats)
}
binding.root.setOnLongClickListener {
listener?.onFolderLongClicked(folderInfo) ?: false
}
}
}
@@ -52,4 +66,5 @@ class FolderListViewHolder(val binding: ListviewFolderBinding): RecyclerView.Vie
interface FolderListAdapterListener {
fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats)
}
fun onFolderLongClicked(folderInfo: FolderInfo): Boolean
}
@@ -0,0 +1,48 @@
package net.syncthing.lite.android
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.util.Log
import net.syncthing.lite.error.ErrorStorage
class Application: Application() {
companion object {
private const val LOG_TAG = "Application"
private val handler = Handler(Looper.getMainLooper())
}
override fun onCreate() {
super.onCreate()
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
val mainThread = Thread.currentThread()
if (defaultHandler == null) {
Log.w(LOG_TAG, "could not get default crash handler")
}
fun handleCrash(ex: Throwable) {
Log.w(LOG_TAG, "app crashed", ex)
ErrorStorage.reportError(
this,
Log.getStackTraceString(ex)
)
if (defaultHandler != null) {
defaultHandler.uncaughtException(mainThread, ex)
} else {
System.exit(1)
}
}
Thread.setDefaultUncaughtExceptionHandler { thread, ex ->
if (Looper.getMainLooper() === Looper.myLooper()) {
handleCrash(ex)
} else {
handler.post { handleCrash(ex) }
}
}
}
}
@@ -0,0 +1,19 @@
package net.syncthing.lite.async
import android.support.v7.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class CoroutineActivity: AppCompatActivity(), CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
@@ -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,19 @@
package net.syncthing.lite.async
import android.support.v4.app.Fragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
abstract class CoroutineFragment: Fragment(), CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
@@ -17,8 +17,9 @@ import android.widget.Toast
import com.google.zxing.BarcodeFormat
import com.google.zxing.WriterException
import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.lite.R
import net.syncthing.lite.databinding.DialogDeviceIdBinding
import net.syncthing.lite.fragments.SyncthingDialogFragment
@@ -62,7 +63,7 @@ class DeviceIdDialogFragment: SyncthingDialogFragment() {
))
}
async (UI) {
GlobalScope.launch (Dispatchers.Main) {
binding.deviceId.text = deviceId.deviceId
binding.deviceId.visibility = View.VISIBLE
@@ -83,7 +84,7 @@ class DeviceIdDialogFragment: SyncthingDialogFragment() {
}
}
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
binding.flipper.displayedChild = 1
binding.qrCode.setImageBitmap(bmp)
}
@@ -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)
}
@@ -0,0 +1,76 @@
package net.syncthing.lite.dialogs
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
import android.support.v4.app.FragmentManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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 net.syncthing.lite.utils.MimeType
class FileMenuDialogFragment: BottomSheetDialogFragment() {
companion object {
private const val ARG_FILE_SPEC = "file spec"
private const val TAG = "DownloadFileDialog"
private const val REQ_SAVE_AS = 1
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
folder = fileInfo.folder,
path = fileInfo.path,
fileName = fileInfo.fileName
))
fun newInstance(fileSpec: DownloadFileSpec) = FileMenuDialogFragment().apply {
arguments = Bundle().apply {
putSerializable(ARG_FILE_SPEC, fileSpec)
}
}
}
val fileSpec: DownloadFileSpec by lazy {
arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = DialogFileBinding.inflate(inflater, container, false)
binding.filename = fileSpec.fileName
binding.saveAsButton.setOnClickListener {
startActivityForResult(
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = MimeType.getFromFilename(fileSpec.fileName)
putExtra(Intent.EXTRA_TITLE, fileSpec.fileName)
},
REQ_SAVE_AS
)
}
return binding.root
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQ_SAVE_AS -> {
if (resultCode == Activity.RESULT_OK) {
DownloadFileDialogFragment.newInstance(fileSpec, data!!.data!!).show(fragmentManager)
dismiss()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
fun show(fragmentManager: FragmentManager?) {
show(fragmentManager, TAG)
}
}
@@ -3,8 +3,6 @@ package net.syncthing.lite.dialogs
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
@@ -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)
}
@@ -7,23 +7,24 @@ import android.arch.lifecycle.ViewModelProviders
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentManager
import android.support.v4.content.FileProvider
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
class DownloadFileDialogFragment : DialogFragment() {
companion object {
private const val ARG_FILE_SPEC = "file spec"
private const val ARG_SAVE_AS_URI = "save as"
private const val TAG = "DownloadFileDialog"
fun newInstance(fileInfo: FileInfo) = newInstance(DownloadFileSpec(
@@ -32,9 +33,16 @@ class DownloadFileDialogFragment : DialogFragment() {
fileName = fileInfo.fileName
))
fun newInstance(fileSpec: DownloadFileSpec) = DownloadFileDialogFragment().apply {
fun newInstance(
fileSpec: DownloadFileSpec,
outputUri: Uri? = null
) = DownloadFileDialogFragment().apply {
arguments = Bundle().apply {
putSerializable(ARG_FILE_SPEC, fileSpec)
if (outputUri != null) {
putParcelable(ARG_SAVE_AS_URI, outputUri)
}
}
}
}
@@ -45,11 +53,17 @@ class DownloadFileDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val fileSpec = arguments!!.getSerializable(ARG_FILE_SPEC) as DownloadFileSpec
val outputUri = if (arguments!!.containsKey(ARG_SAVE_AS_URI))
arguments!!.getParcelable(ARG_SAVE_AS_URI) as Uri
else
null
model.init(
libraryHandler = LibraryHandler(context!!),
fileSpec = fileSpec,
externalCacheDir = context!!.externalCacheDir
externalCacheDir = context!!.externalCacheDir,
outputUri = outputUri,
contentResolver = context!!.contentResolver
)
val progressDialog = ProgressDialog(context).apply {
@@ -73,22 +87,31 @@ class DownloadFileDialogFragment : DialogFragment() {
is DownloadFileStatusDone -> {
dismissAllowingStateLoss()
try {
context!!.startActivity(
Intent(Intent.ACTION_VIEW)
.setDataAndType(
FileProvider.getUriForFile(context!!, "net.syncthing.lite.fileprovider", status.file),
MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(fileSpec.fileName))
)
.newTask()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
} catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "No handler found for file " + status.file.name, e)
}
if (outputUri == null) {
val mimeType = MimeType.getFromFilename(fileSpec.fileName)
context!!.toast(R.string.toast_open_file_failed)
try {
context!!.startActivity(
Intent(Intent.ACTION_VIEW)
.setDataAndType(
CacheFileProviderUrl.fromFile(
filename = fileSpec.fileName,
mimeType = mimeType,
file = status.file,
context = context!!
).serialized,
mimeType
)
.newTask()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
} catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "No handler found for file " + status.file.name, e)
}
context!!.toast(R.string.toast_open_file_failed)
}
}
}
is DownloadFileStatusFailed -> {
@@ -2,12 +2,17 @@ package net.syncthing.lite.dialogs.downloadfile
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel;
import android.arch.lifecycle.ViewModel
import android.content.ContentResolver
import android.net.Uri
import android.support.v4.os.CancellationSignal
import android.util.Log
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.library.DownloadFileTask
import net.syncthing.lite.library.LibraryHandler
import org.apache.commons.io.FileUtils
import java.io.File
class DownloadFileDialogViewModel : ViewModel() {
@@ -20,7 +25,13 @@ class DownloadFileDialogViewModel : ViewModel() {
private val cancellationSignal = CancellationSignal()
val status: LiveData<DownloadFileStatus> = statusInternal
fun init(libraryHandler: LibraryHandler, fileSpec: DownloadFileSpec, externalCacheDir: File) {
fun init(
libraryHandler: LibraryHandler,
fileSpec: DownloadFileSpec,
externalCacheDir: File,
outputUri: Uri?,
contentResolver: ContentResolver
) {
if (isInitialized) {
return
}
@@ -54,10 +65,26 @@ class DownloadFileDialogViewModel : ViewModel() {
statusInternal.value = DownloadFileStatusRunning(newProgress)
}
},
onComplete = {
statusInternal.value = DownloadFileStatusDone(it)
onComplete = { file ->
libraryHandler.stop()
GlobalScope.launch {
try {
if (outputUri != null) {
contentResolver.openOutputStream(outputUri).use { outputStream ->
FileUtils.copyFile(file, outputStream)
}
}
statusInternal.postValue(DownloadFileStatusDone(file))
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "downloading file failed", ex)
}
statusInternal.postValue(DownloadFileStatusFailed)
}
}
},
onError = {
statusInternal.value = DownloadFileStatusFailed
@@ -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,8 +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.experimental.android.UI
import kotlinx.coroutines.experimental.async
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
@@ -33,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 {
@@ -60,10 +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?.configuration { config ->
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()
)
}
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)
@@ -72,15 +66,17 @@ class DevicesFragment : SyncthingFragment() {
return false
}
}
}
private fun updateDeviceList() {
libraryHandler.syncthingClient { syncthingClient ->
async(UI) {
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?) {
@@ -91,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() }
}
}
@@ -2,64 +2,53 @@ package net.syncthing.lite.fragments
import android.arch.lifecycle.Observer
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
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() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val adapter = FoldersListAdapter()
private val TAG = "FoldersFragment"
adapter.listener = object : FolderListAdapterListener {
override fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats) {
startActivity(
activity!!.intentFor<FolderBrowserActivity>(
FolderBrowserActivity.EXTRA_FOLDER_NAME to folderInfo.folderId
)
)
}
private lateinit var binding: FragmentFoldersBinding
override fun onFolderLongClicked(folderInfo: FolderInfo): Boolean {
FolderInfoDialog
.newInstance(folderId = folderInfo.folderId)
.show(fragmentManager!!)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
return true
}
}
val binding = FragmentFoldersBinding.inflate(layoutInflater, container, false)
binding.list.adapter = adapter
libraryHandler.isListeningPortTaken.observe(this, Observer { binding.listeningPortTaken = it })
launch {
libraryHandler.subscribeToFolderStatusList().consumeEach {
adapter.data = it
binding.isEmpty = it.isEmpty()
}
}
return binding.root
}
override fun onLibraryLoaded() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
libraryHandler.folderBrowser { folderBrowser ->
val list = folderBrowser.folderInfoAndStatsList()
async (UI) {
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter().apply { data = list }
binding.list.adapter = adapter
adapter.listener = object : FolderListAdapterListener {
override fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats) {
startActivity(
activity!!.intentFor<FolderBrowserActivity>(
FolderBrowserActivity.EXTRA_FOLDER_NAME to folderInfo.folderId
)
)
}
}
binding.isEmpty = list.isEmpty()
}
}
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
showAllFoldersListView()
}
}
@@ -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() {
@@ -13,19 +20,48 @@ 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!!
)}
@@ -1,15 +1,10 @@
package net.syncthing.lite.fragments
import android.support.v4.app.Fragment
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.async.CoroutineFragment
import net.syncthing.lite.library.LibraryHandler
abstract class SyncthingFragment : Fragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
context = context!!,
onIndexUpdateProgressListener = this::onIndexUpdateProgress,
onIndexUpdateCompleteListener = this::onIndexUpdateComplete
)}
abstract class SyncthingFragment : CoroutineFragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(context = context!!)}
override fun onStart() {
super.onStart()
@@ -27,8 +22,4 @@ abstract class SyncthingFragment : Fragment() {
}
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
}
}
@@ -0,0 +1,105 @@
package net.syncthing.lite.library
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import java.io.File
import java.io.IOException
class CacheFileProvider: ContentProvider() {
companion object {
const val AUTHORITY = "net.syncthing.lite.fileprovider"
}
override fun onCreate() = true
override fun insert(uri: Uri?, values: ContentValues?): Uri {
throw NotImplementedError()
}
override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
throw NotImplementedError()
}
override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int {
throw NotImplementedError()
}
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor {
val url = CacheFileProviderUrl.fromUri(uri)
val file = url.getFile(context)
val resultProjection = projection ?: arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
val resultCursor = MatrixCursor(resultProjection)
if (file.exists()) {
val builder = resultCursor.newRow()
for (row in resultProjection) {
when (row) {
OpenableColumns.DISPLAY_NAME -> builder.add(url.filename)
OpenableColumns.SIZE -> builder.add(file.length())
else -> builder.add(null)
}
}
}
return resultCursor
}
override fun getType(uri: Uri): String = CacheFileProviderUrl.fromUri(uri).mimeType
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
if (mode == "r") {
val url = CacheFileProviderUrl.fromUri(uri)
val file = url.getFile(context)
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
} else {
throw IOException("illegal mode")
}
}
}
data class CacheFileProviderUrl(
val pathInCacheDirectory: String,
val filename: String,
val mimeType: String
) {
companion object {
private const val PATH = "path"
private const val FILENAME = "filename"
private const val MIME_TYPE = "mimeType"
fun fromUri(uri: Uri) = CacheFileProviderUrl(
pathInCacheDirectory = uri.getQueryParameter(PATH),
filename = uri.getQueryParameter(FILENAME),
mimeType = uri.getQueryParameter(MIME_TYPE)
)
fun fromFile(file: File, filename: String, mimeType: String, context: Context) = CacheFileProviderUrl(
filename = filename,
mimeType = mimeType,
pathInCacheDirectory = file.toRelativeString(context.externalCacheDir)
)
}
val serialized: Uri by lazy {
Uri.Builder()
.scheme("content")
.authority(CacheFileProvider.AUTHORITY)
.appendQueryParameter(PATH, pathInCacheDirectory)
.appendQueryParameter(FILENAME, filename)
.appendQueryParameter(MIME_TYPE, mimeType)
.build()
}
fun getFile(context: Context): File {
return File(context.externalCacheDir, pathInCacheDirectory)
}
}
@@ -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 ->
@@ -4,8 +4,9 @@ import android.os.Handler
import android.os.Looper
import android.support.v4.os.CancellationSignal
import android.util.Log
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.suspendCancellableCoroutine
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import net.syncthing.java.bep.BlockPullerStatus
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
@@ -13,6 +14,8 @@ import net.syncthing.lite.BuildConfig
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class DownloadFileTask(private val fileStorageDirectory: File,
syncthingClient: SyncthingClient,
@@ -58,7 +61,7 @@ class DownloadFileTask(private val fileStorageDirectory: File,
init {
val file = DownloadFilePath(fileStorageDirectory, fileInfo.hash!!)
launch {
GlobalScope.launch {
if (file.targetFile.exists()) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "there is already a file")
@@ -69,43 +72,35 @@ class DownloadFileTask(private val fileStorageDirectory: File,
return@launch
}
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
val job = launch {
try {
if (!file.filesDirectory.isDirectory) {
if (!file.filesDirectory.mkdirs()) {
throw IOException("could not create output directory")
}
}
// download the file to a temp location
val inputStream = blockPuller.pullFileCoroutine(fileInfo, this@DownloadFileTask::callProgress)
try {
FileUtils.copyInputStreamToFile(inputStream, file.tempFile)
file.tempFile.renameTo(file.targetFile)
} finally {
file.tempFile.delete()
}
if (BuildConfig.DEBUG) {
Log.i(TAG, "Downloaded file $fileInfo")
}
callComplete(file.targetFile)
} catch (e: Exception) {
callError(e)
if (BuildConfig.DEBUG) {
Log.w(TAG, "Failed to download file $fileInfo", e)
}
try {
if (!file.filesDirectory.isDirectory) {
if (!file.filesDirectory.mkdirs()) {
throw IOException("could not create output directory")
}
}
cancellationSignal.setOnCancelListener {
job.cancel()
// download the file to a temp location
val inputStream = syncthingClient.pullFile(fileInfo, this@DownloadFileTask::callProgress)
try {
FileUtils.copyInputStreamToFile(inputStream, file.tempFile)
file.tempFile.renameTo(file.targetFile)
} finally {
file.tempFile.delete()
}
}, { callError(IOException("could not get block puller for file")) })
if (BuildConfig.DEBUG) {
Log.i(TAG, "Downloaded file $fileInfo")
}
callComplete(file.targetFile)
} catch (e: Exception) {
callError(e)
if (BuildConfig.DEBUG) {
Log.w(TAG, "Failed to download file $fileInfo", e)
}
}
}
}
@@ -5,15 +5,18 @@ import android.arch.lifecycle.MutableLiveData
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.FolderBrowser
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
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.configuration.Configuration
import org.jetbrains.anko.doAsync
import java.util.concurrent.atomic.AtomicBoolean
@@ -24,18 +27,20 @@ import java.util.concurrent.atomic.AtomicBoolean
*
* It's possible to do multiple start and stop cycles with one instance of this class.
*/
class LibraryHandler(context: Context,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit = {_, _ -> },
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit = {}) {
class LibraryHandler(context: Context) {
companion object {
private const val TAG = "LibraryHandler"
private val handler = Handler(Looper.getMainLooper())
}
private val libraryManager = DefaultLibraryManager.with(context)
val libraryManager = DefaultLibraryManager.with(context)
private val isStarted = AtomicBoolean(false)
private val isListeningPortTakenInternal = MutableLiveData<Boolean>().apply { value = false }
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
@@ -61,9 +66,27 @@ class LibraryHandler(context: Context,
val client = libraryInstance.syncthingClient
client.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
client.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
client.discoveryHandler.registerMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
job = Job()
GlobalScope.launch (job) {
libraryInstance.syncthingClient.indexHandler.subscribeToOnFullIndexAcquiredEvents().consumeEach {
indexUpdateCompleteMessages.send(it)
}
}
GlobalScope.launch (job) {
libraryInstance.folderBrowser.folderInfoAndStatusStream().consumeEach {
folderStatusList.send(it)
}
}
GlobalScope.launch (job) {
libraryInstance.syncthingClient.subscribeToConnectionStatus().consumeEach {
connectionStatus.send(it)
}
}
}
}
@@ -72,10 +95,10 @@ class LibraryHandler(context: Context,
throw IllegalStateException("already stopped")
}
job!!.cancel()
syncthingClient {
try {
it.indexHandler.unregisterOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.unregisterOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
it.discoveryHandler.unregisterMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
} catch (e: IllegalArgumentException) {
// ignored, no idea why this is thrown
@@ -85,22 +108,6 @@ class LibraryHandler(context: Context,
libraryManager.stopLibraryUsage()
}
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
async(UI) {
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
}
}
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
async(UI) {
onIndexUpdateCompleteListener(folderInfo)
}
}
/*
* The callback is executed asynchronously.
* As soon as it returns, there is no guarantee about the availability of the library
@@ -138,4 +145,8 @@ class LibraryHandler(context: Context,
fun unregisterMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) {
messageFromUnknownDeviceListeners.remove(listener)
}
fun subscribeToOnFullIndexAcquiredEvents() = indexUpdateCompleteMessages.openSubscription()
fun subscribeToFolderStatusList() = folderStatusList.openSubscription()
fun subscribeToConnectionStatus() = connectionStatus.openSubscription()
}
@@ -2,8 +2,13 @@ package net.syncthing.lite.library
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.repository.EncryptedTempRepository
import net.syncthing.repository.android.SqliteIndexRepository
import net.syncthing.repository.android.TempDirectoryLocalRepository
import net.syncthing.repository.android.database.RepositoryDatabase
@@ -21,7 +26,10 @@ import java.net.SocketException
*
* The creation and the shutdown are synchronous, so keep them out of the UI Thread
*/
class LibraryInstance (context: Context) {
class LibraryInstance (
context: Context,
private val exceptionReportHandler: (ExceptionReport) -> Unit
) {
companion object {
private const val LOG_TAG = "LibraryInstance"
@@ -42,7 +50,11 @@ class LibraryInstance (context: Context) {
}
}
private val tempRepository = TempDirectoryLocalRepository(File(context.filesDir, "temp_repository"))
private val tempRepository = EncryptedTempRepository(
TempDirectoryLocalRepository(
File(context.filesDir, "temp_repository")
)
)
val isListeningPortTaken = checkIsListeningPortTaken() // this must come first to work correctly
val configuration = Configuration(configFolder = context.filesDir)
@@ -51,14 +63,21 @@ class LibraryInstance (context: Context) {
repository = SqliteIndexRepository(
database = RepositoryDatabase.with(context),
closeDatabaseOnClose = false,
clearTempStorageHook = { tempRepository.deleteAllData() }
clearTempStorageHook = { tempRepository.deleteAllTempData() }
),
tempRepository = tempRepository
)
val folderBrowser = syncthingClient.indexHandler.newFolderBrowser()
tempRepository = tempRepository,
exceptionReportHandler = { ex ->
Log.w(LOG_TAG, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
fun shutdown() {
folderBrowser.close()
GlobalScope.launch (Dispatchers.Main) {
exceptionReportHandler(ex)
}
}
)
val folderBrowser = syncthingClient.indexHandler.folderBrowser
val indexBrowser = syncthingClient.indexHandler.indexBrowser
suspend fun shutdown() {
syncthingClient.close()
configuration.persistNow()
}
@@ -2,8 +2,16 @@ package net.syncthing.lite.library
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
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.experimental.suspendCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* This class manages the access to an LibraryInstance
@@ -34,7 +42,7 @@ class LibraryManager (
// only this Thread should access instance and userCounter
private val startStopExecutor = Executors.newSingleThreadExecutor()
private var instance: LibraryInstance? = null
private val instanceStream = ConflatedBroadcastChannel<LibraryInstance?>(null)
private var userCounter = 0
fun startLibraryUsage(callback: (LibraryInstance) -> Unit) {
@@ -42,12 +50,12 @@ class LibraryManager (
val newUserCounter = ++userCounter
handler.post { userCounterListener(newUserCounter) }
if (instance == null) {
instance = synchronousInstanceCreator()
if (instanceStream.value == null) {
instanceStream.offer(synchronousInstanceCreator())
handler.post { isRunningListener(true) }
}
handler.post { callback(instance!!) }
handler.post { callback(instanceStream.value!!) }
}
}
@@ -59,6 +67,16 @@ class LibraryManager (
}
}
suspend fun <T> withLibrary(action: suspend (LibraryInstance) -> T): T {
val instance = startLibraryUsageCoroutine()
return try {
action(instance)
} finally {
stopLibraryUsage()
}
}
fun stopLibraryUsage() {
startStopExecutor.submit {
val newUserCounter = --userCounter
@@ -76,8 +94,8 @@ class LibraryManager (
fun shutdownIfThereAreZeroUsers(listener: (wasShutdownPerformed: Boolean) -> Unit = {}) {
startStopExecutor.submit {
if (userCounter == 0) {
instance?.shutdown()
instance = null
runBlocking { instanceStream.value?.shutdown() }
instanceStream.offer(null)
handler.post { isRunningListener(false) }
handler.post { listener(true) }
@@ -86,4 +104,21 @@ class LibraryManager (
}
}
}
fun streamDirectoryListing(folder: String, path: String) = GlobalScope.produce {
var job = Job()
instanceStream.openSubscription().consumeEach { instance ->
job.cancel()
job = Job()
if (instance != null) {
async (job) {
instance.indexBrowser.streamDirectoryListing(folder, path).consumeEach {
send(it)
}
}
}
}
}
}
@@ -8,35 +8,38 @@ import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import android.util.Log
import kotlinx.coroutines.experimental.cancel
import kotlinx.coroutines.experimental.runBlocking
import net.syncthing.java.bep.IndexBrowser
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.index.browser.DirectoryContentListing
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.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.utils.MimeType
import java.io.FileNotFoundException
import java.net.URLConnection
import java.util.concurrent.CountDownLatch
class SyncthingProvider : DocumentsProvider() {
companion object {
private const val Tag = "SyncthingProvider"
private val DefaultRootProjection = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON)
Root.COLUMN_ICON
)
private val DefaultDocumentProjection = arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_SIZE,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS)
Document.COLUMN_FLAGS
)
}
override fun onCreate(): Boolean {
@@ -45,96 +48,122 @@ class SyncthingProvider : DocumentsProvider() {
}
// this instance is not started -> it connects and disconnects on demand
private val libraryHandler: LibraryHandler by lazy { LibraryHandler(context) }
private val libraryManager: LibraryManager by lazy { DefaultLibraryManager.with(context) }
override fun queryRoots(projection: Array<String>?): Cursor {
Log.d(Tag, "queryRoots($projection)")
val latch = CountDownLatch(1)
var folders: List<Pair<FolderInfo, FolderStats>>? = null
libraryHandler.folderBrowser { folderBrowser ->
folders = folderBrowser.folderInfoAndStatsList()
latch.countDown()
}
latch.await()
val result = MatrixCursor(projection ?: DefaultRootProjection)
folders!!.forEach { folder ->
val row = result.newRow()
row.add(Root.COLUMN_ROOT_ID, folder.first.folderId)
row.add(Root.COLUMN_SUMMARY, folder.first.label)
row.add(Root.COLUMN_FLAGS, 0)
row.add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.first))
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
return runBlocking {
libraryManager.withLibrary { instance ->
MatrixCursor(projection ?: DefaultRootProjection).apply {
instance.folderBrowser.folderInfoAndStatusList().forEach { folder ->
newRow().apply {
add(Root.COLUMN_ROOT_ID, folder.info.folderId)
add(Root.COLUMN_SUMMARY, folder.info.label)
add(Root.COLUMN_FLAGS, 0)
add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.info))
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
}
}
}
}
}
return result
}
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
sortOrder: String?): Cursor {
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
getIndexBrowser(getFolderIdForDocId(parentDocumentId))
.listFiles(getPathForDocId(parentDocumentId))
.forEach { fileInfo ->
includeFile(result, fileInfo)
return runBlocking {
libraryManager.withLibrary { instance ->
val listing = instance.indexBrowser.getDirectoryListing(
folder = getFolderIdForDocId(parentDocumentId),
path = getPathForDocId(parentDocumentId)
)
when (listing) {
is DirectoryNotFoundListing -> throw FileNotFoundException()
is DirectoryContentListing -> {
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
listing.entries.forEach { entry ->
includeFile(result, entry)
}
result
}
}
return result
}
}
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
Log.d(Tag, "queryDocument($documentId, $projection)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
includeFile(result, fileInfo)
return result
return runBlocking {
libraryManager.withLibrary { instance ->
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId)
) ?: throw FileNotFoundException()
MatrixCursor(projection ?: DefaultDocumentProjection).apply {
includeFile(this, fileInfo)
}
}
}
}
@Throws(FileNotFoundException::class)
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?):
ParcelFileDescriptor {
Log.d(Tag, "openDocument($documentId, $mode, $signal)")
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
val accessMode = ParcelFileDescriptor.parseMode(mode)
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
throw NotImplementedError()
}
val outputFile = runBlocking {
signal?.setOnCancelListener {
this.coroutineContext.cancel()
}
return runBlocking {
libraryManager.withLibrary { instance ->
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId)
) ?: throw FileNotFoundException()
val libraryInstance = libraryManager.startLibraryUsageCoroutine()
signal?.setOnCancelListener {
this.coroutineContext.cancel()
}
try {
DownloadFileTask.downloadFileCoroutine(
val outputFile = DownloadFileTask.downloadFileCoroutine(
externalCacheDir = context.externalCacheDir,
syncthingClient = libraryInstance.syncthingClient,
syncthingClient = instance.syncthingClient,
fileInfo = fileInfo,
onProgress = { /* ignore the progress */ }
)
} finally {
libraryManager.stopLibraryUsage()
ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
}
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
val row = result.newRow()
row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
row.add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
row.add(Document.COLUMN_SIZE, fileInfo.size)
val mime = if (fileInfo.isDirectory()) Document.MIME_TYPE_DIR
else URLConnection.guessContentTypeFromName(fileInfo.fileName)
row.add(Document.COLUMN_MIME_TYPE, mime)
row.add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
row.add(Document.COLUMN_FLAGS, 0)
result.newRow().apply {
add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
add(Document.COLUMN_SIZE, fileInfo.size)
add(
Document.COLUMN_MIME_TYPE,
if (fileInfo.isDirectory())
Document.MIME_TYPE_DIR
else
MimeType.getFromFilename(fileInfo.fileName)
)
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
add(Document.COLUMN_FLAGS, 0)
}
}
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
@@ -144,15 +173,4 @@ class SyncthingProvider : DocumentsProvider() {
private fun getDocIdForFile(folderInfo: FolderInfo) = folderInfo.folderId + ":"
private fun getDocIdForFile(fileInfo: FileInfo) = fileInfo.folder + ":" + fileInfo.path
private fun getIndexBrowser(folderId: String): IndexBrowser {
val latch = CountDownLatch(1)
var indexBrowser: IndexBrowser? = null
libraryHandler.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folderId)
latch.countDown()
}
latch.await()
return indexBrowser!!
}
}
@@ -5,6 +5,8 @@ import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.utils.PathUtils
@@ -31,22 +33,28 @@ class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
init {
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
handler.post { onProgress(observer) }
GlobalScope.launch {
try {
val blockPusher = syncthingClient.getBlockPusher(folderId = syncthingFolder)
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
while (!observer.isCompleted()) {
if (isCancelled)
return@getBlockPusher
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
handler.post { onProgress(observer) }
while (!observer.isCompleted()) {
if (isCancelled)
return@launch
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
handler.post { onProgress(observer) }
}
IOUtils.closeQuietly(uploadStream)
handler.post { onComplete() }
} catch (ex: Exception) {
handler.post { onError() }
}
IOUtils.closeQuietly(uploadStream)
handler.post { onComplete() }
}, { handler.post { onError() } })
}
}
fun cancel() {
@@ -0,0 +1,18 @@
package net.syncthing.lite.utils
import android.webkit.MimeTypeMap
import net.syncthing.java.core.utils.PathUtils
object MimeType {
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
private fun getFromExtension(extension: String): String {
val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return mimeType ?: DEFAULT_MIME_TYPE
}
fun getFromFilename(path: String) = getFromExtension(
PathUtils.getFileExtensionFromFilename(path).toLowerCase()
)
}
@@ -4,12 +4,13 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
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
@@ -22,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"
}
@@ -40,21 +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?.configuration { configuration ->
if (!configuration.peerIds.contains(deviceId2)) {
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
configuration.persistLater()
async(UI) {
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 {
async(UI) {
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
}
null
}
}
}
+1
View File
@@ -0,0 +1 @@
googleplay@nutomic.com
View File
+1
View File
@@ -0,0 +1 @@
https://syncthing.net
+1
View File
@@ -0,0 +1 @@
en-GB
@@ -0,0 +1,5 @@
This project is an Android app, that works as a client for a Syncthing share (accessing Syncthing devices in the same way a client-server file sharing app access its proprietary server).
This is a client-oriented implementation, designed to work online by downloading and uploading files from an active device on the network (instead of synchronizing a local copy of the entire repository). This is quite different from the way the syncthing-android works, and its useful from those devices that cannot or wish not to download the entire repository (for example, mobile devices with limited storage available, wishing to access a syncthing share).
Source code: https://github.com/syncthing/syncthing-lite
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

@@ -0,0 +1 @@
A browser app for Syncthing-compatible shares
+1
View File
@@ -0,0 +1 @@
Syncthing Lite
+2
View File
@@ -0,0 +1,2 @@
- changed up button behavior
- updated translations
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="filename"
type="String" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:padding="8dp"
android:textAppearance="?android:textAppearanceMedium"
android:text="@{filename}"
tools:text="Filename.type"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:drawableStart="@drawable/ic_save_black_24dp"
android:id="@+id/save_as_button"
android:padding="8dp"
android:gravity="start|center_vertical"
android:text="@string/dialog_file_save_as"
android:background="?selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
@@ -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>
+22 -13
View File
@@ -13,9 +13,17 @@
<variable
name="info"
type="String" />
<variable
name="info2"
type="String" />
<import type="android.view.View" />
<import type="android.text.TextUtils" />
</data>
<RelativeLayout
<LinearLayout
android:orientation="vertical"
android:background="?selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -29,13 +37,9 @@
android:text="@{folderName}"
android:id="@+id/folder_name_view"
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:textSize="20sp"
android:textStyle="bold"/>
@@ -47,9 +51,7 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/folder_name_view"
android:textSize="14sp"
android:layout_alignParentStart="true" />
android:textSize="14sp" />
<TextView
tools:text="Additional information"
@@ -59,11 +61,18 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/folder_lastmod_info"
android:textSize="14sp"
android:layout_alignParentStart="true" />
android:textSize="14sp" />
<TextView
android:visibility="@{TextUtils.isEmpty(info2) ? View.GONE : View.VISIBLE}"
tools:text="Index Update Progress"
android:text="@{info2}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp" />
</RelativeLayout>
</LinearLayout>
</layout>
</layout>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
app:showAsAction="always"
android:icon="@drawable/ic_home_white_24dp"
android:id="@+id/go_home"
android:title="@string/folder_browser_home" />
</menu>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_background"/>
<foreground android:drawable="@mipmap/ic_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+53 -4
View File
@@ -1,9 +1,9 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
<string name="folder_list_empty_message">Kein Ordner verfügbar</string>
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
<string name="invalid_device_id">Fehler: Ungültige Geräte ID</string>
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
@@ -12,8 +12,11 @@
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
<string name="index_update_progress_label">Index Aktualisierung für Ordner %1$s, %2$d %% synchronisiert</string>
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet...</string>
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
<string name="remove_device_title">Gerät entfernen: %1$s</string>
<string name="remove_device_title">Gerät %1$s entfernen?</string>
<string name="remove_device_message">Gerät %1$s von der Liste der bekannten Geräte entfernen?</string>
<string name="device_import_success">Gerät %1$s erfolgreich importiert</string>
<string name="device_already_known">Gerät ist bereits bekannt $1%s</string>
<string name="folders_label">Ordner</string>
@@ -21,4 +24,50 @@
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d Dateien, %3$d Ordner</string>
<string name="file_info">%1$s, zuletzt modifiziert %2$s</string>
</resources>
<string name="show_device_id">Geräte ID anzeigen</string>
<string name="device_id">Geräte ID</string>
<string name="device_id_copied">Geräte ID in den Zwischenspeicher kopiert</string>
<string name="share_device_id_chooser">Teile Geräte ID mit</string>
<string name="other_syncthing_instance_title">Eine andere Syncthing-Instanz läuft bereits</string>
<string name="other_syncthing_instance_message">Lokale Auffindung wird nicht funktionieren. Stoppen Sie die andere Syncthing Instanz, um die lokale Auffindung zu ermöglichen.</string>
<string name="intro_page_one_title">Willkommen zu Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing ersetzt proprietäre Sync- und Cloud-Services durch etwas Offenes, Vertrauenswürdiges und Dezentrales. Ihre Daten sind allein Ihre Daten, und Sie verdienen es zu wählen, wo sie gespeichert werden, ob sie an Dritte weitergegeben werden und wie sie über das Internet übertragen werden.</string>
<string name="intro_page_two_title">Ein Gerät hinzufügen</string>
<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ätenamen</string>
<string name="settings_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</string>
<string name="settings_shutdown_delay_title">Ausschaltverzögerung</string>
<string name="settings_force_stop">Beenden dieser App erzwingen</string>
<string name="settings_last_error_title">Letzter Fehler</string>
<string name="settings_last_error_summary">Details des letzten Fehlers anzeigen</string>
<string name="settings_report_bug_title">Einen Fehler melden</string>
<string name="settings_report_bug_summary">Den Bugtracker bei GitHub für diese App öffnen</string>
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
<string name="settings_shutdown_delay_10_seconds">10 Sekunden</string>
<string name="settings_shutdown_delay_30_seconds">30 Sekunden</string>
<string name="settings_shutdown_delay_1_minute">1 Minute</string>
<string name="settings_shutdown_delay_5_minutes">5 Minuten</string>
<string name="dialog_warning_reconnect_problem">
Aufgrund des Verhaltens dieser App und des Verhaltens des Syncthing-Servers können Sie sich für einige Minuten nicht verbinden, wenn die App erzwungen beendet wurde (durch das Entfernen aus der Liste der aktiven Apps) oder die Verbindung unterbrochen wurde.
Dies gilt nicht für Verbindungen, die per lokaler Gerätesuche hergestellt wurden.</string>
<string name="dialog_file_save_as">Speichern unter</string>
<string name="pending_index_updates">%d Index-Aktualisierungen verbleibend</string>
<string name="device_status_connecting">Verbinden mit %s</string>
<string name="device_status_connected">Mit %s verbunden</string>
<string name="device_status_disconnected">Verbinden wird bald erneut versucht - es sind %d Adressen bekannt</string>
<string name="device_status_no_address">Keine bekannte Adresse für dieses Gerät</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Ordnersynchronisation für neues Gerät aktivieren</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Möchten Sie den Ordner %1$s mit %2$s (%3$s) synchronisieren?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Synchronisieren</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Nicht synchronisieren</string>
<string name="dialog_folder_info_device_list">Ordner teilen mit:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Etwas in Syncthing Lite hat nicht funktioniert. Sie können die Details in den Einstellungen von Syncthing Lite anzeigen.</string>
</resources>
+52
View File
@@ -0,0 +1,52 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Carpeta no disponible</string>
<string name="clear_local_cache_index_label">Limpia caché/índice local</string>
<string name="devices_list_view_empty_message">Dispositivos no disponibles</string>
<string name="invalid_device_id">Error: identificador de dispositivo inválido</string>
<string name="dialog_downloading_file">Descargando archivo%1$s</string>
<string name="toast_file_download_failed">Falló al descargar el archivo</string>
<string name="toast_open_file_failed">No se ha encontrado una app compatible</string>
<string name="toast_file_upload_failed">Error en la carga de archivos
</string>
<string name="toast_upload_complete">Carga de archivos completa
</string>
<string name="dialog_uploading_file">Cargando archivo %1$s</string>
<string name="clear_cache_and_index_title">¿Borrar la caché local y el índice?</string>
<string name="clear_cache_and_index_body">¿Borrar todos los datos de la caché local y los datos de índice?</string>
<string name="index_update_progress_label">Actualización del índice de las carpetas %1$s, %2$d%% sincronizado</string>
<string name="loading_config_starting_syncthing_client">Cargando configuración, iniciando el cliente de sincronización....</string>
<string name="last_modified_time">Última modificación: %1$s</string>
<string name="remove_device_title">¿Quitar dispositivo %1$s?</string>
<string name="remove_device_message">¿Quitar dispositivo %1$s de la lista de dispositivos conocidos?</string>
<string name="device_import_success">Dispositivo %1$s importado con éxito</string>
<string name="device_already_known">Dispositivo %1$s ya está presente</string>
<string name="folders_label">Carpetas</string>
<string name="devices_label">Dispositivos</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d archivos, %3$ddirectorios</string>
<string name="file_info">%1$s, modificado por última vez %2$s</string>
<string name="show_device_id">Mostrar ID de dispositivo </string>
<string name="device_id">ID de dispositivo</string>
<string name="device_id_copied">ID de dispositivo copiado al portapapeles</string>
<string name="share_device_id_chooser">Compartir ID de dispositivo con</string>
<string name="other_syncthing_instance_title">Otra instancia de Syncthing está siendo ejecutada</string>
<string name="other_syncthing_instance_message">El descubrimiento local no funcionará. Detenga la otra instancia de Syncthing para habilitar la detección local.</string>
<string name="intro_page_one_title">Bienvenido a Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing reemplaza los servicios de sincronización y nube propietarios por algo abierto, fiable y descentralizado. Sus datos son sólo suyos y usted merece elegir dónde se almacenan, si se comparten con terceros y cómo se transmiten a través de Internet.</string>
<string name="intro_page_two_title">Añadir un dispositivo</string>
<string name="intro_page_three_title">Compartir tus carpetas</string>
<string name="intro_page_two_description">Introduce un ID de dispositivo de Syncthing o escanea un ID de dispositivo desde un código QR</string>
<string name="intro_page_three_description">Acepta ahora el dispositivo con ID %1$s, y comparte una carpeta con él. Pueden pasar unos minutos hasta que los dispositivos se conecten.</string>
<string name="settings">Configuración</string>
<string name="settings_app_version_title">Versión de la aplicación</string>
<string name="settings_local_device_name">Nombre del dispositivo local</string>
<string name="settings_local_device_summary">El nombre que otros dispositivos verán para este dispositivo</string>
<string name="settings_shutdown_delay_title">Retardo en el apagado</string>
<string name="device_id_dialog_title">Introducir la ID del dispositivo</string>
<string name="settings_shutdown_delay_10_seconds">10 segundos</string>
<string name="settings_shutdown_delay_30_seconds">30 segundos</string>
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
<string name="settings_shutdown_delay_5_minutes">5 minutos</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+38 -1
View File
@@ -3,7 +3,7 @@
<string name="folder_list_empty_message">Aucun dossier disponible</string>
<string name="clear_local_cache_index_label">Effacer le cache et l\'index local</string>
<string name="devices_list_view_empty_message">Aucun appareil disponible</string>
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
<string name="invalid_device_id">Erreur: ID de l\'appareil invalide</string>
<string name="dialog_downloading_file">Téléchargement du fichier %1$s</string>
<string name="toast_file_download_failed">Le téléchargement du fichier a échoué</string>
<string name="toast_open_file_failed">Aucune appli compatible trouvée</string>
@@ -12,8 +12,11 @@
<string name="dialog_uploading_file">Upload du fichier %1$s</string>
<string name="clear_cache_and_index_title">Effacer le cache local et l\'index?</string>
<string name="clear_cache_and_index_body">Effacer toutes les données du cache local et de l\'index ?</string>
<string name="index_update_progress_label">Mise à jour de l\'index pour le dossier %1$s, %2$d%% synchronisés</string>
<string name="loading_config_starting_syncthing_client">Chargement de la configuration, démarrage du client Syncthing...</string>
<string name="last_modified_time">Dernière modification : %1$s</string>
<string name="remove_device_title">Supprimer l\'appareil %1$s\?</string>
<string name="remove_device_message">Supprimer l\'appareil %1$s de la liste des appareil connus ?</string>
<string name="device_import_success">Appareil %1$s importé avec succès</string>
<string name="device_already_known">Appareil déjà connu %1$s</string>
<string name="folders_label">Dossiers</string>
@@ -21,4 +24,38 @@
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d fichiers, %3$d dossiers</string>
<string name="file_info">%1$s, dernière modification %2$s</string>
<string name="show_device_id">Afficher l\'ID de l\'appareil</string>
<string name="device_id">ID de l\'appareil</string>
<string name="device_id_copied">ID de l\'appareil copié dans le presse-papier</string>
<string name="share_device_id_chooser">Partager l\'ID de l\'appareil avec</string>
<string name="other_syncthing_instance_title">Une autre instance de Syncthing fonctionne</string>
<string name="other_syncthing_instance_message">La découverte locale ne fonctionnera pas. Arrêtez l\'autre instance de Syncthing pour activer la découverte locale.</string>
<string name="intro_page_one_title">Bienvenue dans Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing remplace les services de synchronisation et de cloud propriétaires par quelque chose d\'ouvert, fiable et décentralisé. Vos données sont uniquement vos données et vous méritez de choisir où elles sont stockées, si elles sont partagées avec des tiers et comment elles sont transmises sur Internet.</string>
<string name="intro_page_two_title">Ajouter un appareil</string>
<string name="intro_page_three_title">Partager vos dossiers</string>
<string name="intro_page_two_description">Entrer l\'ID Syncthing de l\'appareil, ou scanner le QR code de l\'ID d\'un appareil.</string>
<string name="intro_page_three_description">Maintenant, acceptez l\'appareil avec l\'ID %1$s, et partagez un dossier avec lui. Cela peut prendre quelques minutes avant que les appareils ne se connectent.</string>
<string name="intro_page_three_searching_device">Essayer de trouver l\'autre appareil. Cela peut prendre un moment.</string>
<string name="settings">Réglages</string>
<string name="settings_app_version_title">Version d\'application</string>
<string name="settings_local_device_name">Nom local de l\'appareil</string>
<string name="settings_local_device_summary">Le nom que les autres appareils verront pour cet appareil</string>
<string name="settings_shutdown_delay_title">Délai d\'arrêt</string>
<string name="settings_force_stop">Forcer l\'arrêt de cette application</string>
<string name="settings_last_error_title">Dernière erreur</string>
<string name="settings_last_error_summary">Voir les détails de la dernière erreur</string>
<string name="settings_report_bug_title">Rapporter un bug</string>
<string name="settings_report_bug_summary">Ouvrir un incident pour cette application sur GitHub</string>
<string name="copy_to_clipboard">Copier dans le presse-papiers</string>
<string name="copied_to_clipboard">Copié dans le presse-papiers</string>
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
<string name="settings_shutdown_delay_10_seconds">10 secondes</string>
<string name="settings_shutdown_delay_30_seconds">30 secondes</string>
<string name="settings_shutdown_delay_1_minute">1 minute</string>
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
<string name="dialog_file_save_as">Enregistrer sous</string>
<string name="pending_index_updates">%d mises à jour d\'index en attente</string>
<string name="device_status_connecting">Connexion à %s</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+50
View File
@@ -0,0 +1,50 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Nincs elérhető mappa</string>
<string name="clear_local_cache_index_label">Helyi gyorsítótár/index törlése</string>
<string name="devices_list_view_empty_message">Nincs elérhető eszköz</string>
<string name="invalid_device_id">Hiba: helytelen eszközazonosítő</string>
<string name="dialog_downloading_file">Fájl letöltése %1$s</string>
<string name="toast_file_download_failed">Hiba a fájl letöltése közben</string>
<string name="toast_open_file_failed">Nem található kompatibilis alkalmazás</string>
<string name="toast_file_upload_failed">Hiba fájl feltöltése közben</string>
<string name="toast_upload_complete">Feltöltés befejezve</string>
<string name="dialog_uploading_file">Fájl feltöltése: %1$s</string>
<string name="clear_cache_and_index_title">Törlöd a helyi gyorsítótárat és index-et?</string>
<string name="clear_cache_and_index_body">Törlöd az összes helyi gyorsítótár-at és index-et?</string>
<string name="index_update_progress_label">Index frissítés szinkronizálva a %1$s,%2$d,%% mappákhoz</string>
<string name="loading_config_starting_syncthing_client">Beállítások betöltése, Syncthing kliens indítása</string>
<string name="last_modified_time">Utolsó módosítás: %1$s</string>
<string name="remove_device_title">Törlöd a %1$s eszközt?</string>
<string name="remove_device_message">Törlöd a %1$s eszközt az ismertek listájáról?</string>
<string name="device_import_success">Eszköz sikeresen importálva: %1$s</string>
<string name="device_already_known">%1$s eszköz már hozzá van adva</string>
<string name="folders_label">Mappák</string>
<string name="devices_label">Eszközök</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d fájlok, %3$d mappák</string>
<string name="file_info">%1$s, utoljára módosítva %2$s</string>
<string name="show_device_id">Eszközazonosító megjelenítése</string>
<string name="device_id">Eszközazonosító</string>
<string name="device_id_copied">Eszközazonosító a vágólapra másolva</string>
<string name="share_device_id_chooser">Eszközazonosító megosztása</string>
<string name="other_syncthing_instance_title">Egy másik Syncthing folyamat fut</string>
<string name="other_syncthing_instance_message">Helyi felfedezés nem fog működni. Állítsd le a másik Syncthing folyamatot a helyi felfedezés bekapcsolásához.</string>
<string name="intro_page_one_title">Üdvözöllek a Syncthing Lite-ban</string>
<string name="intro_page_one_description">A Syncthing a zárt forrású szinkronizáló és felhő szolgáltatásokat egy nyílt, megbízható és decentralizált szoftverrel váltja fel. A te adatod csak a tiéd és te szabod meg, hogy hol tárolod, kivel osztod meg, és hogyan továbbítod az interneten.</string>
<string name="intro_page_two_title">Eszköz hozzáadása</string>
<string name="intro_page_three_title">Mappáid megosztása</string>
<string name="intro_page_two_description">Adj meg egy Syncthing eszközazonosítót, vagy olvasd be QR kódból</string>
<string name="intro_page_three_description">Fogadd el a(z) %1$s azonosítójú eszközt, és ossz meg vele egy mappát. Beletelhet néhány percbe mire az eszközök csatlakoznak.</string>
<string name="settings">Beállítások</string>
<string name="settings_app_version_title">Verzió</string>
<string name="settings_local_device_name">Helyi eszköz neve</string>
<string name="settings_local_device_summary">Név amit a többi eszköz fog látni</string>
<string name="settings_shutdown_delay_title">Leállítás késleltetés</string>
<string name="device_id_dialog_title">Eszközazonosító megadása</string>
<string name="settings_shutdown_delay_10_seconds">10 másodperc</string>
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
<string name="settings_shutdown_delay_1_minute">1 perc</string>
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
+55 -3
View File
@@ -3,8 +3,8 @@
<string name="folder_list_empty_message">Nessuna cartella disponibile</string>
<string name="clear_local_cache_index_label">Cancella cache/indice</string>
<string name="devices_list_view_empty_message">Nessun dispositivo disponibile</string>
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
<string name="dialog_downloading_file">Download del file %1$s</string>
<string name="invalid_device_id">Errore: ID dispositivo non valido</string>
<string name="dialog_downloading_file">Scaricamento del file %1$s</string>
<string name="toast_file_download_failed">Impossibile scaricare il file</string>
<string name="toast_open_file_failed">Nessuna applicazione compatibile trovata</string>
<string name="toast_file_upload_failed">Caricamento file fallito</string>
@@ -13,8 +13,10 @@
<string name="clear_cache_and_index_title">Cancellare la cache locale e l\'indice?</string>
<string name="clear_cache_and_index_body">Cancellare tutti i dati della cache locale e i dati dell\'indice?</string>
<string name="index_update_progress_label">Aggiornamento dell\'indice per la cartella %1$s, %2$d%% sincronizzato</string>
<string name="loading_config_starting_syncthing_client">Caricamento configurazione, avvio del client syncthing...</string>
<string name="last_modified_time">Ultima modifica: %1$s</string>
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</string>
<string name="remove_device_message">Rimuovere %1$s dalla lista dei dispositivi noti?</string>
<string name="device_import_success">Dispositivo %1$s importato con successo</string>
<string name="device_already_known">Dispositivo %1$s già presente</string>
<string name="folders_label">Cartelle</string>
@@ -22,4 +24,54 @@
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d file, %3$d cartelle</string>
<string name="file_info">%1$s, ultima modifica %2$s</string>
</resources>
<string name="show_device_id">Mostra l\'ID del dispositivo</string>
<string name="device_id">ID dispositivo</string>
<string name="device_id_copied">ID dispositivo copiato negli appunti</string>
<string name="share_device_id_chooser">Condividi ID dispositivo con</string>
<string name="other_syncthing_instance_title">Un\'altra istanza di Syncthing è in esecuzione</string>
<string name="other_syncthing_instance_message">L\'individuazione locale non funzionerà. Arresta l\'altra istanza di Syncthing per abilitare l\'individuazione locale.</string>
<string name="intro_page_one_title">Benvenuto in Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing sostituisce servizi proprietari di sincronizzazione e cloud con qualcosa di aperto, affidabile e decentralizzato. I tuoi dati sono solo tuoi e meriti di scegliere dove vengono immagazzinati, se sono condivisi con terze parti e come sono trasmessi attraverso Internet.</string>
<string name="intro_page_two_title">Aggiungi un dispositivo</string>
<string name="intro_page_three_title">Condividi le tue cartelle</string>
<string name="intro_page_two_description">Immetti un ID dispositivo Syncthing o esegui la scansione di un ID dispositivo da un codice QR</string>
<string name="intro_page_three_description">Ora accetta il dispositivo con ID %1$s, e condividi una cartella con esso. Potrebbero essere necessari alcuni minuti prima che i dispositivi si connettano.</string>
<string name="intro_page_three_searching_device">In attesa di trovare altri dispositivi. Questo potrebbe richiedere un momento.</string>
<string name="settings">Impostazioni</string>
<string name="settings_app_version_title">Versione dell\'app</string>
<string name="settings_local_device_name">Nome del dispositivo locale</string>
<string name="settings_local_device_summary">Il nome che altri dispositivi vedranno per questo dispositivo</string>
<string name="settings_shutdown_delay_title">Ritardo di chiusura</string>
<string name="settings_shutdown_delay_summary">Tempo prima della chiusura del client Syncthing dopo l\'ultimo utilizzo</string>
<string name="settings_force_stop">Forza l\'arresto dell\'App</string>
<string name="settings_last_error_title">Ultimo errore</string>
<string name="settings_last_error_summary">Visualizza i dettagli dell\'ultimo errore</string>
<string name="settings_report_bug_title">Segnala un bug</string>
<string name="settings_report_bug_summary">Apri segnalazioni per quest\'App su GitHub</string>
<string name="copy_to_clipboard">Copia negli appunti</string>
<string name="copied_to_clipboard">Copiato negli appunti</string>
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
<string name="settings_shutdown_delay_10_seconds">10 secondi</string>
<string name="settings_shutdown_delay_30_seconds">30 secondi</string>
<string name="settings_shutdown_delay_1_minute">1 minuto</string>
<string name="settings_shutdown_delay_5_minutes">5 minuti</string>
<string name="dialog_warning_reconnect_problem">
A causa del comportamento di quest\'App e del comportamento del Server Syncthing,
non è possibile riconnettersi per alcuni minuti se l\'app è stata chiusa (a causa della rimozione dall\'elenco app recenti)
o la connessione è stata interrotta.
Questo non si applica alle connessioni di individuazione locale
</string>
<string name="dialog_file_save_as">Salva come</string>
<string name="pending_index_updates">%d aggiornamenti degli indici in sospeso</string>
<string name="device_status_connecting">Connessione a %s</string>
<string name="device_status_connected">Connesso a %s</string>
<string name="device_status_disconnected">Nuovo tentativo di connessione a breve: ci sono %d indirizzi noti</string>
<string name="device_status_no_address">Nessun indirizzo conosciuto per il dispositivo</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Abilita sincronizzazione cartella per il nuovo dispositivo</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Vuoi sincronizzare %1$s con %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Sincronizzare</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Non sincronizzare</string>
<string name="dialog_folder_info_device_list">Condividi la cartella con:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Qualcosa non ha funzionato in Syncthing Lite. È possibile visualizzare i dettagli dalle impostazioni di Syncthing Lite.</string>
</resources>
+21 -1
View File
@@ -3,7 +3,7 @@
<string name="folder_list_empty_message">フォルダーがありません</string>
<string name="clear_local_cache_index_label">ローカルキャッシュ/索引をクリア</string>
<string name="devices_list_view_empty_message">デバイスがありません</string>
<string name="device_id_dialog_title">デバイス ID を入力</string>
<string name="invalid_device_id">エラー: デバイス ID が無効です</string>
<string name="dialog_downloading_file">ファイル %1$s のダウンロード中</string>
<string name="toast_file_download_failed">ファイルのダウンロードに失敗しました</string>
<string name="toast_open_file_failed">利用できるアプリが見つかりません</string>
@@ -13,8 +13,10 @@
<string name="clear_cache_and_index_title">ローカルキャッシュと索引をクリアしますか?</string>
<string name="clear_cache_and_index_body">すべてのローカルキャッシュデータと索引データをクリアしますか?</string>
<string name="index_update_progress_label">フォルダー %1$s の索引を更新しました。 %2$d%% 同期しました</string>
<string name="loading_config_starting_syncthing_client">設定の読み込み中、syncthing クライアントの開始中…</string>
<string name="last_modified_time">最終更新: %1$s</string>
<string name="remove_device_title">デバイス %1$sを削除しますか?</string>
<string name="remove_device_message">既存のデバイスのリストから %1$s を削除しますか?</string>
<string name="device_import_success">デバイス %1$s のインポートに成功しました</string>
<string name="device_already_known">デバイスは既に存在します %1$s</string>
<string name="folders_label">フォルダー</string>
@@ -22,4 +24,22 @@
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d ファイル, %3$d ディレクトリー</string>
<string name="file_info">%1$s, 最終更新 %2$s</string>
<string name="show_device_id">デバイス ID を表示</string>
<string name="device_id">デバイス ID</string>
<string name="device_id_copied">デバイス ID をクリップボードにコピーしました</string>
<string name="share_device_id_chooser">次とデバイス ID を共有</string>
<string name="other_syncthing_instance_title">別の Syncthing インスタンスが実行中です</string>
<string name="other_syncthing_instance_message">ローカルの探索は動作しません。他の Syncthing インスタンスを停止して、ローカルの探索を有効にしてください。</string>
<string name="intro_page_one_title">Syncthing Lite へようこそ</string>
<string name="intro_page_one_description">Syncthing は、プロプライエタリな同期およびクラウドサービスを、オープンで信頼性があり、分散化されたものに置き換えます。 あなたのデータはあなただけのものであり、それが第三者と共有され、インターネット経由で送信される場合、保存される場所を選択する必要があります。</string>
<string name="intro_page_two_title">デバイスを追加</string>
<string name="intro_page_three_title">フォルダーを共有</string>
<string name="intro_page_two_description">Syncthing デバイス ID を入力、または QR コードからデバイス ID をスキャンしてください</string>
<string name="intro_page_three_description">ID %1$s のデバイスを承認して、フォルダーを共有しました。デバイスが接続されるまで数分かかることがあります。</string>
<string name="settings">設定</string>
<string name="settings_app_version_title">アプリバージョン</string>
<string name="settings_local_device_name">ローカルのデバイス名</string>
<string name="settings_local_device_summary">他のデバイスがこのデバイスを表示する名前</string>
<string name="device_id_dialog_title">デバイス ID を入力</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
@@ -0,0 +1,45 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Geen map beschikbaar</string>
<string name="clear_local_cache_index_label">Lokaal cachegeheugen/index wissen</string>
<string name="devices_list_view_empty_message">Geen apparaten beschikbaar</string>
<string name="invalid_device_id">Fout: ongeldigen apparaats-ID</string>
<string name="dialog_downloading_file">Bestand %1$s wordt gedownload</string>
<string name="toast_file_download_failed">Download van bestand mislukt</string>
<string name="toast_open_file_failed">Gene compatibelen app gevonden</string>
<string name="toast_file_upload_failed">Uploaden van bestand mislukt</string>
<string name="toast_upload_complete">Uploaden van bestand voltooid</string>
<string name="dialog_uploading_file">Bestand %1$s wordt geüpload</string>
<string name="clear_cache_and_index_title">Lokaal cachegeheugen en index wissen?</string>
<string name="clear_cache_and_index_body">Alle lokale cache- en indexgegevens wissen?</string>
<string name="index_update_progress_label">Index voor map %1$s wordt bijgewerkt, %2$d%% gesynchroniseerd</string>
<string name="loading_config_starting_syncthing_client">Configuratie wordt geladen, Syncthing-cliënt wordt opgestart…</string>
<string name="last_modified_time">Laatst gewijzigd: %1$s</string>
<string name="remove_device_title">Apparaat %1$s verwijderen?</string>
<string name="remove_device_message">%1$s verwijderen uit de lijst van gekende apparaten?</string>
<string name="device_import_success">Apparaat %1$s geïmporteerd</string>
<string name="device_already_known">Apparaat %1$s reeds aanwezig</string>
<string name="folders_label">Mappen</string>
<string name="devices_label">Apparaten</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d bestanden, %3$d mappen</string>
<string name="file_info">%1$s, laatst gewijzigd %2$s</string>
<string name="show_device_id">Apparaats-ID tonen</string>
<string name="device_id">Apparaats-ID</string>
<string name="device_id_copied">Apparaats-ID gekopieerd naar klembord</string>
<string name="share_device_id_chooser">Apparaats-ID delen met</string>
<string name="other_syncthing_instance_title">Een andere Syncthing-instantie wordt reeds uitgevoerd</string>
<string name="other_syncthing_instance_message">Lokale ontdekking gaat niet werken. Stopt de andere Syncthing-instantie voor lokale ontdekking in te schakelen.</string>
<string name="intro_page_one_title">Welkom bij Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing vervangt niet-vrije synchronisatie- en clouddiensten door iets opens, betrouwbaars en gedecentraliseerds. Uw gegevens behoren enkel u toe en gij bepaalt waar dat ze worden opgeslagen, of dat ze worden gedeeld met een derde partij en hoe dat ze over het internet worden verzonden.</string>
<string name="intro_page_two_title">Voegt een apparaat toe</string>
<string name="intro_page_three_title">Deelt uw mappen</string>
<string name="intro_page_two_description">Voert ne Syncthing-apparaats-ID in, of scant nen apparaats-ID van een QR-code</string>
<string name="intro_page_three_description">Aanvaard nu het apparaat met ID %1$s, en deelt er een map mee. Het kan enkele minuten duren vooraleer dat de apparaten verbinding maken.</string>
<string name="settings">Instellingen</string>
<string name="settings_app_version_title">Appversie</string>
<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>
@@ -0,0 +1,2 @@
<resources>
</resources>
+45
View File
@@ -0,0 +1,45 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Geen map beschikbaar</string>
<string name="clear_local_cache_index_label">Lokale cache/index wissen</string>
<string name="devices_list_view_empty_message">Geen apparaten beschikbaar</string>
<string name="invalid_device_id">Fout: ongeldige apparaats-ID</string>
<string name="dialog_downloading_file">Bestand %1$s wordt gedownload</string>
<string name="toast_file_download_failed">Download van bestand mislukt</string>
<string name="toast_open_file_failed">Geen compatibele app gevonden</string>
<string name="toast_file_upload_failed">Uploaden van bestand mislukt</string>
<string name="toast_upload_complete">Uploaden van bestand voltooid</string>
<string name="dialog_uploading_file">Bestand %1$s wordt geüpload</string>
<string name="clear_cache_and_index_title">Lokale cache en index wissen?</string>
<string name="clear_cache_and_index_body">Alle lokale cache- en indexgegevens wissen?</string>
<string name="index_update_progress_label">Index voor map %1$s wordt bijgewerkt, %2$d%% gesynchroniseerd</string>
<string name="loading_config_starting_syncthing_client">Configuratie wordt geladen, Syncthing-cliënt wordt opgestart…</string>
<string name="last_modified_time">Laatst gewijzigd: %1$s</string>
<string name="remove_device_title">Apparaat %1$s verwijderen?</string>
<string name="remove_device_message">%1$s verwijderen uit de lijst van gekende apparaten?</string>
<string name="device_import_success">Apparaat %1$s geïmporteerd</string>
<string name="device_already_known">Apparaat %1$s reeds aanwezig</string>
<string name="folders_label">Mappen</string>
<string name="devices_label">Apparaten</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d bestanden, %3$d mappen</string>
<string name="file_info">%1$s, laatst gewijzigd %2$s</string>
<string name="show_device_id">Apparaats-ID tonen</string>
<string name="device_id">Apparaats-ID</string>
<string name="device_id_copied">Apparaats-ID gekopieerd naar klembord</string>
<string name="share_device_id_chooser">Apparaats-ID delen met</string>
<string name="other_syncthing_instance_title">Een andere Syncthing-instantie wordt reeds uitgevoerd</string>
<string name="other_syncthing_instance_message">Lokale ontdekking zal niet werken. Stop de andere Syncthing-instantie om lokale ontdekking in te schakelen.</string>
<string name="intro_page_one_title">Welkom bij Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing vervangt niet-vrije synchronisatie- en clouddiensten door iets opens, betrouwbaars en gedecentraliseerds. Je gegevens behoren enkel jou toe en jij bepaalt waar ze worden opgeslagen, of ze gedeeld worden met een derde partij en hoe ze over het internet verstuurd worden.</string>
<string name="intro_page_two_title">Voeg een apparaat toe</string>
<string name="intro_page_three_title">Deel je mappen</string>
<string name="intro_page_two_description">Voer een Syncthing-apparaats-ID in, of scan een apparaats-ID van een QR-code</string>
<string name="intro_page_three_description">Accepteer nu het apparaat met ID %1$s, en deel er een map mee. Het kan enkele minuten duren voordat de apparaten verbinden.</string>
<string name="settings">Instellingen</string>
<string name="settings_app_version_title">Appversie</string>
<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>
@@ -0,0 +1,2 @@
<resources>
</resources>
@@ -0,0 +1,2 @@
<resources>
</resources>
+59 -2
View File
@@ -3,7 +3,7 @@
<string name="folder_list_empty_message">Nici un director disponibil</string>
<string name="clear_local_cache_index_label">Curăță indexul/memoria locală</string>
<string name="devices_list_view_empty_message">Nici un dispozitiv disponibil</string>
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
<string name="invalid_device_id">Eroare: ID dispozitiv invalid</string>
<string name="dialog_downloading_file">Se descarcă fișierul %1$s</string>
<string name="toast_file_download_failed">Descărcarea fișierului a eșuat</string>
<string name="toast_open_file_failed">Nu a fost găsită nici o aplicație compatibilă</string>
@@ -12,8 +12,11 @@
<string name="dialog_uploading_file">Se încarcă fișierul %1$s</string>
<string name="clear_cache_and_index_title">Se curăță memoria locală și indexul?</string>
<string name="clear_cache_and_index_body">Se curăță datele din memoria locală și datele indexului?</string>
<string name="index_update_progress_label">Actualizarea indexul pentru directorul %1$s, %2$d %% sincronizat</string>
<string name="loading_config_starting_syncthing_client">Încărcare setări, pornire client syncthing…</string>
<string name="last_modified_time">Modificat ultima dată pe: %1$s</string>
<string name="remove_device_title">Ștergere dispozitiv %1$s\?</string>
<string name="remove_device_message">Șterge %1$s din lista dispozitivelor cunoscute?</string>
<string name="device_import_success">Dispozitiv importat cu succes %1$s</string>
<string name="device_already_known">Dispozitiv deja prezent %1$s</string>
<string name="folders_label">Directoare</string>
@@ -21,4 +24,58 @@
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d fișier(e), %3$d director(oare)</string>
<string name="file_info">%1$s, modificat ultima dată pe %2$s</string>
</resources>
<string name="show_device_id">Arată ID dispozitiv</string>
<string name="device_id">ID dispozitiv</string>
<string name="device_id_copied">ID dispozitiv copiat în memorie</string>
<string name="share_device_id_chooser">Partajează ID dispozitiv cu</string>
<string name="other_syncthing_instance_title">O altă instanță de Syncthing rulează</string>
<string name="other_syncthing_instance_message">Descoperire locală nu va funcționa. Opriți cealaltă instanță de Syncthing pentru a activa descoperirea locală.</string>
<string name="intro_page_one_title">Bine ați venit la Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing înlocuiește serviciile proprietare de sincronizare și stocare
tip cloud cu ceva deschis, de încredere și descentralizat. Datele
dumneavoastră vă aparțin în totalitate și meritați să decideți unde vor
fi stocate, dacă vor fi partajate cu terțe entități precum și cum vor fi
trimise prin Internet.</string>
<string name="intro_page_two_title">Adaugă un dispozitiv</string>
<string name="intro_page_three_title">Partajați-vă directoarele</string>
<string name="intro_page_two_description">Introduceți ID-ul Syncthing al unui dispozitiv sau scanați ID-ul unui dispozitiv dintr-un cod QR</string>
<string name="intro_page_three_description">Acceptați acum dispozitivul cu ID-ul %1$s, și partajați un director cu el. S-ar putea să dureze câteva minute până când dispozitivele se vor conecta.</string>
<string name="intro_page_three_searching_device">Se încearcă găsirea celuilalt dispozitiv. Această operație poate dura un moment.</string>
<string name="settings">Setări</string>
<string name="settings_app_version_title">Versiune aplicație</string>
<string name="settings_local_device_name">Nume local dispozitiv</string>
<string name="settings_local_device_summary">Numele pe care celălalt dispozitiv îl va vedea pentru acest dispozitiv</string>
<string name="settings_shutdown_delay_title">Temporizare oprire</string>
<string name="settings_shutdown_delay_summary">După cât timp se va închide clientul Syncthing în funcție de ultima utilizare</string>
<string name="settings_force_stop">Forțează oprirea acestei aplicații</string>
<string name="settings_last_error_title">Ultima eroare</string>
<string name="settings_last_error_summary">Arată detaliile ultimei erori</string>
<string name="settings_report_bug_title">Raportează o eroare</string>
<string name="settings_report_bug_summary">Deschideți un raport de eroare pentru această aplicație pe GitHub</string>
<string name="copy_to_clipboard">Copiază în memorie</string>
<string name="copied_to_clipboard">Copiat în memorie</string>
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
<string name="settings_shutdown_delay_10_seconds">10 secunde</string>
<string name="settings_shutdown_delay_30_seconds">30 secunde</string>
<string name="settings_shutdown_delay_1_minute">1 minut</string>
<string name="settings_shutdown_delay_5_minutes">5 minute</string>
<string name="dialog_warning_reconnect_problem">
Datorită modului în care această aplicație și serverul Syncthing funcționează,
nu se poate face reconectarea timp de câteva minute după ce aplicația a fost oprită (ștearsă din lista de aplicații care rulează)
sau conexiunea a fost întreruptă.
Aceasta limitare nu se aplica la conexiunile descoperite local.
</string>
<string name="dialog_file_save_as">Salvează ca</string>
<string name="pending_index_updates">%d actualizări de index în așteptare</string>
<string name="device_status_connecting">Conectare la %s</string>
<string name="device_status_connected">Conectat la %s</string>
<string name="device_status_disconnected">Se va încerca conectarea în curând - există %d adrese cunoscute</string>
<string name="device_status_no_address">Nici o adresă cunoscută pentru acest dispozitiv</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Activați sincronizarea directorului pentru un dispozitiv nou</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Doriți să sincronizați %1$s cu %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Se sincronizează</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Nu se sincronizează</string>
<string name="dialog_folder_info_device_list">Partajează directorul cu:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">O eroare s-a produs in Syncthing Lite. Puteți vedea detalii în setările Syncthing Lite.</string>
</resources>
+76
View File
@@ -0,0 +1,76 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Ingen mapp tillgänglig</string>
<string name="clear_local_cache_index_label">Rensa lokala cache/index</string>
<string name="devices_list_view_empty_message">Inga enheter tillgängliga</string>
<string name="invalid_device_id">Fel: Ogiltigt enhets-ID</string>
<string name="dialog_downloading_file">Hämtar filen %1$s</string>
<string name="toast_file_download_failed">Misslyckades med att hämta filen</string>
<string name="toast_open_file_failed">Ingen kompatibel app hittades</string>
<string name="toast_file_upload_failed">Filöverföring misslyckades</string>
<string name="toast_upload_complete">Filöverföring färdig</string>
<string name="dialog_uploading_file">Överför filen %1$s</string>
<string name="clear_cache_and_index_title">Rensa cache och index lokalt?</string>
<string name="clear_cache_and_index_body">Rensa all cachedata och indexdata lokalt?</string>
<string name="index_update_progress_label">Index uppdatering för mappen %1$s, %2$d%% synkroniserad</string>
<string name="loading_config_starting_syncthing_client">Läser in konfiguration, starta synkroniserande klient...</string>
<string name="last_modified_time">Senast ändrad: %1$s</string>
<string name="remove_device_title">Ta bort enheten %1$s\?</string>
<string name="remove_device_message">Ta bort %1$s från listan över kända enheter?</string>
<string name="device_import_success">Importerad enheten %1$s</string>
<string name="device_already_known">Enhet som redan finns %1$s</string>
<string name="folders_label">Mappar</string>
<string name="devices_label">Enheter</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d filer, %3$d kataloger</string>
<string name="file_info">%1$s, senast ändrad %2$s</string>
<string name="show_device_id">Visa enhets-ID</string>
<string name="device_id">Enhets-ID</string>
<string name="device_id_copied">Enhets-ID kopierad till urklipp</string>
<string name="share_device_id_chooser">Dela enhets-ID med</string>
<string name="other_syncthing_instance_title">En annan Syncthing-instans körs</string>
<string name="other_syncthing_instance_message">Lokal upptäckt kommer inte att fungera. Stoppa den andra Syncthing-instansen för att möjliggöra lokal upptäckt.</string>
<string name="intro_page_one_title">Välkommen till Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing ersätter proprietära synkroniserings- och molntjänster med något öppet, pålitligt och decentraliserat. Din data är endast din data och du förtjänar att välja var den lagras, om den delas med en tredjepart och hur den överförs över Internet.</string>
<string name="intro_page_two_title">Lägg till en enhet</string>
<string name="intro_page_three_title">Dela dina mappar</string>
<string name="intro_page_two_description">Ange ett Syncthing enhets-ID, eller skanna ett enhets-ID-nummer från en QR-kod</string>
<string name="intro_page_three_description">Acceptera nu enheten med ID %1$s och dela en mapp med den. Det kan ta några minuter tills enheterna ansluter.</string>
<string name="intro_page_three_searching_device">Försöker hitta den andra enheten. Det kan ta ett ögonblick.</string>
<string name="settings">Inställningar</string>
<string name="settings_app_version_title">Appversion</string>
<string name="settings_local_device_name">Lokala enhetens namn</string>
<string name="settings_local_device_summary">Namnet som andra enheter kommer att se för den här enheten</string>
<string name="settings_shutdown_delay_title">Avstängningsfördröjning</string>
<string name="settings_force_stop">Tvinga stoppa denna App</string>
<string name="settings_last_error_title">Senaste felet</string>
<string name="settings_last_error_summary">Visa detaljerna för det senaste felet</string>
<string name="settings_report_bug_title">Rapportera ett fel</string>
<string name="settings_report_bug_summary">Öppna problemen för den här appen på GitHub</string>
<string name="copy_to_clipboard">Kopiera till urklipp</string>
<string name="copied_to_clipboard">Kopieras till urklippet</string>
<string name="device_id_dialog_title">Ange enhets-ID</string>
<string name="settings_shutdown_delay_10_seconds">10 sekunder</string>
<string name="settings_shutdown_delay_30_seconds">30 sekunder</string>
<string name="settings_shutdown_delay_1_minute">1 minut</string>
<string name="settings_shutdown_delay_5_minutes">5 minuter</string>
<string name="dialog_warning_reconnect_problem">
På grund av beteendet hos denna App och beteendet hos Syncthing-servern,
du kan inte återansluta i några minuter om appen dödades (på grund av att du tog bort från den senaste applistan)
eller anslutningen avbröts.
Detta gäller inte lokala upptäcktsanslutningar.
</string>
<string name="dialog_file_save_as">Spara som</string>
<string name="pending_index_updates">%d indexuppdateringar som väntar</string>
<string name="device_status_connecting">Ansluter till %s</string>
<string name="device_status_connected">Ansluten till %s</string>
<string name="device_status_disconnected">Kommer att försöka ansluta snart - det finns%d kända adresser</string>
<string name="device_status_no_address">Ingen känd adress för enheten</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Aktivera mappsynkronisering för ny enhet</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Vill du synkronisera %1$s med %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Synkronisera</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Synkronisera inte</string>
<string name="dialog_folder_info_device_list">Dela mapp med:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Något gick fel i Syncthing Lite. Du kan visa detaljerna från inställningarna för Syncthing Lite.</string>
</resources>
+2
View File
@@ -0,0 +1,2 @@
<resources>
</resources>
@@ -0,0 +1,49 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">没有可用的文件夹</string>
<string name="clear_local_cache_index_label">清除本地缓存/索引</string>
<string name="devices_list_view_empty_message">没有可用的设备</string>
<string name="invalid_device_id">错误:无效的设备 ID</string>
<string name="dialog_downloading_file">正在下载文件 %1$s</string>
<string name="toast_file_download_failed">下载文件失败</string>
<string name="toast_open_file_failed">未找到兼容的应用</string>
<string name="toast_file_upload_failed">上传文件失败</string>
<string name="toast_upload_complete">文件上传完成</string>
<string name="dialog_uploading_file">正在上传文件 %1$s</string>
<string name="clear_cache_and_index_title">确定清除本地缓存和索引?</string>
<string name="clear_cache_and_index_body">确定清除全部本地缓存数据和索引数据?</string>
<string name="loading_config_starting_syncthing_client">正在载入配置,启动 syncthing 客户端……</string>
<string name="last_modified_time">最后修改:%1$s</string>
<string name="remove_device_title">移除设备: %1$s</string>
<string name="remove_device_message">从已知设备列表中移除 %1$s</string>
<string name="device_import_success">已成功导入设备 %1$s</string>
<string name="device_already_known">设备已存在 %1$s</string>
<string name="folders_label">文件夹</string>
<string name="devices_label">设备</string>
<string name="folder_label_format">%1$s%2$s</string>
<string name="folder_content_info">%1$s%2$d个文件,%3$d个目录</string>
<string name="file_info">%1$s,最后修改 %2$s</string>
<string name="show_device_id">显示设备 ID</string>
<string name="device_id">设备 ID</string>
<string name="device_id_copied">设备 ID 已复制到剪贴板</string>
<string name="share_device_id_chooser">分享设备 ID 于</string>
<string name="other_syncthing_instance_title">另一个 Syncthing 实例正在运行</string>
<string name="other_syncthing_instance_message">本地发现将无法工作。停止其他 Syncthing 实例以启用本地发现。</string>
<string name="intro_page_one_title">欢迎使用 Syncthing Lite</string>
<string name="intro_page_one_description">Syncthing 以开放、可靠并去中心化的软件替换掉封闭的云服务。您的数据仍由您拥有,您可以选择它们的存储位置,如果要共享给第三方您还可以选择如何在互联网上传输它们。</string>
<string name="intro_page_two_title">添加设备</string>
<string name="intro_page_three_title">共享您的文件夹</string>
<string name="intro_page_two_description">输入 Syncthing 设备 ID,或者通过 QR 码扫描设备 ID</string>
<string name="intro_page_three_description">已接受 ID 为%1$s 的设备,并与它共享了一个文件夹。设备连接可能需要花上数分钟。</string>
<string name="settings">设置</string>
<string name="settings_app_version_title">应用版本</string>
<string name="settings_local_device_name">本地设备名称</string>
<string name="settings_local_device_summary">此设备将被其他设备看到的名称</string>
<string name="settings_shutdown_delay_title">关闭延迟</string>
<string name="device_id_dialog_title">输入设备 ID</string>
<string name="settings_shutdown_delay_10_seconds">10 秒</string>
<string name="settings_shutdown_delay_30_seconds">30 秒</string>
<string name="settings_shutdown_delay_1_minute">1 分钟</string>
<string name="settings_shutdown_delay_5_minutes">5 分钟</string>
<string name="dialog_folder_info_device_list_item">%1$s%2$s</string>
</resources>
+24 -2
View File
@@ -36,12 +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_shutdown_delay_summary">Time before shutting down the Syncthing client after its last usage</string>
<string name="settings_force_stop">Force stop this App</string>
<string name="settings_last_error_title">Last error</string>
<string name="settings_last_error_summary">View the details of the last error</string>
<string name="settings_report_bug_title">Report a bug</string>
<string name="settings_report_bug_summary">Open the issues for this App at GitHub</string>
<string name="copy_to_clipboard">Copy to clipboard</string>
<string name="copied_to_clipboard">Copied to the clipboard</string>
<string name="device_id_dialog_title">Enter Device ID</string>
<string name="settings_shutdown_delay_10_seconds">10 seconds</string>
<string name="settings_shutdown_delay_30_seconds">30 seconds</string>
@@ -49,8 +57,22 @@
<string name="settings_shutdown_delay_5_minutes">5 minutes</string>
<string name="dialog_warning_reconnect_problem">
Due to the behaviour of this App and the behaviour of the Syncthing Server,
you can\'t reconnct for some minutes if the App was killed (due to removing from the recent App list)
you can\'t reconnect for some minutes if the App was killed (due to removing from the recent App list)
or the connection was interrupted.
This does not apply to local discovery connections.
</string>
<string name="dialog_file_save_as">Save as</string>
<string name="pending_index_updates">%d index updates pending</string>
<string name="device_status_connecting">Connecting to %s</string>
<string name="device_status_connected">Connected to %s</string>
<string name="device_status_disconnected">Will retry connecting soon - there are %d known addresses</string>
<string name="device_status_no_address">No known address for the device</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Enable folder sync for new device</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Do you want to sync %1$s with %2$s (%3$s)?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">Sync</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">Do not sync</string>
<string name="dialog_folder_info_device_list">Share folder with:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Something went wrong in Syncthing Lite. You can view the details from the settings of Syncthing Lite.</string>
<string name="folder_browser_home">Home</string>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="files" path="/" />
</paths>
+14
View File
@@ -24,10 +24,24 @@
-->
<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" />
</PreferenceCategory>
</PreferenceScreen>
+6 -5
View File
@@ -1,10 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.61'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.2.0'
ext.anko_version = '0.10.7'
ext.kotlin_version = '1.3.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 {
mavenLocal()
@@ -17,15 +17,16 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
classpath 'com.github.triplet.gradle:play-publisher:1.2.0'
}
}
allprojects {
repositories {
google()
jcenter()
maven {
url "https://jitpack.io"
}
google()
}
}
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
set -e
NEW_VERSION_NAME=$1
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}' | tr -d "\"")
if [[ -z ${NEW_VERSION_NAME} ]]
then
echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME"
exit
fi
echo "
Updating Translations
-----------------------------
"
tx push -s
# Force push/pull to make sure this is executed. Apparently tx only compares timestamps, not file
# contents. So if a file was `touch`ed, it won't be updated by default.
tx pull -a -f
git add -A "app/src/main/res/values-*/strings.xml"
if ! git diff --cached --exit-code;
then
git commit -m "Imported translations"
fi
echo "
Updating Version
-----------------------------
"
OLD_VERSION_CODE=$(grep "versionCode" "app/build.gradle" -m 1 | awk '{print $2}')
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" "app/build.gradle"
sed -i "s/versionName \"$OLD_VERSION_NAME\"/versionName \"$NEW_VERSION_NAME\"/" "app/build.gradle"
LIBRARY_NAME="com.github.Nutomic:syncthing-java"
sed -i "s/$LIBRARY_NAME:$OLD_VERSION_NAME/$LIBRARY_NAME:$NEW_VERSION_NAME/" "app/build.gradle"
git add "app/build.gradle"
git commit -m "Version $NEW_VERSION_NAME"
git tag ${NEW_VERSION_NAME}
echo "
Running Lint
-----------------------------
"
./gradlew clean lintVitalRelease
echo "
Update ready.
"
-37
View File
@@ -1,37 +0,0 @@
#!/usr/bin/env bash
set -e
version=$(git describe --tags)
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
if [[ ! ${version} =~ $regex ]]
then
echo "Current commit is not a release"
exit;
fi
echo "
Pushing to Github
-----------------------------
"
git push
git push --tags
echo "
Push to Google Play
-----------------------------
"
read -s -p "Enter signing password: " password
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
# Upload apk and listing to Google Play
SIGNING_PASSWORD=${password} ./gradlew publishRelease
echo "
Release published!
"
+1 -1
View File
@@ -1 +1 @@
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli', ':syncthing-http-relay-client'
include ':app', ':syncthing-repository-android', ':syncthing-repository-default', ':syncthing-relay-client', ':syncthing-bep', ':syncthing-core', ':syncthing-client', ':syncthing-discovery', ':syncthing-client-cli', ':syncthing-temp-repository-encryption'
+1 -2
View File
@@ -6,10 +6,9 @@ dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':syncthing-core')
compile project(':syncthing-relay-client')
compile project(':syncthing-http-relay-client')
compile "net.jpountz.lz4:lz4:1.3.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation "com.google.protobuf:protobuf-lite:$protobuf_lite_version"
}
@@ -15,58 +15,47 @@
package net.syncthing.java.bep
import com.google.protobuf.ByteString
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.channels.Channel
import net.syncthing.java.bep.BlockExchangeProtos.ErrorCode
import net.syncthing.java.bep.BlockExchangeProtos.Request
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.bep.utils.longSumBy
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.interfaces.TempRepository
import net.syncthing.java.core.utils.NetworkUtils
import org.apache.commons.io.FileUtils
import org.bouncycastle.util.encoders.Hex
import org.slf4j.LoggerFactory
import java.io.*
import java.lang.Exception
import java.security.MessageDigest
import java.util.*
import kotlin.collections.HashMap
class BlockPuller internal constructor(private val connectionHandler: ConnectionHandler,
private val indexHandler: IndexHandler,
private val responseHandler: ResponseHandler,
private val tempRepository: TempRepository) {
object BlockPuller {
private val logger = LoggerFactory.getLogger(javaClass)
fun pullFileSync(
suspend fun pullFile(
fileInfo: FileInfo,
progressListener: (status: BlockPullerStatus) -> Unit = { }
progressListener: (status: BlockPullerStatus) -> Unit = { },
connections: List<ConnectionActorWrapper>,
indexHandler: IndexHandler,
tempRepository: TempRepository
): InputStream {
return runBlocking {
pullFileCoroutine(fileInfo, progressListener)
val connectionHelper = MultiConnectionHelper(connections) {
it.hasFolder(fileInfo.folder)
}
}
suspend fun pullFileCoroutine(
fileInfo: FileInfo,
progressListener: (status: BlockPullerStatus) -> Unit = { }
): InputStream {
val fileBlocks = indexHandler.waitForRemoteIndexAcquired(connectionHandler)
.getFileInfoAndBlocksByPath(fileInfo.folder, fileInfo.path)
?.value
?: throw IOException("file not found in local index for folder = ${fileInfo.folder} path = ${fileInfo.path}")
logger.info("pulling file = {}", fileBlocks)
NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileBlocks.folder), { "supplied connection handler $connectionHandler will not share folder ${fileBlocks.folder}" })
// fail early if there is no matching connection
connectionHelper.pickConnection()
val (newFileInfo, fileBlocks) = indexHandler.getFileInfoAndBlocksByPath(fileInfo.folder, fileInfo.path) ?: throw FileNotFoundException()
// the file could have changed since the caller read it
// this would save the file using a wrong name, so throw here
if (fileBlocks.hash != fileInfo.hash) {
throw IllegalStateException("the current file entry hash does not match the hash of the provided one")
}
logger.info("pulling file = {}", fileBlocks)
val blockTempIdByHash = Collections.synchronizedMap(HashMap<String, String>())
var status = BlockPullerStatus(
@@ -75,6 +64,47 @@ class BlockPuller internal constructor(private val connectionHandler: Connection
totalFileSize = fileBlocks.size
)
suspend fun pullBlock(fileBlocks: FileBlocks, block: BlockInfo, timeoutInMillis: Long, connectionActorWrapper: ConnectionActorWrapper): ByteArray {
logger.debug("sent message for block, hash = {}", block.hash)
val response =
withTimeout(timeoutInMillis) {
try {
connectionActorWrapper.sendRequest(
BlockExchangeProtos.Request.newBuilder()
.setFolder(fileBlocks.folder)
.setName(fileBlocks.path)
.setOffset(block.offset)
.setSize(block.size)
.setHash(ByteString.copyFrom(Hex.decode(block.hash)))
.buildPartial()
)
} catch (ex: TimeoutCancellationException) {
// It seems like the TimeoutCancellationException
// is handled differently so that the timeout is ignored.
// Due to that, it's converted to an IOException.
throw IOException("timeout during requesting block")
}
}
if (response.code != BlockExchangeProtos.ErrorCode.NO_ERROR) {
// the server does not have/ want to provide this file -> don't ask him again
connectionHelper.disableConnection(connectionActorWrapper)
throw IOException("received error response ${response.code}")
}
val data = response.data.toByteArray()
val hash = Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(data))
if (hash != block.hash) {
throw IllegalStateException("expected block with hash ${block.hash}, but got block with hash $hash")
}
return data
}
try {
val reportProgressLock = Object()
@@ -94,9 +124,31 @@ class BlockPuller internal constructor(private val connectionHandler: Connection
repeat(4 /* 4 blocks per time */) { workerNumber ->
async {
for (block in pipe) {
logger.debug("request block with hash = {} from worker {}", block.hash, workerNumber)
logger.debug("message block with hash = {} from worker {}", block.hash, workerNumber)
val blockContent = pullBlock(fileBlocks, block, 1000 * 60 /* 60 seconds timeout per block */)
lateinit var blockContent: ByteArray
val attempts = 0..4
for (attempt in attempts) {
try {
blockContent = pullBlock(fileBlocks, block, 1000 * 60 /* 60 seconds timeout per block */, connectionHelper.pickConnection())
break
} catch (ex: IOException) {
if (attempt == attempts.last) {
throw ex
} else {
// will retry after a pause
// 0: 300 ms after the first attempt
// 1: 1200 ms after the second attempt
// 2: 2700 ms after the third attempt
// 3: 4800 ms after the third attempt
// total: 9000 ms
delay((attempt + 1) * (attempt + 1) * 300L)
}
}
}
blockTempIdByHash[block.hash] = tempRepository.pushTempData(blockContent)
@@ -138,57 +190,6 @@ class BlockPuller internal constructor(private val connectionHandler: Connection
throw ex
}
}
private suspend fun pullBlock(fileBlocks: FileBlocks, block: BlockInfo, timeoutInMillis: Long): ByteArray {
logger.debug("sent request for block, hash = {}", block.hash)
val response =
withTimeout(timeoutInMillis) {
try {
doRequest(
Request.newBuilder()
.setFolder(fileBlocks.folder)
.setName(fileBlocks.path)
.setOffset(block.offset)
.setSize(block.size)
.setHash(ByteString.copyFrom(Hex.decode(block.hash)))
)
} catch (ex: TimeoutCancellationException) {
// It seems like the TimeoutCancellationException
// is handled differently so that the timeout is ignored.
// Due to that, it's converted to an IOException.
throw IOException("timeout during requesting block")
}
}
NetworkUtils.assertProtocol(response.code == ErrorCode.NO_ERROR) {
"received error response, code = ${response.code}"
}
val data = response.data.toByteArray()
val hash = Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(data))
if (hash != block.hash) {
throw IllegalStateException("expected block with hash ${block.hash}, but got block with hash $hash")
}
return data
}
private suspend fun doRequest(request: Request.Builder): BlockExchangeProtos.Response {
return suspendCancellableCoroutine { continuation ->
val requestId = responseHandler.registerListener { response ->
continuation.resume(response)
}
connectionHandler.sendMessage(
request
.setId(requestId)
.build()
)
}
}
}
data class BlockPullerStatus(
@@ -1,5 +1,6 @@
/*
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -14,15 +15,21 @@
package net.syncthing.java.bep
import com.google.protobuf.ByteString
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.BlockExchangeProtos.Vector
import net.syncthing.java.core.beans.*
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import net.syncthing.java.bep.index.*
import net.syncthing.java.core.beans.BlockInfo
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FileInfo.Version
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.utils.BlockUtils
import net.syncthing.java.core.utils.NetworkUtils
import net.syncthing.java.core.utils.submitLogging
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.tuple.Pair
import org.bouncycastle.util.encoders.Hex
import org.slf4j.LoggerFactory
import java.io.Closeable
@@ -32,37 +39,36 @@ import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
class BlockPusher internal constructor(private val localDeviceId: DeviceId,
private val connectionHandler: ConnectionHandler,
private val indexHandler: IndexHandler) {
// TODO: refactor this
class BlockPusher(private val localDeviceId: DeviceId,
private val connectionHandler: ConnectionActorWrapper,
private val indexHandler: IndexHandler,
private val requestHandlerRegistry: RequestHandlerRegistry) {
private val logger = LoggerFactory.getLogger(javaClass)
fun pushDelete(folderId: String, targetPath: String): IndexEditObserver {
val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)!!
suspend fun pushDelete(folderId: String, targetPath: String): BlockExchangeProtos.IndexUpdate {
val fileInfo = indexHandler.waitForRemoteIndexAcquiredWithTimeout(connectionHandler).getFileInfoByPath(folderId, targetPath)!!
NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileInfo.folder), {"supplied connection handler $connectionHandler will not share folder ${fileInfo.folder}"})
return IndexEditObserver(sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
return sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
.setName(targetPath)
.setType(BlockExchangeProtos.FileInfoType.valueOf(fileInfo.type.name))
.setDeleted(true), fileInfo.versionList))
.setDeleted(true), fileInfo.versionList)
}
fun pushDir(folder: String, path: String): IndexEditObserver {
suspend fun pushDir(folder: String, path: String): BlockExchangeProtos.IndexUpdate {
NetworkUtils.assertProtocol(connectionHandler.hasFolder(folder), {"supplied connection handler $connectionHandler will not share folder $folder"})
return IndexEditObserver(sendIndexUpdate(folder, BlockExchangeProtos.FileInfo.newBuilder()
return sendIndexUpdate(folder, BlockExchangeProtos.FileInfo.newBuilder()
.setName(path)
.setType(BlockExchangeProtos.FileInfoType.DIRECTORY), null))
.setType(BlockExchangeProtos.FileInfoType.DIRECTORY), null)
}
fun pushFile(inputStream: InputStream, folderId: String, targetPath: String): FileUploadObserver {
val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)
suspend fun pushFile(inputStream: InputStream, folderId: String, targetPath: String): FileUploadObserver {
val fileInfo = indexHandler.waitForRemoteIndexAcquiredWithTimeout(connectionHandler).getFileInfoByPath(folderId, targetPath)
NetworkUtils.assertProtocol(connectionHandler.hasFolder(folderId), {"supplied connection handler $connectionHandler will not share folder $folderId"})
assert(fileInfo == null || fileInfo.folder == folderId)
assert(fileInfo == null || fileInfo.path == targetPath)
@@ -73,56 +79,57 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
val uploadError = AtomicReference<Exception>()
val isCompleted = AtomicBoolean(false)
val updateLock = Object()
val listener = {request: BlockExchangeProtos.Request ->
if (request.folder == folderId && request.name == targetPath) {
val requestFilter = RequestHandlerFilter(
deviceId = connectionHandler.deviceId,
folderId = folderId,
path = targetPath
)
requestHandlerRegistry.registerListener(requestFilter) { request ->
GlobalScope.async {
val hash = Hex.toHexString(request.hash.toByteArray())
logger.debug("handling block request = {}:{}-{} ({})", request.name, request.offset, request.size, hash)
val data = dataSource.getBlock(request.offset, request.size, hash)
val future = connectionHandler.sendMessage(BlockExchangeProtos.Response.newBuilder()
sentBlocks.add(hash)
synchronized(updateLock) {
updateLock.notifyAll()
}
BlockExchangeProtos.Response.newBuilder()
.setCode(BlockExchangeProtos.ErrorCode.NO_ERROR)
.setData(ByteString.copyFrom(data))
.setId(request.id)
.build())
monitoringProcessExecutorService.submitLogging {
try {
future.get()
sentBlocks.add(hash)
synchronized(updateLock) {
updateLock.notifyAll()
}
//TODO retry on error, register error and throw on watcher
} catch (ex: InterruptedException) {
//return and do nothing
} catch (ex: ExecutionException) {
uploadError.set(ex)
synchronized(updateLock) {
updateLock.notifyAll()
}
}
}
.build()
}
}
connectionHandler.registerOnRequestMessageReceivedListeners(listener)
logger.debug("send index update for file = {}", targetPath)
val indexListener = { folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo ->
if (folderInfo.folderId == folderId) {
for (fileInfo2 in newRecords) {
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
// sentBlocks.addAll(dataSource.getHashes());
isCompleted.set(true)
synchronized(updateLock) {
updateLock.notifyAll()
val indexListenerStream = indexHandler.subscribeToOnIndexUpdateEvents()
GlobalScope.launch {
indexListenerStream.consumeEach { event ->
if (event is IndexRecordAcquiredEvent) {
val (indexFolderId, newRecords, _) = event
if (indexFolderId == folderId) {
for (fileInfo2 in newRecords) {
if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid
// sentBlocks.addAll(dataSource.getHashes());
isCompleted.set(true)
synchronized(updateLock) {
updateLock.notifyAll()
}
}
}
}
}
}
}
indexHandler.registerOnIndexRecordAcquiredListener(indexListener)
val indexUpdate = sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder()
.setName(targetPath)
.setSize(fileSize)
.setType(BlockExchangeProtos.FileInfoType.FILE)
.addAllBlocks(dataSource.blocks), fileInfo?.versionList).right
.addAllBlocks(dataSource.blocks), fileInfo?.versionList)
return object : FileUploadObserver() {
override fun progressPercentage() = if (isCompleted.get()) 100 else (sentBlocks.size.toFloat() / dataSource.getHashes().size).toInt()
@@ -133,9 +140,27 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
override fun close() {
logger.debug("closing upload process")
monitoringProcessExecutorService.shutdown()
indexHandler.unregisterOnIndexRecordAcquiredListener(indexListener)
connectionHandler.unregisterOnRequestMessageReceivedListeners(listener)
val fileInfo1 = indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single())
indexListenerStream.cancel()
requestHandlerRegistry.unregisterListener(requestFilter)
val (fileInfo1, folderStatsUpdate) = indexHandler.indexRepository.runInTransaction {
val folderStatsUpdateCollector = FolderStatsUpdateCollector(folderId)
// TODO: notify the IndexBrowsers again (as it was earlier)
val fileInfo = IndexElementProcessor.pushRecord(
it,
indexUpdate.folder,
indexUpdate.filesList.single(),
folderStatsUpdateCollector,
it.findFileInfo(folderId, indexUpdate.filesList.single().name)
)
IndexMessageProcessor.handleFolderStatsUpdate(it, folderStatsUpdateCollector)
val folderStatsUpdate = it.findFolderStats(folderId) ?: FolderStats.createDummy(folderId)
fileInfo to folderStatsUpdate
}
runBlocking { indexHandler.sendFolderStatsUpdate(folderStatsUpdate) }
logger.info("sent file info record = {}", fileInfo1)
}
@@ -153,10 +178,10 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
}
}
private fun sendIndexUpdate(folderId: String, fileInfoBuilder: BlockExchangeProtos.FileInfo.Builder,
oldVersions: Iterable<Version>?): Pair<Future<*>, BlockExchangeProtos.IndexUpdate> {
private suspend fun sendIndexUpdate(folderId: String, fileInfoBuilder: BlockExchangeProtos.FileInfo.Builder,
oldVersions: Iterable<Version>?): BlockExchangeProtos.IndexUpdate {
run {
val nextSequence = indexHandler.sequencer().nextSequence()
val nextSequence = indexHandler.getNextSequenceNumber()
val list = oldVersions ?: emptyList()
logger.debug("version list = {}", list)
val id = ByteBuffer.wrap(localDeviceId.toHashData()).long
@@ -183,7 +208,10 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
.addFiles(fileInfo)
.build()
logger.debug("index update = {}", fileInfo)
return Pair.of(connectionHandler.sendMessage(indexUpdate), indexUpdate)
connectionHandler.sendIndexUpdate(indexUpdate)
return indexUpdate
}
abstract inner class FileUploadObserver : Closeable {
@@ -204,33 +232,6 @@ class BlockPusher internal constructor(private val localDeviceId: DeviceId,
}
}
inner class IndexEditObserver(private val future: Future<*>, private val indexUpdate: BlockExchangeProtos.IndexUpdate) : Closeable {
//throw exception if job has errors
@Throws(InterruptedException::class, ExecutionException::class)
fun isCompleted(): Boolean {
return if (future.isDone) {
future.get()
true
} else {
false
}
}
constructor(pair: Pair<Future<*>, BlockExchangeProtos.IndexUpdate>) : this(pair.left, pair.right)
@Throws(InterruptedException::class, ExecutionException::class)
fun waitForComplete() {
future.get()
}
@Throws(IOException::class)
override fun close() {
indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single())
}
}
private class DataSource @Throws(IOException::class) constructor(private val inputStream: InputStream) {
var size: Long = 0
@@ -1,517 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* 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.bep
import com.google.protobuf.ByteString
import com.google.protobuf.MessageLite
import net.jpountz.lz4.LZ4Factory
import net.syncthing.java.bep.BlockExchangeProtos.*
import net.syncthing.java.client.protocol.rp.RelayClient
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.interfaces.TempRepository
import net.syncthing.java.core.security.KeystoreHandler
import net.syncthing.java.core.utils.NetworkUtils
import net.syncthing.java.core.utils.submitLogging
import net.syncthing.java.httprelay.HttpRelayClient
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.tuple.Pair
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.lang.reflect.InvocationTargetException
import java.nio.ByteBuffer
import java.security.cert.CertificateException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLSocket
class ConnectionHandler(private val configuration: Configuration, val address: DeviceAddress,
private val indexHandler: IndexHandler,
private val tempRepository: TempRepository,
private val onNewFolderSharedListener: (ConnectionHandler, FolderInfo) -> Unit,
private val onConnectionChangedListener: (ConnectionHandler) -> Unit) : Closeable {
private val logger = LoggerFactory.getLogger(javaClass)
private val outExecutorService = Executors.newSingleThreadExecutor()
private val inExecutorService = Executors.newSingleThreadExecutor()
private val messageProcessingService = Executors.newCachedThreadPool()
private val periodicExecutorService = Executors.newSingleThreadScheduledExecutor()
private lateinit var socket: SSLSocket
private var inputStream: DataInputStream? = null
private var outputStream: DataOutputStream? = null
private var lastActive = Long.MIN_VALUE
internal var clusterConfigInfo: ClusterConfigInfo? = null
private set
private val clusterConfigWaitingLock = Object()
private val responseHandler = ResponseHandler()
private val blockPuller = BlockPuller(this, indexHandler, responseHandler, tempRepository)
private val blockPusher = BlockPusher(configuration.localDeviceId, this, indexHandler)
private val onRequestMessageReceivedListeners = mutableSetOf<(Request) -> Unit>()
private var isClosed = false
var isConnected = false
private set
fun deviceId(): DeviceId = address.deviceId()
private fun checkNotClosed() {
NetworkUtils.assertProtocol(!isClosed, {"connection $this closed"})
}
internal fun registerOnRequestMessageReceivedListeners(listener: (Request) -> Unit) {
onRequestMessageReceivedListeners.add(listener)
}
internal fun unregisterOnRequestMessageReceivedListeners(listener: (Request) -> Unit) {
assert(onRequestMessageReceivedListeners.contains(listener))
onRequestMessageReceivedListeners.remove(listener)
}
@Throws(IOException::class, KeystoreHandler.CryptoException::class)
fun connect(): ConnectionHandler {
checkNotClosed()
assert(!isConnected, {"already connected!"})
logger.info("connecting to {}", address.address)
val keystoreHandler = KeystoreHandler.Loader().loadKeystore(configuration)
socket = when (address.getType()) {
DeviceAddress.AddressType.TCP -> {
logger.debug("opening tcp ssl connection")
keystoreHandler.createSocket(address.getSocketAddress(), KeystoreHandler.BEP)
}
DeviceAddress.AddressType.RELAY -> {
logger.debug("opening relay connection")
keystoreHandler.wrapSocket(RelayClient(configuration).openRelayConnection(address), KeystoreHandler.BEP)
}
DeviceAddress.AddressType.HTTP_RELAY, DeviceAddress.AddressType.HTTPS_RELAY -> {
logger.debug("opening http relay connection")
keystoreHandler.wrapSocket(HttpRelayClient().openRelayConnection(address), KeystoreHandler.BEP)
}
else -> throw UnsupportedOperationException("unsupported address type = " + address.getType())
}
inputStream = DataInputStream(socket.inputStream)
outputStream = DataOutputStream(socket.outputStream)
sendHelloMessage(BlockExchangeProtos.Hello.newBuilder()
.setClientName(configuration.clientName)
.setClientVersion(configuration.clientVersion)
.setDeviceName(configuration.localDeviceName)
.build().toByteArray())
markActivityOnSocket()
receiveHelloMessage()
try {
keystoreHandler.checkSocketCertificate(socket, address.deviceId())
} catch (e: CertificateException) {
throw IOException(e)
}
run {
val clusterConfigBuilder = ClusterConfig.newBuilder()
for (folder in configuration.folders) {
val folderBuilder = Folder.newBuilder()
.setId(folder.folderId)
.setLabel(folder.label)
run {
//our device
val deviceBuilder = Device.newBuilder()
.setId(ByteString.copyFrom(configuration.localDeviceId.toHashData()))
.setIndexId(indexHandler.sequencer().indexId())
.setMaxSequence(indexHandler.sequencer().currentSequence())
folderBuilder.addDevices(deviceBuilder)
}
run {
//other device
val deviceBuilder = Device.newBuilder()
.setId(ByteString.copyFrom(DeviceId(address.deviceId).toHashData()))
val indexSequenceInfo = indexHandler.indexRepository.findIndexInfoByDeviceAndFolder(address.deviceId(), folder.folderId)
indexSequenceInfo?.let {
deviceBuilder
.setIndexId(indexSequenceInfo.indexId)
.setMaxSequence(indexSequenceInfo.localSequence)
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
indexSequenceInfo.deviceId,
indexSequenceInfo.indexId,
indexSequenceInfo.localSequence)
}
folderBuilder.addDevices(deviceBuilder)
}
clusterConfigBuilder.addFolders(folderBuilder)
//TODO other devices??
}
sendMessage(clusterConfigBuilder.build())
}
synchronized(clusterConfigWaitingLock) {
startMessageListenerService()
while (clusterConfigInfo == null && !isClosed) {
logger.debug("wait for cluster config")
try {
clusterConfigWaitingLock.wait()
} catch (e: InterruptedException) {
throw IOException(e)
}
}
if (clusterConfigInfo == null) {
throw IOException("unable to retrieve cluster config from peer!")
}
}
for (folder in configuration.folders) {
if (hasFolder(folder.folderId)) {
sendIndexMessage(folder.folderId)
}
}
periodicExecutorService.scheduleWithFixedDelay({ this.sendPing() }, 90, 90, TimeUnit.SECONDS)
isConnected = true
onConnectionChangedListener(this)
return this
}
fun getBlockPuller(): BlockPuller {
return blockPuller
}
fun getBlockPusher(): BlockPusher {
return blockPusher
}
private fun sendIndexMessage(folderId: String) {
sendMessage(Index.newBuilder()
.setFolder(folderId)
.build())
}
fun closeBg() {
Thread { close() }.start()
}
/**
* Receive hello message and save device name to configuration.
*/
@Throws(IOException::class)
private fun receiveHelloMessage() {
val magic = inputStream!!.readInt()
NetworkUtils.assertProtocol(magic == MAGIC, {"magic mismatch, expected $MAGIC, got $magic"})
val length = inputStream!!.readShort().toInt()
NetworkUtils.assertProtocol(length > 0, {"invalid lenght, must be >0, got $length"})
val buffer = ByteArray(length)
inputStream!!.readFully(buffer)
val hello = BlockExchangeProtos.Hello.parseFrom(buffer)
logger.info("Received hello message, deviceName=${hello.deviceName}, clientName=${hello.clientName}, clientVersion=${hello.clientVersion}")
configuration.peers = configuration.peers.map { peer ->
if (peer.deviceId == deviceId()) {
DeviceInfo(deviceId(), hello.deviceName)
} else {
peer
}
}.toSet()
configuration.persistLater()
}
private fun sendHelloMessage(payload: ByteArray): Future<*> {
return outExecutorService.submitLogging {
try {
logger.debug("Sending hello message")
val header = ByteBuffer.allocate(6)
header.putInt(MAGIC)
header.putShort(payload.size.toShort())
outputStream!!.write(header.array())
outputStream!!.write(payload)
outputStream!!.flush()
} catch (ex: IOException) {
if (outExecutorService.isShutdown) {
return@submitLogging
}
logger.error("error writing to output stream", ex)
closeBg()
}
}
}
private fun sendPing(): Future<*> {
return sendMessage(Ping.newBuilder().build())
}
private fun markActivityOnSocket() {
lastActive = System.currentTimeMillis()
}
@Throws(IOException::class)
private fun receiveMessage(): Pair<BlockExchangeProtos.MessageType, MessageLite> {
var headerLength = inputStream!!.readShort().toInt()
while (headerLength == 0) {
logger.warn("got headerLength == 0, skipping short")
headerLength = inputStream!!.readShort().toInt()
}
markActivityOnSocket()
NetworkUtils.assertProtocol(headerLength > 0, {"invalid lenght, must be >0, got $headerLength"})
val headerBuffer = ByteArray(headerLength)
inputStream!!.readFully(headerBuffer)
val header = BlockExchangeProtos.Header.parseFrom(headerBuffer)
var messageLength = 0
while (messageLength == 0) {
logger.warn("received readInt() == 0, expecting 'bep message header length' (int >0), ignoring (keepalive?)")
messageLength = inputStream!!.readInt()
}
NetworkUtils.assertProtocol(messageLength >= 0, {"invalid lenght, must be >=0, got $messageLength"})
var messageBuffer = ByteArray(messageLength)
inputStream!!.readFully(messageBuffer)
markActivityOnSocket()
if (header.compression == BlockExchangeProtos.MessageCompression.LZ4) {
val uncompressedLength = ByteBuffer.wrap(messageBuffer).int
messageBuffer = LZ4Factory.fastestInstance().fastDecompressor().decompress(messageBuffer, 4, uncompressedLength)
}
val messageTypeInfo = messageTypesByProtoMessageType[header.type]
NetworkUtils.assertProtocol(messageTypeInfo != null, {"unsupported message type = ${header.type}"})
try {
val message = messageTypeInfo!!.parseFrom(messageBuffer)
return Pair.of(header.type, message)
} catch (e: Exception) {
when (e) {
is IllegalAccessException, is IllegalArgumentException, is InvocationTargetException, is NoSuchMethodException, is SecurityException ->
throw IOException(e)
else -> throw e
}
}
}
internal fun sendMessage(message: MessageLite): Future<*> {
checkNotClosed()
val messageTypeInfo = messageTypesByJavaClass[message.javaClass]
messageTypeInfo!!
val header = BlockExchangeProtos.Header.newBuilder()
.setCompression(BlockExchangeProtos.MessageCompression.NONE)
// invert map
.setType(messageTypeInfo.protoMessageType)
.build()
val headerData = header.toByteArray()
val messageData = message.toByteArray() //TODO compression
return outExecutorService.submit<Any> {
try {
logger.debug("sending message type = {} {}", header.type, getIdForMessage(message))
markActivityOnSocket()
outputStream!!.writeShort(headerData.size)
outputStream!!.write(headerData)
outputStream!!.writeInt(messageData.size)//with compression, check this
outputStream!!.write(messageData)
outputStream!!.flush()
markActivityOnSocket()
} catch (ex: IOException) {
if (!outExecutorService.isShutdown) {
logger.error("error writing to output stream", ex)
closeBg()
}
throw ex
}
null
}
}
override fun close() {
if (!isClosed) {
sendMessage(Close.getDefaultInstance())
isClosed = true
isConnected = false
periodicExecutorService.shutdown()
outExecutorService.shutdown()
inExecutorService.shutdown()
messageProcessingService.shutdown()
assert(onRequestMessageReceivedListeners.isEmpty())
if (outputStream != null) {
IOUtils.closeQuietly(outputStream)
outputStream = null
}
if (inputStream != null) {
IOUtils.closeQuietly(inputStream)
inputStream = null
}
try {
IOUtils.closeQuietly(socket)
} catch (ex: Exception) {
// ignore this
// this can throw an exception if socket was not yet initialized/ set
// as Kotlin does an check about this, the closeQuietly does not catch it
}
logger.info("closed connection {}", address)
synchronized(clusterConfigWaitingLock) {
clusterConfigWaitingLock.notifyAll()
}
onConnectionChangedListener(this)
try {
periodicExecutorService.awaitTermination(2, TimeUnit.SECONDS)
outExecutorService.awaitTermination(2, TimeUnit.SECONDS)
inExecutorService.awaitTermination(2, TimeUnit.SECONDS)
messageProcessingService.awaitTermination(2, TimeUnit.SECONDS)
} catch (ex: InterruptedException) {
logger.warn("", ex)
}
}
}
/**
* return time elapsed since last activity on socket, inputStream millis
*
* @return
*/
fun getLastActive(): Long {
return System.currentTimeMillis() - lastActive
}
private fun startMessageListenerService() {
inExecutorService.submitLogging {
try {
while (!Thread.interrupted()) {
val message = receiveMessage()
messageProcessingService.submitLogging {
logger.debug("received message type = {} {}", message.left, getIdForMessage(message.right))
when (message.left) {
BlockExchangeProtos.MessageType.INDEX -> {
val index = message.value as Index
indexHandler.handleIndexMessageReceivedEvent(index.folder, index.filesList, this)
}
BlockExchangeProtos.MessageType.INDEX_UPDATE -> {
val update = message.value as IndexUpdate
indexHandler.handleIndexMessageReceivedEvent(update.folder, update.filesList, this)
}
BlockExchangeProtos.MessageType.REQUEST -> {
onRequestMessageReceivedListeners.forEach { it(message.value as Request) }
}
BlockExchangeProtos.MessageType.RESPONSE -> {
responseHandler.handleResponse(message.value as Response)
}
BlockExchangeProtos.MessageType.PING -> logger.debug("ping message received")
BlockExchangeProtos.MessageType.CLOSE -> {
val close = message.value as BlockExchangeProtos.Close
logger.info("received close message, reason=${close.reason}")
closeBg()
}
BlockExchangeProtos.MessageType.CLUSTER_CONFIG -> {
NetworkUtils.assertProtocol(clusterConfigInfo == null, {"received cluster config message twice!"})
clusterConfigInfo = ClusterConfigInfo()
val clusterConfig = message.value as ClusterConfig
for (folder in clusterConfig.foldersList ?: emptyList()) {
val folderInfo = ClusterConfigFolderInfo(folder.id, folder.label)
val devicesById = (folder.devicesList ?: emptyList())
.associateBy { input ->
DeviceId.fromHashData(input.id!!.toByteArray())
}
val otherDevice = devicesById[address.deviceId()]
val ourDevice = devicesById[configuration.localDeviceId]
if (otherDevice != null) {
folderInfo.isAnnounced = true
}
if (ourDevice != null) {
folderInfo.isShared = true
logger.info("folder shared from device = {} folder = {}", address.deviceId, folderInfo)
val folderIds = configuration.folders.map { it.folderId }
if (!folderIds.contains(folderInfo.folderId)) {
val fi = FolderInfo(folderInfo.folderId, folderInfo.label)
configuration.folders = configuration.folders + fi
onNewFolderSharedListener(this, fi)
logger.info("new folder shared = {}", folderInfo)
}
} else {
logger.info("folder not shared from device = {} folder = {}", address.deviceId, folderInfo)
}
clusterConfigInfo!!.putFolderInfo(folderInfo)
}
configuration.persistLater()
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
synchronized(clusterConfigWaitingLock) {
clusterConfigWaitingLock.notifyAll()
}
}
}
}
}
} catch (ex: IOException) {
if (inExecutorService.isShutdown) {
return@submitLogging
}
logger.error("error receiving message", ex)
closeBg()
}
}
}
override fun toString(): String {
return "ConnectionHandler{" + "address=" + address + ", lastActive=" + getLastActive() / 1000.0 + "secs ago}"
}
internal inner class ClusterConfigInfo {
private val folderInfoById = ConcurrentHashMap<String, ClusterConfigFolderInfo>()
fun getSharedFolders(): Set<String> = folderInfoById.values.filter { it.isShared }.map { it.folderId }.toSet()
fun putFolderInfo(folderInfo: ClusterConfigFolderInfo) {
folderInfoById[folderInfo.folderId] = folderInfo
}
}
fun hasFolder(folder: String): Boolean {
return clusterConfigInfo!!.getSharedFolders().contains(folder)
}
companion object {
private const val MAGIC = 0x2EA7D90B
private val messageTypes = listOf(
MessageTypeInfo(MessageType.CLOSE, Close::class.java) { Close.parseFrom(it) },
MessageTypeInfo(MessageType.CLUSTER_CONFIG, ClusterConfig::class.java) { ClusterConfig.parseFrom(it) },
MessageTypeInfo(MessageType.DOWNLOAD_PROGRESS, DownloadProgress::class.java) { DownloadProgress.parseFrom(it) },
MessageTypeInfo(MessageType.INDEX, Index::class.java) { Index.parseFrom(it) },
MessageTypeInfo(MessageType.INDEX_UPDATE, IndexUpdate::class.java) { IndexUpdate.parseFrom(it) },
MessageTypeInfo(MessageType.PING, Ping::class.java) { Ping.parseFrom(it) },
MessageTypeInfo(MessageType.REQUEST, Request::class.java) { Request.parseFrom(it) },
MessageTypeInfo(MessageType.RESPONSE, Response::class.java) { Response.parseFrom(it) }
)
private val messageTypesByProtoMessageType = messageTypes.map { it.protoMessageType to it }.toMap()
private val messageTypesByJavaClass = messageTypes.map { it.javaClass to it }.toMap()
/**
* get id for message bean/instance, for log tracking
*
* @param message
* @return id for message bean
*/
private fun getIdForMessage(message: MessageLite): String {
return when (message) {
is Request -> Integer.toString(message.id)
is Response -> Integer.toString(message.id)
else -> Integer.toString(Math.abs(message.hashCode()))
}
}
}
data class MessageTypeInfo(
val protoMessageType: MessageType,
val javaClass: Class<out MessageLite>,
val parseFrom: (data: ByteArray) -> MessageLite
)
}
@@ -1,58 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.java.core.interfaces.IndexRepository
import java.io.Closeable
class FolderBrowser internal constructor(private val indexHandler: IndexHandler) : Closeable {
private val folderStatsCache = mutableMapOf<String, FolderStats>()
private val indexRepositoryEventListener = { event: IndexRepository.FolderStatsUpdatedEvent ->
addFolderStats(event.getFolderStats())
}
fun folderInfoAndStatsList(): List<Pair<FolderInfo, FolderStats>> =
indexHandler.folderInfoList()
.map { folderInfo -> Pair(folderInfo, getFolderStats(folderInfo.folderId)) }
.sortedBy { it.first.label }
init {
indexHandler.indexRepository.setOnFolderStatsUpdatedListener(indexRepositoryEventListener)
addFolderStats(indexHandler.indexRepository.findAllFolderStats())
}
private fun addFolderStats(folderStatsList: List<FolderStats>) {
for (folderStats in folderStatsList) {
folderStatsCache.put(folderStats.folderId, folderStats)
}
}
fun getFolderStats(folder: String): FolderStats {
return folderStatsCache[folder] ?: let {
FolderStats.Builder()
.setFolder(folder)
.build()
}
}
fun getFolderInfo(folder: String): FolderInfo? {
return indexHandler.getFolderInfo(folder)
}
override fun close() {
indexHandler.indexRepository.setOnFolderStatsUpdatedListener(null)
}
}
@@ -1,184 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.java.core.utils.awaitTerminationSafe
import net.syncthing.java.core.utils.submitLogging
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.util.*
import java.util.concurrent.Executors
class IndexBrowser internal constructor(private val indexRepository: IndexRepository, private val indexHandler: IndexHandler,
val folder: String, private val includeParentInList: Boolean = false,
private val allowParentInRoot: Boolean = false, ordering: Comparator<FileInfo>?) : Closeable {
private fun isParent(fileInfo: FileInfo) = PathUtils.isParent(fileInfo.path)
val ALPHA_ASC_DIR_FIRST: Comparator<FileInfo> =
compareBy<FileInfo>({!isParent(it)}, {!it.isDirectory()})
.thenBy { it.fileName.toLowerCase() }
val LAST_MOD_DESC: Comparator<FileInfo> =
compareBy<FileInfo>({!isParent(it)}, {it.lastModified})
.thenBy { it.fileName.toLowerCase() }
private val ordering = ordering ?: ALPHA_ASC_DIR_FIRST
private val logger = LoggerFactory.getLogger(javaClass)
var currentPath: String = PathUtils.ROOT_PATH
private set
private val PARENT_FILE_INFO: FileInfo
private val ROOT_FILE_INFO: FileInfo
private val executorService = Executors.newSingleThreadScheduledExecutor()
private val preloadJobs = mutableSetOf<String>()
private val preloadJobsLock = Any()
private var mOnPathChangedListener: (() -> Unit)? = null
private fun isCacheReady(): Boolean {
synchronized(preloadJobsLock) {
return preloadJobs.isEmpty()
}
}
internal fun onIndexChangedevent(folder: String, newRecord: FileInfo) {
if (folder == this.folder) {
preloadFileInfoForCurrentPath()
}
}
fun currentPathInfo(): FileInfo = getFileInfoByAbsolutePath(currentPath)
fun currentPathFileName(): String? = PathUtils.getFileName(currentPath)
fun isRoot(): Boolean = PathUtils.isRoot(currentPath)
init {
assert(folder.isNotEmpty())
PARENT_FILE_INFO = FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.PARENT_PATH)
ROOT_FILE_INFO = FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.ROOT_PATH)
navigateToAbsolutePath(PathUtils.ROOT_PATH)
}
fun setOnFolderChangedListener(onPathChangedListener: (() -> Unit)?) {
mOnPathChangedListener = onPathChangedListener
}
private fun preloadFileInfoForCurrentPath() {
logger.debug("trigger preload for folder = '{}'", folder)
synchronized(preloadJobsLock) {
currentPath.let<String, Any> { currentPath ->
if (preloadJobs.contains(currentPath)) {
preloadJobs.remove(currentPath)
preloadJobs.add(currentPath) ///add last
} else {
preloadJobs.add(currentPath)
executorService.submitLogging(object : Runnable {
override fun run() {
val preloadPath =
synchronized(preloadJobsLock) {
assert(!preloadJobs.isEmpty())
preloadJobs.last() //pop last job
}
logger.info("folder preload BEGIN for folder = '{}' path = '{}'", folder, preloadPath)
getFileInfoByAbsolutePath(preloadPath)
if (!PathUtils.isRoot(preloadPath)) {
val parent = PathUtils.getParentPath(preloadPath)
getFileInfoByAbsolutePath(parent)
listFiles(parent)
}
for (record in listFiles(preloadPath)) {
if (record.path == PARENT_FILE_INFO.path && record.isDirectory()) {
listFiles(record.path)
}
}
logger.info("folder preload END for folder = '{}' path = '{}'", folder, preloadPath)
synchronized(preloadJobsLock) {
preloadJobs.remove(preloadPath)
if (isCacheReady()) {
logger.info("cache ready, notify listeners")
mOnPathChangedListener?.invoke()
} else {
logger.info("still {} job[s] left in cache loader", preloadJobs.size)
executorService.submitLogging(this)
}
}
}
})
}
}
}
}
fun listFiles(path: String = currentPath): List<FileInfo> {
logger.debug("doListFiles for path = '{}' BEGIN", path)
val list = ArrayList(indexRepository.findNotDeletedFilesByFolderAndParent(folder, path))
logger.debug("doListFiles for path = '{}' : {} records loaded)", path, list.size)
if (includeParentInList && (!PathUtils.isRoot(path) || allowParentInRoot)) {
list.add(0, PARENT_FILE_INFO)
}
return list.sortedWith(ordering)
}
fun getFileInfoByAbsolutePath(path: String): FileInfo {
return if (PathUtils.isRoot(path)) {
ROOT_FILE_INFO
} else {
logger.debug("doGetFileInfoByAbsolutePath for path = '{}' BEGIN", path)
val fileInfo = indexRepository.findNotDeletedFileInfo(folder, path) ?: error("file not found for path = $path")
logger.debug("doGetFileInfoByAbsolutePath for path = '{}' END", path)
fileInfo
}
}
fun navigateTo(fileInfo: FileInfo) {
assert(fileInfo.isDirectory())
assert(fileInfo.folder == folder)
return if (fileInfo.path == PARENT_FILE_INFO.path)
navigateToAbsolutePath(PathUtils.getParentPath(currentPath))
else
navigateToAbsolutePath(fileInfo.path)
}
fun navigateToNearestPath(oldPath: String) {
if (!StringUtils.isBlank(oldPath)) {
navigateToAbsolutePath(oldPath)
}
}
private fun navigateToAbsolutePath(newPath: String) {
if (PathUtils.isRoot(newPath)) {
currentPath = PathUtils.ROOT_PATH
} else {
val fileInfo = getFileInfoByAbsolutePath(newPath)
assert(fileInfo.isDirectory(), {"cannot navigate to path ${fileInfo.path}: not a directory"})
currentPath = fileInfo.path
}
logger.info("navigate to path = '{}'", currentPath)
preloadFileInfoForCurrentPath()
}
override fun close() {
logger.info("closing")
indexHandler.unregisterIndexBrowser(this)
executorService.shutdown()
executorService.awaitTerminationSafe()
}
}
@@ -1,453 +0,0 @@
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.core.beans.*
import net.syncthing.java.core.beans.FileInfo.Version
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.interfaces.IndexRepository
import net.syncthing.java.core.interfaces.Sequencer
import net.syncthing.java.core.interfaces.TempRepository
import net.syncthing.java.core.utils.BlockUtils
import net.syncthing.java.core.utils.NetworkUtils
import net.syncthing.java.core.utils.awaitTerminationSafe
import net.syncthing.java.core.utils.submitLogging
import org.apache.commons.lang3.tuple.Pair
import org.apache.http.util.TextUtils
import org.bouncycastle.util.encoders.Hex
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
class IndexHandler(private val configuration: Configuration, val indexRepository: IndexRepository,
private val tempRepository: TempRepository) : Closeable {
private val logger = LoggerFactory.getLogger(javaClass)
private val folderInfoByFolder = mutableMapOf<String, FolderInfo>()
private val indexMessageProcessor = IndexMessageProcessor()
private var lastIndexActivity: Long = 0
private val writeAccessLock = Object()
private val indexWaitLock = Object()
private val indexBrowsers = mutableSetOf<IndexBrowser>()
private val onIndexRecordAcquiredListeners = mutableSetOf<(FolderInfo, List<FileInfo>, IndexInfo) -> Unit>()
private val onFullIndexAcquiredListeners = mutableSetOf<(FolderInfo) -> Unit>()
private fun lastActive(): Long = System.currentTimeMillis() - lastIndexActivity
fun sequencer(): Sequencer = indexRepository.getSequencer()
fun folderList(): List<String> = folderInfoByFolder.keys.toList()
fun folderInfoList(): List<FolderInfo> = folderInfoByFolder.values.toList()
private fun markActive() {
lastIndexActivity = System.currentTimeMillis()
}
fun registerOnIndexRecordAcquiredListener(listener: (FolderInfo, List<FileInfo>, IndexInfo) -> Unit) {
onIndexRecordAcquiredListeners.add(listener)
}
fun unregisterOnIndexRecordAcquiredListener(listener: (FolderInfo, List<FileInfo>, IndexInfo) -> Unit) {
assert(onIndexRecordAcquiredListeners.contains(listener))
onIndexRecordAcquiredListeners.remove(listener)
}
fun registerOnFullIndexAcquiredListenersListener(listener: (FolderInfo) -> Unit) {
onFullIndexAcquiredListeners.add(listener)
}
fun unregisterOnFullIndexAcquiredListenersListener(listener: (FolderInfo) -> Unit) {
assert(onFullIndexAcquiredListeners.contains(listener))
onFullIndexAcquiredListeners.remove(listener)
}
init {
loadFolderInfoFromConfig()
}
private fun loadFolderInfoFromConfig() {
synchronized(writeAccessLock) {
for (folderInfo in configuration.folders) {
folderInfoByFolder.put(folderInfo.folderId, folderInfo) //TODO reference 'folder info' repository
}
}
}
@Synchronized
fun clearIndex() {
synchronized(writeAccessLock) {
indexRepository.clearIndex()
folderInfoByFolder.clear()
loadFolderInfoFromConfig()
}
}
internal fun isRemoteIndexAcquired(clusterConfigInfo: ConnectionHandler.ClusterConfigInfo, peerDeviceId: DeviceId): Boolean {
var ready = true
for (folder in clusterConfigInfo.getSharedFolders()) {
val indexSequenceInfo = indexRepository.findIndexInfoByDeviceAndFolder(peerDeviceId, folder)
if (indexSequenceInfo == null || indexSequenceInfo.localSequence < indexSequenceInfo.maxSequence) {
logger.debug("waiting for index on folder = {} sequenceInfo = {}", folder, indexSequenceInfo)
ready = false
}
}
return ready
}
@Throws(InterruptedException::class)
fun waitForRemoteIndexAcquired(connectionHandler: ConnectionHandler, timeoutSecs: Long? = null): IndexHandler {
val timeoutMillis = (timeoutSecs ?: DEFAULT_INDEX_TIMEOUT) * 1000
synchronized(indexWaitLock) {
while (!isRemoteIndexAcquired(connectionHandler.clusterConfigInfo!!, connectionHandler.deviceId())) {
indexWaitLock.wait(timeoutMillis)
NetworkUtils.assertProtocol(connectionHandler.getLastActive() < timeoutMillis || lastActive() < timeoutMillis,
{"unable to acquire index from connection $connectionHandler, timeout reached!"})
}
}
logger.debug("acquired all indexes on connection {}", connectionHandler)
return this
}
fun handleClusterConfigMessageProcessedEvent(clusterConfig: BlockExchangeProtos.ClusterConfig) {
synchronized(writeAccessLock) {
for (folderRecord in clusterConfig.foldersList) {
val folder = folderRecord.id
val folderInfo = updateFolderInfo(folder, folderRecord.label)
logger.debug("acquired folder info from cluster config = {}", folderInfo)
for (deviceRecord in folderRecord.devicesList) {
val deviceId = DeviceId.fromHashData(deviceRecord.id.toByteArray())
if (deviceRecord.hasIndexId() && deviceRecord.hasMaxSequence()) {
val folderIndexInfo = updateIndexInfo(folder, deviceId, deviceRecord.indexId, deviceRecord.maxSequence, null)
logger.debug("acquired folder index info from cluster config = {}", folderIndexInfo)
}
}
}
}
}
fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, connectionHandler: ConnectionHandler) {
indexMessageProcessor.handleIndexMessageReceivedEvent(folderId, filesList, connectionHandler)
}
fun pushRecord(folder: String, bepFileInfo: BlockExchangeProtos.FileInfo): FileInfo? {
var fileBlocks: FileBlocks? = null
val builder = FileInfo.Builder()
.setFolder(folder)
.setPath(bepFileInfo.name)
.setLastModified(Date(bepFileInfo.modifiedS * 1000 + bepFileInfo.modifiedNs / 1000000))
.setVersionList((if (bepFileInfo.hasVersion()) bepFileInfo.version.countersList else null ?: emptyList()).map { record -> Version(record.id, record.value) })
.setDeleted(bepFileInfo.deleted)
when (bepFileInfo.type) {
BlockExchangeProtos.FileInfoType.FILE -> {
fileBlocks = FileBlocks(folder, builder.getPath()!!, ((bepFileInfo.blocksList ?: emptyList())).map { record ->
BlockInfo(record.offset, record.size, Hex.toHexString(record.hash.toByteArray()))
})
builder
.setTypeFile()
.setHash(fileBlocks.hash)
.setSize(bepFileInfo.size)
}
BlockExchangeProtos.FileInfoType.DIRECTORY -> builder.setTypeDir()
else -> {
logger.warn("unsupported file type = {}, discarding file info", bepFileInfo.type)
return null
}
}
return addRecord(builder.build(), fileBlocks)
}
private fun updateIndexInfo(folder: String, deviceId: DeviceId, indexId: Long?, maxSequence: Long?, localSequence: Long?): IndexInfo {
synchronized(writeAccessLock) {
var indexSequenceInfo = indexRepository.findIndexInfoByDeviceAndFolder(deviceId, folder)
var shouldUpdate = false
val builder: IndexInfo.Builder
if (indexSequenceInfo == null) {
shouldUpdate = true
assert(indexId != null, {"index sequence info not found, and supplied null index id (folder = $folder, device = $deviceId)"})
builder = IndexInfo.newBuilder()
.setFolder(folder)
.setDeviceId(deviceId.deviceId)
.setIndexId(indexId!!)
.setLocalSequence(0)
.setMaxSequence(-1)
} else {
builder = indexSequenceInfo.copyBuilder()
}
if (indexId != null && indexId != builder.getIndexId()) {
shouldUpdate = true
builder.setIndexId(indexId)
}
if (maxSequence != null && maxSequence > builder.getMaxSequence()) {
shouldUpdate = true
builder.setMaxSequence(maxSequence)
}
if (localSequence != null && localSequence > builder.getLocalSequence()) {
shouldUpdate = true
builder.setLocalSequence(localSequence)
}
if (shouldUpdate) {
indexSequenceInfo = builder.build()
indexRepository.updateIndexInfo(indexSequenceInfo)
}
return indexSequenceInfo!!
}
}
private fun addRecord(record: FileInfo, fileBlocks: FileBlocks?): FileInfo? {
synchronized(writeAccessLock) {
val lastModified = indexRepository.findFileInfoLastModified(record.folder, record.path)
return if (lastModified != null && !record.lastModified.after(lastModified)) {
logger.trace("discarding record = {}, modified before local record", record)
null
} else {
indexRepository.updateFileInfo(record, fileBlocks)
logger.trace("loaded new record = {}", record)
indexBrowsers.forEach {
it.onIndexChangedevent(record.folder, record)
}
record
}
}
}
fun getFileInfoByPath(folder: String, path: String): FileInfo? {
return indexRepository.findFileInfo(folder, path)
}
fun getFileInfoAndBlocksByPath(folder: String, path: String): Pair<FileInfo, FileBlocks>? {
val fileInfo = getFileInfoByPath(folder, path)
return if (fileInfo == null) {
null
} else {
assert(fileInfo.isFile())
val fileBlocks = indexRepository.findFileBlocks(folder, path)
checkNotNull(fileBlocks, {"file blocks not found for file info = $fileInfo"})
FileInfo.checkBlocks(fileInfo, fileBlocks!!)
Pair.of(fileInfo, fileBlocks)
}
}
private fun updateFolderInfo(folder: String, label: String?): FolderInfo {
var folderInfo: FolderInfo? = folderInfoByFolder[folder]
if (folderInfo == null || !TextUtils.isEmpty(label)) {
folderInfo = FolderInfo(folder, label)
folderInfoByFolder.put(folderInfo.folderId, folderInfo)
}
return folderInfo
}
fun getFolderInfo(folder: String): FolderInfo? {
return folderInfoByFolder[folder]
}
fun getIndexInfo(device: DeviceId, folder: String): IndexInfo? {
return indexRepository.findIndexInfoByDeviceAndFolder(device, folder)
}
fun newFolderBrowser(): FolderBrowser {
return FolderBrowser(this)
}
fun newIndexBrowser(folder: String, includeParentInList: Boolean = false, allowParentInRoot: Boolean = false,
ordering: Comparator<FileInfo>? = null): IndexBrowser {
val indexBrowser = IndexBrowser(indexRepository, this, folder, includeParentInList, allowParentInRoot, ordering)
indexBrowsers.add(indexBrowser)
return indexBrowser
}
internal fun unregisterIndexBrowser(indexBrowser: IndexBrowser) {
assert(indexBrowsers.contains(indexBrowser))
indexBrowsers.remove(indexBrowser)
}
override fun close() {
assert(indexBrowsers.isEmpty())
assert(onIndexRecordAcquiredListeners.isEmpty())
assert(onFullIndexAcquiredListeners.isEmpty())
indexMessageProcessor.stop()
}
private inner class IndexMessageProcessor {
private val executorService = Executors.newSingleThreadExecutor()
private var queuedMessages = 0
private var queuedRecords: Long = 0
// private long lastRecordProcessingTime = 0;
// , delay = 0;
// private boolean addProcessingDelayForInterface = true;
// private final int MIN_DELAY = 0, MAX_DELAY = 5000, MAX_RECORD_PER_PROCESS = 16, DELAY_FACTOR = 1;
private var startTime: Long? = null
fun handleIndexMessageReceivedEvent(folderId: String, filesList: List<BlockExchangeProtos.FileInfo>, connectionHandler: ConnectionHandler) {
logger.info("received index message event, preparing (queued records = {} event record count = {})", queuedRecords, filesList.size)
markActive()
val clusterConfigInfo = connectionHandler.clusterConfigInfo
val peerDeviceId = connectionHandler.deviceId()
// List<BlockExchangeProtos.FileInfo> fileList = event.getFilesList();
// for (int index = 0; index < fileList.size(); index += MAX_RECORD_PER_PROCESS) {
// BlockExchangeProtos.IndexUpdate data = BlockExchangeProtos.IndexUpdate.newBuilder()
// .addAllFiles(Iterables.limit(Iterables.skip(fileList, index), MAX_RECORD_PER_PROCESS))
// .setFolder(event.getFolder())
// .build();
// if (queuedMessages > 0) {
// storeAndProcessBg(data, clusterConfigInfo, peerDeviceId);
// } else {
// processBg(data, clusterConfigInfo, peerDeviceId);
// }
// }
val data = BlockExchangeProtos.IndexUpdate.newBuilder()
.addAllFiles(filesList)
.setFolder(folderId)
.build()
if (queuedMessages > 0) {
storeAndProcessBg(data, clusterConfigInfo, peerDeviceId)
} else {
processBg(data, clusterConfigInfo, peerDeviceId)
}
}
private fun processBg(data: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
logger.debug("received index message event, queuing for processing")
queuedMessages++
queuedRecords += data.filesCount.toLong()
executorService.submitLogging(object : ProcessingRunnable() {
override fun runProcess() {
doHandleIndexMessageReceivedEvent(data, clusterConfigInfo, peerDeviceId)
}
})
}
private fun storeAndProcessBg(data: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
val key = tempRepository.pushTempData(data.toByteArray())
logger.debug("received index message event, stored to temp record {}, queuing for processing", key)
queuedMessages++
queuedRecords += data.filesCount.toLong()
executorService.submitLogging(object : ProcessingRunnable() {
override fun runProcess() {
try {
doHandleIndexMessageReceivedEvent(key, clusterConfigInfo, peerDeviceId)
} catch (ex: IOException) {
logger.error("error processing index message", ex)
}
}
})
}
private abstract inner class ProcessingRunnable : Runnable {
override fun run() {
startTime = System.currentTimeMillis()
runProcess()
queuedMessages--
// lastRecordProcessingTime = stopwatch.elapsed(TimeUnit.MILLISECONDS) - delay;
// logger.info("processed a bunch of records, {}*{} remaining", queuedMessages, MAX_RECORD_PER_PROCESS);
// logger.debug("processed index message in {} secs", lastRecordProcessingTime / 1000d);
startTime = null
}
protected abstract fun runProcess()
// private boolean isVersionOlderThanSequence(BlockExchangeProtos.FileInfo fileInfo, long localSequence) {
// long fileSequence = fileInfo.getSequence();
// //TODO should we check last version instead of sequence? verify
// return fileSequence < localSequence;
// }
@Throws(IOException::class)
protected fun doHandleIndexMessageReceivedEvent(key: String, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
logger.debug("processing index message event from temp record {}", key)
markActive()
val data = tempRepository.popTempData(key)
val message = BlockExchangeProtos.IndexUpdate.parseFrom(data)
doHandleIndexMessageReceivedEvent(message, clusterConfigInfo, peerDeviceId)
}
protected fun doHandleIndexMessageReceivedEvent(message: BlockExchangeProtos.IndexUpdate, clusterConfigInfo: ConnectionHandler.ClusterConfigInfo?, peerDeviceId: DeviceId) {
// synchronized (writeAccessLock) {
// if (addProcessingDelayForInterface) {
// delay = Math.min(MAX_DELAY, Math.max(MIN_DELAY, lastRecordProcessingTime * DELAY_FACTOR));
// logger.info("add delay of {} secs before processing index message (to allow UI to process)", delay / 1000d);
// try {
// Thread.sleep(delay);
// } catch (InterruptedException ex) {
// logger.warn("interrupted", ex);
// }
// } else {
// delay = 0;
// }
logger.info("processing index message with {} records (queue size: messages = {} records = {})", message.filesCount, queuedMessages, queuedRecords)
// String deviceId = connectionHandler.getDeviceId();
val folderId = message.folder
var sequence: Long = -1
val newRecords = mutableListOf<FileInfo>()
// IndexInfo oldIndexInfo = indexRepository.findIndexInfoByDeviceAndFolder(deviceId, folder);
// Stopwatch stopwatch = Stopwatch.createStarted();
logger.debug("processing {} index records for folder {}", message.filesList.size, folderId)
for (fileInfo in message.filesList) {
markActive()
// if (oldIndexInfo != null && isVersionOlderThanSequence(fileInfo, oldIndexInfo.getLocalSequence())) {
// logger.trace("skipping file {}, version older than sequence {}", fileInfo, oldIndexInfo.getLocalSequence());
// } else {
val newRecord = pushRecord(folderId, fileInfo)
if (newRecord != null) {
newRecords.add(newRecord)
}
sequence = Math.max(fileInfo.sequence, sequence)
markActive()
// }
}
val newIndexInfo = updateIndexInfo(folderId, peerDeviceId, null, null, sequence)
val elap = System.currentTimeMillis() - startTime!!
queuedRecords -= message.filesCount.toLong()
logger.info("processed {} index records, acquired {} ({} secs, {} record/sec)", message.filesCount, newRecords.size, elap / 1000.0, Math.round(message.filesCount / (elap / 1000.0) * 100) / 100.0)
if (logger.isInfoEnabled && newRecords.size <= 10) {
for (fileInfo in newRecords) {
logger.info("acquired record = {}", fileInfo)
}
}
val folderInfo = folderInfoByFolder[folderId]
if (!newRecords.isEmpty()) {
onIndexRecordAcquiredListeners.forEach { it(folderInfo!!, newRecords, newIndexInfo) }
}
logger.debug("index info = {}", newIndexInfo)
if (isRemoteIndexAcquired(clusterConfigInfo!!, peerDeviceId)) {
logger.debug("index acquired")
onFullIndexAcquiredListeners.forEach { it(folderInfo!!)}
}
// IndexHandler.this.notifyAll();
markActive()
synchronized(indexWaitLock) {
indexWaitLock.notifyAll()
}
}
}
fun stop() {
logger.info("stopping index record processor")
executorService.shutdown()
executorService.awaitTerminationSafe()
}
}
companion object {
private const val DEFAULT_INDEX_TIMEOUT: Long = 30
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import net.syncthing.java.bep.connectionactor.ConnectionActorWrapper
import java.io.IOException
import java.util.*
class MultiConnectionHelper (
initialConnections: List<ConnectionActorWrapper>,
private val connectionFilter: (ConnectionActorWrapper) -> Boolean
) {
companion object {
private val random = Random()
}
private val usableConnections = initialConnections.toMutableList()
fun pickConnection(): ConnectionActorWrapper {
val possibleConnections = synchronized(usableConnections) {
usableConnections.filter { it.isConnected and connectionFilter(it) }
}
if (possibleConnections.isEmpty()) {
throw IOException("no matching connection is available")
} else if (possibleConnections.size == 1) {
return possibleConnections.first()
} else {
return possibleConnections[random.nextInt(possibleConnections.size)]
}
}
fun disableConnection(wrapper: ConnectionActorWrapper) {
synchronized(usableConnections) {
usableConnections.remove(wrapper)
}
}
}
@@ -0,0 +1,54 @@
package net.syncthing.java.bep
import kotlinx.coroutines.Deferred
import net.syncthing.java.core.beans.DeviceId
import java.io.IOException
class RequestHandlerRegistry {
private val listeners = mutableMapOf<RequestHandlerFilter, (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>>()
suspend fun handleRequest(source: DeviceId, request: BlockExchangeProtos.Request): BlockExchangeProtos.Response {
val rule = RequestHandlerFilter(
deviceId = source,
folderId = request.folder,
path = request.name
)
val matchingListener = synchronized(listeners) {
listeners[rule]
}
if (matchingListener != null) {
return matchingListener(request).await()
} else {
return BlockExchangeProtos.Response.newBuilder()
.setId(request.id)
.setCode(BlockExchangeProtos.ErrorCode.GENERIC)
.build()
}
}
fun registerListener(filter: RequestHandlerFilter, listener: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>) {
synchronized(listeners) {
val oldListener = listeners[filter]
if (oldListener != null) {
throw IOException("there is already an listener for this filter")
}
listeners[filter] = listener
}
}
fun unregisterListener(filter: RequestHandlerFilter) {
synchronized(listeners) {
listeners.remove(filter)
}
}
}
data class RequestHandlerFilter(
val deviceId: DeviceId,
val folderId: String,
val path: String
)
@@ -1,50 +0,0 @@
/*
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
import org.slf4j.LoggerFactory
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.HashMap
class ResponseHandler {
companion object {
private val logger = LoggerFactory.getLogger(ResponseHandler::class.java)
}
private val responseListeners = Collections.synchronizedMap(HashMap<Int, (BlockExchangeProtos.Response) -> Unit>())
private val nextRequestId = AtomicInteger(0)
fun registerListener(listener: (BlockExchangeProtos.Response) -> Unit): Int {
val requestId = nextRequestId.getAndIncrement()
responseListeners[requestId] = listener
return requestId
}
fun unregisterListener(requestId: Int) {
responseListeners.remove(requestId)
}
fun handleResponse(response: BlockExchangeProtos.Response) {
val listener = responseListeners.remove(response.id)
if (listener != null) {
listener(response)
} else {
logger.warn("received response for {} without associated handler", response.id)
}
}
}
@@ -0,0 +1,180 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* 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.bep.connectionactor
import com.google.protobuf.ByteString
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.Configuration
import org.slf4j.LoggerFactory
object ClusterConfigHandler {
private val logger = LoggerFactory.getLogger(ClusterConfigHandler::class.java)
fun buildClusterConfig(
configuration: Configuration,
indexHandler: IndexHandler,
deviceId: DeviceId
): BlockExchangeProtos.ClusterConfig {
val builder = BlockExchangeProtos.ClusterConfig.newBuilder()
indexHandler.indexRepository.runInTransaction { indexTransaction ->
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 other device
val indexSequenceInfo = indexTransaction.findIndexInfoByDeviceAndFolder(deviceId, folder.folderId)
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)
}
}
)
builder.addFolders(folderBuilder)
// TODO: add the other devices to the cluster config
}
}
return builder.build()
}
// TODO: understand this
internal suspend fun handleReceivedClusterConfig(
clusterConfig: BlockExchangeProtos.ClusterConfig,
configuration: Configuration,
otherDeviceId: DeviceId,
indexHandler: IndexHandler
): ClusterConfigInfo {
val folderInfoList = mutableListOf<ClusterConfigFolderInfo>()
val newSharedFolders = mutableListOf<FolderInfo>()
configuration.update { oldConfig ->
val configFolders = oldConfig.folders.toMutableSet()
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)
}
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)
}
oldConfig.copy(folders = configFolders)
}
configuration.persistLater()
indexHandler.handleClusterConfigMessageProcessedEvent(clusterConfig)
return ClusterConfigInfo(folderInfoList, newSharedFolders)
}
}
class ClusterConfigInfo (val folderInfo: List<ClusterConfigFolderInfo>, val newSharedFolders: List<FolderInfo>) {
companion object {
val dummy = ClusterConfigInfo(folderInfo = emptyList(), newSharedFolders = emptyList())
}
val folderInfoById = folderInfo.associateBy { it.folderId }
val sharedFolderIds: Set<String> by lazy {
folderInfo.filter { it.isShared && it.isDeviceInSharedFolderWhitelist }.map { it.folderId }.toSet()
}
}
data class ClusterConfigFolderInfo(
val folderId: String,
val label: String = folderId,
val isAnnounced: Boolean = false,
val isShared: Boolean = false,
val isDeviceInSharedFolderWhitelist: Boolean
) {
init {
assert(folderId.isNotEmpty())
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.connectionactor
import kotlinx.coroutines.CompletableDeferred
import net.syncthing.java.bep.BlockExchangeProtos
sealed class ConnectionAction
object CloseConnectionAction: ConnectionAction()
class SendRequestConnectionAction(
val request: BlockExchangeProtos.Request,
val completableDeferred: CompletableDeferred<BlockExchangeProtos.Response>
): ConnectionAction()
class ConfirmIsConnectedAction(val completableDeferred: CompletableDeferred<ClusterConfigInfo>): ConnectionAction()
class SendIndexUpdateAction(
val message: BlockExchangeProtos.IndexUpdate,
val completableDeferred: CompletableDeferred<Unit?>
): ConnectionAction()
@@ -0,0 +1,256 @@
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* 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.bep.connectionactor
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import net.syncthing.java.bep.BlockExchangeProtos
import net.syncthing.java.bep.index.IndexHandler
import net.syncthing.java.core.beans.DeviceAddress
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)
private fun deviceAddressesGenerator(deviceAddress: ReceiveChannel<DeviceAddress>) = GlobalScope.produce<List<DeviceAddress>> (capacity = Channel.CONFLATED) {
val addresses = mutableMapOf<String, DeviceAddress>()
deviceAddress.consumeEach { address ->
val isNew = addresses[address.address] == null
addresses[address.address] = address
if (isNew) {
send(
addresses.values.sortedBy { it.score }
)
}
}
}
private fun <T> waitForFirstValue(source: ReceiveChannel<T>, time: Long) = GlobalScope.produce<T> {
source.consume {
val firstValue = source.receive()
var lastValue = firstValue
try {
withTimeout(time) {
while (true) {
lastValue = source.receive()
}
}
throw IllegalStateException()
} catch (ex: TimeoutCancellationException) {
// this is expected here
}
send(lastValue)
// other values without delay
for (value in source) {
send(value)
}
}
}
fun generateConnectionActors(
deviceAddress: ReceiveChannel<DeviceAddress>,
configuration: Configuration,
indexHandler: IndexHandler,
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
) = generateConnectionActorsFromDeviceAddressList(
deviceAddressSource = waitForFirstValue(
source = deviceAddressesGenerator(deviceAddress),
time = 1000
),
configuration = configuration,
indexHandler = indexHandler,
requestHandler = requestHandler
)
fun generateConnectionActorsFromDeviceAddressList(
deviceAddressSource: ReceiveChannel<List<DeviceAddress>>,
configuration: Configuration,
indexHandler: IndexHandler,
requestHandler: (BlockExchangeProtos.Request) -> Deferred<BlockExchangeProtos.Response>
) = 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
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)
newActor to clusterConfig
} catch (ex: Exception) {
logger.warn("failed to connect to $deviceAddress", ex)
when (ex) {
is IOException -> {/* expected -> ignore */}
is InterruptedException -> {/* expected -> ignore */}
else -> throw ex
}
null
}
suspend fun tryConnectingToAddress(deviceAddress: DeviceAddress): Boolean {
closeCurrent()
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 run {handleCancel(); false}
}
logger.debug("connected to $deviceAddress")
currentStatus = currentStatus.copy(
status = ConnectionStatus.Connected,
currentAddress = deviceAddress
)
dispatchConnection(connection.first, connection.second, deviceAddress)
return true
}
fun isConnected() = !currentActor.isClosedForSend
invokeOnClose {
currentActor.close()
}
val reconnectTicker = ticker(delayMillis = 30 * 1000, initialDelayMillis = 0)
deviceAddressSource.consume {
while (true) {
run {
// get the new list version if there is any
val newDeviceAddressList = deviceAddressSource.poll()
if (newDeviceAddressList != null) {
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
dispatchStatus()
}
}
if (isConnected()) {
val deviceAddressList = currentStatus.addresses
if (deviceAddressList.isNotEmpty()) {
if (reconnectTicker.poll() != null) {
if (currentDeviceAddress != deviceAddressList.first()) {
val oldDeviceAddress = currentDeviceAddress!!
if (!tryConnectingToAddress(deviceAddressList.first())) {
tryConnectingToAddress(oldDeviceAddress)
}
}
}
} else {
closeCurrent()
}
delay(500) // don't take too much CPU
} else /* is not connected */ {
if (currentStatus.status == ConnectionStatus.Connected) {
currentStatus = currentStatus.copy(status = ConnectionStatus.Disconnected)
dispatchStatus()
}
val deviceAddressList = currentStatus.addresses
// try all addresses
for (address in deviceAddressList) {
if (tryConnectingToAddress(address)) {
break
}
}
// reset countdown before trying other connection if it would be time now
// this does not reset if it has not counted down the whole time yet
reconnectTicker.poll()
// wait for new device address list but not more than 15 seconds before the next iteration
val newDeviceAddressList = withTimeoutOrNull(15 * 1000) {
deviceAddressSource.receive()
}
if (newDeviceAddressList != null) {
currentStatus = currentStatus.copy(addresses = newDeviceAddressList)
dispatchStatus()
}
}
}
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.connectionactor
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.SendChannel
import net.syncthing.java.bep.BlockExchangeProtos
import java.io.IOException
object ConnectionActorUtil {
suspend fun waitUntilConnected(actor: SendChannel<ConnectionAction>): ClusterConfigInfo {
val deferred = CompletableDeferred<ClusterConfigInfo>()
actor.send(ConfirmIsConnectedAction(deferred))
actor.invokeOnClose { deferred.cancel() }
return deferred.await()
}
suspend fun sendRequest(request: BlockExchangeProtos.Request, actor: SendChannel<ConnectionAction>): BlockExchangeProtos.Response {
try {
val deferred = CompletableDeferred<BlockExchangeProtos.Response>()
actor.send(SendRequestConnectionAction(request, deferred))
return deferred.await()
} catch (ex: Exception) {
throw IOException("not connected", ex)
}
}
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate, actor: SendChannel<ConnectionAction>) {
try {
val deferred = CompletableDeferred<Unit?>()
actor.send(SendIndexUpdateAction(update, deferred))
deferred.await()
} catch (ex: Exception) {
throw IOException("not connected", ex)
}
}
suspend fun disconnect(actor: SendChannel<ConnectionAction>) {
try {
actor.send(CloseConnectionAction)
} catch (ex: Exception) {
// ignore if the channel is closed already
}
}
}
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep.connectionactor
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.channels.ReceiveChannel
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<Connection, ConnectionInfo>>,
val deviceId: DeviceId,
private val exceptionReportHandler: (ExceptionReport) -> Unit
) {
private val job = Job()
private var connection: Connection? = null
private val connectionInfo = ConflatedBroadcastChannel<ConnectionInfo>(ConnectionInfo.empty)
val isConnected
get() = connectionInfo.valueOrNull?.status == ConnectionStatus.Connected
init {
GlobalScope.async (job) {
source.consumeEach { (connection, connectionInfo) ->
this@ConnectionActorWrapper.connection = connection
this@ConnectionActorWrapper.connectionInfo.send(connectionInfo)
}
}.reportExceptions("ConnectionActorWrapper(${deviceId.deviceId})", exceptionReportHandler)
}
suspend fun sendRequest(request: BlockExchangeProtos.Request) = ConnectionActorUtil.sendRequest(
request,
connection?.actor ?: throw IOException("not connected")
)
suspend fun sendIndexUpdate(update: BlockExchangeProtos.IndexUpdate) = ConnectionActorUtil.sendIndexUpdate(
update,
connection?.actor ?: throw IOException("not connected")
)
fun hasFolder(folderId: String) = connection?.clusterConfigInfo?.sharedFolderIds?.contains(folderId) ?: false
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 = connection?.actor
GlobalScope.launch {
if (actor != null) {
ConnectionActorUtil.disconnect(actor)
}
}
}
fun subscribeToConnectionInfo() = connectionInfo.openSubscription()
}
@@ -1,5 +1,6 @@
/*
/*
* Copyright (C) 2016 Davide Imbriaco
* Copyright (C) 2018 Jonas Lochmann
*
* This Java file is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -11,13 +12,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.syncthing.java.bep
internal data class ClusterConfigFolderInfo(val folderId: String, var label: String = folderId,
var isAnnounced: Boolean = false, var isShared: Boolean = false) {
init {
assert(folderId.isNotEmpty())
}
package net.syncthing.java.bep.connectionactor
object ConnectionConstants {
const val MAGIC = 0x2EA7D90B
}

Some files were not shown because too many files have changed in this diff Show More