42 Commits

Author SHA1 Message Date
Felix Ableitner 3b9ba7d33b Version 0.2.0 2018-02-08 02:43:32 +09:00
Felix Ableitner c6950493e6 Fixed lint issues 2018-02-08 02:43:14 +09:00
Felix Ableitner b71740d044 Imported translations 2018-02-08 02:28:32 +09:00
Felix Ableitner dafe262c1c Fixed crash when cancelling file upload/download 2018-02-08 02:25:36 +09:00
Felix Ableitner acb1f75c5c Rewrite SyncthingClient API 2018-02-08 02:12:30 +09:00
Poussinou 03cc4f931d Update README.md (#26) 2018-02-07 02:47:27 +09:00
Felix Ableitner 967d65b3f9 Change min sdk to 21 2018-02-05 02:38:52 +09:00
Felix Ableitner ce6e7e2130 Move index updates to library, some bug fixes 2018-02-02 13:14:35 +09:00
Felix Ableitner 809eff7354 Fixed bug that prevented devices from being deleted 2018-02-01 16:33:52 +09:00
Felix Ableitner 944cecce1f Use snackbar to show index updates 2018-02-01 14:20:16 +09:00
Felix Ableitner 31abff58c1 Implement Storage Provider 2018-02-01 11:11:01 +09:00
Felix Ableitner ef401378e2 Version 0.1.5 2018-01-29 23:08:08 +09:00
Felix Ableitner 2c0be54e61 Imported translations 2018-01-29 23:08:08 +09:00
Felix Ableitner cb4b838082 Rewrite configuration 2018-01-29 21:53:54 +09:00
Felix Ableitner 6a6b40a89d Fix file uploads, use system chooser for uploads 2018-01-28 21:30:11 +09:00
Felix Ableitner 1b7dbd91e6 Version 0.1.4 2018-01-27 17:31:27 +09:00
Felix Ableitner cb7a2d362f Imported translations 2018-01-27 17:31:27 +09:00
Felix Ableitner ef2d7fe9d7 Updated release script 2018-01-27 17:30:51 +09:00
Felix Ableitner 0bd96302e0 Adjust for library changes 2018-01-27 17:10:31 +09:00
Felix Ableitner e0a95a0314 Added transifex link to readme 2018-01-27 05:36:48 +09:00
Felix Ableitner c2e7f7cbc2 Add Transifex integration 2018-01-27 00:09:38 +09:00
Felix Ableitner 631a2a4fe3 Fixed some crashes during index update 2018-01-25 17:23:29 +09:00
Felix Ableitner 36e54f5d24 Implement proper string formatting 2018-01-23 16:40:11 +09:00
Felix Ableitner 3506df6b22 Code cleanup 2018-01-22 22:06:47 +09:00
Felix Ableitner 2e1369d9a8 Update dependencies 2018-01-22 01:52:13 +09:00
Felix Ableitner a3495784f7 Version 0.1.3 2018-01-18 01:36:17 +09:00
Felix Ableitner 82c92c9031 Use FileProvider to share downloaded files (fixes #13) 2018-01-18 01:15:59 +09:00
Felix Ableitner bd5d89c158 Adjust to library changes 2018-01-18 00:38:20 +09:00
Felix Ableitner 9cf96d86dd Improve LibraryHandler to prevent various crashes (fixes #8) 2018-01-09 14:51:15 +09:00
Felix Ableitner f4c1e6a0f0 Fixed crash related to loading dialog (ref #11) 2018-01-09 03:32:57 +09:00
Felix Ableitner 036d3846bc Added release scripts 2018-01-04 16:39:52 +09:00
Felix Ableitner 6696f0ff88 Version 0.1.2 2018-01-04 16:28:10 +09:00
Felix Ableitner ffff6a7617 Improve folder browser code 2018-01-04 14:39:52 +09:00
Felix Ableitner 3bd1f6e44a Better error logging 2018-01-04 14:20:27 +09:00
Patrick S a2f46b358d Externalized strings + german translation (#10)
* externalized some strings

* externalized some more strings

* Yay! More strings externalized

* externalized strings

* translated to german

* translation finished

* fixed mistake

* fixed compile error

* externalized strings
translated said strings into german

* finishing touches

* even more finishing touches

* finishing touches

* fixed missing space

* added space

* Revert "fixed compile error"

This reverts commit 0225ce6d79.
2018-01-04 14:19:32 +09:00
ImPat a0e749e879 Various fixes related to capitalization and added some question marks and a space (#9)
* fixed missing space and capitalized two word

* added capitalization

* Added Captialization
2017-12-31 13:28:04 +09:00
Felix Ableitner e566b3c58d Minor fixes 2017-12-30 01:08:45 +09:00
Felix Ableitner acff8c1f5c Simplify code with anko library 2017-12-29 14:07:05 +09:00
Felix Ableitner 4353fc5597 Move library initialization to seperate class 2017-12-29 04:35:28 +09:00
Felix Ableitner 64b2b7424b Fix screen rotation in MainActivity (fixes #7) 2017-12-29 00:24:25 +09:00
Felix Ableitner 2fa0dd2e59 Clarified build instructions 2017-12-28 17:32:43 +09:00
Felix Ableitner 2e1fa76b44 Use own activity for upload file picker 2017-12-27 13:30:23 +09:00
47 changed files with 1177 additions and 901 deletions
+9
View File
@@ -0,0 +1,9 @@
[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
+9 -2
View File
@@ -13,15 +13,22 @@ example, mobile devices with limited storage available, wishing to access a sync
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
[<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-android/syncthing-lite/).
## Building
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
you install [Android Studio][4] and import the project.
The syncthing-java library is not stable yet. If you encounter any build errors, you probably have
to build it from source. To do this, clone the repo and run `gradle install`.
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][5].
+31 -9
View File
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.ben-manes.versions'
android {
compileSdkVersion 27
@@ -8,10 +9,10 @@ android {
dataBinding.enabled = true
defaultConfig {
applicationId "net.syncthing.lite"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 25
versionCode 3
versionName "0.1.1"
versionCode 7
versionName "0.2.0"
multiDexEnabled true
}
sourceSets {
@@ -21,26 +22,47 @@ 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 {
release {
signingConfig signingConfigs.release
}
}
packagingOptions {
exclude 'META-INF/*'
}
}
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"
kapt "com.android.databinding:compiler:$build_tools_version"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:recyclerview-v7:$support_version"
implementation "com.android.support:support-v4:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:cardview-v7:$support_version"
implementation ("com.github.Nutomic:syncthing-java:0.1") {
implementation ("com.github.Nutomic:syncthing-java:0.2.0") {
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'
}
// NOTE: httpclient-android seems to be used via reflection somehow. Removing this dependency
// silently breaks the app.
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.nononsenseapps:filepicker:2.5.2'
implementation 'uk.co.markormesher:android-fab:2.0.0'
implementation ('uk.co.markormesher:android-fab:2.0.0') {
exclude group: "org.jetbrains.kotlin"
}
}
+10
View File
@@ -0,0 +1,10 @@
<?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>
+18 -9
View File
@@ -2,7 +2,6 @@
<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:allowBackup="true"
@@ -20,15 +19,25 @@
</activity>
<activity android:name=".activities.FolderBrowserActivity"
android:parentActivityName=".activities.MainActivity"/>
<activity
android:name=".activities.MIVFilePickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="net.syncthing.lite.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<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.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</activity>
</provider>
</application>
</manifest>
</manifest>
@@ -1,221 +1,132 @@
package net.syncthing.lite.activities
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.databinding.DataBindingUtil
import android.os.AsyncTask
import android.os.Bundle
import android.os.Environment
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.Objects.equal
import com.google.common.base.Preconditions.checkArgument
import com.nononsenseapps.filepicker.FilePickerActivity
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.databinding.ActivityFolderBrowserBinding
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.utils.DownloadFileTask
import net.syncthing.lite.utils.UploadFileTask
import net.syncthing.lite.utils.FileDownloadDialog
import net.syncthing.lite.utils.FileUploadDialog
class FolderBrowserActivity : SyncthingActivity() {
companion object {
private val TAG = "FolderBrowserActivity"
private val REQUEST_WRITE_STORAGE = 142
private const val TAG = "FolderBrowserActivity"
private const val REQUEST_SELECT_UPLOAD_FILE = 171
val EXTRA_FOLDER_NAME = "folder_name"
const val EXTRA_FOLDER_NAME = "folder_name"
}
private lateinit var binding: ActivityFolderBrowserBinding
private var indexBrowser: IndexBrowser? = null
private var loadingDialog: AlertDialog? = null
private var adapter: FolderContentsAdapter? = null
private var runWhenPermissionsReceived: Runnable? = null
private lateinit var indexBrowser: IndexBrowser
private lateinit var adapter: FolderContentsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
showFolderListView(intent.getStringExtra(EXTRA_FOLDER_NAME), null)
adapter = FolderContentsAdapter(this)
binding.listView.adapter = adapter
binding.listView.setOnItemClickListener { _, _, position, _ ->
val fileInfo = binding.listView.getItemAtPosition(position) as FileInfo
navigateToFolder(fileInfo)
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
libraryHandler?.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
}
override fun onDestroy() {
super.onDestroy()
Thread {
indexBrowser?.close()
indexBrowser = null
indexBrowser.setOnFolderChangedListener(null)
indexBrowser.close()
}.start()
cancelLoadingDialog()
}
override fun onBackPressed() {
val listView = binding.mainFolderAndFilesListView
val listView = binding.listView
//click item '0', ie '..' (go to parent)
listView.performItemClick(adapter!!.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
listView.performItemClick(adapter.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent) {
if (resultCode == Activity.RESULT_OK) {
UploadFileTask(this, syncthingClient(), intent.data, indexBrowser!!.folder,
indexBrowser!!.currentPath, { this.updateFolderListView() }).uploadFile()
}
}
private fun showLoadingDialog(message: String) {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
layoutInflater, R.layout.dialog_loading, null, false)
binding.loadingText.text = message
loadingDialog = android.app.AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
}
private fun cancelLoadingDialog() {
loadingDialog?.cancel()
loadingDialog = null
}
private fun showFolderListView(folder: String, previousPath: String?) {
if (indexBrowser != null && equal(folder, indexBrowser!!.folder)) {
Log.d(TAG, "reuse current index browser")
indexBrowser!!.navigateToNearestPath(previousPath)
} else {
if (indexBrowser != null) {
indexBrowser!!.close()
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
libraryHandler?.syncthingClient { syncthingClient ->
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
{ showFolderListView(indexBrowser.currentPath) } )
}
Log.d(TAG, "open new index browser")
indexBrowser = syncthingClient().indexHandler
.newIndexBrowserBuilder()
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
.includeParentInList(true).allowParentInRoot(true)
.setFolder(folder)
.buildToNearestPath(previousPath)
}
adapter = FolderContentsAdapter(this)
binding.mainFolderAndFilesListView.adapter = adapter
binding.mainFolderAndFilesListView.setOnItemClickListener { _, _, position, _ ->
val fileInfo = binding.mainFolderAndFilesListView.getItemAtPosition(position) as FileInfo
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser!!.currentPath + "'")
navigateToFolder(fileInfo)
}
navigateToFolder(indexBrowser!!.currentPathInfo)
}
private fun showFolderListView(path: String) {
indexBrowser.navigateToNearestPath(path)
navigateToFolder(indexBrowser.currentPathInfo())
}
private fun navigateToFolder(fileInfo: FileInfo) {
if (indexBrowser!!.isRoot && PathUtils.isParent(fileInfo.path)) {
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
finish()
} else {
if (fileInfo.isDirectory) {
indexBrowser!!.navigateTo(fileInfo)
val newFileInfo = if (PathUtils.isParent(fileInfo.path)) indexBrowser!!.currentPathInfo else fileInfo
if (!indexBrowser!!.isCacheReadyAfterALittleWait) {
Log.d(TAG, "load folder cache bg")
object : AsyncTask<Void?, Void?, Void?>() {
override fun onPreExecute() {
// TODO: show ProgressBar in ListView instead of dialog
showLoadingDialog("open directory: " +
if (indexBrowser!!.isRoot) folderBrowser().getFolderInfo(indexBrowser!!.folder).label
else indexBrowser!!.currentPathFileName)
}
override fun doInBackground(vararg voids: Void?): Void? {
indexBrowser!!.waitForCacheReady()
return null
}
override fun onPostExecute(aVoid: Void?) {
Log.d(TAG, "cache ready, navigate to folder")
cancelLoadingDialog()
navigateToFolder(newFileInfo)
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
} else {
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.mainFolderAndFilesListView.setSelection(0)
supportActionBar!!.setTitle(if (indexBrowser!!.isRoot)
folderBrowser().getFolderInfo(indexBrowser!!.folder).label
else
newFileInfo.fileName)
}
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() })
libraryHandler?.syncthingClient { FileDownloadDialog(this, it, fileInfo) }
}
}
}
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)
assert(!list.isEmpty())//list must contain at least the 'parent' path
adapter.clear()
adapter.addAll(list)
adapter.notifyDataSetChanged()
binding.listView.setSelection(0)
if (indexBrowser.isRoot())
libraryHandler?.folderBrowser {
supportActionBar?.title = it.getFolderInfo(indexBrowser.folder)?.label
}
else
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
}
}
private fun updateFolderListView() {
showFolderListView(indexBrowser!!.folder, indexBrowser!!.currentPath)
showFolderListView(indexBrowser.currentPath)
}
private fun showUploadHereDialog() {
executeWithPermissions(Runnable {
val i = Intent(this, MIVFilePickerActivity::class.java)
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
Log.i(TAG, "showUploadHereDialog path = " + path)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, path)
startActivityForResult(i, 0)
})
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.mainIndexProgressBarLabel.text = ("index update, folder "
+ folder.label + " " + percentage + "% synchronized")
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
updateFolderListView()
}
override fun onIndexUpdateComplete() {
binding.mainIndexProgressBar.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,19 +0,0 @@
package net.syncthing.lite.activities
import com.nononsenseapps.filepicker.AbstractFilePickerActivity
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import net.syncthing.lite.fragments.MIVFilePickerFragment
import java.io.File
class MIVFilePickerActivity : AbstractFilePickerActivity<File>() {
override fun getFragment(startPath: String, mode: Int, allowMultiple: Boolean,
allowCreateDir: Boolean): AbstractFilePickerFragment<File> {
// Only the fragment in this line needs to be changed
val fragment = MIVFilePickerFragment()
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
return fragment
}
}
@@ -4,22 +4,22 @@ 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 android.view.View
import net.syncthing.java.core.beans.FolderInfo
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ActivityMainBinding
import net.syncthing.lite.fragments.DevicesFragment
import net.syncthing.lite.fragments.FoldersFragment
import net.syncthing.lite.utils.UpdateIndexTask
import net.syncthing.lite.fragments.SyncthingFragment
class MainActivity : SyncthingActivity() {
private lateinit var binding: ActivityMainBinding
private var drawerToggle: ActionBarDrawerToggle? = null
private var currentFragment: SyncthingFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -34,15 +34,19 @@ class MainActivity : SyncthingActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
/**
* Sync the toggle state and fragment after onRestoreInstanceState has occurred.
*/
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle!!.syncState()
}
override fun onLibraryLoaded() {
super.onLibraryLoaded()
setContentFragment(FoldersFragment())
drawerToggle!!.syncState()
val menu = binding.navigation.menu
val selection = (0 until menu.size())
.map { menu.getItem(it) }
.find { it.isChecked }
?: menu.getItem(0)
onNavigationItemSelectedListener(selection)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -57,17 +61,15 @@ class MainActivity : SyncthingActivity() {
true
} else super.onOptionsItemSelected(item)
// Handle your other action bar items...
}
private fun onNavigationItemSelectedListener(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.folders -> setContentFragment(FoldersFragment())
R.id.devices -> setContentFragment(DevicesFragment())
R.id.update_index -> UpdateIndexTask(this, syncthingClient()).updateIndex()
R.id.clear_index -> AlertDialog.Builder(this)
.setTitle("clear cache and index")
.setMessage("clear all cache data and index data?")
.setTitle(getString(R.string.clear_cache_and_index_title))
.setMessage(getString(R.string.clear_cache_and_index_body))
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.yes) { _, _ -> cleanCacheAndIndex() }
.setNegativeButton(android.R.string.no, null)
@@ -77,7 +79,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)
@@ -85,17 +88,9 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
syncthingClient().clearCacheAndIndex()
recreate()
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.mainIndexProgressBar.visibility = View.VISIBLE
binding.mainIndexProgressBarLabel.text = ("index update, folder "
+ folder.label + " " + percentage + "% synchronized")
}
override fun onIndexUpdateComplete() {
binding.mainIndexProgressBar.visibility = View.GONE
async(UI) {
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
recreate()
}
}
}
@@ -1,109 +1,67 @@
package net.syncthing.lite.activities
import android.app.AlertDialog
import android.content.Context
import android.databinding.DataBindingUtil
import android.os.AsyncTask
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.util.Log
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.databinding.DialogLoadingBinding
import net.syncthing.lite.utils.LibraryHandler
import net.syncthing.lite.utils.UpdateIndexTask
import net.syncthing.lite.library.LibraryHandler
import org.jetbrains.anko.contentView
import org.slf4j.impl.HandroidLoggerAdapter
import java.util.*
abstract class SyncthingActivity : AppCompatActivity() {
companion object {
private val TAG = "SyncthingActivity"
private var activityCount = 0
private var libraryHandler: LibraryHandler? = null
}
fun syncthingClient(): SyncthingClient = libraryHandler!!.syncthingClient!!
fun configuration(): ConfigurationService = libraryHandler!!.configuration!!
fun folderBrowser(): FolderBrowser = libraryHandler!!.folderBrowser!!
var libraryHandler: LibraryHandler? = null
private set
private var loadingDialog: AlertDialog? = null
private var snackBar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
activityCount++
if (libraryHandler == null) {
InitTask(this, this::onLibraryLoaded).execute()
}
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(this, this::onLibraryLoadedInternal,
this::onIndexUpdateProgress, this::onIndexUpdateComplete)
}
override fun onDestroy() {
super.onDestroy()
activityCount--
Thread {
if (activityCount == 0) {
libraryHandler!!.destroy()
libraryHandler = null
}
}.start()
libraryHandler?.close()
loadingDialog?.dismiss()
}
private class InitTask(val context: Context, val onLibraryLoaded: () -> Unit)
: AsyncTask<Void?, Void?, Void?>() {
private var loadingDialog: AlertDialog? = null
override fun onPreExecute() {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(context), R.layout.dialog_loading, null, false)
binding.loadingText.text = "loading config, starting syncthing client"
loadingDialog = android.app.AlertDialog.Builder(context)
.setCancelable(false)
.setView(binding.root)
.show()
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
if (!isDestroyed) {
loadingDialog?.dismiss()
}
onLibraryLoaded()
}
override fun doInBackground(vararg voidd: Void?): Void? {
libraryHandler = LibraryHandler()
libraryHandler!!.init(context)
return null
}
override fun onPostExecute(voidd: Void?) {
loadingDialog!!.cancel()
libraryHandler!!.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
onIndexUpdateProgress(folder, percentage)
}
override fun onIndexUpdateComplete() {
onIndexUpdateComplete()
}
})
//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()
}
onLibraryLoaded()
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {
val message = getString(R.string.index_update_progress_label, folderInfo.label, percentage)
snackBar?.setText(message) ?: run {
snackBar = Snackbar.make(contentView!!, message, Snackbar.LENGTH_INDEFINITE)
snackBar?.show()
}
}
open fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete() {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {
snackBar?.dismiss()
snackBar = null
}
open fun onLibraryLoaded() {}
}
@@ -6,13 +6,12 @@ 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.DeviceStats
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewDeviceBinding
class DevicesAdapter(context: Context) :
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, Lists.newArrayList()) {
ArrayAdapter<DeviceInfo>(context, R.layout.listview_device, mutableListOf()) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewDeviceBinding
@@ -24,10 +23,10 @@ class DevicesAdapter(context: Context) :
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
if (deviceStats.isConnected!!) {
R.drawable.ic_laptop_green_24dp
} else {
R.drawable.ic_laptop_red_24dp
}
binding.deviceIcon.setImageResource(icon)
return binding.root
@@ -7,14 +7,13 @@ 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
class FolderContentsAdapter(context: Context) :
ArrayAdapter<FileInfo>(context, R.layout.listview_file, Lists.newArrayList()) {
ArrayAdapter<FileInfo>(context, R.layout.listview_file, mutableListOf()) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewFileBinding =
@@ -25,15 +24,15 @@ class FolderContentsAdapter(context: Context) :
}
val fileInfo = getItem(position)
binding.fileLabel.text = fileInfo!!.fileName
if (fileInfo.isDirectory) {
if (fileInfo.isDirectory()) {
binding.fileIcon.setImageResource(R.drawable.ic_folder_black_24dp)
binding.fileSize.visibility = View.GONE
} else {
binding.fileIcon.setImageResource(R.drawable.ic_image_black_24dp)
binding.fileSize.visibility = View.VISIBLE
binding.fileSize.text = (FileUtils.byteCountToDisplaySize(fileInfo.size!!)
+ " - last modified "
+ DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.fileSize.text = context.getString(R.string.file_info,
FileUtils.byteCountToDisplaySize(fileInfo.size!!),
DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
}
return binding.root
}
@@ -11,9 +11,8 @@ 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 org.apache.commons.lang3.tuple.Pair
class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderStats>>) :
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 {
@@ -23,15 +22,13 @@ class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderSta
} 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)
"last modified: unknown"
else "last modified: " +
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"
val folderInfo = getItem(position)!!.first
val folderStats = getItem(position)!!.second
binding.folderName.text = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
binding.folderLastmodInfo.text = context.getString(R.string.last_modified_time,
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.folderContentInfo.text = context.getString(R.string.folder_content_info, folderStats.describeSize(), folderStats.fileCount, folderStats.dirCount)
return binding.root
}
@@ -5,8 +5,6 @@ import android.content.Context
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -14,26 +12,22 @@ import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.core.beans.DeviceId
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.activities.SyncthingActivity
import net.syncthing.lite.adapters.DevicesAdapter
import net.syncthing.lite.databinding.FragmentDevicesBinding
import net.syncthing.lite.utils.UpdateIndexTask
import net.syncthing.lite.utils.FragmentIntentIntegrator
import org.apache.commons.lang3.StringUtils.isBlank
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
import uk.co.markormesher.android_fab.SpeedDialMenuItem
import java.io.IOException
import java.security.InvalidParameterException
class DevicesFragment : Fragment() {
class DevicesFragment : SyncthingFragment() {
companion object {
private val TAG = "DevicesFragment"
}
private lateinit var syncthingActivity: SyncthingActivity
private lateinit var binding: FragmentDevicesBinding
private lateinit var adapter: DevicesAdapter
@@ -45,34 +39,54 @@ class DevicesFragment : Fragment() {
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
syncthingActivity = activity as SyncthingActivity
override fun onResume() {
super.onResume()
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener(this::onConnectionChanged) }
}
override fun onPause() {
super.onPause()
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener(this::onConnectionChanged) }
}
private fun onConnectionChanged(deviceId: DeviceId) {
updateDeviceList()
}
override fun onLibraryLoaded() {
initDeviceList()
updateDeviceList()
}
private fun initDeviceList() {
adapter = DevicesAdapter(syncthingActivity)
adapter = DevicesAdapter(context!!)
binding.list.adapter = adapter
binding.list.setOnItemLongClickListener { _, _, position, _ ->
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
AlertDialog.Builder(syncthingActivity)
.setTitle("remove device " + deviceId.substring(0, 7))
.setMessage("remove device" + deviceId.substring(0, 7) + " from list of known devices?")
val device = adapter.getItem(position)
AlertDialog.Builder(context)
.setTitle(getString(R.string.remove_device_title, device.name))
.setMessage(getString(R.string.remove_device_message, device.deviceId.deviceId.substring(0, 7)))
.setPositiveButton(android.R.string.yes) { _, _ ->
syncthingActivity.configuration().edit().removePeer(deviceId).persistLater() }
libraryHandler?.configuration { config ->
config.peers = config.peers.filterNot { it.deviceId == device.deviceId }.toSet()
config.persistLater()
updateDeviceList()
}
}
.setNegativeButton(android.R.string.no, null)
.show()
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
false
}
}
private fun updateDeviceList() {
adapter.clear()
adapter.addAll(syncthingActivity.syncthingClient().devicesHandler.deviceStatsList)
adapter.notifyDataSetChanged()
async(UI) {
libraryHandler?.syncthingClient { syncthingClient ->
adapter.clear()
adapter.addAll(syncthingClient.getPeerStatus())
adapter.notifyDataSetChanged()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
@@ -86,22 +100,26 @@ class DevicesFragment : Fragment() {
}
}
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
}
private fun importDeviceId(deviceIdString: String) {
libraryHandler?.configuration { configuration ->
async(UI) {
val deviceId =
try {
DeviceId(deviceIdString)
} catch (e: IOException) {
Toast.makeText(this@DevicesFragment.context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
return@async
}
val modified = syncthingActivity.configuration().edit().addPeers(DeviceInfo(deviceId, null))
if (modified) {
syncthingActivity.configuration().edit().persistLater()
Toast.makeText(context, "successfully imported device: " + deviceId, Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
UpdateIndexTask(syncthingActivity, syncthingActivity.syncthingClient()).updateIndex()
} else {
Toast.makeText(context, "device already present: " + deviceId, Toast.LENGTH_SHORT).show()
if (!configuration.peerIds.contains(deviceId)) {
configuration.peers = configuration.peers + DeviceInfo(deviceId, null)
configuration.persistLater()
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_import_success, deviceId), Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
} else {
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_already_known, deviceId), Toast.LENGTH_SHORT).show()
}
}
}
}
@@ -1,32 +1,22 @@
package net.syncthing.lite.fragments
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.activities.SyncthingActivity
import net.syncthing.lite.adapters.FoldersListAdapter
import net.syncthing.lite.databinding.FragmentFoldersBinding
import org.apache.commons.lang3.tuple.Pair
import java.util.*
import org.jetbrains.anko.intentFor
class FoldersFragment : Fragment() {
class FoldersFragment : SyncthingFragment() {
companion object {
private val TAG = "FoldersFragment"
}
private val TAG = "FoldersFragment"
private lateinit var syncthingActivity: SyncthingActivity
private lateinit var binding: FragmentFoldersBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@@ -36,24 +26,26 @@ class FoldersFragment : Fragment() {
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
syncthingActivity = activity as SyncthingActivity
override fun onLibraryLoaded() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
val list = Lists.newArrayList(syncthingActivity.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 = Intent(context, FolderBrowserActivity::class.java)
intent.putExtra(FolderBrowserActivity.EXTRA_FOLDER_NAME, folder)
startActivity(intent)
libraryHandler?.folderBrowser { folderBrowser ->
val list = folderBrowser.folderInfoAndStatsList()
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)!!.first.folderId
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
startActivity(intent)
}
}
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
showAllFoldersListView()
}
}
@@ -1,31 +0,0 @@
package net.syncthing.lite.fragments
import android.view.View
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import com.nononsenseapps.filepicker.FilePickerFragment
import java.io.File
class MIVFilePickerFragment : FilePickerFragment() {
override fun onClickCheckable(v: View, vh: AbstractFilePickerFragment<File>.CheckableViewHolder) {
// auto open file on click
if (!allowMultiple) {
// Clear is necessary, in case user clicked some checkbox directly
mCheckedItems.clear()
mCheckedItems.add(vh.file)
onClickOk(null)
} else {
super.onClickCheckable(v, vh)
}
}
// private static final String EXTENSION = ".*[.](jpg|png|jpeg)";
override fun isItemVisible(file: File): Boolean {
// return isDir(file) || file.getName().toLowerCase().matches(EXTENSION);
return true
}
}
@@ -0,0 +1,34 @@
package net.syncthing.lite.fragments
import android.os.Bundle
import android.support.v4.app.Fragment
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.library.LibraryHandler
abstract class SyncthingFragment : Fragment() {
var libraryHandler: LibraryHandler? = null
private set
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
this::onIndexUpdateComplete)
}
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
onLibraryLoaded()
}
override fun onDestroy() {
super.onDestroy()
libraryHandler?.close()
}
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
}
@@ -0,0 +1,49 @@
package net.syncthing.lite.library
import android.content.Context
import android.util.Log
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
class DownloadFileTask(private val context: Context, syncthingClient: SyncthingClient,
private val fileInfo: FileInfo,
private val onProgress: (DownloadFileTask, BlockPuller.FileDownloadObserver) -> Unit,
private val onComplete: (File) -> Unit,
private val onError: () -> Unit) {
private val Tag = "DownloadFileTask"
private var isCancelled = false
init {
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
val observer = blockPuller.pullFile(fileInfo)
onProgress(this, observer)
try {
while (!observer.isCompleted()) {
if (isCancelled)
return@getBlockPuller
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage())
onProgress(this, observer)
}
val outputFile = File("${context.externalCacheDir}/${fileInfo.folder}/${fileInfo.path}")
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
Log.i(Tag, "Downloaded file $fileInfo")
onComplete(outputFile)
} catch (e: IOException) {
onError()
Log.w(Tag, "Failed to download file $fileInfo", e)
}
}, { onError() })
}
fun cancel() {
isCancelled = true
}
}
@@ -0,0 +1,151 @@
package net.syncthing.lite.library
import android.content.Context
import android.os.Handler
import android.util.Log
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.lite.utils.Util
import org.jetbrains.anko.doAsync
import java.util.*
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit) {
companion object {
private var instanceCount = 0
private var configuration: Configuration? = null
private var syncthingClient: SyncthingClient? = null
private var folderBrowser: FolderBrowser? = null
private val callbacks = ArrayList<(Configuration, SyncthingClient, FolderBrowser) -> Unit>()
private var isLoading = false
}
private val TAG = "LibraryHandler"
init {
instanceCount++
if (configuration == null && !isLoading) {
isLoading = true
doAsync {
init(context)
async(UI) {
onLibraryLoaded(this@LibraryHandler)
}
isLoading = false
}
} else {
onLibraryLoaded(this)
}
syncthingClient {
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
}
}
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
async(UI) {
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
}
}
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
async(UI) {
onIndexUpdateCompleteListener(folderInfo)
}
}
private fun init(context: Context) {
val configuration = Configuration(configFolder = context.filesDir, cacheFolder = context.externalCacheDir)
configuration.localDeviceName = Util.getDeviceName()
configuration.persistLater()
val syncthingClient = SyncthingClient(configuration)
//TODO listen for device events, update device list
val folderBrowser = syncthingClient.indexHandler.newFolderBrowser()
if (instanceCount == 0) {
Log.d(TAG, "All LibraryHandler instances were closed during init")
syncthingClient.close()
folderBrowser.close()
}
async(UI) {
callbacks.forEach { it(configuration, syncthingClient, folderBrowser) }
}
LibraryHandler.configuration = configuration
LibraryHandler.syncthingClient = syncthingClient
LibraryHandler.folderBrowser = folderBrowser
}
private fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
// https://stackoverflow.com/a/35522422/1837158
fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? {
return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
}
safeLet(configuration, syncthingClient, folderBrowser) { c, s, f ->
callback(c, s, f)
} ?: run {
if (isLoading) {
callbacks.add(callback)
}
}
}
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) }
}
/**
* Unregisters index update listener and decreases instance count.
*
* We wait a bit before closing [[syncthingClient]] etc, in case LibraryHandler is opened again
* soon (eg in case of device rotation).
*/
fun close() {
syncthingClient {
try {
it.indexHandler.unregisterOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.unregisterOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
} catch (e: IllegalArgumentException) {
// ignored, no idea why this is thrown
}
}
instanceCount--
Handler().postDelayed({
Thread {
if (instanceCount == 0) {
folderBrowser?.close()
folderBrowser = null
syncthingClient?.close()
syncthingClient = null
configuration = null
}
}.start()
}, 60 * 1000)
}
}
@@ -0,0 +1,151 @@
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 net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import java.io.File
import java.io.FileNotFoundException
import java.net.URLConnection
import java.util.concurrent.CountDownLatch
class SyncthingProvider : DocumentsProvider() {
companion object {
private const val Tag = "SyncthingProvider"
private val DefaultRootProjection = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON)
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
}
private fun getLibraryHandler(): LibraryHandler {
val latch = CountDownLatch(1)
val libraryHandler = LibraryHandler(context, { latch.countDown() }, { _, _ -> }, {})
latch.await()
return libraryHandler
}
override fun queryRoots(projection: Array<String>?): Cursor {
Log.d(Tag, "queryRoots($projection)")
val latch = CountDownLatch(1)
var folders: List<Pair<FolderInfo, FolderStats>>? = null
getLibraryHandler().folderBrowser { folderBrowser ->
folders = folderBrowser.folderInfoAndStatsList()
latch.countDown()
}
latch.await()
val result = MatrixCursor(projection ?: DefaultRootProjection)
folders!!.forEach { folder ->
val row = result.newRow()
row.add(Root.COLUMN_ROOT_ID, folder.first.folderId)
row.add(Root.COLUMN_SUMMARY, folder.first.label)
row.add(Root.COLUMN_FLAGS, 0)
row.add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.first))
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
}
return result
}
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
sortOrder: String?): Cursor {
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
getIndexBrowser(getFolderIdForDocId(parentDocumentId))
.listFiles(getPathForDocId(parentDocumentId))
.forEach { fileInfo ->
includeFile(result, fileInfo)
}
return result
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
Log.d(Tag, "queryDocument($documentId, $projection)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
includeFile(result, fileInfo)
return result
}
@Throws(FileNotFoundException::class)
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?):
ParcelFileDescriptor {
Log.d(Tag, "openDocument($documentId, $mode, $signal)")
val fileInfo = FileInfo(folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId), type = FileInfo.FileType.FILE)
val accessMode = ParcelFileDescriptor.parseMode(mode)
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
throw NotImplementedError()
}
val latch = CountDownLatch(1)
var outputFile: File? = null
getLibraryHandler().syncthingClient { syncthingClient ->
DownloadFileTask(context, syncthingClient, fileInfo,
{ t, _ -> if (signal?.isCanceled == true) t.cancel() }, {
outputFile = it
latch.countDown()
}, {})
}
latch.await()
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
val row = result.newRow()
row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
row.add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
row.add(Document.COLUMN_SIZE, fileInfo.size)
val mime = if (fileInfo.isDirectory()) Document.MIME_TYPE_DIR
else URLConnection.guessContentTypeFromName(fileInfo.fileName)
row.add(Document.COLUMN_MIME_TYPE, mime)
row.add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
row.add(Document.COLUMN_FLAGS, 0)
}
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
private fun getIndexBrowser(folderId: String): IndexBrowser {
val latch = CountDownLatch(1)
var indexBrowser: IndexBrowser? = null
getLibraryHandler().syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folderId)
latch.countDown()
}
latch.await()
return indexBrowser!!
}
}
@@ -0,0 +1,48 @@
package net.syncthing.lite.library
import android.content.Context
import android.net.Uri
import android.util.Log
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.utils.Util
import org.apache.commons.io.IOUtils
// TODO: this should be an IntentService with notification
class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
localFile: Uri, private val syncthingFolder: String,
syncthingSubFolder: String,
private val onProgress: (BlockPusher.FileUploadObserver) -> Unit,
private val onComplete: () -> Unit,
private val onError: () -> Unit) {
private val TAG = "UploadFileTask"
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
private val uploadStream = context.contentResolver.openInputStream(localFile)
private var isCancelled = false
init {
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
onProgress(observer)
while (!observer.isCompleted()) {
if (isCancelled)
return@getBlockPusher
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
onProgress(observer)
}
IOUtils.closeQuietly(uploadStream)
onComplete()
}, { onError() })
}
fun cancel() {
isCancelled = true
}
}
@@ -1,106 +0,0 @@
package net.syncthing.lite.utils
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.os.Handler
import android.support.annotation.StringRes
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import java.io.File
import java.io.IOException
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient, private val mFileInfo: FileInfo) {
private val mMainHandler: Handler = Handler()
private lateinit var progressDialog: ProgressDialog
private var cancelled = false
fun downloadFile() {
showDialog()
// TODO: can just pass FileInfo directly?
Thread {
mSyncthingClient.pullFile(mFileInfo.folder, mFileInfo.path, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted) {
if (cancelled)
return@pullFile
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage)
onProgress(observer)
}
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) }
}.start()
}
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()
}
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
mMainHandler.post {
progressDialog.isIndeterminate = false
progressDialog.max = (mFileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress * mFileInfo.size!!).toInt()
}
}
private fun onComplete(file: File) {
progressDialog.dismiss()
if (cancelled)
return
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(file), mimeType)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
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) {
progressDialog.dismiss()
mMainHandler.post { Toast.makeText(mContext, error, Toast.LENGTH_SHORT).show() }
}
companion object {
private val TAG = "DownloadFileTask"
}
}
@@ -0,0 +1,82 @@
package net.syncthing.lite.utils
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.support.v4.content.FileProvider
import android.util.Log
import android.webkit.MimeTypeMap
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import net.syncthing.lite.library.DownloadFileTask
import org.apache.commons.io.FilenameUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.newTask
import org.jetbrains.anko.toast
import java.io.File
class FileDownloadDialog(context: Context, syncthingClient: SyncthingClient,
private val fileInfo: FileInfo) : AlertDialog(context) {
private val Tag = "FileDownloadDialog"
private lateinit var progressDialog: ProgressDialog
private var downloadFileTask: DownloadFileTask? = null
init {
showDialog()
doAsync {
downloadFileTask = DownloadFileTask(context, syncthingClient, fileInfo,
this@FileDownloadDialog::onProgress, this@FileDownloadDialog::onComplete,
this@FileDownloadDialog::onError)
}
}
private fun showDialog() {
progressDialog = ProgressDialog(context)
progressDialog.setMessage(context.getString(R.string.dialog_downloading_file, fileInfo.fileName))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { downloadFileTask?.cancel() }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(downloadFileTask: DownloadFileTask, fileDownloadObserver: BlockPuller.FileDownloadObserver) {
async(UI) {
progressDialog.isIndeterminate = false
progressDialog.max = (fileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress() * fileInfo.size!!).toInt()
}
}
private fun onComplete(file: File) {
async(UI) {
progressDialog.dismiss()
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
val uri = FileProvider.getUriForFile(this@FileDownloadDialog.context, "net.syncthing.lite.fileprovider", file)
intent.setDataAndType(uri, mimeType)
intent.newTask()
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
this@FileDownloadDialog.context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
this@FileDownloadDialog.context.toast(R.string.toast_open_file_failed)
Log.w(Tag, "No handler found for file " + file.name, e)
}
}
}
private fun onError() {
async(UI) {
progressDialog.cancel()
this@FileDownloadDialog.context.toast(R.string.toast_file_download_failed)
}
}
}
@@ -0,0 +1,64 @@
package net.syncthing.lite.utils
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import net.syncthing.lite.library.UploadFileTask
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,
syncthingSubFolder: String,
private val onUploadCompleteListener: () -> Unit) {
private lateinit var progressDialog: ProgressDialog
private var uploadFileTask: UploadFileTask? = null
init {
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) {
async(UI) {
progressDialog.isIndeterminate = false
progressDialog.progress = observer.progressPercentage()
progressDialog.max = 100
}
}
private fun onComplete() {
async(UI) {
progressDialog.dismiss()
this@FileUploadDialog.context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
private fun onError() {
async(UI) {
progressDialog.dismiss()
this@FileUploadDialog.context.toast(R.string.toast_file_upload_failed)
}
}
}
@@ -1,4 +1,4 @@
package net.syncthing.lite.fragments
package net.syncthing.lite.utils
import android.content.Intent
import android.support.v4.app.Fragment
@@ -1,84 +0,0 @@
package net.syncthing.lite.utils
import android.content.Context
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.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.java.core.security.KeystoreHandler
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
class LibraryHandler {
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()
}
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 setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
mOnIndexUpdatedListener = onIndexUpdatedListener
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
@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())
}
@Subscribe
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
}
})
}
fun destroy() {
folderBrowser!!.close()
syncthingClient!!.close()
configuration!!.close()
}
companion object {
private val TAG = "LibConnectionHandler"
}
}
@@ -1,43 +0,0 @@
package net.syncthing.lite.utils
import android.content.Context
import android.os.Handler
import android.preference.PreferenceManager
import android.widget.Toast
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import java.util.*
class UpdateIndexTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient) {
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(mContext)
private val mMainHandler = Handler()
fun updateIndex() {
if (sIndexUpdateInProgress)
return
sIndexUpdateInProgress = true
mSyncthingClient.updateIndexFromPeers { _, failures ->
sIndexUpdateInProgress = false
if (failures.isEmpty()) {
showToast(mContext.getString(R.string.toast_index_update_successful))
} else {
showToast(mContext.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) {
mMainHandler.post { Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show() }
}
companion object {
val LAST_INDEX_UPDATE_TS_PREF = "LAST_INDEX_UPDATE_TS"
private var sIndexUpdateInProgress: Boolean = false
}
}
@@ -1,94 +0,0 @@
package net.syncthing.lite.utils
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.util.Log
import android.widget.Toast
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 java.io.IOException
// TODO: this should be an IntentService with notification
class UploadFileTask(private val context: Context, private val syncthingClient: SyncthingClient,
private val localFile: Uri, private val syncthingFolder: String,
syncthingSubFolder: String,
private val onUploadCompleteListener: () -> Unit) {
companion object {
private val TAG = "UploadFileTask"
}
private val fileName = Util.getContentFileName(context, localFile)
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
private val mainHandler = Handler()
private lateinit var mProgressDialog: ProgressDialog
private var mCancelled = false
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
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = " + observer.progressMessage)
onProgress(observer)
}
} catch (e: InterruptedException) {
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) {
mainHandler.post {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource.size.toInt()
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
}
}
private fun onComplete() {
mProgressDialog.dismiss()
if (mCancelled)
return
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
mainHandler.post {
Toast.makeText(context, R.string.toast_upload_complete, Toast.LENGTH_SHORT).show()
onUploadCompleteListener()
}
}
private fun onError() {
mProgressDialog.dismiss()
mainHandler.post { Toast.makeText(context, R.string.toast_file_upload_failed, Toast.LENGTH_SHORT).show() }
}
}
@@ -3,18 +3,15 @@ package net.syncthing.lite.utils
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import com.google.common.base.Objects.equal
import com.google.common.base.Strings.nullToEmpty
import android.provider.OpenableColumns
import org.apache.commons.lang3.StringUtils.capitalize
import java.io.File
import java.security.InvalidParameterException
object Util {
fun getDeviceName(): String {
val manufacturer = nullToEmpty(Build.MANUFACTURER)
val model = nullToEmpty(Build.MODEL)
val manufacturer = Build.MANUFACTURER ?: ""
val model = Build.MODEL ?: ""
val deviceName =
if (model.startsWith(manufacturer)) {
capitalize(model)
@@ -22,19 +19,14 @@ object Util {
capitalize(manufacturer) + " " + model
}
return deviceName ?: "android"
}
}
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
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))
}
return fileName
}
}
@@ -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="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>
@@ -6,7 +6,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--center content BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -14,45 +13,17 @@
android:divider="?android:listDivider"
android:showDividers="middle">
<!--index loading progress BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/main_index_progress_bar"
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"/>
<TextView
android:id="@+id/main_index_progress_bar_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-->
<!--main list view BEGIN-->
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/main_folder_and_files_list_view"
android:id="@+id/list_view"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<TextView
android:id="@+id/main_list_view_empty_element"
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
@@ -60,12 +31,16 @@
android:text="@string/folder_list_empty_message"
android:textSize="20sp"
android:visibility="gone" />
<!--main list view END-->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone"/>
</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"
@@ -77,7 +52,6 @@
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:src="@drawable/ic_file_upload_white_24dp"/>
<!--upload here overlay button END-->
</RelativeLayout>
+2 -29
View File
@@ -5,40 +5,12 @@
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/main_index_progress_bar"
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/main_index_progress_bar_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"
@@ -46,6 +18,7 @@
android:layout_gravity="start"
android:background="@android:color/white"
app:menu="@menu/drawer_view" />
</android.support.v4.widget.DrawerLayout>
</layout>
+10 -17
View File
@@ -4,27 +4,20 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/abc_action_bar_content_inset_material"
android:padding="24dp"
android:theme="?alertDialogTheme"
android:orientation="vertical">
android:orientation="horizontal"
android:gravity="center">
<LinearLayout
android:layout_width="match_parent"
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
android:layout_marginEnd="24dp" />
<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>
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
+2 -2
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
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:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="12dp">
<ImageView
+2 -2
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
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:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="12dp">
<TextView
+3 -9
View File
@@ -4,20 +4,14 @@
<item
android:id="@+id/folders"
android:checked="true"
android:icon="@drawable/ic_folder_gray_24dp"
android:title="Folders"
android:checked="true"/>
android:title="@string/folders_label" />
<item
android:id="@+id/devices"
android:icon="@drawable/ic_laptop_gray_24dp"
android:title="Devices" />
<item
android:id="@+id/update_index"
android:icon="@drawable/ic_refresh_gray_24dp"
android:title="@string/update_remote_index_label"
android:checkable="false"/>
android:title="@string/devices_label" />
<item
android:id="@+id/clear_index"
+29
View File
@@ -0,0 +1,29 @@
<resources>
<string name="app_name">Syncthing Lite</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="devices_list_view_empty_message">Keine Geräte verfügbar</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="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="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="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
<string name="remove_device_title">Gerät entfernen:</string>
<string name="remove_device_message">Gerät %1$s entfernen?</string>
<string name="device_import_success">Gerät %1$s erfolgreich importiert</string>
<string name="device_already_known">Gerät ist bereits bekannt $1%s</string>
<string name="folders_label">Ordner</string>
<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>
</resources>
+29
View File
@@ -0,0 +1,29 @@
<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="scan_qr_code">Scanner le QR Code</string>
<string name="enter_device_id">Entrer l\'ID de l\'appareil</string>
<string name="invalid_device_id">ID de l\'appareil invalide</string>
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</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="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>
</resources>
+29
View File
@@ -0,0 +1,29 @@
<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="scan_qr_code">Scansiona codice QR</string>
<string name="enter_device_id">Inserisci ID dispositivo</string>
<string name="invalid_device_id">ID dispositivo non valido</string>
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
<string name="dialog_downloading_file">Download 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="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 il dispositivo %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>
</resources>
+29
View File
@@ -0,0 +1,29 @@
<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="scan_qr_code">QR コードをスキャン</string>
<string name="enter_device_id">デバイス ID を入力</string>
<string name="invalid_device_id">デバイス ID が無効です</string>
<string name="device_id_dialog_title">デバイス ID を入力</string>
<string name="dialog_downloading_file">ファイル %1$s のダウンロード中</string>
<string name="toast_file_download_failed">ファイルのダウンロードに失敗しました</string>
<string name="toast_open_file_failed">利用できるアプリが見つかりません</string>
<string name="toast_file_upload_failed">ファイルのアップロードに失敗しました</string>
<string name="toast_upload_complete">ファイルのアップロードが完了しました</string>
<string name="dialog_uploading_file">ファイル %1$s のアップロード中</string>
<string name="clear_cache_and_index_title">ローカルキャッシュと索引をクリアしますか?</string>
<string name="clear_cache_and_index_body">すべてのローカルキャッシュデータと索引データをクリアしますか?</string>
<string name="loading_config_starting_syncthing_client">設定の読み込み中、syncthing クライアントの開始中</string>
<string name="last_modified_time">最終更新: %1$s</string>
<string name="remove_device_title">デバイス %1$sを削除しますか?</string>
<string name="remove_device_message">既存のデバイスリストからデバイス %1$s を削除しますか?</string>
<string name="device_import_success">デバイス %1$s のインポートに成功しました</string>
<string name="device_already_known">デバイスは既に存在します %1$s</string>
<string name="folders_label">フォルダー</string>
<string name="devices_label">デバイス</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d ファイル, %3$d ディレクトリー</string>
<string name="file_info">%1$s, 最終更新 %2$s</string>
</resources>
+29
View File
@@ -0,0 +1,29 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Nici un director disponibil</string>
<string name="clear_local_cache_index_label">Curăță indexul/memoria locală</string>
<string name="devices_list_view_empty_message">Nici un dispozitiv disponibil</string>
<string name="scan_qr_code">Scanează cod QR</string>
<string name="enter_device_id">Introduceți ID dispozitiv</string>
<string name="invalid_device_id">ID dispozitiv invalid</string>
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
<string name="dialog_downloading_file">Se descarcă fișierul %1$s</string>
<string name="toast_file_download_failed">Descărcarea fișierului a eșuat</string>
<string name="toast_open_file_failed">Nu a fost găsită nici o aplicație compatibilă</string>
<string name="toast_file_upload_failed">Încărcarea fișierului a eșuat</string>
<string name="toast_upload_complete">Încărcarea fișierelor finalizată</string>
<string name="dialog_uploading_file">Se încarcă fișierul %1$s</string>
<string name="clear_cache_and_index_title">Se curăță memoria locală și indexul?</string>
<string name="clear_cache_and_index_body">Se curăță datele din memoria locală și datele indexului?</string>
<string name="loading_config_starting_syncthing_client">Încărcare setări, pornire client syncthing…</string>
<string name="last_modified_time">Modificat ultima dată pe: %1$s</string>
<string name="remove_device_title">Ștergere dispozitiv %1$s\?</string>
<string name="remove_device_message">Se va șterge %1$s din lista dispozitivelor cunoscute?</string>
<string name="device_import_success">Dispozitiv importat cu succes %1$s</string>
<string name="device_already_known">Dispozitiv deja prezent %1$s</string>
<string name="folders_label">Directoare</string>
<string name="devices_label">Dispozitive</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d fișier(e), %3$d director(oare)</string>
<string name="file_info">%1$s, modificat ultima dată pe %2$s</string>
</resources>
-4
View File
@@ -2,11 +2,7 @@
<resources>
<color name="primary">#f43703</color>
<color name="primary_dark">#d13602</color>
<color name="white_on_primary">#fefefe</color>
<color name="accent">#FFC107</color>
<color name="divider">#1F000000</color>
<color name="device_online_active">#ff99cc00</color>
<color name="device_online_inactive">#f43703</color>
<color name="device_offline">#aaaaaa</color>
</resources>
+17 -8
View File
@@ -1,21 +1,30 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="index_update_progress_message">index update…</string>
<string name="folder_list_empty_message">no folder available</string>
<string name="clear_local_cache_index_label">clear local cache/index</string>
<string name="update_remote_index_label">update remote index</string>
<string name="devices_list_view_empty_message">no devices available</string>
<string name="toast_write_storage_permission_required">Write storage permission is required for this functionality</string>
<string name="folder_list_empty_message">No folder available</string>
<string name="clear_local_cache_index_label">Clear local cache/index</string>
<string name="devices_list_view_empty_message">No devices available</string>
<string name="scan_qr_code">Scan QR code</string>
<string name="enter_device_id">Enter device ID</string>
<string name="invalid_device_id">Invalid device ID</string>
<string name="device_id_dialog_title">Enter Device ID</string>
<string name="toast_index_update_successful">Index update successful</string>
<string name="toast_index_update_failed">Index update failed for %1$d devices</string>
<string name="dialog_downloading_file">Downloading file %1$s</string>
<string name="toast_file_download_failed">Failed to download file</string>
<string name="toast_open_file_failed">No compatible app found</string>
<string name="toast_file_upload_failed">File upload failed</string>
<string name="toast_upload_complete">File upload complete</string>
<string name="dialog_uploading_file">Uploading file %1$s</string>
<string name="clear_cache_and_index_title">Clear local cache and index?</string>
<string name="clear_cache_and_index_body">Clear all local cache data and index data?</string>
<string name="index_update_progress_label">Index update for folder %1$s, %2$d%% synchronized</string>
<string name="loading_config_starting_syncthing_client">Loading config, starting syncthing client</string>
<string name="last_modified_time">Last modified: %1$s</string>
<string name="remove_device_title">Remove device %1$s\?</string>
<string name="remove_device_message">Remove device %1$s from list of known devices?</string>
<string name="device_import_success">Successfully imported device %1$s</string>
<string name="device_already_known">Device already present %1$s</string>
<string name="folders_label">Folders</string>
<string name="devices_label">Devices</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d files, %3$d directories</string>
<string name="file_info">%1$s, last modified %2$s</string>
</resources>
-13
View File
@@ -6,17 +6,4 @@
<item name="colorAccent">@color/accent</item>
</style>
<style name="FilePickerTheme" parent="NNF_BaseTheme">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="files" path="/" />
</paths>
+3 -4
View File
@@ -1,9 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.0'
ext.kotlin_version = '1.2.20'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.0.1'
ext.anko_version = '0.10.4'
repositories {
mavenLocal()
jcenter()
@@ -13,9 +14,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:$build_tools_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
}
}
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
set -e
NEW_VERSION_NAME=$1
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}' | tr -d "\"")
if [[ -z ${NEW_VERSION_NAME} ]]
then
echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME"
exit
fi
echo "
Updating Translations
-----------------------------
"
tx push -s
# Force push/pull to make sure this is executed. Apparently tx only compares timestamps, not file
# contents. So if a file was `touch`ed, it won't be updated by default.
tx pull -a -f
git add -A "app/src/main/res/values-*/strings.xml"
if ! git diff --cached --exit-code;
then
git commit -m "Imported translations"
fi
echo "
Updating Version
-----------------------------
"
OLD_VERSION_CODE=$(grep "versionCode" "app/build.gradle" -m 1 | awk '{print $2}')
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" "app/build.gradle"
sed -i "s/versionName \"$OLD_VERSION_NAME\"/versionName \"$NEW_VERSION_NAME\"/" "app/build.gradle"
LIBRARY_NAME="com.github.Nutomic:syncthing-java"
sed -i "s/$LIBRARY_NAME:$OLD_VERSION_NAME/$LIBRARY_NAME:$NEW_VERSION_NAME/" "app/build.gradle"
git add "app/build.gradle"
git commit -m "Version $NEW_VERSION_NAME"
git tag ${NEW_VERSION_NAME}
echo "
Running Lint
-----------------------------
"
./gradlew clean lintVitalRelease
echo "
Update ready.
"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -e
version=$(git describe --tags)
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
if [[ ! ${version} =~ $regex ]]
then
echo "Current commit is not a release"
exit;
fi
echo "
Pushing to Github
-----------------------------
"
git push
git push --tags
echo "
Push to Google Play
-----------------------------
"
read -s -p "Enter signing password: " password
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
# Upload apk and listing to Google Play
SIGNING_PASSWORD=${password} ./gradlew publishRelease
echo "
Release published!
"