14 Commits

Author SHA1 Message Date
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
65 changed files with 681 additions and 223 deletions
+5 -2
View File
@@ -8,11 +8,13 @@ Syncthing devices in the same way a client-server file sharing app accesses its
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,
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 reconnect for some minutes if the App was killed (due to removing from the recent App list) or the connection was interrupted.
@@ -24,6 +26,7 @@ This does not apply to local discovery connections.
## 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
+10 -3
View File
@@ -19,8 +19,8 @@ android {
applicationId "net.syncthing.lite"
minSdkVersion 21
targetSdkVersion 26
versionCode 12
versionName "0.3.2"
versionCode 13
versionName "0.3.3"
multiDexEnabled true
playAccountConfig = playAccountConfigs.defaultAccountConfig
}
@@ -60,11 +60,18 @@ 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"
+2 -6
View File
@@ -22,14 +22,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"
@@ -5,8 +5,10 @@ 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 kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
@@ -16,10 +18,11 @@ 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.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
import org.jetbrains.anko.doAsync
class FolderBrowserActivity : SyncthingActivity() {
@@ -44,6 +47,16 @@ class FolderBrowserActivity : SyncthingActivity() {
override fun onItemClicked(fileInfo: FileInfo) {
navigateToFolder(fileInfo)
}
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 {
@@ -70,13 +83,15 @@ class FolderBrowserActivity : SyncthingActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
libraryHandler?.syncthingClient { syncthingClient ->
async (UI) {
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()
}
}
} else {
super.onActivityResult(requestCode, resultCode, intent)
}
}
@@ -91,7 +106,7 @@ class FolderBrowserActivity : SyncthingActivity() {
finish()
} else {
if (fileInfo.isDirectory()) {
async {
doAsync {
indexBrowser.navigateTo(fileInfo)
}
@@ -108,32 +123,33 @@ class FolderBrowserActivity : SyncthingActivity() {
}
private fun onFolderChanged() {
runOnUiThread {
binding.isLoading = false
GlobalScope.launch {
val list = indexBrowser.listFiles()
async {
val list = indexBrowser.listFiles()
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
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
val title = if (indexBrowser.isRoot()) {
val result = CompletableDeferred<String?>()
async(UI) {
supportActionBar?.title = title
}
}
else
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
libraryHandler.folderBrowser {
result.complete(it.getFolderInfo(indexBrowser.folder)?.label)
}
result.await()
} else {
indexBrowser.currentPathInfo().fileName
}
runOnUiThread {
binding.isLoading = false
adapter.data = list
binding.listView.scrollToPosition(0)
supportActionBar?.title = title
}
}
}
}
private fun updateFolderListView() {
showFolderListView(indexBrowser.currentPath)
@@ -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
@@ -15,8 +13,9 @@ import android.view.ViewGroup
import android.widget.Button
import com.github.paolorotolo.appintro.AppIntro
import com.google.zxing.integration.android.IntentIntegrator
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.lite.R
import net.syncthing.lite.databinding.FragmentIntroOneBinding
@@ -183,7 +182,7 @@ class IntroActivity : AppIntro() {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
libraryHandler.library { config, client, _ ->
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
client.addOnConnectionChangedListener(this@IntroFragmentThree::onConnectionChanged)
val deviceId = config.localDeviceId.deviceId
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
@@ -196,7 +195,7 @@ class IntroActivity : AppIntro() {
private fun onConnectionChanged(deviceId: DeviceId) {
libraryHandler.library { config, client, _ ->
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
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,7 +102,7 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
libraryHandler.syncthingClient { it.clearCacheAndIndex() }
recreate()
}
@@ -1,7 +1,6 @@
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
@@ -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)
@@ -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,79 @@
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 android.webkit.MimeTypeMap
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.databinding.DialogFileBinding
import net.syncthing.lite.dialogs.downloadfile.DownloadFileDialogFragment
import net.syncthing.lite.dialogs.downloadfile.DownloadFileSpec
import org.apache.commons.io.FilenameUtils
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 = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
FilenameUtils.getExtension(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
@@ -7,15 +7,16 @@ 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 org.jetbrains.anko.newTask
@@ -24,6 +25,7 @@ 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 +34,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 +54,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 +88,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 = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(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
@@ -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.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.lite.R
import net.syncthing.lite.adapters.DeviceAdapterListener
@@ -76,7 +77,7 @@ class DevicesFragment : SyncthingFragment() {
private fun updateDeviceList() {
libraryHandler.syncthingClient { syncthingClient ->
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
adapter.data = syncthingClient.getPeerStatus()
binding.isEmpty = adapter.data.isEmpty()
}
@@ -6,8 +6,9 @@ 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.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.activities.FolderBrowserActivity
@@ -39,7 +40,7 @@ class FoldersFragment : SyncthingFragment() {
libraryHandler.folderBrowser { folderBrowser ->
val list = folderBrowser.folderInfoAndStatsList()
async (UI) {
GlobalScope.launch (Dispatchers.Main) {
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter().apply { data = list }
binding.list.adapter = adapter
@@ -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,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")
@@ -6,8 +6,9 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.DeviceId
@@ -35,7 +36,7 @@ class LibraryHandler(context: Context,
private 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) }
val isListeningPortTaken: LiveData<Boolean> = isListeningPortTakenInternal
@@ -88,7 +89,7 @@ class LibraryHandler(context: Context,
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
}
}
@@ -96,7 +97,7 @@ class LibraryHandler(context: Context,
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
onIndexUpdateCompleteListener(folderInfo)
}
}
@@ -8,8 +8,8 @@ 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 kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
@@ -4,8 +4,9 @@ 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
@@ -47,12 +48,12 @@ object Util {
if (!configuration.peerIds.contains(deviceId2)) {
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
configuration.persistLater()
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
onComplete()
}
} else {
async(UI) {
GlobalScope.launch (Dispatchers.Main) {
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
}
}
+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
+6
View File
@@ -0,0 +1,6 @@
- add option to export files
- send correct file names to apps by which files are opened
- adaptive icon
- updated translations
- validate discovery servers
- bugfixes
@@ -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,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

+1 -1
View File
@@ -19,7 +19,7 @@
<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$simportado con éxito</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>
+7 -1
View File
@@ -40,5 +40,11 @@
<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="settings_shutdown_delay_summary">A Syncthing leállítása ennyi idő elteltével a kliens utolsó csatlakozása után</string>
<string name="device_id_dialog_title">Eszközazonosító megadása</string>
</resources>
<string name="settings_shutdown_delay_10_seconds">10 másodperc</string>
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
<string name="settings_shutdown_delay_1_minute">1 perc</string>
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
</resources>
@@ -0,0 +1,2 @@
<resources>
</resources>
+28 -14
View File
@@ -3,33 +3,47 @@
<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="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_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="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="remove_device_title">移除设备: %1$s</string>
<string name="device_import_success">成功导入的设备: %1$s</string>
<string name="device_already_known">已经存在的设备: %1$s</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_two_title">添加一个设备</string>
<string name="intro_page_three_title">分享您的文件夹</string>
<string name="intro_page_two_description">输入一个 Syncthing 设备 ID,或者通过 QR 码扫描一个设备 ID</string>
<string name="settings">设定</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_local_device_summary">此设备将被其他设备看到的名</string>
<string name="settings_shutdown_delay_title">关闭延迟</string>
<string name="settings_shutdown_delay_summary">关闭 Syncthing 客户端与其最后使用之间的时间</string>
<string name="device_id_dialog_title">输入设备 ID</string>
</resources>
<string name="settings_shutdown_delay_10_seconds">10 秒</string>
<string name="settings_shutdown_delay_30_seconds">30 秒</string>
<string name="settings_shutdown_delay_1_minute">1 分钟</string>
<string name="settings_shutdown_delay_5_minutes">5 分钟</string>
</resources>
+1
View File
@@ -53,4 +53,5 @@
or the connection was interrupted.
This does not apply to local discovery connections.
</string>
<string name="dialog_file_save_as">Save as</string>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="files" path="/" />
</paths>
+1 -1
View File
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.61'
ext.kotlin_version = '1.3.0'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.2.0'
ext.anko_version = '0.10.7'
+1 -1
View File
@@ -9,7 +9,7 @@ dependencies {
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,8 +15,8 @@
package net.syncthing.java.bep
import com.google.protobuf.ByteString
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import net.syncthing.java.bep.BlockExchangeProtos.ErrorCode
import net.syncthing.java.bep.BlockExchangeProtos.Request
import net.syncthing.java.bep.utils.longSumBy
@@ -25,14 +25,16 @@ 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.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.SequenceInputStream
import java.security.MessageDigest
import java.util.*
import kotlin.collections.HashMap
import kotlin.coroutines.resume
class BlockPuller internal constructor(private val connectionHandler: ConnectionHandler,
private val indexHandler: IndexHandler,
@@ -17,7 +17,6 @@ import com.google.protobuf.ByteString
import net.syncthing.java.bep.BlockExchangeProtos.Vector
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.utils.BlockUtils
import net.syncthing.java.core.utils.NetworkUtils
import net.syncthing.java.core.utils.submitLogging
@@ -122,7 +122,7 @@ class ConnectionHandler(private val configuration: Configuration, val address: D
receiveHelloMessage()
try {
keystoreHandler.checkSocketCertificate(socket, address.deviceId())
KeystoreHandler.assertSocketCertificateValid(socket, address.deviceId())
} catch (e: CertificateException) {
throw IOException(e)
}
@@ -19,7 +19,6 @@ 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
@@ -209,7 +208,7 @@ class IndexHandler(private val configuration: Configuration, val indexRepository
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)) {
return if (lastModified != null && record.lastModified < lastModified) {
logger.trace("discarding record = {}, modified before local record", record)
null
} else {
@@ -13,12 +13,12 @@
*/
package net.syncthing.java.client.cli
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.repository.repo.SqlRepository
import net.syncthing.java.client.SyncthingClient
import org.apache.commons.cli.*
import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
@@ -35,6 +35,7 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
class SyncthingClient(
private val configuration: Configuration,
@@ -4,7 +4,6 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import net.syncthing.java.core.utils.NetworkUtils
import org.apache.commons.codec.binary.Base32
import org.slf4j.LoggerFactory
import java.io.IOException
data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) {
@@ -14,8 +14,7 @@
package net.syncthing.java.core.beans
import org.apache.commons.io.FileUtils
import java.util.Date
import java.util.*
class FolderStats private constructor(val fileCount: Long, val dirCount: Long, val size: Long, val lastUpdate: Date, folder: String, label: String?) : FolderInfo(folder, label) {
@@ -12,7 +12,8 @@ data class Config(
val folders: Set<FolderInfo>,
val localDeviceName: String,
val localDeviceId: String,
val discoveryServers: Set<String>,
val customDiscoveryServers: Set<DiscoveryServer>,
val useDefaultDiscoveryServers: Boolean,
val keystoreAlgorithm: String,
val keystoreData: String
) {
@@ -21,7 +22,8 @@ data class Config(
private const val FOLDERS = "folders"
private const val LOCAL_DEVICE_NAME = "localDeviceName"
private const val LOCAL_DEVICE_ID = "localDeviceId"
private const val DISCOVERY_SERVERS = "discoveryServers"
private const val USE_DEFAULT_DISCOVERY_SERVERS = "useDefaultDiscoveryServers"
private const val CUSTOM_DISCOVERY_SERVERS = "customDiscoveryServers"
private const val KEYSTORE_ALGORITHM = "keystoreAlgorithm"
private const val KEYSTORE_DATA = "keystoreData"
@@ -30,7 +32,8 @@ data class Config(
var folders: Set<FolderInfo>? = null
var localDeviceName: String? = null
var localDeviceId: String? = null
var discoveryServers: Set<String>? = null
var customDiscoveryServers = emptySet<DiscoveryServer>() // this field was added later, so it needs an default value
var useDefaultDiscoveryServers = true // this field was added later, so it needs an default value
var keystoreAlgorithm: String? = null
var keystoreData: String? = null
@@ -61,17 +64,16 @@ data class Config(
}
LOCAL_DEVICE_NAME -> localDeviceName = reader.nextString()
LOCAL_DEVICE_ID -> localDeviceId = reader.nextString()
DISCOVERY_SERVERS -> {
val newDiscoveryServers = HashSet<String>()
reader.beginArray()
while (reader.hasNext()) {
newDiscoveryServers.add(reader.nextString())
CUSTOM_DISCOVERY_SERVERS -> {
customDiscoveryServers = mutableSetOf<DiscoveryServer>().apply {
reader.beginArray()
while (reader.hasNext()) {
add(DiscoveryServer.parse(reader))
}
reader.endArray()
}
reader.endArray()
discoveryServers = Collections.unmodifiableSet(newDiscoveryServers)
}
USE_DEFAULT_DISCOVERY_SERVERS -> useDefaultDiscoveryServers = reader.nextBoolean()
KEYSTORE_ALGORITHM -> keystoreAlgorithm = reader.nextString()
KEYSTORE_DATA -> keystoreData = reader.nextString()
else -> reader.skipValue()
@@ -84,7 +86,8 @@ data class Config(
folders = folders!!,
localDeviceName = localDeviceName!!,
localDeviceId = localDeviceId!!,
discoveryServers = discoveryServers!!,
customDiscoveryServers = customDiscoveryServers,
useDefaultDiscoveryServers = useDefaultDiscoveryServers,
keystoreAlgorithm = keystoreAlgorithm!!,
keystoreData = keystoreData!!
)
@@ -105,10 +108,12 @@ data class Config(
writer.name(LOCAL_DEVICE_NAME).value(localDeviceName)
writer.name(LOCAL_DEVICE_ID).value(localDeviceId)
writer.name(DISCOVERY_SERVERS).beginArray()
discoveryServers.forEach { writer.value(it) }
writer.name(CUSTOM_DISCOVERY_SERVERS).beginArray()
customDiscoveryServers.forEach { it.serialize(writer) }
writer.endArray()
writer.name(USE_DEFAULT_DISCOVERY_SERVERS).value(useDefaultDiscoveryServers)
writer.name(KEYSTORE_ALGORITHM).value(keystoreAlgorithm)
writer.name(KEYSTORE_DATA).value(keystoreData)
@@ -117,5 +122,6 @@ data class Config(
// Exclude keystoreData from toString()
override fun toString() = "Config(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " +
"localDeviceId=$localDeviceId, discoveryServers=$discoveryServers, keystoreAlgorithm=$keystoreAlgorithm)"
"localDeviceId=$localDeviceId, customDiscoveryServers=$customDiscoveryServers, " +
"useDefaultDiscoveryServers=$useDefaultDiscoveryServers, keystoreAlgorithm=$keystoreAlgorithm)"
}
@@ -38,26 +38,15 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
isSaved = false
config = Config(peers = setOf(), folders = setOf(),
localDeviceName = localDeviceName,
discoveryServers = Companion.DiscoveryServers,
localDeviceId = keystoreData.first.deviceId,
keystoreData = Base64.toBase64String(keystoreData.second),
keystoreAlgorithm = keystoreData.third)
keystoreAlgorithm = keystoreData.third,
customDiscoveryServers = emptySet(),
useDefaultDiscoveryServers = true
)
persistNow()
} else {
config = Config.parse(JsonReader(StringReader(configFile.readText())))
// automatic migration if the old config was used
if (config.discoveryServers == OldDiscoveryServers) {
config = Config(
peers = config.peers,
folders = config.folders,
localDeviceName = config.localDeviceName,
localDeviceId = config.localDeviceId,
discoveryServers = Companion.DiscoveryServers,
keystoreAlgorithm = config.keystoreAlgorithm,
keystoreData = config.keystoreData
)
}
}
logger.debug("Loaded config = $config")
}
@@ -66,11 +55,6 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
private val DefaultConfigFolder = File(System.getProperty("user.home"), ".config/syncthing-java/")
private const val ConfigFileName = "config.json"
private const val DatabaseFolderName = "database"
private val DiscoveryServers = setOf(
"discovery.syncthing.net", "discovery-v4.syncthing.net", "discovery-v6.syncthing.net")
private val OldDiscoveryServers = setOf(
"discovery-v4-1.syncthing.net", "discovery-v4-2.syncthing.net", "discovery-v4-3.syncthing.net",
"discovery-v6-1.syncthing.net", "discovery-v6-2.syncthing.net", "discovery-v6-3.syncthing.net")
}
val instanceId = Math.abs(Random().nextLong())
@@ -78,8 +62,8 @@ class Configuration(configFolder: File = DefaultConfigFolder) {
val localDeviceId: DeviceId
get() = DeviceId(config.localDeviceId)
val discoveryServers: Set<String>
get() = config.discoveryServers
val discoveryServers: Set<DiscoveryServer>
get() = config.customDiscoveryServers + (if (config.useDefaultDiscoveryServers) DiscoveryServer.defaultDiscoveryServers else emptySet())
val keystoreData: ByteArray
get() = Base64.decode(config.keystoreData)
@@ -0,0 +1,86 @@
package net.syncthing.java.core.configuration
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import net.syncthing.java.core.beans.DeviceId
data class DiscoveryServer(
val hostname: String,
val useForLookup: Boolean,
val useForAnnounce: Boolean,
val deviceId: DeviceId?
) {
companion object {
private const val JSON_HOSTNAME = "host"
private const val JSON_LOOKUP = "lookup"
private const val JSON_ANNOUNCE = "announce"
private const val JSON_DEVICE_ID = "deviceId"
// from https://github.com/syncthing/syncthing/blob/add12b43aa0bdf5e67d8f421c57a5ecafb3d25fa/lib/config/config.go#L50-L64
// (if you update it, use the most recent commit)
private val serverDeviceId = DeviceId("LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW")
private val lookupServer = DiscoveryServer(
hostname = "discovery.syncthing.net",
useForLookup = true,
useForAnnounce = false,
deviceId = serverDeviceId
)
private val announceIpV4Server = DiscoveryServer(
hostname = "discovery-v4.syncthing.net",
useForLookup = false,
useForAnnounce = true,
deviceId = serverDeviceId
)
private val announceIpV6Server = DiscoveryServer(
hostname = "discovery-v6.syncthing.net",
useForLookup = false,
useForAnnounce = true,
deviceId = serverDeviceId
)
val defaultDiscoveryServers = setOf(lookupServer, announceIpV4Server, announceIpV6Server)
fun parse(reader: JsonReader): DiscoveryServer {
var hostname: String? = null
var useForLookup: Boolean? = null
var useForAnnounce: Boolean? = null
var deviceId: DeviceId? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
JSON_HOSTNAME -> hostname = reader.nextString()
JSON_LOOKUP -> useForLookup = reader.nextBoolean()
JSON_ANNOUNCE -> useForAnnounce = reader.nextBoolean()
JSON_DEVICE_ID -> deviceId = DeviceId(reader.nextString())
else -> reader.skipValue()
}
}
reader.endObject()
return DiscoveryServer(
hostname = hostname!!,
useForLookup = useForLookup!!,
useForAnnounce = useForAnnounce!!,
deviceId = deviceId
)
}
}
fun serialize(writer: JsonWriter) {
writer.beginObject()
writer.name(JSON_HOSTNAME).value(hostname)
writer.name(JSON_LOOKUP).value(useForLookup)
writer.name(JSON_ANNOUNCE).value(useForAnnounce)
if (deviceId != null) {
writer.name(JSON_DEVICE_ID).value(deviceId.deviceId)
}
writer.endObject()
}
}
@@ -44,8 +44,6 @@ import javax.security.auth.x500.X500Principal
class KeystoreHandler private constructor(private val keyStore: KeyStore) {
private val logger = LoggerFactory.getLogger(javaClass)
class CryptoException internal constructor(t: Throwable) : GeneralSecurityException(t)
private val socketFactory: SSLSocketFactory
@@ -116,21 +114,6 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
}
}
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
fun checkSocketCertificate(socket: SSLSocket, deviceId: DeviceId) {
val session = socket.session
val certs = session.peerCertificates.toList()
val certificateFactory = CertificateFactory.getInstance("X.509")
val certPath = certificateFactory.generateCertPath(certs)
val certificate = certPath.certificates[0]
NetworkUtils.assertProtocol(certificate is X509Certificate)
val derData = certificate.encoded
val deviceIdFromCertificate = derDataToDeviceId(derData)
logger.trace("remote pem certificate =\n{}", derToPem(derData))
NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId, {"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"})
logger.debug("remote ssl certificate match deviceId = {}", deviceId)
}
@Throws(CryptoException::class, IOException::class)
fun wrapSocket(relayConnection: RelayConnection, protocol: String): SSLSocket {
return wrapSocket(relayConnection.getSocket(), relayConnection.isServerSocket(), protocol)
@@ -269,6 +252,29 @@ class KeystoreHandler private constructor(private val keyStore: KeyStore) {
const val BEP = "bep/1.0"
const val RELAY = "bep-relay"
private val logger = LoggerFactory.getLogger(KeystoreHandler::class.java)
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
fun assertSocketCertificateValid(socket: SSLSocket, deviceId: DeviceId) {
val session = socket.session
val certs = session.peerCertificates.toList()
val certificateFactory = CertificateFactory.getInstance("X.509")
val certPath = certificateFactory.generateCertPath(certs)
val certificate = certPath.certificates[0]
assertSocketCertificateValid(certificate, deviceId)
}
@Throws(SSLPeerUnverifiedException::class, CertificateException::class)
fun assertSocketCertificateValid(certificate: Certificate, deviceId: DeviceId) {
NetworkUtils.assertProtocol(certificate is X509Certificate)
val derData = certificate.encoded
val deviceIdFromCertificate = derDataToDeviceId(derData)
logger.trace("remote pem certificate =\n{}", derToPem(derData))
NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId, {"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"})
logger.debug("remote ssl certificate match deviceId = {}", deviceId)
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ dependencies {
compile "commons-cli:commons-cli:1.4"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.google.protobuf:protobuf-lite:$protobuf_lite_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
}
run {
@@ -14,10 +14,10 @@
*/
package net.syncthing.java.discovery
import kotlinx.coroutines.experimental.CancellationException
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.experimental.selects.select
import kotlinx.coroutines.experimental.withTimeout
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withTimeout
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import org.slf4j.LoggerFactory
@@ -13,8 +13,8 @@
*/
package net.syncthing.java.discovery
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.ReceiveChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
@@ -14,8 +14,8 @@
*/
package net.syncthing.java.discovery
import kotlinx.coroutines.experimental.GlobalScope
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.configuration.Configuration
@@ -14,14 +14,14 @@
*/
package net.syncthing.java.discovery.protocol
import kotlinx.coroutines.experimental.GlobalScope
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.coroutineScope
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.discovery.utils.AddressRanker
import net.syncthing.java.core.configuration.DiscoveryServer
import org.slf4j.LoggerFactory
import java.io.IOException
@@ -38,7 +38,7 @@ internal class GlobalDiscoveryHandler(private val configuration: Configuration)
}
suspend fun query(deviceIds: Collection<DeviceId>): List<DeviceAddress> {
val discoveryServers = pickAnnounceServers()
val discoveryServers = getLookupServers()
return coroutineScope {
deviceIds
@@ -57,15 +57,13 @@ internal class GlobalDiscoveryHandler(private val configuration: Configuration)
}
suspend fun query(deviceId: DeviceId) = queryAnnounceServers(
servers = pickAnnounceServers(),
servers = getLookupServers(),
deviceId = deviceId
)
suspend fun pickAnnounceServers() = AddressRanker
.pingAddresses(configuration.discoveryServers.map { DeviceAddress(it, "tcp://$it:443") })
.map { it.deviceId }
fun getLookupServers() = configuration.discoveryServers.filter { it.useForLookup }
suspend fun queryAnnounceServers(servers: List<String>, deviceId: DeviceId) = coroutineScope {
suspend fun queryAnnounceServers(servers: List<DiscoveryServer>, deviceId: DeviceId) = coroutineScope {
servers
.map { server ->
async {
@@ -91,9 +89,13 @@ internal class GlobalDiscoveryHandler(private val configuration: Configuration)
}
companion object {
suspend fun queryAnnounceServer(server: String, deviceId: DeviceId) =
suspend fun queryAnnounceServer(server: DiscoveryServer, deviceId: DeviceId) =
GlobalDiscoveryUtil
.queryAnnounceServer(server, deviceId)
.queryAnnounceServer(
server = server.hostname,
requestedDeviceId = deviceId,
serverDeviceId = server.deviceId
)
.addresses.map { DeviceAddress(deviceId.deviceId, it) }
}
}
@@ -15,15 +15,17 @@
package net.syncthing.java.discovery.protocol
import com.google.gson.stream.JsonReader
import kotlinx.coroutines.experimental.Dispatchers
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.security.KeystoreHandler
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
@@ -34,11 +36,33 @@ object GlobalDiscoveryUtil {
private fun queryAnnounceServerUrl(server: String, deviceId: DeviceId) =
"https://$server/v2/?device=${deviceId.deviceId}"
suspend fun queryAnnounceServer(server: String, deviceId: DeviceId): AnnouncementMessage {
suspend fun queryAnnounceServer(
server: String,
requestedDeviceId: DeviceId,
serverDeviceId: DeviceId?
): AnnouncementMessage {
return withContext(Dispatchers.IO) {
val url = URL(queryAnnounceServerUrl(server, deviceId))
val url = URL(queryAnnounceServerUrl(server, requestedDeviceId))
val connection = (url.openConnection() as HttpsURLConnection).apply {
hostnameVerifier = HostnameVerifier { _, _ -> true }
hostnameVerifier = HostnameVerifier { _, session ->
try {
if (serverDeviceId != null) {
if (session.peerCertificates.isEmpty()) {
throw IOException("no certificate found")
}
KeystoreHandler.assertSocketCertificateValid(session.peerCertificates.first(), serverDeviceId)
}
true
} catch (ex: Exception) {
when (ex) {
is IOException -> false
is CertificateException -> false
else -> throw ex
}
}
}
sslSocketFactory = SSLContext.getInstance("SSL").apply {
init(null, arrayOf(object: X509TrustManager {
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {
@@ -14,10 +14,10 @@
*/
package net.syncthing.java.discovery.protocol
import kotlinx.coroutines.experimental.GlobalScope
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.consumeEach
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.configuration.Configuration
import org.slf4j.LoggerFactory
@@ -15,11 +15,11 @@
package net.syncthing.java.discovery.protocol
import com.google.protobuf.ByteString
import kotlinx.coroutines.experimental.Dispatchers
import kotlinx.coroutines.experimental.GlobalScope
import kotlinx.coroutines.experimental.channels.ReceiveChannel
import kotlinx.coroutines.experimental.channels.produce
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.withContext
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.utils.NetworkUtils
@@ -14,7 +14,7 @@
*/
package net.syncthing.java.discovery.utils
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.*
import net.syncthing.java.core.beans.DeviceAddress
import net.syncthing.java.core.beans.DeviceAddress.AddressType
import org.slf4j.LoggerFactory