1 Commits

Author SHA1 Message Date
Felix Ableitner def8630f8f Version 0.1.2 2018-01-04 16:17:05 +09:00
226 changed files with 1256 additions and 13322 deletions
-32
View File
@@ -1,32 +0,0 @@
sudo: required
language: android
jdk: oraclejdk8
dist: trusty
# Install Android SDK
android:
components:
- tools
- platform-tools
- build-tools-28.0.2
- android-28
- extra-android-m2repository
before_install:
# Hack to accept Android licenses
- yes | sdkmanager "platforms;android-27"
- yes | sdkmanager "platforms;android-28"
# Cache gradle dependencies
# https://docs.travis-ci.com/user/languages/android/#Caching
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
script:
- ./gradlew lint
- ./gradlew assembleDebug
-9
View File
@@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[syncthing-lite.stringsxml]
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en
type = ANDROID
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw, id:in
+14 -22
View File
@@ -1,42 +1,34 @@
# Syncthing Lite
[![Build Status](https://travis-ci.org/syncthing/syncthing-lite.svg?branch=master)](https://travis-ci.org/syncthing/syncthing-lite)
[![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 accesses its proprietary server).
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).
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
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
example, mobile devices with limited storage available, wishing to access a syncthing share).
This project is based on syncthing-java (which is in this repository too), a java implementation of Syncthing protocols.
This project is based on [syncthing-java][3], 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.
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)
## 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 to
install [Android Studio][3] and import the project.
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.
## License
All code is licensed under the [MPLv2 License][4].
All code is licensed under the [MPLv2 License][5].
[1]: https://syncthing.net/
[2]: https://github.com/syncthing/syncthing-android
[3]: https://developer.android.com/studio/index.html
[4]: LICENSE
[3]: https://github.com/Nutomic/syncthing-java
[4]: https://developer.android.com/studio/index.html
[5]: LICENSE
-9
View File
@@ -1,9 +0,0 @@
# 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
@@ -1,20 +0,0 @@
# 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>)
+18 -64
View File
@@ -1,27 +1,18 @@
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'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 27
buildToolsVersion "27.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 19
versionName "0.3.9"
minSdkVersion 19
targetSdkVersion 25
versionCode 3
versionName "0.1.2"
multiDexEnabled true
playAccountConfig = playAccountConfigs.defaultAccountConfig
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
@@ -30,63 +21,26 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
release {
storeFile = {
def path = System.getenv("SYNCTHING_LITE_RELEASE_STORE_FILE")
return (path) ? file(path) : null
}()
storePassword System.getenv("SIGNING_PASSWORD") ?: ""
keyAlias System.getenv("SYNCTHING_LITE_RELEASE_KEY_ALIAS") ?: ""
keyPassword System.getenv("SIGNING_PASSWORD") ?: ""
}
}
buildTypes {
debug {
minifyEnabled false
}
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/*'
}
dataBinding {
enabled = true
}
}
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-android:1.0.0'
implementation "com.android.support:design:$support_version"
implementation "com.android.support:preference-v14:$support_version"
implementation "org.jetbrains.anko:anko-commons:0.10.4"
kapt "com.android.databinding:compiler:$build_tools_version"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:support-v4:$support_version"
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation(project(':syncthing-client')) {
exclude group: 'commons-logging', module: 'commons-logging'
implementation "com.android.support:design:$support_version"
implementation "com.android.support:cardview-v7:$support_version"
implementation ("com.github.Nutomic:syncthing-java:0.1.2") {
exclude group: 'commons-logging', module:'commons-logging'
exclude group: 'commons-codec'
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'
implementation 'com.google.zxing:core:3.3.0'
implementation 'com.github.apl-devs:appintro:v4.2.3'
implementation project(':syncthing-repository-android')
implementation project(':syncthing-temp-repository-encryption')
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'uk.co.markormesher:android-fab:2.0.0'
}
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="GoogleAppIndexingWarning" severity="ignore" />
<issue id="InvalidPackage" severity="ignore" />
<issue id="OldTargetApi" severity="ignore" />
</lint>
-92
View File
@@ -1,92 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/jonas/android-studio/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# ensure that stack traces make sense
-keepattributes SourceFile,LineNumberTable
# this library uses factories with reflection
-keep class net.jpountz.lz4.** { *; }
# from https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/kotlinx-coroutines-android/example-app/app/proguard-rules.pro
# kotlin coroutines crash without it
-keepclassmembernames class kotlinx.** {
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
-dontwarn net.jpountz.util.UnsafeUtils
-dontwarn org.bouncycastle.cert.dane.fetcher.JndiDANEFetcherFactory
-dontwarn org.bouncycastle.cert.dane.fetcher.JndiDANEFetcherFactory$1
-dontwarn org.bouncycastle.jce.provider.X509LDAPCertStoreSpi
-dontwarn org.bouncycastle.mail.smime.CMSProcessableBodyPart
-dontwarn org.bouncycastle.mail.smime.CMSProcessableBodyPartInbound
-dontwarn org.bouncycastle.mail.smime.CMSProcessableBodyPartOutbound
-dontwarn org.bouncycastle.mail.smime.examples.CreateCompressedMail
-dontwarn org.bouncycastle.mail.smime.examples.CreateEncryptedMail
-dontwarn org.bouncycastle.mail.smime.examples.CreateLargeCompressedMail
-dontwarn org.bouncycastle.mail.smime.examples.CreateLargeEncryptedMail
-dontwarn org.bouncycastle.mail.smime.examples.CreateLargeSignedMail
-dontwarn org.bouncycastle.mail.smime.examples.CreateSignedMail
-dontwarn org.bouncycastle.mail.smime.examples.CreateSignedMultipartMail
-dontwarn org.bouncycastle.mail.smime.examples.ExampleUtils
-dontwarn org.bouncycastle.mail.smime.examples.ReadCompressedMail
-dontwarn org.bouncycastle.mail.smime.examples.ReadEncryptedMail
-dontwarn org.bouncycastle.mail.smime.examples.ReadLargeCompressedMail
-dontwarn org.bouncycastle.mail.smime.examples.ReadLargeEncryptedMail
-dontwarn org.bouncycastle.mail.smime.examples.ReadLargeSignedMail
-dontwarn org.bouncycastle.mail.smime.examples.ReadSignedMail
-dontwarn org.bouncycastle.mail.smime.examples.SendSignedAndEncryptedMail
-dontwarn org.bouncycastle.mail.smime.examples.ValidateSignedMail
-dontwarn org.bouncycastle.mail.smime.handlers.multipart_signed
-dontwarn org.bouncycastle.mail.smime.handlers.multipart_signed$LineOutputStream
-dontwarn org.bouncycastle.mail.smime.handlers.PKCS7ContentHandler
-dontwarn org.bouncycastle.mail.smime.handlers.pkcs7_mime
-dontwarn org.bouncycastle.mail.smime.handlers.pkcs7_signature
-dontwarn org.bouncycastle.mail.smime.handlers.x_pkcs7_mime
-dontwarn org.bouncycastle.mail.smime.handlers.x_pkcs7_signature
-dontwarn org.bouncycastle.mail.smime.SMIMECompressed
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedGenerator
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedGenerator$1
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedGenerator$ContentCompressor
-dontwarn org.bouncycastle.mail.smime.SMIMECompressedParser
-dontwarn org.bouncycastle.mail.smime.SMIMEEnveloped
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator$1
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator$ContentEncryptor
-dontwarn org.bouncycastle.mail.smime.SMIMEEnvelopedParser
-dontwarn org.bouncycastle.mail.smime.SMIMEGenerator
-dontwarn org.bouncycastle.mail.smime.SMIMESigned
-dontwarn org.bouncycastle.mail.smime.SMIMESigned$1
-dontwarn org.bouncycastle.mail.smime.SMIMESignedGenerator
-dontwarn org.bouncycastle.mail.smime.SMIMESignedGenerator$1
-dontwarn org.bouncycastle.mail.smime.SMIMESignedGenerator$ContentSigner
-dontwarn org.bouncycastle.mail.smime.SMIMESignedParser
-dontwarn org.bouncycastle.mail.smime.SMIMESignedParser$1
-dontwarn org.bouncycastle.mail.smime.SMIMEToolkit
-dontwarn org.bouncycastle.mail.smime.SMIMEUtil
-dontwarn org.bouncycastle.mail.smime.SMIMEUtil$LineOutputStream
-dontwarn org.bouncycastle.mail.smime.SMIMEUtil$WriteOnceFileBackedMimeBodyPart
-dontwarn org.bouncycastle.mail.smime.util.FileBackedMimeBodyPart
-dontwarn org.bouncycastle.mail.smime.util.SharedFileInputStream
-dontwarn org.bouncycastle.mail.smime.validator.SignedMailValidator
-dontwarn org.bouncycastle.x509.util.LDAPStoreHelper
+4 -19
View File
@@ -2,9 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.syncthing.lite">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".android.Application"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -18,25 +18,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activities.IntroActivity"
android:theme="@style/Theme.Syncthing.NoActionBar"/>
<activity android:name=".activities.FolderBrowserActivity"
android:parentActivityName=".activities.MainActivity"/>
<provider
android:name=".library.CacheFileProvider"
android:authorities="net.syncthing.lite.fileprovider"
android:grantUriPermissions="true"
android:exported="false" />
<provider
android:name=".library.SyncthingProvider"
android:authorities="net.syncthing.lite.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<activity android:name=".activities.FilePickerActivity"
android:parentActivityName=".activities.FolderBrowserActivity" />
</application>
</manifest>
</manifest>
@@ -0,0 +1,101 @@
package net.syncthing.lite.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import net.syncthing.lite.R
import java.io.File
import java.util.*
/**
* Activity that allows selecting a directory in the local file system.
*/
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
private lateinit var mListView: ListView
private lateinit var mFilesAdapter: FileAdapter
private lateinit var mLocation: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_folder_picker)
mListView = findViewById(android.R.id.list)
mListView.onItemClickListener = this
mListView.emptyView = findViewById(android.R.id.empty)
mFilesAdapter = FileAdapter(this)
mListView.adapter = mFilesAdapter
displayFolder(Environment.getExternalStorageDirectory())
}
/**
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
*/
private fun displayFolder(location: File) {
mLocation = location
mFilesAdapter.clear()
// In case we don't have read access to the location, just display nothing.
val contents = location.listFiles() ?: arrayOf()
Arrays.sort(contents) { f1, f2 ->
if (f1.isDirectory && f2.isFile)
return@sort -1
if (f1.isFile && f2.isDirectory)
return@sort 1
f1.name.compareTo(f2.name)
}
for (f in contents) {
mFilesAdapter.add(f)
}
mListView.adapter = mFilesAdapter
}
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val f = mFilesAdapter.getItem(i)
if (f!!.isDirectory) {
displayFolder(f)
} else {
val intent = Intent()
intent.data = Uri.fromFile(f)
setResult(Activity.RESULT_OK, intent)
finish()
}
}
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val title = view.findViewById<TextView>(android.R.id.text1)
val f = getItem(position)!!
title.text = f.name
val icon =
if (f.isDirectory) R.drawable.ic_folder_black_24dp
else R.drawable.ic_image_black_24dp
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
return view
}
}
override fun onBackPressed() {
if (mLocation != Environment.getExternalStorageDirectory()) {
displayFolder(mLocation.parentFile)
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
@@ -1,187 +1,180 @@
package net.syncthing.lite.activities
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.databinding.DataBindingUtil
import android.os.AsyncTask
import android.os.Bundle
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 android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import android.widget.Toast
import com.google.common.base.Preconditions.checkArgument
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.utils.FileInfoOrdering
import net.syncthing.java.core.utils.PathUtils
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 net.syncthing.lite.library.DownloadFileTask
import net.syncthing.lite.library.UploadFileTask
import org.jetbrains.anko.intentFor
class FolderBrowserActivity : SyncthingActivity() {
companion object {
private const val REQUEST_SELECT_UPLOAD_FILE = 171
private const val STATUS_PATH = "path"
const val EXTRA_FOLDER_NAME = "folder_name"
private val TAG = "FolderBrowserActivity"
private val REQUEST_WRITE_STORAGE = 142
private val REQUEST_SELECT_UPLOAD_FILE = 171
val EXTRA_FOLDER_NAME = "folder_name"
}
private lateinit var folder: String
private val path = ConflatedBroadcastChannel<String>()
private val listing = ConflatedBroadcastChannel<DirectoryListing?>(null)
private lateinit var binding: ActivityFolderBrowserBinding
private lateinit var indexBrowser: IndexBrowser
private lateinit var adapter: FolderContentsAdapter
private var runWhenPermissionsReceived: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityFolderBrowserBinding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
val adapter = FolderContentsAdapter()
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
adapter = FolderContentsAdapter(this)
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) {
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
}
}
}
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)
}
}
binding.listView.setOnItemClickListener { _, _, position, _ ->
val fileInfo = binding.listView.getItemAtPosition(position) as FileInfo
navigateToFolder(fileInfo)
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
indexBrowser = syncthingClient().indexHandler
.newIndexBrowserBuilder()
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
.includeParentInList(true)
.allowParentInRoot(true)
.setFolder(folder)
.build()
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
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 onDestroy() {
super.onDestroy()
Thread {
indexBrowser.setOnFolderChangedListener(null)
indexBrowser.close()
}.start()
}
override fun onBackPressed() {
if (!goUp()) {
super.onBackPressed()
}
val listView = binding.listView
//click item '0', ie '..' (go to parent)
listView.performItemClick(adapter.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
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,
folder,
path.value,
{ /* nothing to do on success */ }
).show()
}
}
UploadFileTask(this, syncthingClient(), intent!!.data, indexBrowser.folder,
indexBrowser.currentPath, { this.updateFolderListView() }).uploadFile()
}
}
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 {
super.onActivityResult(requestCode, resultCode, intent)
if (fileInfo.isDirectory) {
indexBrowser.navigateTo(fileInfo)
Log.d(TAG, "load folder cache bg")
binding.listView.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
} else {
Log.i(TAG, "pulling file = " + fileInfo)
executeWithPermissions(
Runnable { DownloadFileTask(this, syncthingClient(), fileInfo).downloadFile() })
}
}
}
private fun onFolderChanged() {
runOnUiThread {
binding.progressBar.visibility = View.GONE
binding.listView.visibility = View.VISIBLE
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)
checkArgument(!list.isEmpty())//list must contain at least the 'parent' path
adapter.clear()
adapter.addAll(list)
adapter.notifyDataSetChanged()
binding.listView.setSelection(0)
val title =
if (indexBrowser.isRoot)
folderBrowser()?.getFolderInfo(indexBrowser.folder)?.label
else
indexBrowser.currentPathInfo.fileName
supportActionBar!!.setTitle(title)
}
}
private fun updateFolderListView() {
showFolderListView(indexBrowser.currentPath)
}
private fun showUploadHereDialog() {
executeWithPermissions(Runnable {
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
})
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder)
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
updateFolderListView()
}
override fun onIndexUpdateComplete() {
binding.indexUpdate.visibility = View.GONE
updateFolderListView()
}
private fun executeWithPermissions(runnable: Runnable) {
val permissionState = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permissionState != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_STORAGE)
runWhenPermissionsReceived = runnable
} else {
runnable.run()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
when (requestCode) {
REQUEST_WRITE_STORAGE -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.toast_write_storage_permission_required,
Toast.LENGTH_LONG).show()
} else {
runWhenPermissionsReceived!!.run()
}
runWhenPermissionsReceived = null
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
@@ -1,208 +0,0 @@
package net.syncthing.lite.activities
import android.arch.lifecycle.Observer
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.text.Html
import android.view.LayoutInflater
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.channels.consumeEach
import kotlinx.coroutines.launch
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.lite.R
import net.syncthing.lite.databinding.FragmentIntroOneBinding
import net.syncthing.lite.databinding.FragmentIntroThreeBinding
import net.syncthing.lite.databinding.FragmentIntroTwoBinding
import net.syncthing.lite.fragments.SyncthingFragment
import net.syncthing.lite.utils.FragmentIntentIntegrator
import net.syncthing.lite.utils.Util
import org.jetbrains.anko.defaultSharedPreferences
import org.jetbrains.anko.intentFor
import java.io.IOException
/**
* Shown when a user first starts the app. Shows some info and helps the user to add their first
* device and folder.
*/
class IntroActivity : AppIntro() {
/**
* Initialize fragments and library parameters.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addSlide(IntroFragmentOne())
addSlide(IntroFragmentTwo())
addSlide(IntroFragmentThree())
setSeparatorColor(ContextCompat.getColor(this, android.R.color.primary_text_dark))
showSkipButton(true)
isProgressButtonEnabled = true
pager.isPagingEnabled = false
}
override fun onSkipPressed(currentFragment: Fragment) {
onDonePressed(currentFragment)
}
override fun onDonePressed(currentFragment: Fragment) {
defaultSharedPreferences.edit().putBoolean(MainActivity.PREF_IS_FIRST_START, false).apply()
startActivity(intentFor<MainActivity>())
finish()
}
/**
* 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)
libraryHandler.isListeningPortTaken.observe(this, Observer { binding.listeningPortTaken = it })
return binding.root
}
}
/**
* Display device ID entry field and QR scanner option.
*/
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 {
FragmentIntentIntegrator(this@IntroFragmentTwo).initiateScan()
}
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
}
}
/**
* Checks if the entered device ID is valid. If yes, imports it and returns true. If not,
* sets an error on the textview and returns false.
*/
fun isDeviceIdValid(): Boolean {
return try {
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)
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() {
super.onResume()
binding.foundDevices.removeAllViews()
addedDeviceIds.clear()
libraryHandler.registerMessageFromUnknownDeviceListener(onDeviceFound)
}
override fun onPause() {
super.onPause()
libraryHandler.unregisterMessageFromUnknownDeviceListener(onDeviceFound)
}
private val onDeviceFound: (DeviceId) -> Unit = {
deviceId ->
if (addedDeviceIds.add(deviceId)) {
binding.foundDevices.addView(
Button(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
text = deviceId.deviceId
setOnClickListener {
binding.enterDeviceId.deviceId.setText(deviceId.deviceId)
binding.enterDeviceId.deviceIdHolder.isErrorEnabled = false
binding.scroll.scrollTo(0, 0)
}
}
)
}
}
}
/**
* Waits until remote device connects with new folder.
*/
class IntroFragmentThree : SyncthingFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = FragmentIntroThreeBinding.inflate(inflater, container, false)
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
}
}
}
@@ -4,39 +4,27 @@ import android.app.AlertDialog
import android.content.res.Configuration
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.ActionBarDrawerToggle
import android.view.Gravity
import android.view.MenuItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import android.view.View
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ActivityMainBinding
import net.syncthing.lite.dialogs.DeviceIdDialogFragment
import net.syncthing.lite.fragments.DevicesFragment
import net.syncthing.lite.fragments.FoldersFragment
import net.syncthing.lite.fragments.SettingsFragment
import org.jetbrains.anko.defaultSharedPreferences
import org.jetbrains.anko.intentFor
import net.syncthing.lite.fragments.SyncthingFragment
import net.syncthing.lite.library.UpdateIndexTask
class MainActivity : SyncthingActivity() {
companion object {
const val PREF_IS_FIRST_START = "net.syncthing.lite.activities.MainActivity.IS_FIRST_START"
}
private lateinit var binding: ActivityMainBinding
private var drawerToggle: ActionBarDrawerToggle? = null
private var currentFragment: SyncthingFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (defaultSharedPreferences.getBoolean(PREF_IS_FIRST_START, true)) {
startActivity(intentFor<IntroActivity>())
finish()
}
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
drawerToggle = ActionBarDrawerToggle(
@@ -62,6 +50,10 @@ class MainActivity : SyncthingActivity() {
onNavigationItemSelectedListener(selection)
}
override fun onLibraryLoaded() {
currentFragment?.onLibraryLoaded()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
drawerToggle!!.onConfigurationChanged(newConfig)
@@ -80,8 +72,7 @@ class MainActivity : SyncthingActivity() {
when (menuItem.itemId) {
R.id.folders -> setContentFragment(FoldersFragment())
R.id.devices -> setContentFragment(DevicesFragment())
R.id.settings -> setContentFragment(SettingsFragment())
R.id.device_id -> DeviceIdDialogFragment().show(supportFragmentManager)
R.id.update_index -> UpdateIndexTask(this, syncthingClient()).updateIndex()
R.id.clear_index -> AlertDialog.Builder(this)
.setTitle(getString(R.string.clear_cache_and_index_title))
.setMessage(getString(R.string.clear_cache_and_index_body))
@@ -94,7 +85,8 @@ class MainActivity : SyncthingActivity() {
return true
}
private fun setContentFragment(fragment: Fragment) {
private fun setContentFragment(fragment: SyncthingFragment) {
currentFragment = fragment
supportFragmentManager
.beginTransaction()
.replace(R.id.content_frame, fragment)
@@ -102,10 +94,17 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
launch {
libraryHandler.libraryManager.withLibrary {
it.syncthingClient.clearCacheAndIndex()
}
}
syncthingClient().clearCacheAndIndex()
recreate()
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder) + " "
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
}
override fun onIndexUpdateComplete() {
binding.indexUpdate.visibility = View.GONE
}
}
@@ -3,58 +3,85 @@ package net.syncthing.lite.activities
import android.app.AlertDialog
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.bep.FolderBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
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.InitLibraryTask
import net.syncthing.lite.library.LibraryHandler
import org.slf4j.impl.HandroidLoggerAdapter
abstract class SyncthingActivity : CoroutineActivity() {
val libraryHandler: LibraryHandler by lazy {
LibraryHandler(
context = this@SyncthingActivity
)
abstract class SyncthingActivity : AppCompatActivity() {
companion object {
private var activityCount = 0
private var libraryHandler: LibraryHandler? = null
}
private var loadingDialog: AlertDialog? = null
private var snackBar: Snackbar? = null
fun syncthingClient(): SyncthingClient {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler!!.syncthingClient!!
}
fun configuration(): ConfigurationService {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler!!.configuration!!
}
fun folderBrowser(): FolderBrowser? {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler?.folderBrowser
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
}
override fun onStart() {
super.onStart()
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
loadingDialog = AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
libraryHandler.start {
if (!isDestroyed) {
loadingDialog?.dismiss()
}
onLibraryLoaded()
activityCount++
if (libraryHandler == null) {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
loadingDialog = AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
InitLibraryTask(this, this::onLibraryLoaded, this::onIndexUpdateProgress, this::onIndexUpdateComplete)
}
}
override fun onStop() {
super.onStop()
libraryHandler.stop()
loadingDialog?.dismiss()
override fun onDestroy() {
super.onDestroy()
activityCount--
Thread {
if (activityCount == 0) {
libraryHandler?.destroy()
libraryHandler = null
}
}.start()
}
open fun onLibraryLoaded() {
// nothing to do
private fun onLibraryLoaded(libraryHandler: LibraryHandler) {
if (activityCount == 0)
return
SyncthingActivity.libraryHandler = libraryHandler
loadingDialog!!.cancel()
onLibraryLoaded()
}
open fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete() {}
open fun onLibraryLoaded() {}
}
@@ -1,60 +1,35 @@
package net.syncthing.lite.adapters
import android.support.v7.widget.RecyclerView
import android.content.Context
import android.databinding.DataBindingUtil
import android.view.LayoutInflater
import android.view.View
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 android.widget.ArrayAdapter
import com.google.common.collect.Lists
import net.syncthing.java.core.beans.DeviceStats
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewDeviceBinding
import kotlin.properties.Delegates
class DevicesAdapter: RecyclerView.Adapter<DeviceViewHolder>() {
var data: List<Pair<DeviceInfo, ConnectionInfo>> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
}
class DevicesAdapter(context: Context) :
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, Lists.newArrayList()) {
var listener: DeviceAdapterListener? = null
init {
setHasStableIds(true)
}
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].first.deviceId.deviceId.hashCode().toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DeviceViewHolder(
ListviewDeviceBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: DeviceViewHolder, position: Int) {
val binding = holder.binding
val context = binding.root.context
val (deviceInfo, connectionInfo) = data[position]
binding.name = deviceInfo.name
binding.isConnected = connectionInfo.status == ConnectionStatus.Connected
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()
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewDeviceBinding
= if (v == null) {
DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.listview_device, parent, false)
} else {
DataBindingUtil.bind(v)
}
val deviceStats = getItem(position)
binding.deviceName.text = deviceStats!!.name
val icon =
when (deviceStats.status) {
DeviceStats.DeviceStatus.OFFLINE -> R.drawable.ic_laptop_red_24dp
DeviceStats.DeviceStatus.ONLINE_INACTIVE,
DeviceStats.DeviceStatus.ONLINE_ACTIVE -> R.drawable.ic_laptop_green_24dp
}
binding.deviceIcon.setImageResource(icon)
return binding.root
}
}
interface DeviceAdapterListener {
fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean
}
class DeviceViewHolder(val binding: ListviewDeviceBinding): RecyclerView.ViewHolder(binding.root)
@@ -1,69 +1,40 @@
package net.syncthing.lite.adapters
import android.support.v7.widget.RecyclerView
import android.content.Context
import android.databinding.DataBindingUtil
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import com.google.common.collect.Lists
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewFileBinding
import org.apache.commons.io.FileUtils
import kotlin.properties.Delegates
// TODO: enable setHasStableIds and add a good way to get an id
class FolderContentsAdapter: RecyclerView.Adapter<FolderContentsViewHolder>() {
var data: List<FileInfo> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
}
class FolderContentsAdapter(context: Context) :
ArrayAdapter<FileInfo>(context, R.layout.listview_file, Lists.newArrayList()) {
var listener: FolderContentsListener? = null
init {
// setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderContentsViewHolder(
ListviewFileBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: FolderContentsViewHolder, position: Int) {
val binding = holder.binding
val fileInfo = data[position]
binding.fileName = fileInfo.fileName
if (fileInfo.isDirectory()) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewFileBinding =
if (v == null) {
DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.listview_file, parent, false)
} else {
DataBindingUtil.bind(v)
}
val fileInfo = getItem(position)
binding.fileLabel.text = fileInfo!!.fileName
if (fileInfo.isDirectory) {
binding.fileIcon.setImageResource(R.drawable.ic_folder_black_24dp)
binding.fileSize = null
binding.fileSize.visibility = View.GONE
} else {
binding.fileIcon.setImageResource(R.drawable.ic_image_black_24dp)
binding.fileSize = binding.root.context.getString(R.string.file_info,
FileUtils.byteCountToDisplaySize(fileInfo.size!!),
DateUtils.getRelativeDateTimeString(binding.root.context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.fileSize.visibility = View.VISIBLE
binding.fileSize.text = (FileUtils.byteCountToDisplaySize(fileInfo.size!!)
+ context.getString(R.string.last_modified)
+ DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
}
binding.root.setOnClickListener {
listener?.onItemClicked(fileInfo)
}
binding.root.setOnLongClickListener {
listener?.onItemLongClicked(fileInfo) ?: false
}
binding.executePendingBindings()
return binding.root
}
override fun getItemCount() = data.size
// override fun getItemId(position: Int) = data[position].fileName.hashCode().toLong()
}
interface FolderContentsListener {
fun onItemClicked(fileInfo: FileInfo)
fun onItemLongClicked(fileInfo: FileInfo): Boolean
}
class FolderContentsViewHolder(val binding: ListviewFileBinding): RecyclerView.ViewHolder(binding.root)
@@ -1,70 +1,38 @@
package net.syncthing.lite.adapters
import android.support.v7.widget.RecyclerView
import android.content.Context
import android.databinding.DataBindingUtil
import android.text.format.DateUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import net.syncthing.java.bep.folder.FolderStatus
import android.widget.ArrayAdapter
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewFolderBinding
import kotlin.properties.Delegates
import org.apache.commons.lang3.tuple.Pair
class FoldersListAdapter: RecyclerView.Adapter<FolderListViewHolder>() {
var data: List<FolderStatus> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewFolderBinding =
if (v == null) {
DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.listview_folder, parent, false)
} else {
DataBindingUtil.bind(v)
}
val folderInfo = getItem(position)!!.left
val folderStats = getItem(position)!!.right
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
binding.folderLastmodInfo.text =
if (folderStats.lastUpdate == null)
context.getString(R.string.last_modified_unknown)
else context.getString(R.string.last_modified_known) + " " +
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
return binding.root
}
var listener: FolderListAdapterListener? = null
init {
setHasStableIds(true)
}
override fun getItemCount() = data.size
override fun getItemId(position: Int) = data[position].info.folderId.hashCode().toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FolderListViewHolder (
ListviewFolderBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: FolderListViewHolder, position: Int) {
val binding = holder.binding
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.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
}
}
}
class FolderListViewHolder(val binding: ListviewFolderBinding): RecyclerView.ViewHolder(binding.root)
interface FolderListAdapterListener {
fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats)
fun onFolderLongClicked(folderInfo: FolderInfo): Boolean
}
@@ -1,48 +0,0 @@
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) }
}
}
}
}
@@ -1,19 +0,0 @@
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()
}
}
@@ -1,19 +0,0 @@
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()
}
}
@@ -1,19 +0,0 @@
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()
}
}
@@ -1,107 +0,0 @@
package net.syncthing.lite.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.support.v4.app.FragmentManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import com.google.zxing.BarcodeFormat
import com.google.zxing.WriterException
import com.google.zxing.qrcode.QRCodeWriter
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
import org.jetbrains.anko.doAsync
class DeviceIdDialogFragment: SyncthingDialogFragment() {
companion object {
private const val QR_RESOLUTION = 512
private const val TAG = "DeviceIdDialog"
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val binding = DialogDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
// use an placeholder to prevent size changes; this string is never shown
binding.deviceId.text = "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"
binding.deviceId.visibility = View.INVISIBLE
binding.qrCode.setImageBitmap(Bitmap.createBitmap(QR_RESOLUTION, QR_RESOLUTION, Bitmap.Config.RGB_565))
libraryHandler.library { configuration, _, _ ->
val deviceId = configuration.localDeviceId
fun copyDeviceId() {
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(context!!.getString(R.string.device_id), deviceId.deviceId)
clipboard.primaryClip = clip
Toast.makeText(context, context!!.getString(R.string.device_id_copied), Toast.LENGTH_SHORT)
.show()
}
fun shareDeviceId() {
context!!.startActivity(Intent.createChooser(
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, deviceId.deviceId)
},
context!!.getString(R.string.share_device_id_chooser)
))
}
GlobalScope.launch (Dispatchers.Main) {
binding.deviceId.text = deviceId.deviceId
binding.deviceId.visibility = View.VISIBLE
binding.deviceId.setOnClickListener { copyDeviceId() }
binding.share.setOnClickListener { shareDeviceId() }
}
doAsync {
val writer = QRCodeWriter()
try {
val bitMatrix = writer.encode(deviceId.deviceId, BarcodeFormat.QR_CODE, QR_RESOLUTION, QR_RESOLUTION)
val width = bitMatrix.width
val height = bitMatrix.height
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until height) {
bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
}
}
GlobalScope.launch (Dispatchers.Main) {
binding.flipper.displayedChild = 1
binding.qrCode.setImageBitmap(bmp)
}
} catch (e: WriterException) {
Log.w(TAG, e)
}
}
}
return AlertDialog.Builder(context!!, theme)
.setTitle(context!!.getString(R.string.device_id))
.setView(binding.root)
.setPositiveButton(android.R.string.ok, null)
.create()
}
fun show(manager: FragmentManager?) {
super.show(manager, TAG)
}
}
@@ -1,125 +0,0 @@
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)
}
@@ -1,52 +0,0 @@
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)
}
@@ -1,76 +0,0 @@
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.getFromUrl(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)
}
}
@@ -1,57 +0,0 @@
package net.syncthing.lite.dialogs
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import net.syncthing.lite.library.UploadFileTask
import net.syncthing.lite.utils.Util
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
class FileUploadDialog(private val context: Context, private val syncthingClient: SyncthingClient,
private val localFile: Uri, private val syncthingFolder: String,
private val syncthingSubFolder: String,
private val onUploadCompleteListener: () -> Unit) {
private lateinit var progressDialog: ProgressDialog
private var uploadFileTask: UploadFileTask? = null
fun show() {
showDialog()
doAsync {
uploadFileTask = UploadFileTask(context, syncthingClient, localFile, syncthingFolder,
syncthingSubFolder, this@FileUploadDialog::onProgress,
this@FileUploadDialog::onComplete, this@FileUploadDialog::onError)
}
}
private fun showDialog() {
progressDialog = ProgressDialog(context)
progressDialog.setMessage(context.getString(R.string.dialog_uploading_file, Util.getContentFileName(context, localFile)))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { uploadFileTask?.cancel() }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
progressDialog.isIndeterminate = false
progressDialog.progress = observer.progressPercentage()
progressDialog.max = 100
}
private fun onComplete() {
progressDialog.dismiss()
this@FileUploadDialog.context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
private fun onError() {
progressDialog.dismiss()
this@FileUploadDialog.context.toast(R.string.toast_file_upload_failed)
}
}
@@ -1,105 +0,0 @@
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)
}
@@ -1,32 +0,0 @@
package net.syncthing.lite.dialogs
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentActivity
import android.support.v7.app.AlertDialog
import net.syncthing.lite.R
import org.jetbrains.anko.defaultSharedPreferences
class ReconnectIssueDialogFragment: DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?) = AlertDialog.Builder(context!!, theme)
.setMessage(R.string.dialog_warning_reconnect_problem)
.setPositiveButton(android.R.string.ok) { _, _ ->
context!!.defaultSharedPreferences.edit()
.putBoolean(SETTINGS_PARAM, true)
.apply()
}
.create()
companion object {
private const val DIALOG_TAG = "ReconnectIssueDialog"
private const val SETTINGS_PARAM = "has_educated_about_reconnect_issues"
fun showIfNeeded(activity: FragmentActivity) {
if (!activity.defaultSharedPreferences.getBoolean(SETTINGS_PARAM, false)) {
if (activity.supportFragmentManager.findFragmentByTag(DIALOG_TAG) == null) {
ReconnectIssueDialogFragment().show(activity.supportFragmentManager, DIALOG_TAG)
}
}
}
}
}
@@ -1,137 +0,0 @@
package net.syncthing.lite.dialogs.downloadfile
import android.app.Dialog
import android.app.ProgressDialog
import android.arch.lifecycle.Observer
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.util.Log
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 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(
folder = fileInfo.folder,
path = fileInfo.path,
fileName = fileInfo.fileName
))
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)
}
}
}
}
val model: DownloadFileDialogViewModel by lazy {
ViewModelProviders.of(this).get(DownloadFileDialogViewModel::class.java)
}
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,
outputUri = outputUri,
contentResolver = context!!.contentResolver
)
val progressDialog = ProgressDialog(context).apply {
setMessage(context!!.getString(R.string.dialog_downloading_file, fileSpec.fileName))
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
isCancelable = true
isIndeterminate = true
max = DownloadFileStatusRunning.MAX_PROGRESS
}
model.status.observe(this, Observer {
status ->
when (status) {
is DownloadFileStatusRunning -> {
progressDialog.apply {
isIndeterminate = false
progress = status.progress
}
}
is DownloadFileStatusDone -> {
dismissAllowingStateLoss()
if (outputUri == null) {
val mimeType = MimeType.getFromUrl(fileSpec.fileName)
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 -> {
dismissAllowingStateLoss()
context!!.toast(R.string.toast_file_download_failed)
}
}
})
return progressDialog
}
override fun onCancel(dialog: DialogInterface?) {
super.onCancel(dialog)
model.cancel()
}
fun show(fragmentManager: FragmentManager?) {
show(fragmentManager, TAG)
}
}
@@ -1,118 +0,0 @@
package net.syncthing.lite.dialogs.downloadfile
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
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() {
companion object {
private const val TAG = "DownloadFileDialog"
}
private var isInitialized = false
private val statusInternal = MutableLiveData<DownloadFileStatus>()
private val cancellationSignal = CancellationSignal()
val status: LiveData<DownloadFileStatus> = statusInternal
fun init(
libraryHandler: LibraryHandler,
fileSpec: DownloadFileSpec,
externalCacheDir: File,
outputUri: Uri?,
contentResolver: ContentResolver
) {
if (isInitialized) {
return
}
isInitialized = true
libraryHandler.start()
// this keeps the client only active as long as the block is running
// but the file downloading is not synchronous.
// Due to that, the start and stop calls are used.
libraryHandler.syncthingClient {
syncthingClient ->
try {
val fileInfo = syncthingClient.indexHandler.getFileInfoByPath(
folder = fileSpec.folder,
path = fileSpec.path
)!!
val task = DownloadFileTask(
fileStorageDirectory = externalCacheDir,
syncthingClient = syncthingClient,
fileInfo = fileInfo,
onProgress = { status ->
val newProgress = (status.downloadedBytes * DownloadFileStatusRunning.MAX_PROGRESS / status.totalTransferSize).toInt()
val currentStatus = statusInternal.value
// only update if it changed
if (!(currentStatus is DownloadFileStatusRunning) || currentStatus.progress != newProgress) {
statusInternal.value = DownloadFileStatusRunning(newProgress)
}
},
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
libraryHandler.stop()
}
)
cancellationSignal.setOnCancelListener {
task.cancel()
}
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(TAG, "downloading file failed", ex)
}
statusInternal.postValue(DownloadFileStatusFailed)
}
}
}
override fun onCleared() {
super.onCleared()
cancel()
}
fun cancel() {
cancellationSignal.cancel()
}
}
@@ -1,5 +0,0 @@
package net.syncthing.lite.dialogs.downloadfile
import java.io.Serializable
data class DownloadFileSpec(val folder: String, val path: String, val fileName: String): Serializable
@@ -1,12 +0,0 @@
package net.syncthing.lite.dialogs.downloadfile
import java.io.File
sealed class DownloadFileStatus
object DownloadFileStatusFailed: DownloadFileStatus()
data class DownloadFileStatusDone(val file: File): DownloadFileStatus()
data class DownloadFileStatusRunning(val progress: Int): DownloadFileStatus() {
companion object {
const val MAX_PROGRESS = 100
}
}
@@ -1,17 +0,0 @@
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")
}
@@ -5,120 +5,134 @@ import android.content.Context
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import com.google.zxing.integration.android.IntentIntegrator
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.java.core.beans.DeviceStats
import net.syncthing.java.core.security.KeystoreHandler
import net.syncthing.lite.R
import net.syncthing.lite.adapters.DeviceAdapterListener
import net.syncthing.lite.adapters.DevicesAdapter
import net.syncthing.lite.databinding.FragmentDevicesBinding
import net.syncthing.lite.databinding.ViewEnterDeviceIdBinding
import net.syncthing.lite.library.UpdateIndexTask
import net.syncthing.lite.utils.FragmentIntentIntegrator
import net.syncthing.lite.utils.Util
import java.io.IOException
import org.apache.commons.lang3.StringUtils.isBlank
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
import uk.co.markormesher.android_fab.SpeedDialMenuItem
import java.security.InvalidParameterException
class DevicesFragment : SyncthingFragment() {
companion object {
private val TAG = "DevicesFragment"
}
private lateinit var binding: FragmentDevicesBinding
private val adapter = DevicesAdapter()
private var addDeviceDialog: AlertDialog? = null
private var addDeviceDialogBinding: ViewEnterDeviceIdBinding? = null
private lateinit var adapter: DevicesAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_devices, container, false)
binding.addDevice.setOnClickListener { showDialog() }
binding.list.adapter = adapter
adapter.listener = object: DeviceAdapterListener {
override fun onDeviceLongClicked(deviceInfo: DeviceInfo): Boolean {
AlertDialog.Builder(context)
.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) { _, _ ->
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)
.show()
return false
}
}
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()
}
}
binding.list.emptyView = binding.empty
binding.fab.speedDialMenuAdapter = FabMenuAdapter()
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()) {
addDeviceDialogBinding?.deviceId?.setText(scanResult.contents)
override fun onLibraryLoadedAndActivityCreated() {
initDeviceList()
updateDeviceList()
}
private fun initDeviceList() {
adapter = DevicesAdapter(context!!)
binding.list.adapter = adapter
binding.list.setOnItemLongClickListener { _, _, position, _ ->
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
AlertDialog.Builder(context)
.setTitle(getString(R.string.remove_device_title) + " " + deviceId.substring(0, 7) + "?")
.setMessage(getString(R.string.remove_device_body_1) + " " + deviceId.substring(0, 7) + " " + getString(R.string.remove_device_body_2))
.setPositiveButton(android.R.string.yes) { _, _ ->
getSyncthingActivity().configuration().edit().removePeer(deviceId).persistLater() }
.setNegativeButton(android.R.string.no, null)
.show()
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
false
}
}
private fun showDialog() {
val binding = ViewEnterDeviceIdBinding.inflate(LayoutInflater.from(context), null, false)
addDeviceDialogBinding = binding
private fun updateDeviceList() {
adapter.clear()
adapter.addAll(getSyncthingActivity().syncthingClient().devicesHandler.deviceStatsList)
adapter.notifyDataSetChanged()
}
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)
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
// Check if this was a QR code scan.
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
if (scanResult != null) {
val deviceId = scanResult.contents
if (!isBlank(deviceId)) {
importDeviceId(deviceId)
}
}
}
// Use different listener to keep dialog open after button click.
// https://stackoverflow.com/a/15619098
dialog.getButton(AlertDialog.BUTTON_POSITIVE)!!.setOnClickListener { handleAddClick() }
private fun importDeviceId(deviceId: String) {
try {
KeystoreHandler.validateDeviceId(deviceId)
} catch (e: IllegalArgumentException) {
Toast.makeText(context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
return
}
val modified = getSyncthingActivity().configuration().edit().addPeers(DeviceInfo(deviceId, null))
if (modified) {
getSyncthingActivity().configuration().edit().persistLater()
Toast.makeText(context, getString(R.string.device_import_success) + " " + deviceId, Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
UpdateIndexTask(context!!, getSyncthingActivity().syncthingClient()).updateIndex()
} else {
Toast.makeText(context, getString(R.string.device_already_known) + " " + deviceId, Toast.LENGTH_SHORT).show()
}
}
private inner class FabMenuAdapter : SpeedDialMenuAdapter() {
override fun getCount(): Int {
return 2
}
override fun getMenuItem(context: Context, position: Int): SpeedDialMenuItem {
when (position) {
0 -> return SpeedDialMenuItem(context, R.drawable.ic_qr_code_white_24dp, R.string.scan_qr_code)
1 -> return SpeedDialMenuItem(context, R.drawable.ic_edit_white_24dp, R.string.enter_device_id)
}
throw InvalidParameterException()
}
override fun onMenuItemClick(position: Int): Boolean {
when (position) {
0 -> FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
1 -> {
val editText = EditText(context)
val dialog = AlertDialog.Builder(context)
.setTitle(R.string.device_id_dialog_title)
.setView(editText)
.setPositiveButton(android.R.string.ok) { _, _ -> importDeviceId(editText.text.toString()) }
.setNegativeButton(android.R.string.cancel, null)
.create()
dialog.setOnShowListener {
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
dialog.show()
}
}
return true
}
}
}
@@ -1,54 +1,53 @@
package net.syncthing.lite.fragments
import android.arch.lifecycle.Observer
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import com.google.common.collect.Lists
import com.google.common.collect.Ordering
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
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.apache.commons.lang3.tuple.Pair
import org.jetbrains.anko.intentFor
import java.util.*
class FoldersFragment : SyncthingFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val adapter = FoldersListAdapter()
adapter.listener = object : FolderListAdapterListener {
override fun onFolderClicked(folderInfo: FolderInfo, folderStats: FolderStats) {
startActivity(
activity!!.intentFor<FolderBrowserActivity>(
FolderBrowserActivity.EXTRA_FOLDER_NAME to folderInfo.folderId
)
)
}
companion object {
private val TAG = "FoldersFragment"
}
override fun onFolderLongClicked(folderInfo: FolderInfo): Boolean {
FolderInfoDialog
.newInstance(folderId = folderInfo.folderId)
.show(fragmentManager!!)
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()
}
}
private lateinit var binding: FragmentFoldersBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_folders, container, false)
binding.list.emptyView = binding.empty
return binding.root
}
override fun onLibraryLoadedAndActivityCreated() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
val list = Lists.newArrayList(getSyncthingActivity().folderBrowser()!!.folderInfoAndStatsList)
Collections.sort(list, Ordering.natural<Comparable<String>>()
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter(context, list)
binding.list.adapter = adapter
binding.list.setOnItemClickListener { _, _, position, _ ->
val folder = adapter.getItem(position)!!.left.folder
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
startActivity(intent)
}
}
}
@@ -1,67 +0,0 @@
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.dialogs.ErrorReportDialog
import net.syncthing.lite.error.ErrorStorage
import net.syncthing.lite.library.DefaultLibraryManager
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences)
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!!)
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,22 +0,0 @@
package net.syncthing.lite.fragments
import net.syncthing.lite.async.CoroutineDialogFragment
import net.syncthing.lite.library.LibraryHandler
abstract class SyncthingDialogFragment : CoroutineDialogFragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(
context = context!!
)}
override fun onStart() {
super.onStart()
libraryHandler.start()
}
override fun onStop() {
super.onStop()
libraryHandler.stop()
}
}
@@ -1,25 +1,31 @@
package net.syncthing.lite.fragments
import net.syncthing.lite.async.CoroutineFragment
import net.syncthing.lite.library.LibraryHandler
import android.os.Bundle
import android.support.v4.app.Fragment
import net.syncthing.lite.activities.SyncthingActivity
abstract class SyncthingFragment : CoroutineFragment() {
val libraryHandler: LibraryHandler by lazy { LibraryHandler(context = context!!)}
/**
* Handle connection to [[SyncthingActivity]], and make sure device rotation are handled correctly.
*/
abstract class SyncthingFragment : Fragment() {
override fun onStart() {
super.onStart()
protected fun getSyncthingActivity() = activity as SyncthingActivity
libraryHandler.start {
// TODO: check if this is still useful
onLibraryLoaded()
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
checkConditions()
}
fun onLibraryLoaded() {
checkConditions()
}
private fun checkConditions() {
if (activity != null && getSyncthingActivity().folderBrowser() != null ) {
onLibraryLoadedAndActivityCreated()
}
}
override fun onStop() {
super.onStop()
libraryHandler.stop()
open fun onLibraryLoadedAndActivityCreated() {
}
open fun onLibraryLoaded() {}
}
}
@@ -1,105 +0,0 @@
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)
}
}
@@ -1,76 +0,0 @@
package net.syncthing.lite.library
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 {
private const val LOG_TAG = "DefaultLibraryManager"
private var instance: LibraryManager? = null
private val lock = Object()
private val handler = Handler(Looper.getMainLooper())
fun with(context: Context) = withApplicationContext(context.applicationContext)
private fun withApplicationContext(context: Context): LibraryManager {
if (instance == null) {
synchronized(lock) {
if (instance == null) {
val shutdownRunnable = Runnable {
instance!!.shutdownIfThereAreZeroUsers()
}
fun scheduleShutdown() {
val shutdownDelay = context.defaultSharedPreferences.getString(
"shutdown_delay",
context.getString(R.string.default_shutdown_delay)
).toLong()
handler.postDelayed(shutdownRunnable, shutdownDelay)
}
fun cancelShutdown() {
handler.removeCallbacks(shutdownRunnable)
}
instance = LibraryManager(
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 ->
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "user counter updated to $newUserCounter")
}
val isUsed = newUserCounter > 0
if (isUsed) {
cancelShutdown()
} else {
scheduleShutdown()
}
}
)
}
}
}
return instance!!
}
}
@@ -1,9 +0,0 @@
package net.syncthing.lite.library
import java.io.File
data class DownloadFilePath (val baseDirectory: File, val fileHash: String) {
val filesDirectory = File(baseDirectory, fileHash.substring(0, 2))
val targetFile = File(filesDirectory, fileHash.substring(2))
val tempFile = File(filesDirectory, fileHash.substring(2) + "_temp")
}
@@ -1,143 +1,107 @@
package net.syncthing.lite.library
import android.os.Handler
import android.os.Looper
import android.support.v4.os.CancellationSignal
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.support.annotation.StringRes
import android.util.Log
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import net.syncthing.java.bep.BlockPullerStatus
import android.webkit.MimeTypeMap
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.newTask
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.File
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class DownloadFileTask(private val fileStorageDirectory: File,
syncthingClient: SyncthingClient,
private val fileInfo: FileInfo,
private val onProgress: (status: BlockPullerStatus) -> Unit,
private val onComplete: (File) -> Unit,
private val onError: (Exception) -> Unit) {
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient,
private val mFileInfo: FileInfo) {
companion object {
private const val TAG = "DownloadFileTask"
private val handler = Handler(Looper.getMainLooper())
suspend fun downloadFileCoroutine(
externalCacheDir: File,
syncthingClient: SyncthingClient,
fileInfo: FileInfo,
onProgress: (status: BlockPullerStatus) -> Unit
) = suspendCancellableCoroutine<File> {
continuation ->
val task = DownloadFileTask(
externalCacheDir,
syncthingClient,
fileInfo,
onProgress,
{
continuation.resume(it)
},
{
continuation.resumeWithException(it)
}
)
continuation.invokeOnCancellation {
task.cancel()
}
}
}
private val cancellationSignal = CancellationSignal()
private var doneListenerCalled = false
init {
val file = DownloadFilePath(fileStorageDirectory, fileInfo.hash!!)
GlobalScope.launch {
if (file.targetFile.exists()) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "there is already a file")
}
callComplete(file.targetFile)
return@launch
}
private val TAG = "DownloadFileTask"
private lateinit var progressDialog: ProgressDialog
private var cancelled = false
fun downloadFile() {
showDialog()
mSyncthingClient.pullFile(mFileInfo, { observer ->
onProgress(observer)
try {
if (!file.filesDirectory.isDirectory) {
if (!file.filesDirectory.mkdirs()) {
throw IOException("could not create output directory")
}
while (!observer.isCompleted) {
if (cancelled)
return@pullFile
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage)
onProgress(observer)
}
// download the file to a temp location
val inputStream = syncthingClient.pullFile(fileInfo, this@DownloadFileTask::callProgress)
val outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val outputFile = File(outputDir, mFileInfo.fileName)
FileUtils.copyInputStreamToFile(observer.inputStream, outputFile)
Log.i(TAG, "downloaded file = " + mFileInfo.path)
onComplete(outputFile)
} catch (e: IOException) {
onError(R.string.toast_file_download_failed)
Log.w(TAG, "Failed to download file " + mFileInfo, e)
} catch (e: InterruptedException) {
onError(R.string.toast_file_download_failed)
Log.w(TAG, "Failed to download file " + mFileInfo, e)
}
}) { onError(R.string.toast_file_download_failed) }
}
try {
FileUtils.copyInputStreamToFile(inputStream, file.tempFile)
file.tempFile.renameTo(file.targetFile)
} finally {
file.tempFile.delete()
}
private fun showDialog() {
progressDialog = ProgressDialog(mContext)
progressDialog.setMessage(mContext.getString(R.string.dialog_downloading_file, mFileInfo.fileName))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { cancelled = true }
progressDialog.isIndeterminate = true
progressDialog.show()
}
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)
}
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
doAsync {
uiThread {
progressDialog.isIndeterminate = false
progressDialog.max = (mFileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress * mFileInfo.size!!).toInt()
}
}
}
private fun callProgress(status: BlockPullerStatus) {
handler.post {
if (!doneListenerCalled) {
if (BuildConfig.DEBUG) {
Log.i("pullFile", "download progress = $status")
}
private fun onComplete(file: File) {
progressDialog.dismiss()
if (cancelled)
return
onProgress(status)
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(file), mimeType)
intent.newTask()
try {
mContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(R.string.toast_open_file_failed)
Log.w(TAG, "No handler found for file " + file.name, e)
}
}
private fun onError(@StringRes error: Int) {
doAsync {
uiThread {
progressDialog.dismiss()
mContext.toast(error)
}
}
}
private fun callComplete(file: File) {
handler.post {
if (!doneListenerCalled) {
doneListenerCalled = true
onComplete(file)
}
}
}
private fun callError(exception: Exception) {
handler.post {
if (!doneListenerCalled) {
doneListenerCalled = true
onError(exception)
}
}
}
fun cancel() {
cancellationSignal.cancel()
callError(InterruptedException())
}
}
}
@@ -0,0 +1,43 @@
package net.syncthing.lite.library
import android.content.Context
import android.preference.PreferenceManager
import android.util.Log
import net.syncthing.java.core.beans.FolderInfo
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
class InitLibraryTask(private val context: Context, private val onLibraryLoaded: (LibraryHandler) -> Unit,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
private val onIndexUpdateCompleteListener: () -> Unit) {
private val TAG = "InitLibraryTask"
init {
doAsync {
val libraryHandler = LibraryHandler()
libraryHandler.init(context)
libraryHandler.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
onIndexUpdateProgressListener(folder, percentage)
}
override fun onIndexUpdateComplete() {
onIndexUpdateCompleteListener()
}
})
//trigger update if last was more than 10mins ago
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
val lastUpdateTimeAgo = Date().time - lastUpdateMillis
if (lastUpdateMillis == -1L || lastUpdateTimeAgo > 10 * 60 * 1000) {
Log.d(TAG, "trigger index update, last was " + Date(lastUpdateMillis))
UpdateIndexTask(context, libraryHandler.syncthingClient!!).updateIndex()
}
uiThread {
onLibraryLoaded(libraryHandler)
}
}
}
}
@@ -1,152 +1,82 @@
package net.syncthing.lite.library
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.content.Context
import android.os.Handler
import android.os.Looper
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 android.util.Log
import com.google.common.eventbus.Subscribe
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.bep.IndexHandler
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.DeviceId
import net.syncthing.java.core.configuration.Configuration
import org.jetbrains.anko.doAsync
import java.util.concurrent.atomic.AtomicBoolean
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.java.core.security.KeystoreHandler
import net.syncthing.lite.utils.Util
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
/**
* This class helps when using the library.
* It's required to start and stop it to make the callbacks fire (or stop to fire).
*
* It's possible to do multiple start and stop cycles with one instance of this class.
*/
class LibraryHandler(context: Context) {
class LibraryHandler {
companion object {
private const val TAG = "LibraryHandler"
private val handler = Handler(Looper.getMainLooper())
private val TAG = "LibConnectionHandler"
private var mOnIndexUpdatedListener: OnIndexUpdatedListener? = null
var configuration: ConfigurationService? = null
private set
var syncthingClient: SyncthingClient? = null
private set
var folderBrowser: FolderBrowser? = null
private set
interface OnIndexUpdatedListener {
fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int)
fun onIndexUpdateComplete()
}
val libraryManager = DefaultLibraryManager.with(context)
private val isStarted = AtomicBoolean(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
private val messageFromUnknownDeviceListeners = HashSet<(DeviceId) -> Unit>()
private val internalMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {
deviceId ->
handler.post {
messageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) }
fun init(context: Context) {
configuration = ConfigurationService.newLoader()
.setCache(File(context.externalCacheDir, "cache"))
.setDatabase(File(context.getExternalFilesDir(null), "database"))
.loadFrom(File(context.getExternalFilesDir(null), "config.properties"))
configuration!!.edit().setDeviceName(Util.getDeviceName())
try {
FileUtils.cleanDirectory(configuration!!.temp)
} catch (ex: IOException) {
Log.e(TAG, "error", ex)
destroy()
}
KeystoreHandler.newLoader().loadAndStore(configuration!!)
configuration!!.edit().persistLater()
Log.i(TAG, "loaded mConfiguration = " + configuration!!.newWriter().dumpToString())
Log.i(TAG, "storage space = " + configuration!!.storageInfo.dumpAvailableSpace())
syncthingClient = net.syncthing.java.client.SyncthingClient(configuration!!)
//TODO listen for device events, update device list
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
}
fun start(onLibraryLoaded: (LibraryHandler) -> Unit = {}) {
if (isStarted.getAndSet(true) == true) {
throw IllegalStateException("already started")
}
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
mOnIndexUpdatedListener = onIndexUpdatedListener
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
libraryManager.startLibraryUsage {
libraryInstance ->
isListeningPortTakenInternal.value = libraryInstance.isListeningPortTaken
onLibraryLoaded(this)
val client = libraryInstance.syncthingClient
client.discoveryHandler.registerMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
job = Job()
GlobalScope.launch (job) {
libraryInstance.syncthingClient.indexHandler.subscribeToOnFullIndexAcquiredEvents().consumeEach {
indexUpdateCompleteMessages.send(it)
}
@Subscribe
fun handleIndexRecordAquiredEvent(event: IndexHandler.IndexRecordAquiredEvent) {
val folder = syncthingClient!!.indexHandler.getFolderInfo(event.folder)
val indexInfo = event.indexInfo
event.newRecords.size
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
mOnIndexUpdatedListener!!.onIndexUpdateProgress(folder, (indexInfo.completed * 100).toInt())
}
GlobalScope.launch (job) {
libraryInstance.folderBrowser.folderInfoAndStatusStream().consumeEach {
folderStatusList.send(it)
}
@Subscribe
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
}
GlobalScope.launch (job) {
libraryInstance.syncthingClient.subscribeToConnectionStatus().consumeEach {
connectionStatus.send(it)
}
}
}
})
}
fun stop() {
if (isStarted.getAndSet(false) == false) {
throw IllegalStateException("already stopped")
}
job!!.cancel()
syncthingClient {
try {
it.discoveryHandler.unregisterMessageFromUnknownDeviceListener(internalMessageFromUnknownDeviceListener)
} catch (e: IllegalArgumentException) {
// ignored, no idea why this is thrown
}
}
libraryManager.stopLibraryUsage()
fun destroy() {
folderBrowser!!.close()
syncthingClient!!.close()
configuration!!.close()
}
/*
* The callback is executed asynchronously.
* As soon as it returns, there is no guarantee about the availability of the library
*/
fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
libraryManager.startLibraryUsage {
doAsync {
try {
callback(it.configuration, it.syncthingClient, it.folderBrowser)
} finally {
libraryManager.stopLibraryUsage()
}
}
}
}
fun syncthingClient(callback: (SyncthingClient) -> Unit) {
library { _, s, _ -> callback(s) }
}
fun configuration(callback: (Configuration) -> Unit) {
library { c, _, _ -> callback(c) }
}
fun folderBrowser(callback: (FolderBrowser) -> Unit) {
library { _, _, f -> callback(f) }
}
// these listeners are called at the UI Thread
// there is no need to unregister because they removed from the library when close is called
fun registerMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) {
messageFromUnknownDeviceListeners.add(listener)
}
fun unregisterMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) {
messageFromUnknownDeviceListeners.remove(listener)
}
fun subscribeToOnFullIndexAcquiredEvents() = indexUpdateCompleteMessages.openSubscription()
fun subscribeToFolderStatusList() = folderStatusList.openSubscription()
fun subscribeToConnectionStatus() = connectionStatus.openSubscription()
}
@@ -1,84 +0,0 @@
package net.syncthing.lite.library
import android.content.Context
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.exception.ExceptionReport
import net.syncthing.java.repository.EncryptedTempRepository
import net.syncthing.repository.android.SqliteIndexRepository
import net.syncthing.repository.android.TempDirectoryLocalRepository
import net.syncthing.repository.android.database.RepositoryDatabase
import java.io.File
import java.net.DatagramSocket
import java.net.InetAddress
import java.net.SocketException
/**
* This class is used internally to access the syncthing-java library
* There should be never more than 1 instance of this class
*
* This class can not be recycled. This means that after doing a shutdown of it,
* a new instance must be created
*
* The creation and the shutdown are synchronous, so keep them out of the UI Thread
*/
class LibraryInstance (
context: Context,
private val exceptionReportHandler: (ExceptionReport) -> Unit
) {
companion object {
private const val LOG_TAG = "LibraryInstance"
/**
* Check if listening port for local discovery is taken by another app. Do this check here to
* avoid adding another callback.
*/
private fun checkIsListeningPortTaken(): Boolean {
try {
DatagramSocket(21027, InetAddress.getByName("0.0.0.0")).close()
return false
} catch (e: SocketException) {
Log.w(LOG_TAG, e)
return true
}
}
}
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)
val syncthingClient = SyncthingClient(
configuration = configuration,
repository = SqliteIndexRepository(
database = RepositoryDatabase.with(context),
closeDatabaseOnClose = false,
clearTempStorageHook = { tempRepository.deleteAllTempData() }
),
tempRepository = tempRepository,
exceptionReportHandler = { ex ->
Log.w(LOG_TAG, "${ex.component}\n${ex.detailsReadableString}\n${Log.getStackTraceString(ex.exception)}")
GlobalScope.launch (Dispatchers.Main) {
exceptionReportHandler(ex)
}
}
)
val folderBrowser = syncthingClient.indexHandler.folderBrowser
val indexBrowser = syncthingClient.indexHandler.indexBrowser
suspend fun shutdown() {
syncthingClient.close()
configuration.persistNow()
}
}
@@ -1,124 +0,0 @@
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.resume
import kotlin.coroutines.suspendCoroutine
/**
* This class manages the access to an LibraryInstance
*
* Users can get an instance with startLibraryUsage()
* If they are done with it, the should call stopLibraryUsage()
* After this, it's NOT safe to continue using the received LibraryInstance
*
* Every call to startLibraryUsage should be followed by an call to stopLibraryUsage,
* even if the callback was not called yet. It can still be called, so users should watch out.
*
* All listeners are executed at the UI Thread (except the synchronousInstanceCreator)
*
* The userCounterListener is always called before the isRunningListener
*
* The listeners are called for all changes, nothing is skipped or batched
*/
class LibraryManager (
val synchronousInstanceCreator: () -> LibraryInstance,
val userCounterListener: (Int) -> Unit = {},
val isRunningListener: (isRunning: Boolean) -> Unit = {}
) {
companion object {
private val handler = Handler(Looper.getMainLooper())
}
// this must be a SingleThreadExecutor to avoid race conditions
// only this Thread should access instance and userCounter
private val startStopExecutor = Executors.newSingleThreadExecutor()
private val instanceStream = ConflatedBroadcastChannel<LibraryInstance?>(null)
private var userCounter = 0
fun startLibraryUsage(callback: (LibraryInstance) -> Unit) {
startStopExecutor.submit {
val newUserCounter = ++userCounter
handler.post { userCounterListener(newUserCounter) }
if (instanceStream.value == null) {
instanceStream.offer(synchronousInstanceCreator())
handler.post { isRunningListener(true) }
}
handler.post { callback(instanceStream.value!!) }
}
}
suspend fun startLibraryUsageCoroutine(): LibraryInstance {
return suspendCoroutine { continuation ->
startLibraryUsage { instance ->
continuation.resume(instance)
}
}
}
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
if (newUserCounter < 0) {
userCounter = 0
throw IllegalStateException("can not stop library usage if there are 0 users")
}
handler.post { userCounterListener(newUserCounter) }
}
}
fun shutdownIfThereAreZeroUsers(listener: (wasShutdownPerformed: Boolean) -> Unit = {}) {
startStopExecutor.submit {
if (userCounter == 0) {
runBlocking { instanceStream.value?.shutdown() }
instanceStream.offer(null)
handler.post { isRunningListener(false) }
handler.post { listener(true) }
} else {
handler.post { listener(false) }
}
}
}
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)
}
}
}
}
}
}
@@ -1,176 +0,0 @@
package net.syncthing.lite.library
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import android.util.Log
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.lite.R
import net.syncthing.lite.utils.MimeType
import java.io.FileNotFoundException
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
)
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
)
}
override fun onCreate(): Boolean {
Log.d(Tag, "onCreate()")
return true
}
// this instance is not started -> it connects and disconnects on demand
private val libraryManager: LibraryManager by lazy { DefaultLibraryManager.with(context) }
override fun queryRoots(projection: Array<String>?): Cursor {
Log.d(Tag, "queryRoots($projection)")
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)
}
}
}
}
}
}
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
sortOrder: String?): Cursor {
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
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
}
}
}
}
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
Log.d(Tag, "queryDocument($documentId, $projection)")
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 accessMode = ParcelFileDescriptor.parseMode(mode)
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
throw NotImplementedError()
}
return runBlocking {
libraryManager.withLibrary { instance ->
val fileInfo = instance.indexBrowser.getFileInfoByAbsolutePathAllowNull(
folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId)
) ?: throw FileNotFoundException()
signal?.setOnCancelListener {
this.coroutineContext.cancel()
}
val outputFile = DownloadFileTask.downloadFileCoroutine(
externalCacheDir = context.externalCacheDir,
syncthingClient = instance.syncthingClient,
fileInfo = fileInfo,
onProgress = { /* ignore the progress */ }
)
ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
}
}
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
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.getFromUrl(fileInfo.fileName)
)
add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
add(Document.COLUMN_FLAGS, 0)
}
}
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
private fun getPathForDocId(docId: String) = docId.split(":")[1]
private fun getDocIdForFile(folderInfo: FolderInfo) = folderInfo.folderId + ":"
private fun getDocIdForFile(fileInfo: FileInfo) = fileInfo.folder + ":" + fileInfo.path
}
@@ -0,0 +1,47 @@
package net.syncthing.lite.library
import android.content.Context
import android.preference.PreferenceManager
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.util.*
class UpdateIndexTask(private val androidContext: Context, private val syncthingClient: SyncthingClient) {
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(androidContext)
fun updateIndex() {
if (sIndexUpdateInProgress)
return
sIndexUpdateInProgress = true
syncthingClient.updateIndexFromPeers { _, failures ->
sIndexUpdateInProgress = false
if (failures.isEmpty()) {
showToast(androidContext.getString(R.string.toast_index_update_successful))
} else {
showToast(androidContext.getString(R.string.toast_index_update_failed, failures.size))
}
mPreferences.edit()
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
.apply()
}
}
private fun showToast(message: String) {
doAsync {
uiThread {
androidContext.toast(message)
}
}
}
companion object {
val LAST_INDEX_UPDATE_TS_PREF = "LAST_INDEX_UPDATE_TS"
private var sIndexUpdateInProgress: Boolean = false
}
}
@@ -1,63 +1,103 @@
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.Context
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
import net.syncthing.lite.R
import net.syncthing.lite.utils.Util
import org.apache.commons.io.IOUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.IOException
// TODO: this should be an IntentService with notification
class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
localFile: Uri, private val syncthingFolder: String,
class UploadFileTask(private val context: Context, private val syncthingClient: SyncthingClient,
private val localFile: Uri, private val syncthingFolder: String,
syncthingSubFolder: String,
private val onProgress: (BlockPusher.FileUploadObserver) -> Unit,
private val onComplete: () -> Unit,
private val onError: () -> Unit) {
private val onUploadCompleteListener: () -> Unit) {
companion object {
private const val TAG = "UploadFileTask"
private val handler = Handler(Looper.getMainLooper())
private val TAG = "UploadFileTask"
}
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
private val uploadStream = context.contentResolver.openInputStream(localFile)
private val fileName = Util.getContentFileName(context, localFile)
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
private var isCancelled = false
private lateinit var mProgressDialog: ProgressDialog
private var mCancelled = false
init {
fun uploadFile() {
createDialog()
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
try {
val uploadStream = context.contentResolver.openInputStream(localFile)
syncthingClient.pushFile(uploadStream, syncthingFolder, syncthingPath, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted) {
if (mCancelled)
return@pushFile
GlobalScope.launch {
try {
val blockPusher = syncthingClient.getBlockPusher(folderId = syncthingFolder)
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
handler.post { onProgress(observer) }
while (!observer.isCompleted()) {
if (isCancelled)
return@launch
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
handler.post { onProgress(observer) }
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = " + observer.progressMessage)
onProgress(observer)
}
} catch (e: InterruptedException) {
onError()
}
IOUtils.closeQuietly(uploadStream)
handler.post { onComplete() }
} catch (ex: Exception) {
handler.post { onError() }
onComplete()
}, { this.onError() })
} catch (e: IOException) {
onError()
}
}
private fun createDialog() {
mProgressDialog = ProgressDialog(context)
mProgressDialog.setMessage(context.getString(R.string.dialog_uploading_file, fileName))
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
mProgressDialog.setCancelable(true)
mProgressDialog.setOnCancelListener { mCancelled = true }
mProgressDialog.isIndeterminate = true
mProgressDialog.show()
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
doAsync {
uiThread {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource.size.toInt()
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
}
}
}
fun cancel() {
isCancelled = true
private fun onComplete() {
if (mCancelled)
return
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
}
}
private fun onError() {
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_file_upload_failed)
}
}
}
}
@@ -1,17 +0,0 @@
package net.syncthing.lite.utils
import android.webkit.MimeTypeMap
object MimeType {
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
private fun getFromExtension(extension: String): String {
val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return mimeType ?: DEFAULT_MIME_TYPE
}
fun getFromUrl(url: String) = getFromExtension(
MimeTypeMap.getFileExtensionFromUrl(url).toLowerCase()
)
}
@@ -3,73 +3,38 @@ package net.syncthing.lite.utils
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
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.LibraryManager
import android.provider.MediaStore
import android.util.Log
import com.google.common.base.Objects.equal
import com.google.common.base.Strings.nullToEmpty
import org.apache.commons.lang3.StringUtils.capitalize
import org.jetbrains.anko.toast
import java.io.IOException
import java.security.InvalidParameterException
import java.util.*
import java.io.File
object Util {
fun getDeviceName(): String {
val manufacturer = Build.MANUFACTURER ?: ""
val model = Build.MODEL ?: ""
val manufacturer = nullToEmpty(Build.MANUFACTURER)
val model = nullToEmpty(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"
}
fun getContentFileName(context: Context, uri: Uri): String {
context.contentResolver.query(uri, null, null, null, null, null).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) {
throw InvalidParameterException("Cursor is null or empty")
}
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
@Throws(IOException::class)
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))
}
null
fun getContentFileName(context: Context, contentUri: Uri): String {
var fileName = File(contentUri.lastPathSegment).name
if (equal(contentUri.scheme, "content")) {
context.contentResolver.query(contentUri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)!!.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
val path = cursor.getString(columnIndex)
Log.d("Main", "recovered 'content' uri real path = " + path)
fileName = File(Uri.parse(path).lastPathSegment).name
}
}
return fileName
}
}
-1
View File
@@ -1 +0,0 @@
googleplay@nutomic.com
View File
-1
View File
@@ -1 +0,0 @@
https://syncthing.net
-1
View File
@@ -1 +0,0 @@
en-GB
@@ -1,5 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

@@ -1 +0,0 @@
A browser app for Syncthing-compatible shares
-1
View File
@@ -1 +0,0 @@
Syncthing Lite
-7
View File
@@ -1,7 +0,0 @@
- update translations
- encrypt some temporarily data which is stored on disk
- update path validation
- fix crash when cleaning the cache
- fix crash of the index handler after cleaning the cache
- convert file extensions to lower case for detecting Apps to open it
- fix crash when accessing (e.g. trying to close) closed connections
@@ -1,9 +0,0 @@
<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="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
</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="#FFFFFFFF"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>
@@ -1,9 +0,0 @@
<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="M13,3h-2v10h2L13,3zM17.83,5.17l-1.42,1.42C17.99,7.86 19,9.81 19,12c0,3.87 -3.13,7 -7,7s-7,-3.13 -7,-7c0,-2.19 1.01,-4.14 2.58,-5.42L6.17,5.17C4.23,6.82 3,9.26 3,12c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.74 -1.23,-5.18 -3.17,-6.83z"/>
</vector>
@@ -1,9 +0,0 @@
<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="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
</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="#7D000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
@@ -1,9 +0,0 @@
<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>
@@ -1,9 +0,0 @@
<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="#7D000000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>
@@ -1,9 +0,0 @@
<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="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>
@@ -2,43 +2,91 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="isLoading"
type="Boolean" />
<import type="android.view.View" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:visibility="@{safeUnbox(isLoading) ? View.GONE : View.VISIBLE}"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list_view" />
<!--center content BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:divider="?android:listDivider"
android:showDividers="middle">
<ProgressBar
android:visibility="@{safeUnbox(isLoading) ? View.VISIBLE : View.GONE}"
android:layout_centerInParent="true"
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!--index loading progress BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18sp"
android:textColor="@color/white_on_primary"
android:text="@string/index_update_progress_message"
android:layout_gravity="start"
android:textAlignment="gravity"
/>
</LinearLayout>
<!--index loading progress END-->
<android.support.design.widget.FloatingActionButton
android:id="@+id/main_list_view_upload_here_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:src="@drawable/ic_file_upload_white_24dp"/>
<!--main list view BEGIN-->
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/list_view"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="center"
android:text="@string/folder_list_empty_message"
android:textSize="20sp"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone"/>
<!--main list view END-->
</LinearLayout>
<!--center content END-->
<!--upload here overlay button BEGIN-->
<android.support.design.widget.FloatingActionButton
android:id="@+id/main_list_view_upload_here_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:src="@drawable/ic_file_upload_white_24dp"/>
<!--upload here overlay button END-->
</RelativeLayout>
@@ -0,0 +1,22 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/directory_empty" />
</FrameLayout>
</LinearLayout>
+29 -2
View File
@@ -5,12 +5,40 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18sp"
android:textColor="@color/white_on_primary"
android:text="@string/index_update_progress_message"
android:layout_gravity="start"
android:textAlignment="gravity"
/>
</LinearLayout>
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<android.support.design.widget.NavigationView
android:id="@+id/navigation"
android:layout_width="wrap_content"
@@ -18,7 +46,6 @@
android:layout_gravity="start"
android:background="@android:color/white"
app:menu="@menu/drawer_view" />
</android.support.v4.widget.DrawerLayout>
</layout>
@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/device_id"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clickable="true"
android:drawableEnd="@drawable/ic_content_copy_black_24dp"
android:focusable="true"
android:fontFamily="monospace"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
tools:text="ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD"/>
<TextView
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="8dp"
android:drawableEnd="@drawable/ic_share_black_24dp"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<ViewFlipper
android:id="@+id/flipper"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter" />
</ViewFlipper>
</LinearLayout>
</ScrollView>
</layout>
-35
View File
@@ -1,35 +0,0 @@
<?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>
@@ -1,36 +0,0 @@
<?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>
+17 -10
View File
@@ -4,20 +4,27 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:padding="@dimen/abc_action_bar_content_inset_material"
android:theme="?alertDialogTheme"
android:orientation="horizontal"
android:gravity="center">
android:orientation="vertical">
<ProgressBar
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp" />
android:orientation="horizontal"
android:gravity="center">
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/abc_action_bar_content_inset_material" />
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
+20 -32
View File
@@ -2,44 +2,32 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="isEmpty"
type="Boolean" />
<import type="android.view.View" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<TextView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/devices_list_view_empty_message"
android:textSize="20sp"
android:visibility="gone" />
<uk.co.markormesher.android_fab.FloatingActionButton
android:id="@+id/fab"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list"
android:divider="@color/divider"
android:dividerHeight="2dp">
</android.support.v7.widget.RecyclerView>
<TextView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/devices_list_view_empty_message"
android:textSize="20sp"
android:visibility="@{safeUnbox(isEmpty) ? View.VISIBLE : View.GONE}" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/add_device"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
app:useCompatPadding="true"
android:src="@drawable/ic_add_white_24dp" />
app:buttonIcon="@drawable/ic_add_white_24dp"
app:buttonBackgroundColour="@color/accent"/>
</RelativeLayout>
+17 -60
View File
@@ -2,69 +2,26 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="isEmpty"
type="Boolean" />
<import type="android.view.View" />
<variable
name="listeningPortTaken"
type="Boolean" />
</data>
<LinearLayout
android:orientation="vertical"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_weight="1"
<ListView
android:layout_width="match_parent"
android:layout_height="0dp">
android:layout_height="match_parent"
android:id="@+id/list"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<TextView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/folder_list_empty_message"
android:textSize="20sp"
android:visibility="gone" />
<android.support.v7.widget.RecyclerView
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list"
android:visibility="@{safeUnbox(isEmpty) ? View.GONE : View.VISIBLE}" />
</FrameLayout>
<TextView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/folder_list_empty_message"
android:textSize="20sp"
android:visibility="@{safeUnbox(isEmpty) ? View.VISIBLE : View.GONE}" />
</FrameLayout>
<LinearLayout
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:background="?colorPrimary"
android:visibility="@{safeUnbox(listeningPortTaken) ? View.VISIBLE : View.GONE}"
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/other_syncthing_instance_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/other_syncthing_instance_message"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
</layout>
</layout>
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="listeningPortTaken"
type="Boolean" />
<import type="android.view.View" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/intro_primary"
android:padding="28dp"
android:gravity="center_horizontal">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:textColor="#eee"
android:textSize="24sp"
android:text="@string/intro_page_one_title"/>
<ImageView
android:layout_width="160dp"
android:layout_height="0dp"
android:layout_weight="1"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
android:textSize="16sp"
android:textAlignment="center"
android:textColor="#eee"
android:text="@string/intro_page_one_description" />
<TextView
android:visibility="@{safeUnbox(listeningPortTaken) ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="#eee"
android:text="@string/other_syncthing_instance_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_marginBottom="48dp"
android:visibility="@{safeUnbox(listeningPortTaken) ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="#eee"
android:text="@string/other_syncthing_instance_message"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/intro_primary"
android:padding="28dp"
android:gravity="center_horizontal">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:textColor="#eee"
android:textSize="24sp"
android:text="@string/intro_page_three_title"/>
<ProgressBar
android:layout_width="72dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
android:textSize="16sp"
android:textAlignment="center"
android:textColor="#eee"
tools:text="@string/intro_page_three_description" />
</LinearLayout>
</layout>
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/intro_primary"
android:padding="28dp"
android:gravity="center_horizontal">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:textColor="#eee"
android:textSize="24sp"
android:text="@string/intro_page_two_title"/>
<FrameLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp">
<ScrollView
android:layout_gravity="center"
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:id="@+id/enter_device_id"
layout="@layout/view_enter_device_id" />
<LinearLayout
android:id="@+id/found_devices"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--
Found device ids will be put here as buttons
This does not use an ListView or RecyclerView because this allows using
wrap_content as height and because it's expected to be an small list
-->
</LinearLayout>
</LinearLayout>
</ScrollView>
</FrameLayout>
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
android:textSize="16sp"
android:textAlignment="center"
android:textColor="#eee"
android:text="@string/intro_page_two_description" />
</LinearLayout>
</layout>
@@ -0,0 +1,13 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:gravity="center_vertical"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:minHeight="48dp" />
</layout>
+24 -52
View File
@@ -1,65 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="name"
type="String" />
<variable
name="status"
type="String" />
<variable
name="isConnected"
type="Boolean" />
</data>
<LinearLayout
android:orientation="horizontal"
android:background="?selectableItemBackground"
android:padding="8dp"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingTop="12dp">
<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:src="@drawable/ic_laptop_green_24dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
<View
android:layout_width="16dp"
android:layout_height="0dp" />
<LinearLayout
android:orientation="vertical"
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content">
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"/>
<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>
</RelativeLayout>
</layout>
+1 -19
View File
@@ -2,21 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="fileName"
type="String" />
<variable
name="fileSize"
type="String" />
<import type="android.view.View" />
<import type="android.text.TextUtils" />
</data>
<RelativeLayout
android:background="?selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
@@ -33,8 +19,6 @@
android:layout_alignParentTop="true" />
<TextView
tools:text="Test Directory"
android:text="@{fileName}"
android:id="@+id/file_label"
android:maxLines="1"
android:layout_width="wrap_content"
@@ -45,9 +29,7 @@
android:textSize="22sp" />
<TextView
android:visibility="@{TextUtils.isEmpty(fileSize) ? View.GONE : View.VISIBLE}"
tools:text="250 MB"
android:text="@{fileSize}"
android:id="@+id/file_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
+17 -47
View File
@@ -1,78 +1,48 @@
<?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="folderName"
type="String" />
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<variable
name="lastModification"
type="String" />
<variable
name="info"
type="String" />
<variable
name="info2"
type="String" />
<import type="android.view.View" />
<import type="android.text.TextUtils" />
</data>
<LinearLayout
android:orientation="vertical"
android:background="?selectableItemBackground"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingTop="12dp">
<TextView
tools:text="Music"
android:text="@{folderName}"
android:id="@+id/folder_name_view"
android:id="@+id/folder_name"
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"/>
<TextView
tools:text="Last modified: two minutes ago"
android:text="@{lastModification}"
android:id="@+id/folder_lastmod_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp" />
android:layout_below="@id/folder_name"
android:textSize="14sp"
android:layout_alignParentStart="true" />
<TextView
tools:text="Additional information"
android:text="@{info}"
android:id="@+id/folder_content_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp" />
android:layout_below="@id/folder_lastmod_info"
android:textSize="14sp"
android:layout_alignParentStart="true" />
<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" />
</LinearLayout>
</RelativeLayout>
</layout>
</layout>
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<android.support.design.widget.TextInputLayout
android:id="@+id/device_id_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:errorEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/device_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:inputType="textNoSuggestions|textMultiLine|textCapCharacters"
tools:text="VPNPKMK-VL2SOQN-SS5I2AB-G4BV7ZK-RO5ODEE-Y2G3CZ4-C4FUW4P-ZEMJOAF"/>
</android.support.design.widget.TextInputLayout>
<ImageButton
android:id="@+id/scan_qr_code"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:padding="8dp"
android:layout_marginBottom="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_qr_code_black_24dp"
android:background="?android:selectableItemBackgroundBorderless"/>
</LinearLayout>
</layout>
+4 -9
View File
@@ -14,15 +14,10 @@
android:title="@string/devices_label" />
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings_gray_24dp"
android:title="@string/settings" />
<item
android:id="@+id/device_id"
android:icon="@drawable/ic_qr_code_black_24dp"
android:title="@string/show_device_id"
android:checkable="false" />
android:id="@+id/update_index"
android:icon="@drawable/ic_refresh_gray_24dp"
android:title="@string/update_remote_index_label"
android:checkable="false"/>
<item
android:id="@+id/clear_index"
@@ -1,5 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

+24 -55
View File
@@ -1,68 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Kein Ordner verfügbar</string>
<string name="index_update_progress_message">Index wird aktualisiert...</string>
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
<string name="update_remote_index_label">Index aktualisieren</string>
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
<string name="invalid_device_id">Fehler: Ungültige Geräte ID</string>
<string name="toast_write_storage_permission_required">Schreibrechte werden für diese Funktion benötigt</string>
<string name="scan_qr_code">QR code scannen</string>
<string name="enter_device_id">Geräte ID eingeben</string>
<string name="invalid_device_id">Ungültige Geräte ID</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
<string name="toast_index_update_successful">Index erfolgreich aktualisiert</string>
<string name="toast_index_update_failed">Index update für %1$d Geräte fehlgeschlagen</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>
<string name="toast_file_upload_failed">Hochladen gescheitert</string>
<string name="toast_upload_complete">Hochladen erfolgreich</string>
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
<string name="directory_empty">Ordner ist leer</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 %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="index_update_folder">Index aktualisierung, Ordner</string>
<string name="index_update_percent_synchronized">% synchronisiert</string>
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
<string name="last_modified">- zuletzt modifiziert</string>
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</string>
<string name="last_modified_known">zuletzt modifiziert:</string>
<string name="remove_device_title">Gerät entfernen:</string>
<string name="remove_device_body_1">Gerät</string>
<string name="remove_device_body_2">von den bekannten Geräten entfernen?</string>
<string name="device_import_success">Gerät erfolgreich importiert:</string>
<string name="device_already_known">Gerät ist bereits bekannt:</string>
<string name="folders_label">Ordner</string>
<string name="devices_label">Geräte</string>
<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>
<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äte Namen</string>
<string name="settings_local_device_summary">Name, den das andere Gerät für dieses Gerät sehen wird</string>
<string name="settings_force_stop">Beenden dieser App erzwingen</string>
<string name="settings_last_error_title">Letzter Fehler</string>
<string name="settings_last_error_summary">Details des letzten Fehlers anzeigen</string>
<string name="settings_report_bug_title">Einen Fehler melden</string>
<string name="settings_report_bug_summary">Den Bugtracker bei GitHub für diese App öffnen</string>
<string name="copy_to_clipboard">In die Zwischenablage kopieren</string>
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
<string name="dialog_warning_reconnect_problem">
Aufgrund des Verhaltens dieser App und des Verhaltens des Syncthing-Servers können Sie sich für einige Minuten nicht verbinden, wenn die App erzwungen beendet wurde (durch das Entfernen aus der Liste der aktiven Apps) oder die Verbindung unterbrochen wurde.
Dies gilt nicht für Verbindungen, die per lokaler Gerätesuche hergestellt wurden.</string>
<string name="dialog_file_save_as">Speichern unter</string>
<string name="pending_index_updates">%d Index-Updates verbleibend</string>
<string name="device_status_connecting">Verbinden mit %s</string>
<string name="device_status_connected">Mit %s verbunden</string>
<string name="device_status_disconnected">Verbinden wird bald erneut versucht - es sind %d Adressen bekannt</string>
<string name="device_status_no_address">Keine bekannte Adresse für dieses Gerät</string>
<string name="dialog_enable_folder_sync_for_new_device_title">Ordnersynchronisation für neues Gerät aktivieren</string>
<string name="dialog_enable_folder_sync_for_new_device_text">Möchten Sie den Ordner %1$s mit %2$s (%3$s) synchronisieren?</string>
<string name="dialog_enable_folder_sync_for_new_device_positive">synchronisieren</string>
<string name="dialog_enable_folder_sync_for_new_device_negative">nicht synchronisieren</string>
<string name="dialog_folder_info_device_list">Ordner teilen mit:</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
<string name="toast_error">Etwas in Syncthing Lite hat nicht funktioniert. Sie können die Details in den Einstellungen von Syncthing Lite anzeigen.</string>
</resources>
</resources>
-53
View File
@@ -1,53 +0,0 @@
<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="settings_shutdown_delay_summary">Tiempo antes de apagar el cliente Syncthing después de su último uso</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>
-51
View File
@@ -1,51 +0,0 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<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="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>
<string name="toast_file_upload_failed">Échec de l\'upload</string>
<string name="toast_upload_complete">Upload du fichier terminé</string>
<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>
<string name="devices_label">Appareils</string>
<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="settings">Réglages</string>
<string name="settings_app_version_title">Version d\'application</string>
<string name="settings_local_device_name">Nom local de l\'appareil</string>
<string name="settings_local_device_summary">Le nom que les autres appareils verront pour cet appareil</string>
<string name="settings_shutdown_delay_title">Délai d\'arrêt</string>
<string name="settings_shutdown_delay_summary">Délai avant d\'arrêter le client Syncthing après sa dernière utilisation</string>
<string name="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_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
-51
View File
@@ -1,51 +0,0 @@
<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="settings_shutdown_delay_summary">A Syncthing leállítása ennyi idő elteltével a kliens utolsó csatlakozása után</string>
<string name="device_id_dialog_title">Eszközazonosító megadása</string>
<string name="settings_shutdown_delay_10_seconds">10 másodperc</string>
<string name="settings_shutdown_delay_30_seconds">30 másodperc</string>
<string name="settings_shutdown_delay_1_minute">1 perc</string>
<string name="settings_shutdown_delay_5_minutes">5 perc</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
-51
View File
@@ -1,51 +0,0 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<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="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>
<string name="toast_upload_complete">Caricamento file completato</string>
<string name="dialog_uploading_file">Caricamento del file %1$s</string>
<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>
<string name="devices_label">Dispositivi</string>
<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>
<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="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="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_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
-45
View File
@@ -1,45 +0,0 @@
<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="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>
<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="device_id_dialog_title">デバイス ID を入力</string>
<string name="dialog_folder_info_device_list_item">%1$s (%2$s)</string>
</resources>
@@ -1,45 +0,0 @@
<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>
@@ -1,2 +0,0 @@
<resources>
</resources>
-45
View File
@@ -1,45 +0,0 @@
<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>

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