Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7dbd91e6 | |||
| cb7a2d362f | |||
| ef2d7fe9d7 | |||
| 0bd96302e0 | |||
| e0a95a0314 | |||
| c2e7f7cbc2 | |||
| 631a2a4fe3 | |||
| 36e54f5d24 | |||
| 3506df6b22 | |||
| 2e1369d9a8 | |||
| a3495784f7 | |||
| 82c92c9031 | |||
| bd5d89c158 | |||
| 9cf96d86dd | |||
| f4c1e6a0f0 | |||
| 036d3846bc | |||
| 6696f0ff88 | |||
| ffff6a7617 | |||
| 3bd1f6e44a | |||
| a2f46b358d | |||
| a0e749e879 | |||
| e566b3c58d | |||
| acff8c1f5c | |||
| 4353fc5597 | |||
| 64b2b7424b | |||
| 2fa0dd2e59 | |||
| 2e1fa76b44 | |||
| f447e1ecad | |||
| ee9098b4f1 | |||
| f378329da3 | |||
| 0caec11826 |
@@ -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
|
||||
@@ -13,13 +13,20 @@ 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 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].
|
||||
|
||||
+27
-8
@@ -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
|
||||
@@ -10,8 +11,8 @@ android {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 25
|
||||
versionCode 2
|
||||
versionName "0.1"
|
||||
versionCode 6
|
||||
versionName "0.1.4"
|
||||
multiDexEnabled true
|
||||
}
|
||||
sourceSets {
|
||||
@@ -21,26 +22,44 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.1.4") {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,18 @@
|
||||
</activity>
|
||||
<activity android:name=".activities.FolderBrowserActivity"
|
||||
android:parentActivityName=".activities.MainActivity"/>
|
||||
<activity
|
||||
android:name=".activities.MIVFilePickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".activities.FilePickerActivity"
|
||||
android:parentActivityName=".activities.FolderBrowserActivity" />
|
||||
<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>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import net.syncthing.lite.R
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Activity that allows selecting a directory in the local file system.
|
||||
*/
|
||||
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
|
||||
|
||||
private lateinit var mListView: ListView
|
||||
private lateinit var mFilesAdapter: FileAdapter
|
||||
private lateinit var mLocation: File
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_folder_picker)
|
||||
mListView = findViewById(android.R.id.list)
|
||||
mListView.onItemClickListener = this
|
||||
mListView.emptyView = findViewById(android.R.id.empty)
|
||||
mFilesAdapter = FileAdapter(this)
|
||||
mListView.adapter = mFilesAdapter
|
||||
|
||||
displayFolder(Environment.getExternalStorageDirectory())
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
|
||||
*/
|
||||
private fun displayFolder(location: File) {
|
||||
mLocation = location
|
||||
mFilesAdapter.clear()
|
||||
// In case we don't have read access to the location, just display nothing.
|
||||
val contents = location.listFiles() ?: arrayOf()
|
||||
|
||||
Arrays.sort(contents) { f1, f2 ->
|
||||
if (f1.isDirectory && f2.isFile)
|
||||
return@sort -1
|
||||
if (f1.isFile && f2.isDirectory)
|
||||
return@sort 1
|
||||
f1.name.compareTo(f2.name)
|
||||
}
|
||||
|
||||
for (f in contents) {
|
||||
mFilesAdapter.add(f)
|
||||
}
|
||||
mListView.adapter = mFilesAdapter
|
||||
}
|
||||
|
||||
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
|
||||
val f = mFilesAdapter.getItem(i)
|
||||
if (f!!.isDirectory) {
|
||||
displayFolder(f)
|
||||
} else {
|
||||
val intent = Intent()
|
||||
intent.data = Uri.fromFile(f)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val title = view.findViewById<TextView>(android.R.id.text1)
|
||||
val f = getItem(position)!!
|
||||
title.text = f.name
|
||||
val icon =
|
||||
if (f.isDirectory) R.drawable.ic_folder_black_24dp
|
||||
else R.drawable.ic_image_black_24dp
|
||||
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mLocation != Environment.getExternalStorageDirectory()) {
|
||||
displayFolder(mLocation.parentFile)
|
||||
} else {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,32 +2,24 @@ 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.library.DownloadFileTask
|
||||
import net.syncthing.lite.library.UploadFileTask
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
@@ -35,158 +27,119 @@ class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
private val TAG = "FolderBrowserActivity"
|
||||
private val REQUEST_WRITE_STORAGE = 142
|
||||
private val REQUEST_SELECT_UPLOAD_FILE = 171
|
||||
|
||||
val EXTRA_FOLDER_NAME = "folder_name"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityFolderBrowserBinding
|
||||
private var indexBrowser: IndexBrowser? = null
|
||||
private var loadingDialog: AlertDialog? = null
|
||||
private var adapter: FolderContentsAdapter? = null
|
||||
private lateinit var indexBrowser: IndexBrowser
|
||||
private lateinit var adapter: FolderContentsAdapter
|
||||
private var runWhenPermissionsReceived: Runnable? = null
|
||||
|
||||
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 ->
|
||||
UploadFileTask(this@FolderBrowserActivity, syncthingClient, intent!!.data,
|
||||
indexBrowser.folder, indexBrowser.currentPath,
|
||||
this@FolderBrowserActivity::showUploadHereDialog).uploadFile()
|
||||
}
|
||||
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() })
|
||||
Runnable { libraryHandler?.syncthingClient { DownloadFileTask(this, it, fileInfo).downloadFile() } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFolderChanged() {
|
||||
runOnUiThread {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.listView.visibility = View.VISIBLE
|
||||
val list = indexBrowser.listFiles()
|
||||
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
|
||||
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
|
||||
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)
|
||||
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
binding.mainIndexProgressBarLabel.text = ("index update, folder "
|
||||
+ folder.label + " " + percentage + "% synchronized")
|
||||
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
|
||||
binding.indexUpdate.visibility = View.VISIBLE
|
||||
binding.indexUpdateLabel.text = getString(R.string.index_update_progress_label, folder, percentage)
|
||||
updateFolderListView()
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
binding.mainIndexProgressBar.visibility = View.GONE
|
||||
binding.indexUpdate.visibility = View.GONE
|
||||
updateFolderListView()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,24 @@ 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
|
||||
import net.syncthing.lite.library.UpdateIndexTask
|
||||
|
||||
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 +36,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 +63,16 @@ 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.update_index -> libraryHandler?.syncthingClient { UpdateIndexTask(this@MainActivity, it).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 +82,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 +91,22 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
syncthingClient().clearCacheAndIndex()
|
||||
recreate()
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { it.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 onIndexUpdateProgress(folder: String, percentage: Int) {
|
||||
async(UI) {
|
||||
binding.indexUpdate.visibility = View.VISIBLE
|
||||
binding.indexUpdateLabel.text = getString(R.string.index_update_progress_label, folder, percentage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
binding.mainIndexProgressBar.visibility = View.GONE
|
||||
async(UI) {
|
||||
binding.indexUpdate.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,52 @@
|
||||
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.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.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
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
|
||||
val lastUpdate =
|
||||
if (lastUpdateMillis < 0) null
|
||||
else Date(lastUpdateMillis)
|
||||
//trigger update if last was more than 10mins ago
|
||||
if (lastUpdate == null || Date().time - lastUpdate.time > 10 * 60 * 1000) {
|
||||
Log.d(TAG, "trigger index update, last was " + lastUpdate!!)
|
||||
UpdateIndexTask(context, libraryHandler!!.syncthingClient!!).updateIndex()
|
||||
}
|
||||
onLibraryLoaded()
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
if (!isDestroyed) {
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
onLibraryLoaded()
|
||||
}
|
||||
|
||||
open fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {}
|
||||
open fun onIndexUpdateProgress(folder: String, percentage: Int) {}
|
||||
|
||||
open fun onIndexUpdateComplete() {}
|
||||
|
||||
|
||||
@@ -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.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewDeviceBinding
|
||||
|
||||
class DevicesAdapter(context: Context) :
|
||||
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, Lists.newArrayList()) {
|
||||
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, mutableListOf()) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewDeviceBinding
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 {
|
||||
@@ -25,13 +25,14 @@ class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderSta
|
||||
}
|
||||
val folderInfo = getItem(position)!!.left
|
||||
val folderStats = getItem(position)!!.right
|
||||
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
|
||||
binding.folderName.text = context.getString(R.string.folder_label_format, 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"
|
||||
context.getString(R.string.last_modified_unknown)
|
||||
else 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,24 @@ 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.library.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 +41,34 @@ class DevicesFragment : Fragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
syncthingActivity = activity as SyncthingActivity
|
||||
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 = (binding.list.getItemAtPosition(position) as DeviceStats)
|
||||
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 { it.Editor().removePeer(device.deviceId).persistLater() }
|
||||
}
|
||||
.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()
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
adapter.clear()
|
||||
adapter.addAll(syncthingClient.devicesHandler.getDeviceStatsList())
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
@@ -86,22 +82,27 @@ 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?.library { configuration, syncthingClient, _ ->
|
||||
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()
|
||||
val modified = configuration.Editor().addPeers(DeviceInfo(deviceId, null))
|
||||
if (modified) {
|
||||
configuration.Editor().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)
|
||||
UpdateIndexTask(this@DevicesFragment.context!!, syncthingClient).updateIndex()
|
||||
} else {
|
||||
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_already_known, deviceId), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
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 +25,21 @@ 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().sortedBy { it.left.label }
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
|
||||
val adapter = FoldersListAdapter(context, list)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemClickListener { _, _, position, _ ->
|
||||
val folder = adapter.getItem(position)!!.left.folder
|
||||
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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,33 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
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(folder: String, percentage: Int) {}
|
||||
|
||||
open fun onIndexUpdateComplete() {}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.annotation.StringRes
|
||||
import android.support.v4.content.FileProvider
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.newTask
|
||||
import org.jetbrains.anko.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient,
|
||||
private val mFileInfo: FileInfo) {
|
||||
|
||||
private val TAG = "DownloadFileTask"
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var cancelled = false
|
||||
|
||||
fun downloadFile() {
|
||||
showDialog()
|
||||
mSyncthingClient.pullFile(mFileInfo, { observer ->
|
||||
onProgress(observer)
|
||||
try {
|
||||
while (!observer.isCompleted()) {
|
||||
if (cancelled)
|
||||
return@pullFile
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i("pullFile", "download progress = " + observer.progressMessage())
|
||||
onProgress(observer)
|
||||
}
|
||||
|
||||
val outputFile = File("${mContext.externalCacheDir}/${mFileInfo.folder}/${mFileInfo.path}")
|
||||
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) }
|
||||
}
|
||||
|
||||
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) {
|
||||
doAsync {
|
||||
uiThread {
|
||||
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)
|
||||
val uri = FileProvider.getUriForFile(mContext, "net.syncthing.lite.fileprovider", file)
|
||||
intent.setDataAndType(uri, mimeType)
|
||||
intent.newTask()
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
try {
|
||||
mContext.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
onError(R.string.toast_open_file_failed)
|
||||
Log.w(TAG, "No handler found for file " + file.name, e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(@StringRes error: Int) {
|
||||
doAsync {
|
||||
uiThread {
|
||||
progressDialog.dismiss()
|
||||
mContext.toast(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
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.IndexInfo
|
||||
import net.syncthing.java.core.configuration.ConfigurationService
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
|
||||
private val onIndexUpdateProgressListener: (String, Int) -> Unit,
|
||||
private val onIndexUpdateCompleteListener: () -> Unit) {
|
||||
|
||||
companion object {
|
||||
private var instanceCount = 0
|
||||
private var configuration: ConfigurationService? = null
|
||||
private var syncthingClient: SyncthingClient? = null
|
||||
private var folderBrowser: FolderBrowser? = null
|
||||
private val callbacks = ArrayList<(ConfigurationService, SyncthingClient, FolderBrowser) -> Unit>()
|
||||
private var isLoading = false
|
||||
}
|
||||
|
||||
private val TAG = "LibConnectionHandler"
|
||||
|
||||
private val onIndexUpdateListener: Any
|
||||
|
||||
init {
|
||||
instanceCount++
|
||||
if (configuration == null && !isLoading) {
|
||||
doAsync {
|
||||
init(context)
|
||||
//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))
|
||||
syncthingClient { UpdateIndexTask(context, it).updateIndex() }
|
||||
}
|
||||
uiThread {
|
||||
onLibraryLoaded(this@LibraryHandler)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onLibraryLoaded(this)
|
||||
}
|
||||
|
||||
onIndexUpdateListener = object : Any() {
|
||||
}
|
||||
syncthingClient {
|
||||
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
|
||||
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIndexRecordAcquired(folderId: String, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
|
||||
newRecords.size
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
onIndexUpdateProgressListener(folderId, (indexInfo.getCompleted() * 100).toInt())
|
||||
}
|
||||
|
||||
private fun onRemoteIndexAcquired(folderId: String) {
|
||||
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
|
||||
onIndexUpdateCompleteListener()
|
||||
}
|
||||
|
||||
private fun init(context: Context) {
|
||||
isLoading = true
|
||||
val configuration = ConfigurationService.Loader()
|
||||
.setCache(File(context.externalCacheDir, ".cache"))
|
||||
.setDatabase(File(context.getExternalFilesDir(null), "database"))
|
||||
.loadFrom(File(context.getExternalFilesDir(null), "config.properties"))
|
||||
configuration.Editor().setDeviceName(Util.getDeviceName())
|
||||
try {
|
||||
FileUtils.cleanDirectory(configuration.temp)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to delete temporary files", e)
|
||||
close()
|
||||
}
|
||||
|
||||
KeystoreHandler.Loader().loadAndStore(configuration)
|
||||
configuration.Editor().persistLater()
|
||||
Log.i(TAG, "loaded mConfiguration = " + configuration.Writer().dumpToString())
|
||||
Log.i(TAG, "storage space = " + configuration.getStorageInfo().dumpAvailableSpace())
|
||||
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")
|
||||
configuration.close()
|
||||
syncthingClient.close()
|
||||
folderBrowser.close()
|
||||
}
|
||||
|
||||
async(UI) {
|
||||
callbacks.forEach { it(configuration, syncthingClient, folderBrowser) }
|
||||
}
|
||||
LibraryHandler.configuration = configuration
|
||||
LibraryHandler.syncthingClient = syncthingClient
|
||||
LibraryHandler.folderBrowser = folderBrowser
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
fun library(callback: (ConfigurationService, 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: (ConfigurationService) -> 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?.close()
|
||||
configuration = null
|
||||
}
|
||||
}.start()
|
||||
}, 1000)
|
||||
|
||||
}
|
||||
}
|
||||
+14
-10
@@ -1,28 +1,28 @@
|
||||
package net.syncthing.lite.utils
|
||||
package net.syncthing.lite.library
|
||||
|
||||
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 org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.toast
|
||||
import org.jetbrains.anko.uiThread
|
||||
import java.util.*
|
||||
|
||||
class UpdateIndexTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient) {
|
||||
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||
private val mMainHandler = Handler()
|
||||
class UpdateIndexTask(private val androidContext: Context, private val syncthingClient: SyncthingClient) {
|
||||
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(androidContext)
|
||||
|
||||
fun updateIndex() {
|
||||
if (sIndexUpdateInProgress)
|
||||
return
|
||||
|
||||
sIndexUpdateInProgress = true
|
||||
mSyncthingClient.updateIndexFromPeers { _, failures ->
|
||||
syncthingClient.updateIndexFromPeers { _, failures ->
|
||||
sIndexUpdateInProgress = false
|
||||
if (failures.isEmpty()) {
|
||||
showToast(mContext.getString(R.string.toast_index_update_successful))
|
||||
showToast(androidContext.getString(R.string.toast_index_update_successful))
|
||||
} else {
|
||||
showToast(mContext.getString(R.string.toast_index_update_failed, failures.size))
|
||||
showToast(androidContext.getString(R.string.toast_index_update_failed, failures.size))
|
||||
}
|
||||
mPreferences.edit()
|
||||
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
|
||||
@@ -31,7 +31,11 @@ class UpdateIndexTask(private val mContext: Context, private val mSyncthingClien
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
mMainHandler.post { Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show() }
|
||||
doAsync {
|
||||
uiThread {
|
||||
androidContext.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
+18
-15
@@ -1,15 +1,17 @@
|
||||
package net.syncthing.lite.utils
|
||||
package net.syncthing.lite.library
|
||||
|
||||
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 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.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.IOException
|
||||
|
||||
// TODO: this should be an IntentService with notification
|
||||
@@ -24,7 +26,6 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
|
||||
|
||||
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
|
||||
@@ -37,12 +38,12 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
|
||||
syncthingClient.pushFile(uploadStream, syncthingFolder, syncthingPath, { observer ->
|
||||
onProgress(observer)
|
||||
try {
|
||||
while (!observer.isCompleted) {
|
||||
while (!observer.isCompleted()) {
|
||||
if (mCancelled)
|
||||
return@pushFile
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = " + observer.progressMessage)
|
||||
Log.i(TAG, "upload progress = " + observer.progressMessage())
|
||||
onProgress(observer)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
@@ -68,27 +69,29 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
|
||||
}
|
||||
|
||||
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
|
||||
mainHandler.post {
|
||||
async(UI) {
|
||||
mProgressDialog.isIndeterminate = false
|
||||
mProgressDialog.max = observer.dataSource.size.toInt()
|
||||
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
|
||||
mProgressDialog.max = observer.dataSource().getSize().toInt()
|
||||
mProgressDialog.progress = (observer.progress() * observer.dataSource().getSize()).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()
|
||||
async(UI) {
|
||||
mProgressDialog.dismiss()
|
||||
this@UploadFileTask.context.toast(R.string.toast_upload_complete)
|
||||
onUploadCompleteListener()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError() {
|
||||
mProgressDialog.dismiss()
|
||||
mainHandler.post { Toast.makeText(context, R.string.toast_file_upload_failed, Toast.LENGTH_SHORT).show() }
|
||||
async(UI) {
|
||||
mProgressDialog.dismiss()
|
||||
this@UploadFileTask.context.toast(R.string.toast_file_upload_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,16 @@ 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 org.apache.commons.lang3.StringUtils.capitalize
|
||||
import java.io.File
|
||||
|
||||
object Util {
|
||||
|
||||
private val Tag = "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,16 +22,16 @@ 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")) {
|
||||
if (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)
|
||||
Log.d(Tag, "recovered 'content' uri real path = " + path)
|
||||
fileName = File(Uri.parse(path).lastPathSegment).name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/main_index_progress_bar"
|
||||
android:id="@+id/index_update"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone">
|
||||
@@ -27,9 +27,10 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:paddingStart="12dp"/>
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"/>
|
||||
<TextView
|
||||
android:id="@+id/main_index_progress_bar_label"
|
||||
android:id="@+id/index_update_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
@@ -47,12 +48,13 @@
|
||||
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,6 +62,13 @@
|
||||
android:text="@string/folder_list_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
<!--main list view END-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/directory_empty" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -11,7 +11,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/main_index_progress_bar"
|
||||
android:id="@+id/index_update"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone">
|
||||
@@ -22,7 +22,7 @@
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"/>
|
||||
<TextView
|
||||
android:id="@+id/main_index_progress_bar_label"
|
||||
android:id="@+id/index_update_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
</layout>
|
||||
@@ -4,14 +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" />
|
||||
android:title="@string/devices_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/update_index"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="index_update_progress_message">Index wird aktualisiert…</string>
|
||||
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
|
||||
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
|
||||
<string name="update_remote_index_label">Index aktualisieren</string>
|
||||
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
|
||||
<string name="toast_write_storage_permission_required">Schreibrechte werden für diese Funktion benötigt</string>
|
||||
<string name="scan_qr_code">QR code scannen</string>
|
||||
<string name="enter_device_id">Geräte ID eingeben</string>
|
||||
<string name="invalid_device_id">Ungültige Geräte ID</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
<string name="toast_index_update_successful">Index erfolgreich aktualisiert</string>
|
||||
<string name="toast_index_update_failed">Index update für %1$d Geräte fehlgeschlagen</string>
|
||||
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
|
||||
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
|
||||
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
|
||||
<string name="toast_file_upload_failed">Hochladen gescheitert</string>
|
||||
<string name="toast_upload_complete">Hochladen erfolgreich</string>
|
||||
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
|
||||
<string name="directory_empty">Ordner ist leer</string>
|
||||
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
|
||||
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
|
||||
<string name="index_update_progress_label">Index aktualisierung für Ordner %1$s, %2$d\\% synchronisiert</string>
|
||||
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
|
||||
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</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>
|
||||
@@ -6,7 +6,4 @@
|
||||
<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>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<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="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="scan_qr_code">Scan QR code</string>
|
||||
<string name="enter_device_id">Enter device ID</string>
|
||||
@@ -18,4 +18,20 @@
|
||||
<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="directory_empty">Directory is empty</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_unknown">Last modified: unknown</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="files" path="/" />
|
||||
</paths>
|
||||
+3
-4
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Executable
+54
@@ -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.
|
||||
"
|
||||
Executable
+37
@@ -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!
|
||||
"
|
||||
Reference in New Issue
Block a user