21 Commits

Author SHA1 Message Date
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
28 changed files with 533 additions and 515 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
+4
View File
@@ -15,6 +15,10 @@ This project is based on [syncthing-java][3], a java implementation of Syncthing
[<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
+30 -8
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
@@ -10,8 +11,8 @@ android {
applicationId "net.syncthing.lite"
minSdkVersion 19
targetSdkVersion 25
versionCode 3
versionName "0.1.2"
versionCode 7
versionName "0.1.5"
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:0.10.4"
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: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.2") {
implementation ("com.github.Nutomic:syncthing-java:0.1.5") {
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"
}
}
+11 -4
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,8 +19,16 @@
</activity>
<activity android:name=".activities.FolderBrowserActivity"
android:parentActivityName=".activities.MainActivity"/>
<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>
@@ -1,101 +0,0 @@
package net.syncthing.lite.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import net.syncthing.lite.R
import java.io.File
import java.util.*
/**
* Activity that allows selecting a directory in the local file system.
*/
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
private lateinit var mListView: ListView
private lateinit var mFilesAdapter: FileAdapter
private lateinit var mLocation: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_folder_picker)
mListView = findViewById(android.R.id.list)
mListView.onItemClickListener = this
mListView.emptyView = findViewById(android.R.id.empty)
mFilesAdapter = FileAdapter(this)
mListView.adapter = mFilesAdapter
displayFolder(Environment.getExternalStorageDirectory())
}
/**
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
*/
private fun displayFolder(location: File) {
mLocation = location
mFilesAdapter.clear()
// In case we don't have read access to the location, just display nothing.
val contents = location.listFiles() ?: arrayOf()
Arrays.sort(contents) { f1, f2 ->
if (f1.isDirectory && f2.isFile)
return@sort -1
if (f1.isFile && f2.isDirectory)
return@sort 1
f1.name.compareTo(f2.name)
}
for (f in contents) {
mFilesAdapter.add(f)
}
mListView.adapter = mFilesAdapter
}
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val f = mFilesAdapter.getItem(i)
if (f!!.isDirectory) {
displayFolder(f)
} else {
val intent = Intent()
intent.data = Uri.fromFile(f)
setResult(Activity.RESULT_OK, intent)
finish()
}
}
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val title = view.findViewById<TextView>(android.R.id.text1)
val f = getItem(position)!!
title.text = f.name
val icon =
if (f.isDirectory) R.drawable.ic_folder_black_24dp
else R.drawable.ic_image_black_24dp
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
return view
}
}
override fun onBackPressed() {
if (mLocation != Environment.getExternalStorageDirectory()) {
displayFolder(mLocation.parentFile)
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
@@ -1,45 +1,34 @@
package net.syncthing.lite.activities
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.databinding.DataBindingUtil
import android.os.AsyncTask
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import android.widget.Toast
import com.google.common.base.Preconditions.checkArgument
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.utils.FileInfoOrdering
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.R
import net.syncthing.lite.adapters.FolderContentsAdapter
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
import net.syncthing.lite.library.DownloadFileTask
import net.syncthing.lite.library.UploadFileTask
import org.jetbrains.anko.intentFor
class FolderBrowserActivity : SyncthingActivity() {
companion object {
private val TAG = "FolderBrowserActivity"
private val REQUEST_WRITE_STORAGE = 142
private val REQUEST_SELECT_UPLOAD_FILE = 171
private const val TAG = "FolderBrowserActivity"
private const val REQUEST_WRITE_STORAGE = 142
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 lateinit var indexBrowser: IndexBrowser
private lateinit var adapter: FolderContentsAdapter
private var runWhenPermissionsReceived: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -52,14 +41,10 @@ class FolderBrowserActivity : SyncthingActivity() {
navigateToFolder(fileInfo)
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
indexBrowser = syncthingClient().indexHandler
.newIndexBrowserBuilder()
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
.includeParentInList(true)
.allowParentInRoot(true)
.setFolder(folder)
.build()
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
libraryHandler?.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
}
override fun onDestroy() {
@@ -78,30 +63,32 @@ class FolderBrowserActivity : SyncthingActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
UploadFileTask(this, syncthingClient(), intent!!.data, indexBrowser.folder,
indexBrowser.currentPath, { this.updateFolderListView() }).uploadFile()
libraryHandler?.syncthingClient { syncthingClient ->
UploadFileTask(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
{ showFolderListView(indexBrowser.currentPath) } ).uploadFile()
}
}
}
private fun showFolderListView(path: String) {
indexBrowser.navigateToNearestPath(path)
navigateToFolder(indexBrowser.currentPathInfo)
navigateToFolder(indexBrowser.currentPathInfo())
}
private fun navigateToFolder(fileInfo: FileInfo) {
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
if (indexBrowser.isRoot && PathUtils.isParent(fileInfo.path)) {
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
finish()
} else {
if (fileInfo.isDirectory) {
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 { DownloadFileTask(this, it, fileInfo).downloadFile() }
}
}
}
@@ -113,17 +100,17 @@ class FolderBrowserActivity : SyncthingActivity() {
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
assert(!list.isEmpty())//list must contain at least the 'parent' path
adapter.clear()
adapter.addAll(list)
adapter.notifyDataSetChanged()
binding.listView.setSelection(0)
val title =
if (indexBrowser.isRoot)
folderBrowser()?.getFolderInfo(indexBrowser.folder)?.label
else
indexBrowser.currentPathInfo.fileName
supportActionBar!!.setTitle(title)
if (indexBrowser.isRoot())
libraryHandler?.folderBrowser {
supportActionBar?.title = it.getFolderInfo(indexBrowser.folder)?.label
}
else
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
}
}
@@ -132,49 +119,20 @@ class FolderBrowserActivity : SyncthingActivity() {
}
private fun showUploadHereDialog() {
executeWithPermissions(Runnable {
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
})
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) {
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder)
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
binding.indexUpdateLabel.text = getString(R.string.index_update_progress_label, folder, percentage)
updateFolderListView()
}
override fun onIndexUpdateComplete() {
override fun onIndexUpdateComplete(folder: String) {
binding.indexUpdate.visibility = View.GONE
updateFolderListView()
}
private fun executeWithPermissions(runnable: Runnable) {
val permissionState = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permissionState != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_STORAGE)
runWhenPermissionsReceived = runnable
} else {
runnable.run()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
when (requestCode) {
REQUEST_WRITE_STORAGE -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.toast_write_storage_permission_required,
Toast.LENGTH_LONG).show()
} else {
runWhenPermissionsReceived!!.run()
}
runWhenPermissionsReceived = null
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
@@ -8,7 +8,8 @@ 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
@@ -50,10 +51,6 @@ class MainActivity : SyncthingActivity() {
onNavigationItemSelectedListener(selection)
}
override fun onLibraryLoaded() {
currentFragment?.onLibraryLoaded()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
drawerToggle!!.onConfigurationChanged(newConfig)
@@ -72,7 +69,7 @@ class MainActivity : SyncthingActivity() {
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(getString(R.string.clear_cache_and_index_title))
.setMessage(getString(R.string.clear_cache_and_index_body))
@@ -94,17 +91,18 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
syncthingClient().clearCacheAndIndex()
recreate()
async(UI) {
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
recreate()
}
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder) + " "
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
binding.indexUpdateLabel.text = getString(R.string.index_update_progress_label, folder, percentage)
}
override fun onIndexUpdateComplete() {
override fun onIndexUpdateComplete(folder: String) {
binding.indexUpdate.visibility = View.GONE
}
}
@@ -5,83 +5,50 @@ import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.library.InitLibraryTask
import net.syncthing.lite.library.LibraryHandler
import org.slf4j.impl.HandroidLoggerAdapter
abstract class SyncthingActivity : AppCompatActivity() {
companion object {
private var activityCount = 0
private var libraryHandler: LibraryHandler? = null
}
var libraryHandler: LibraryHandler? = null
private set
private var loadingDialog: AlertDialog? = null
fun syncthingClient(): SyncthingClient {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler!!.syncthingClient!!
}
fun configuration(): ConfigurationService {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler!!.configuration!!
}
fun folderBrowser(): FolderBrowser? {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler?.folderBrowser
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
activityCount++
if (libraryHandler == null) {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
loadingDialog = AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
InitLibraryTask(this, this::onLibraryLoaded, this::onIndexUpdateProgress, this::onIndexUpdateComplete)
}
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 fun onLibraryLoaded(libraryHandler: LibraryHandler) {
if (activityCount == 0)
return
SyncthingActivity.libraryHandler = libraryHandler
loadingDialog!!.cancel()
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() {}
open fun onIndexUpdateComplete(folder: String) {}
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.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!!)
+ context.getString(R.string.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
}
@@ -25,13 +25,11 @@ class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderSt
}
val folderInfo = getItem(position)!!.left
val folderStats = getItem(position)!!.right
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
binding.folderLastmodInfo.text =
if (folderStats.lastUpdate == null)
context.getString(R.string.last_modified_unknown)
else context.getString(R.string.last_modified_known) + " " +
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
binding.folderName.text = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folder)
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,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -13,9 +12,11 @@ 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.adapters.DevicesAdapter
import net.syncthing.lite.databinding.FragmentDevicesBinding
@@ -24,14 +25,11 @@ 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 : SyncthingFragment() {
companion object {
private val TAG = "DevicesFragment"
}
private lateinit var binding: FragmentDevicesBinding
private lateinit var adapter: DevicesAdapter
@@ -43,7 +41,7 @@ class DevicesFragment : SyncthingFragment() {
return binding.root
}
override fun onLibraryLoadedAndActivityCreated() {
override fun onLibraryLoaded() {
initDeviceList()
updateDeviceList()
}
@@ -52,23 +50,27 @@ class DevicesFragment : SyncthingFragment() {
adapter = DevicesAdapter(context!!)
binding.list.adapter = adapter
binding.list.setOnItemLongClickListener { _, _, position, _ ->
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
val device = (binding.list.getItemAtPosition(position) as DeviceStats)
AlertDialog.Builder(context)
.setTitle(getString(R.string.remove_device_title) + " " + deviceId.substring(0, 7) + "?")
.setMessage(getString(R.string.remove_device_body_1) + " " + deviceId.substring(0, 7) + " " + getString(R.string.remove_device_body_2))
.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) { _, _ ->
getSyncthingActivity().configuration().edit().removePeer(deviceId).persistLater() }
libraryHandler?.configuration { config ->
config.peers = config.peers.filterNot { config.localDeviceId == device.deviceId }.toSet()
}
}
.setNegativeButton(android.R.string.no, null)
.show()
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
false
}
}
private fun updateDeviceList() {
adapter.clear()
adapter.addAll(getSyncthingActivity().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?) {
@@ -82,22 +84,27 @@ class DevicesFragment : SyncthingFragment() {
}
}
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 = getSyncthingActivity().configuration().edit().addPeers(DeviceInfo(deviceId, null))
if (modified) {
getSyncthingActivity().configuration().edit().persistLater()
Toast.makeText(context, getString(R.string.device_import_success) + " " + deviceId, Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
UpdateIndexTask(context!!, getSyncthingActivity().syncthingClient()).updateIndex()
} else {
Toast.makeText(context, getString(R.string.device_already_known) + " " + deviceId, Toast.LENGTH_SHORT).show()
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)
UpdateIndexTask(this@DevicesFragment.context!!, syncthingClient).updateIndex()
} else {
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_already_known, deviceId), Toast.LENGTH_SHORT).show()
}
}
}
}
@@ -6,23 +6,15 @@ 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.adapters.FoldersListAdapter
import net.syncthing.lite.databinding.FragmentFoldersBinding
import org.apache.commons.lang3.tuple.Pair
import org.jetbrains.anko.intentFor
import java.util.*
class FoldersFragment : SyncthingFragment() {
companion object {
private val TAG = "FoldersFragment"
}
private val TAG = "FoldersFragment"
private lateinit var binding: FragmentFoldersBinding
@@ -33,21 +25,21 @@ class FoldersFragment : SyncthingFragment() {
return binding.root
}
override fun onLibraryLoadedAndActivityCreated() {
override fun onLibraryLoaded() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
val list = Lists.newArrayList(getSyncthingActivity().folderBrowser()!!.folderInfoAndStatsList)
Collections.sort(list, Ordering.natural<Comparable<String>>()
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter(context, list)
binding.list.adapter = adapter
binding.list.setOnItemClickListener { _, _, position, _ ->
val folder = adapter.getItem(position)!!.left.folder
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
startActivity(intent)
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)
}
}
}
}
@@ -2,30 +2,32 @@ package net.syncthing.lite.fragments
import android.os.Bundle
import android.support.v4.app.Fragment
import net.syncthing.lite.activities.SyncthingActivity
import net.syncthing.lite.library.LibraryHandler
/**
* Handle connection to [[SyncthingActivity]], and make sure device rotation are handled correctly.
*/
abstract class SyncthingFragment : Fragment() {
protected fun getSyncthingActivity() = activity as SyncthingActivity
var libraryHandler: LibraryHandler? = null
private set
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
checkConditions()
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
this::onIndexUpdateComplete)
}
fun onLibraryLoaded() {
checkConditions()
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
onLibraryLoaded()
}
private fun checkConditions() {
if (activity != null && getSyncthingActivity().folderBrowser() != null ) {
onLibraryLoadedAndActivityCreated()
}
override fun onDestroy() {
super.onDestroy()
libraryHandler?.close()
}
open fun onLibraryLoadedAndActivityCreated() {
}
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folder: String, percentage: Int) {}
open fun onIndexUpdateComplete(folder: String) {}
}
@@ -4,9 +4,8 @@ import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.support.annotation.StringRes
import android.support.v4.content.FileProvider
import android.util.Log
import android.webkit.MimeTypeMap
import net.syncthing.java.bep.BlockPuller
@@ -34,18 +33,17 @@ class DownloadFileTask(private val mContext: Context, private val mSyncthingClie
mSyncthingClient.pullFile(mFileInfo, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted) {
while (!observer.isCompleted()) {
if (cancelled)
return@pullFile
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage)
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)
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) {
@@ -73,7 +71,7 @@ class DownloadFileTask(private val mContext: Context, private val mSyncthingClie
uiThread {
progressDialog.isIndeterminate = false
progressDialog.max = (mFileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress * mFileInfo.size!!).toInt()
progressDialog.progress = (fileDownloadObserver.progress() * mFileInfo.size!!).toInt()
}
}
}
@@ -85,15 +83,16 @@ class DownloadFileTask(private val mContext: Context, private val mSyncthingClie
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(file), mimeType)
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) {
@@ -1,43 +0,0 @@
package net.syncthing.lite.library
import android.content.Context
import android.preference.PreferenceManager
import android.util.Log
import net.syncthing.java.core.beans.FolderInfo
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
class InitLibraryTask(private val context: Context, private val onLibraryLoaded: (LibraryHandler) -> Unit,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
private val onIndexUpdateCompleteListener: () -> Unit) {
private val TAG = "InitLibraryTask"
init {
doAsync {
val libraryHandler = LibraryHandler()
libraryHandler.init(context)
libraryHandler.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
onIndexUpdateProgressListener(folder, percentage)
}
override fun onIndexUpdateComplete() {
onIndexUpdateCompleteListener()
}
})
//trigger update if last was more than 10mins ago
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
val lastUpdateTimeAgo = Date().time - lastUpdateMillis
if (lastUpdateMillis == -1L || lastUpdateTimeAgo > 10 * 60 * 1000) {
Log.d(TAG, "trigger index update, last was " + Date(lastUpdateMillis))
UpdateIndexTask(context, libraryHandler.syncthingClient!!).updateIndex()
}
uiThread {
onLibraryLoaded(libraryHandler)
}
}
}
}
@@ -1,82 +1,164 @@
package net.syncthing.lite.library
import android.content.Context
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import com.google.common.eventbus.Subscribe
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
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 net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.lite.utils.Util
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
class LibraryHandler {
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
private val onIndexUpdateProgressListener: (String, Int) -> Unit,
private val onIndexUpdateCompleteListener: (String) -> 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 = "LibConnectionHandler"
private var mOnIndexUpdatedListener: OnIndexUpdatedListener? = null
var configuration: ConfigurationService? = null
private set
var syncthingClient: SyncthingClient? = null
private set
var folderBrowser: FolderBrowser? = null
private set
private val onIndexUpdateListener: Any
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()
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)
}
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!!)
onIndexUpdateListener = object : Any() {
}
syncthingClient {
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
}
}
private fun onIndexRecordAcquired(folderId: String, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
async(UI) {
onIndexUpdateProgressListener(folderId, (indexInfo.getCompleted() * 100).toInt())
}
}
private fun onRemoteIndexAcquired(folderId: String) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
async(UI) {
onIndexUpdateCompleteListener(folderId)
}
}
private fun init(context: Context) {
isLoading = true
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
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
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
isLoading = false
}
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
mOnIndexUpdatedListener = onIndexUpdatedListener
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
@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())
// 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)
}
@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()
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()
}, 1000)
}
}
@@ -4,14 +4,15 @@ import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import android.util.Log
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.doAsync
import org.apache.commons.io.IOUtils
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.IOException
// TODO: this should be an IntentService with notification
@@ -26,6 +27,7 @@ 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 uploadStream = context.contentResolver.openInputStream(localFile)
private lateinit var mProgressDialog: ProgressDialog
private var mCancelled = false
@@ -34,16 +36,15 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
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) {
while (!observer.isCompleted()) {
if (mCancelled)
return@pushFile
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = " + observer.progressMessage)
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
onProgress(observer)
}
} catch (e: InterruptedException) {
@@ -51,11 +52,11 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
}
onComplete()
}, { this.onError() })
}, { onError() })
} catch (e: IOException) {
Log.w(TAG, e)
onError()
}
}
private fun createDialog() {
@@ -69,35 +70,31 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
doAsync {
uiThread {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource.size.toInt()
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
}
async(UI) {
mProgressDialog.isIndeterminate = false
mProgressDialog.progress = observer.progressPercentage()
mProgressDialog.max = 100
}
}
private fun onComplete() {
IOUtils.closeQuietly(uploadStream)
if (mCancelled)
return
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
async(UI) {
mProgressDialog.dismiss()
this@UploadFileTask.context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
private fun onError() {
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_file_upload_failed)
}
IOUtils.closeQuietly(uploadStream)
async(UI) {
mProgressDialog.dismiss()
this@UploadFileTask.context.toast(R.string.toast_file_upload_failed)
}
}
}
}
@@ -3,18 +3,17 @@ 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 {
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)
@@ -24,17 +23,13 @@ object Util {
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
}
}
+11 -12
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="index_update_progress_message">Index wird aktualisiert...</string>
<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>
@@ -21,17 +21,16 @@
<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_folder">Index aktualisierung, Ordner</string>
<string name="index_update_percent_synchronized">% synchronisiert</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">- zuletzt modifiziert</string>
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</string>
<string name="last_modified_known">zuletzt modifiziert:</string>
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
<string name="remove_device_title">Gerät entfernen:</string>
<string name="remove_device_body_1">Gerät</string>
<string name="remove_device_body_2">von den bekannten Geräten entfernen?</string>
<string name="device_import_success">Gerät erfolgreich importiert:</string>
<string name="device_already_known">Gerät ist bereits bekannt:</string>
<string name="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>
</resources>
<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>
+36
View File
@@ -0,0 +1,36 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="index_update_progress_message">Aggiornamento indice ...</string>
<string name="folder_list_empty_message">Nessuna cartella disponibile</string>
<string name="clear_local_cache_index_label">Cancella cache/indice</string>
<string name="update_remote_index_label">Aggiorna indice remoto</string>
<string name="devices_list_view_empty_message">Nessun dispositivo disponibile</string>
<string name="toast_write_storage_permission_required">Per questa funzionalità è richiesto il permesso di scrittura</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="toast_index_update_successful">Aggiornamento indice riuscito</string>
<string name="toast_index_update_failed">Aggiornamento indice non riuscito per %1$d dispositivi</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="directory_empty">La cartella è vuota</string>
<string name="clear_cache_and_index_title">Cancellare la cache locale e l\'indice?</string>
<string name="clear_cache_and_index_body">Cancellare tutti i dati della cache locale e i dati dell\'indice?</string>
<string name="index_update_progress_label">Aggiornamento indice per la cartella %1$s, %2$d% sincronizzato</string>
<string name="loading_config_starting_syncthing_client">Caricamento configurazione, avvio del client syncthing</string>
<string name="last_modified_time">Ultima modifica: %1$s</string>
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</string>
<string name="remove_device_message">Rimuovere 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>
+2
View File
@@ -0,0 +1,2 @@
<resources>
</resources>
-3
View File
@@ -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>
+10 -11
View File
@@ -1,5 +1,5 @@
<resources>
<string name="app_name" translatable="false">Syncthing Lite</string>
<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>
@@ -21,17 +21,16 @@
<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_folder">Index update, folder</string>
<string name="index_update_percent_synchronized">% synchronized</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">- last modified</string>
<string name="last_modified_unknown">last modified: unknown</string>
<string name="last_modified_known">last modified:</string>
<string name="remove_device_title">Remove device:</string>
<string name="remove_device_body_1">Remove device</string>
<string name="remove_device_body_2">from list of known devices?</string>
<string name="device_import_success">Successfully imported device:</string>
<string name="device_already_known">Device already present:</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>
+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!
"