1 Commits

Author SHA1 Message Date
Felix Ableitner def8630f8f Version 0.1.2 2018-01-04 16:17:05 +09:00
44 changed files with 858 additions and 983 deletions
-9
View File
@@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[syncthing-lite.stringsxml]
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en
type = ANDROID
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw, id:in
-6
View File
@@ -13,14 +13,8 @@ example, mobile devices with limited storage available, wishing to access a sync
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
[<img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80">](https://f-droid.org/packages/net.syncthing.lite/)
[<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80">](https://play.google.com/store/apps/details?id=net.syncthing.lite)
## Translations
The project is translated on [Transifex](https://www.transifex.com/syncthing-android/syncthing-lite/).
## Building
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
+9 -31
View File
@@ -1,7 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.ben-manes.versions'
android {
compileSdkVersion 27
@@ -9,10 +8,10 @@ android {
dataBinding.enabled = true
defaultConfig {
applicationId "net.syncthing.lite"
minSdkVersion 21
minSdkVersion 19
targetSdkVersion 25
versionCode 7
versionName "0.2.0"
versionCode 3
versionName "0.1.2"
multiDexEnabled true
}
sourceSets {
@@ -22,47 +21,26 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
release {
storeFile = {
def path = System.getenv("SYNCTHING_LITE_RELEASE_STORE_FILE")
return (path) ? file(path) : null
}()
storePassword System.getenv("SIGNING_PASSWORD") ?: ""
keyAlias System.getenv("SYNCTHING_LITE_RELEASE_KEY_ALIAS") ?: ""
keyPassword System.getenv("SIGNING_PASSWORD") ?: ""
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
packagingOptions {
exclude 'META-INF/*'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.anko:anko-commons:$anko_version"
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
implementation "org.jetbrains.anko:anko-commons:0.10.4"
kapt "com.android.databinding:compiler:$build_tools_version"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:support-v4:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:cardview-v7:$support_version"
implementation ("com.github.Nutomic:syncthing-java:0.2.0") {
implementation ("com.github.Nutomic:syncthing-java:0.1.2") {
exclude group: 'commons-logging', module:'commons-logging'
exclude group: 'commons-codec'
exclude group: 'org.apache.httpcomponents', module:'httpclient'
exclude group: 'org.slf4j'
exclude group: 'ch.qos.logback'
}
// 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 ('uk.co.markormesher:android-fab:2.0.0') {
exclude group: "org.jetbrains.kotlin"
}
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'uk.co.markormesher:android-fab:2.0.0'
}
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="GoogleAppIndexingWarning" severity="ignore" />
<issue id="InvalidPackage" severity="ignore" />
<issue id="OldTargetApi" severity="ignore" />
</lint>
+4 -20
View File
@@ -2,6 +2,7 @@
<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"
@@ -19,25 +20,8 @@
</activity>
<activity android:name=".activities.FolderBrowserActivity"
android:parentActivityName=".activities.MainActivity"/>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="net.syncthing.lite.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<provider
android:name=".library.SyncthingProvider"
android:authorities="net.syncthing.lite.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<activity android:name=".activities.FilePickerActivity"
android:parentActivityName=".activities.FolderBrowserActivity" />
</application>
</manifest>
</manifest>
@@ -0,0 +1,101 @@
package net.syncthing.lite.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import net.syncthing.lite.R
import java.io.File
import java.util.*
/**
* Activity that allows selecting a directory in the local file system.
*/
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
private lateinit var mListView: ListView
private lateinit var mFilesAdapter: FileAdapter
private lateinit var mLocation: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_folder_picker)
mListView = findViewById(android.R.id.list)
mListView.onItemClickListener = this
mListView.emptyView = findViewById(android.R.id.empty)
mFilesAdapter = FileAdapter(this)
mListView.adapter = mFilesAdapter
displayFolder(Environment.getExternalStorageDirectory())
}
/**
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
*/
private fun displayFolder(location: File) {
mLocation = location
mFilesAdapter.clear()
// In case we don't have read access to the location, just display nothing.
val contents = location.listFiles() ?: arrayOf()
Arrays.sort(contents) { f1, f2 ->
if (f1.isDirectory && f2.isFile)
return@sort -1
if (f1.isFile && f2.isDirectory)
return@sort 1
f1.name.compareTo(f2.name)
}
for (f in contents) {
mFilesAdapter.add(f)
}
mListView.adapter = mFilesAdapter
}
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val f = mFilesAdapter.getItem(i)
if (f!!.isDirectory) {
displayFolder(f)
} else {
val intent = Intent()
intent.data = Uri.fromFile(f)
setResult(Activity.RESULT_OK, intent)
finish()
}
}
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val title = view.findViewById<TextView>(android.R.id.text1)
val f = getItem(position)!!
title.text = f.name
val icon =
if (f.isDirectory) R.drawable.ic_folder_black_24dp
else R.drawable.ic_image_black_24dp
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
return view
}
}
override fun onBackPressed() {
if (mLocation != Environment.getExternalStorageDirectory()) {
displayFolder(mLocation.parentFile)
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
@@ -1,34 +1,45 @@
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.utils.FileDownloadDialog
import net.syncthing.lite.utils.FileUploadDialog
import net.syncthing.lite.library.DownloadFileTask
import net.syncthing.lite.library.UploadFileTask
import org.jetbrains.anko.intentFor
class FolderBrowserActivity : SyncthingActivity() {
companion object {
private const val TAG = "FolderBrowserActivity"
private const val REQUEST_SELECT_UPLOAD_FILE = 171
private val TAG = "FolderBrowserActivity"
private val REQUEST_WRITE_STORAGE = 142
private val REQUEST_SELECT_UPLOAD_FILE = 171
const val EXTRA_FOLDER_NAME = "folder_name"
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)
@@ -41,10 +52,14 @@ class FolderBrowserActivity : SyncthingActivity() {
navigateToFolder(fileInfo)
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
libraryHandler?.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
indexBrowser = syncthingClient().indexHandler
.newIndexBrowserBuilder()
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
.includeParentInList(true)
.allowParentInRoot(true)
.setFolder(folder)
.build()
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
override fun onDestroy() {
@@ -63,32 +78,30 @@ class FolderBrowserActivity : SyncthingActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
libraryHandler?.syncthingClient { syncthingClient ->
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
{ showFolderListView(indexBrowser.currentPath) } )
}
UploadFileTask(this, syncthingClient(), intent!!.data, indexBrowser.folder,
indexBrowser.currentPath, { this.updateFolderListView() }).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)
libraryHandler?.syncthingClient { FileDownloadDialog(this, it, fileInfo) }
executeWithPermissions(
Runnable { DownloadFileTask(this, syncthingClient(), fileInfo).downloadFile() })
}
}
}
@@ -100,17 +113,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)
assert(!list.isEmpty())//list must contain at least the 'parent' path
checkArgument(!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
val title =
if (indexBrowser.isRoot)
folderBrowser()?.getFolderInfo(indexBrowser.folder)?.label
else
indexBrowser.currentPathInfo.fileName
supportActionBar!!.setTitle(title)
}
}
@@ -119,14 +132,49 @@ class FolderBrowserActivity : SyncthingActivity() {
}
private fun showUploadHereDialog() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
executeWithPermissions(Runnable {
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
})
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder)
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
updateFolderListView()
}
override fun onIndexUpdateComplete() {
binding.indexUpdate.visibility = View.GONE
updateFolderListView()
}
private fun executeWithPermissions(runnable: Runnable) {
val permissionState = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permissionState != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_STORAGE)
runWhenPermissionsReceived = runnable
} else {
runnable.run()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
when (requestCode) {
REQUEST_WRITE_STORAGE -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.toast_write_storage_permission_required,
Toast.LENGTH_LONG).show()
} else {
runWhenPermissionsReceived!!.run()
}
runWhenPermissionsReceived = null
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
@@ -7,13 +7,14 @@ import android.os.Bundle
import android.support.v7.app.ActionBarDrawerToggle
import android.view.Gravity
import android.view.MenuItem
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import android.view.View
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ActivityMainBinding
import net.syncthing.lite.fragments.DevicesFragment
import net.syncthing.lite.fragments.FoldersFragment
import net.syncthing.lite.fragments.SyncthingFragment
import net.syncthing.lite.library.UpdateIndexTask
class MainActivity : SyncthingActivity() {
@@ -49,6 +50,10 @@ class MainActivity : SyncthingActivity() {
onNavigationItemSelectedListener(selection)
}
override fun onLibraryLoaded() {
currentFragment?.onLibraryLoaded()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
drawerToggle!!.onConfigurationChanged(newConfig)
@@ -67,6 +72,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.clear_index -> AlertDialog.Builder(this)
.setTitle(getString(R.string.clear_cache_and_index_title))
.setMessage(getString(R.string.clear_cache_and_index_body))
@@ -88,9 +94,17 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
async(UI) {
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
recreate()
}
syncthingClient().clearCacheAndIndex()
recreate()
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder) + " "
+ folder.label + " " + percentage + getString(R.string.index_update_percent_synchronized))
}
override fun onIndexUpdateComplete() {
binding.indexUpdate.visibility = View.GONE
}
}
@@ -3,65 +3,85 @@ package net.syncthing.lite.activities
import android.app.AlertDialog
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.library.InitLibraryTask
import net.syncthing.lite.library.LibraryHandler
import org.jetbrains.anko.contentView
import org.slf4j.impl.HandroidLoggerAdapter
abstract class SyncthingActivity : AppCompatActivity() {
var libraryHandler: LibraryHandler? = null
private set
companion object {
private var activityCount = 0
private var libraryHandler: LibraryHandler? = null
}
private var loadingDialog: AlertDialog? = null
private var snackBar: Snackbar? = null
fun syncthingClient(): SyncthingClient {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler!!.syncthingClient!!
}
fun configuration(): ConfigurationService {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler!!.configuration!!
}
fun folderBrowser(): FolderBrowser? {
if (isDestroyed)
throw IllegalStateException("activity is already destroyed")
return libraryHandler?.folderBrowser
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
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)
activityCount++
if (libraryHandler == null) {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
loadingDialog = AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
InitLibraryTask(this, this::onLibraryLoaded, this::onIndexUpdateProgress, this::onIndexUpdateComplete)
}
}
override fun onDestroy() {
super.onDestroy()
libraryHandler?.close()
loadingDialog?.dismiss()
activityCount--
Thread {
if (activityCount == 0) {
libraryHandler?.destroy()
libraryHandler = null
}
}.start()
}
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
if (!isDestroyed) {
loadingDialog?.dismiss()
}
private fun onLibraryLoaded(libraryHandler: LibraryHandler) {
if (activityCount == 0)
return
SyncthingActivity.libraryHandler = libraryHandler
loadingDialog!!.cancel()
onLibraryLoaded()
}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {
val message = getString(R.string.index_update_progress_label, folderInfo.label, percentage)
snackBar?.setText(message) ?: run {
snackBar = Snackbar.make(contentView!!, message, Snackbar.LENGTH_INDEFINITE)
snackBar?.show()
}
}
open fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {
snackBar?.dismiss()
snackBar = null
}
open fun onIndexUpdateComplete() {}
open fun onLibraryLoaded() {}
}
@@ -6,12 +6,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import net.syncthing.java.core.beans.DeviceInfo
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<DeviceInfo>(context, R.layout.listview_device, mutableListOf()) {
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, Lists.newArrayList()) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewDeviceBinding
@@ -23,10 +24,10 @@ class DevicesAdapter(context: Context) :
val deviceStats = getItem(position)
binding.deviceName.text = deviceStats!!.name
val icon =
if (deviceStats.isConnected!!) {
R.drawable.ic_laptop_green_24dp
} else {
R.drawable.ic_laptop_red_24dp
when (deviceStats.status) {
DeviceStats.DeviceStatus.OFFLINE -> R.drawable.ic_laptop_red_24dp
DeviceStats.DeviceStatus.ONLINE_INACTIVE,
DeviceStats.DeviceStatus.ONLINE_ACTIVE -> R.drawable.ic_laptop_green_24dp
}
binding.deviceIcon.setImageResource(icon)
return binding.root
@@ -7,13 +7,14 @@ 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, mutableListOf()) {
ArrayAdapter<FileInfo>(context, R.layout.listview_file, Lists.newArrayList()) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
val binding: ListviewFileBinding =
@@ -24,15 +25,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 = 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))
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))
}
return binding.root
}
@@ -11,6 +11,7 @@ import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewFolderBinding
import org.apache.commons.lang3.tuple.Pair
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
@@ -22,13 +23,15 @@ class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderSt
} else {
DataBindingUtil.bind(v)
}
val folderInfo = getItem(position)!!.first
val folderStats = getItem(position)!!.second
binding.folderName.text = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
binding.folderLastmodInfo.text = context.getString(R.string.last_modified_time,
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.folderContentInfo.text = context.getString(R.string.folder_content_info, folderStats.describeSize(), folderStats.fileCount, folderStats.dirCount)
val folderInfo = getItem(position)!!.left
val folderStats = getItem(position)!!.right
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
binding.folderLastmodInfo.text =
if (folderStats.lastUpdate == null)
context.getString(R.string.last_modified_unknown)
else context.getString(R.string.last_modified_known) + " " +
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
return binding.root
}
@@ -5,6 +5,7 @@ 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
@@ -12,22 +13,25 @@ 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
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 : SyncthingFragment() {
companion object {
private val TAG = "DevicesFragment"
}
private lateinit var binding: FragmentDevicesBinding
private lateinit var adapter: DevicesAdapter
@@ -39,21 +43,7 @@ class DevicesFragment : SyncthingFragment() {
return binding.root
}
override fun onResume() {
super.onResume()
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener(this::onConnectionChanged) }
}
override fun onPause() {
super.onPause()
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener(this::onConnectionChanged) }
}
private fun onConnectionChanged(deviceId: DeviceId) {
updateDeviceList()
}
override fun onLibraryLoaded() {
override fun onLibraryLoadedAndActivityCreated() {
initDeviceList()
updateDeviceList()
}
@@ -62,31 +52,23 @@ class DevicesFragment : SyncthingFragment() {
adapter = DevicesAdapter(context!!)
binding.list.adapter = adapter
binding.list.setOnItemLongClickListener { _, _, position, _ ->
val device = adapter.getItem(position)
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
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)))
.setTitle(getString(R.string.remove_device_title) + " " + deviceId.substring(0, 7) + "?")
.setMessage(getString(R.string.remove_device_body_1) + " " + deviceId.substring(0, 7) + " " + getString(R.string.remove_device_body_2))
.setPositiveButton(android.R.string.yes) { _, _ ->
libraryHandler?.configuration { config ->
config.peers = config.peers.filterNot { it.deviceId == device.deviceId }.toSet()
config.persistLater()
updateDeviceList()
}
}
getSyncthingActivity().configuration().edit().removePeer(deviceId).persistLater() }
.setNegativeButton(android.R.string.no, null)
.show()
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
false
}
}
private fun updateDeviceList() {
async(UI) {
libraryHandler?.syncthingClient { syncthingClient ->
adapter.clear()
adapter.addAll(syncthingClient.getPeerStatus())
adapter.notifyDataSetChanged()
}
}
adapter.clear()
adapter.addAll(getSyncthingActivity().syncthingClient().devicesHandler.deviceStatsList)
adapter.notifyDataSetChanged()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
@@ -100,26 +82,22 @@ class DevicesFragment : SyncthingFragment() {
}
}
private fun importDeviceId(deviceIdString: String) {
libraryHandler?.configuration { configuration ->
async(UI) {
val deviceId =
try {
DeviceId(deviceIdString)
} catch (e: IOException) {
Toast.makeText(this@DevicesFragment.context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
return@async
}
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
}
if (!configuration.peerIds.contains(deviceId)) {
configuration.peers = configuration.peers + DeviceInfo(deviceId, null)
configuration.persistLater()
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_import_success, deviceId), Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
} else {
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_already_known, deviceId), Toast.LENGTH_SHORT).show()
}
}
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()
}
}
@@ -6,16 +6,23 @@ 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() {
private val TAG = "FoldersFragment"
companion object {
private val TAG = "FoldersFragment"
}
private lateinit var binding: FragmentFoldersBinding
@@ -26,26 +33,21 @@ class FoldersFragment : SyncthingFragment() {
return binding.root
}
override fun onLibraryLoaded() {
override fun onLibraryLoadedAndActivityCreated() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
libraryHandler?.folderBrowser { folderBrowser ->
val list = folderBrowser.folderInfoAndStatsList()
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter(context, list)
binding.list.adapter = adapter
binding.list.setOnItemClickListener { _, _, position, _ ->
val folder = adapter.getItem(position)!!.first.folderId
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
startActivity(intent)
}
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)
}
}
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
showAllFoldersListView()
}
}
@@ -2,33 +2,30 @@ package net.syncthing.lite.fragments
import android.os.Bundle
import android.support.v4.app.Fragment
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.library.LibraryHandler
import net.syncthing.lite.activities.SyncthingActivity
/**
* Handle connection to [[SyncthingActivity]], and make sure device rotation are handled correctly.
*/
abstract class SyncthingFragment : Fragment() {
var libraryHandler: LibraryHandler? = null
private set
protected fun getSyncthingActivity() = activity as SyncthingActivity
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
this::onIndexUpdateComplete)
checkConditions()
}
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
onLibraryLoaded()
fun onLibraryLoaded() {
checkConditions()
}
override fun onDestroy() {
super.onDestroy()
libraryHandler?.close()
private fun checkConditions() {
if (activity != null && getSyncthingActivity().folderBrowser() != null ) {
onLibraryLoadedAndActivityCreated()
}
}
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
open fun onLibraryLoadedAndActivityCreated() {
}
}
@@ -1,49 +1,107 @@
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.support.annotation.StringRes
import android.util.Log
import 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 context: Context, syncthingClient: SyncthingClient,
private val fileInfo: FileInfo,
private val onProgress: (DownloadFileTask, BlockPuller.FileDownloadObserver) -> Unit,
private val onComplete: (File) -> Unit,
private val onError: () -> Unit) {
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient,
private val mFileInfo: FileInfo) {
private val Tag = "DownloadFileTask"
private var isCancelled = false
private val TAG = "DownloadFileTask"
private lateinit var progressDialog: ProgressDialog
private var cancelled = false
init {
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
val observer = blockPuller.pullFile(fileInfo)
onProgress(this, observer)
fun downloadFile() {
showDialog()
mSyncthingClient.pullFile(mFileInfo, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted()) {
if (isCancelled)
return@getBlockPuller
while (!observer.isCompleted) {
if (cancelled)
return@pullFile
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage())
onProgress(this, observer)
Log.i("pullFile", "download progress = " + observer.progressMessage)
onProgress(observer)
}
val outputFile = File("${context.externalCacheDir}/${fileInfo.folder}/${fileInfo.path}")
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
Log.i(Tag, "Downloaded file $fileInfo")
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()
Log.w(Tag, "Failed to download file $fileInfo", e)
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() })
}) { onError(R.string.toast_file_download_failed) }
}
fun cancel() {
isCancelled = true
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)
intent.setDataAndType(Uri.fromFile(file), mimeType)
intent.newTask()
try {
mContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(R.string.toast_open_file_failed)
Log.w(TAG, "No handler found for file " + file.name, e)
}
}
private fun onError(@StringRes error: Int) {
doAsync {
uiThread {
progressDialog.dismiss()
mContext.toast(error)
}
}
}
}
@@ -0,0 +1,43 @@
package net.syncthing.lite.library
import android.content.Context
import android.preference.PreferenceManager
import android.util.Log
import net.syncthing.java.core.beans.FolderInfo
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.util.*
class InitLibraryTask(private val context: Context, private val onLibraryLoaded: (LibraryHandler) -> Unit,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
private val onIndexUpdateCompleteListener: () -> Unit) {
private val TAG = "InitLibraryTask"
init {
doAsync {
val libraryHandler = LibraryHandler()
libraryHandler.init(context)
libraryHandler.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
onIndexUpdateProgressListener(folder, percentage)
}
override fun onIndexUpdateComplete() {
onIndexUpdateCompleteListener()
}
})
//trigger update if last was more than 10mins ago
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
val lastUpdateTimeAgo = Date().time - lastUpdateMillis
if (lastUpdateMillis == -1L || lastUpdateTimeAgo > 10 * 60 * 1000) {
Log.d(TAG, "trigger index update, last was " + Date(lastUpdateMillis))
UpdateIndexTask(context, libraryHandler.syncthingClient!!).updateIndex()
}
uiThread {
onLibraryLoaded(libraryHandler)
}
}
}
}
@@ -1,151 +1,82 @@
package net.syncthing.lite.library
import android.content.Context
import android.os.Handler
import android.util.Log
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import 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.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.IndexInfo
import net.syncthing.java.core.configuration.Configuration
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.java.core.security.KeystoreHandler
import net.syncthing.lite.utils.Util
import org.jetbrains.anko.doAsync
import java.util.*
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit) {
class LibraryHandler {
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
interface OnIndexUpdatedListener {
fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int)
fun onIndexUpdateComplete()
}
private val TAG = "LibraryHandler"
init {
instanceCount++
if (configuration == null && !isLoading) {
isLoading = true
doAsync {
init(context)
async(UI) {
onLibraryLoaded(this@LibraryHandler)
}
isLoading = false
}
} else {
onLibraryLoaded(this)
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()
}
syncthingClient {
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
}
}
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
async(UI) {
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
}
}
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
async(UI) {
onIndexUpdateCompleteListener(folderInfo)
}
}
private fun init(context: Context) {
val configuration = Configuration(configFolder = context.filesDir, cacheFolder = context.externalCacheDir)
configuration.localDeviceName = Util.getDeviceName()
configuration.persistLater()
val syncthingClient = SyncthingClient(configuration)
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
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
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
}
private fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
mOnIndexUpdatedListener = onIndexUpdatedListener
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
// 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 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())
}
}
}
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
@Subscribe
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
}
}
instanceCount--
Handler().postDelayed({
Thread {
if (instanceCount == 0) {
folderBrowser?.close()
folderBrowser = null
syncthingClient?.close()
syncthingClient = null
configuration = null
}
}.start()
}, 60 * 1000)
})
}
fun destroy() {
folderBrowser!!.close()
syncthingClient!!.close()
configuration!!.close()
}
}
@@ -1,151 +0,0 @@
package net.syncthing.lite.library
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import android.util.Log
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import java.io.File
import java.io.FileNotFoundException
import java.net.URLConnection
import java.util.concurrent.CountDownLatch
class SyncthingProvider : DocumentsProvider() {
companion object {
private const val Tag = "SyncthingProvider"
private val DefaultRootProjection = arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON)
private val DefaultDocumentProjection = arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_SIZE,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS)
}
override fun onCreate(): Boolean {
Log.d(Tag, "onCreate()")
return true
}
private fun getLibraryHandler(): LibraryHandler {
val latch = CountDownLatch(1)
val libraryHandler = LibraryHandler(context, { latch.countDown() }, { _, _ -> }, {})
latch.await()
return libraryHandler
}
override fun queryRoots(projection: Array<String>?): Cursor {
Log.d(Tag, "queryRoots($projection)")
val latch = CountDownLatch(1)
var folders: List<Pair<FolderInfo, FolderStats>>? = null
getLibraryHandler().folderBrowser { folderBrowser ->
folders = folderBrowser.folderInfoAndStatsList()
latch.countDown()
}
latch.await()
val result = MatrixCursor(projection ?: DefaultRootProjection)
folders!!.forEach { folder ->
val row = result.newRow()
row.add(Root.COLUMN_ROOT_ID, folder.first.folderId)
row.add(Root.COLUMN_SUMMARY, folder.first.label)
row.add(Root.COLUMN_FLAGS, 0)
row.add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.first))
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
}
return result
}
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
sortOrder: String?): Cursor {
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
getIndexBrowser(getFolderIdForDocId(parentDocumentId))
.listFiles(getPathForDocId(parentDocumentId))
.forEach { fileInfo ->
includeFile(result, fileInfo)
}
return result
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
Log.d(Tag, "queryDocument($documentId, $projection)")
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
includeFile(result, fileInfo)
return result
}
@Throws(FileNotFoundException::class)
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?):
ParcelFileDescriptor {
Log.d(Tag, "openDocument($documentId, $mode, $signal)")
val fileInfo = FileInfo(folder = getFolderIdForDocId(documentId),
path = getPathForDocId(documentId), type = FileInfo.FileType.FILE)
val accessMode = ParcelFileDescriptor.parseMode(mode)
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
throw NotImplementedError()
}
val latch = CountDownLatch(1)
var outputFile: File? = null
getLibraryHandler().syncthingClient { syncthingClient ->
DownloadFileTask(context, syncthingClient, fileInfo,
{ t, _ -> if (signal?.isCanceled == true) t.cancel() }, {
outputFile = it
latch.countDown()
}, {})
}
latch.await()
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
}
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
val row = result.newRow()
row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
row.add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
row.add(Document.COLUMN_SIZE, fileInfo.size)
val mime = if (fileInfo.isDirectory()) Document.MIME_TYPE_DIR
else URLConnection.guessContentTypeFromName(fileInfo.fileName)
row.add(Document.COLUMN_MIME_TYPE, mime)
row.add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
row.add(Document.COLUMN_FLAGS, 0)
}
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
private fun getPathForDocId(docId: String) = docId.split(":")[1]
private fun getDocIdForFile(folderInfo: FolderInfo) = folderInfo.folderId + ":"
private fun getDocIdForFile(fileInfo: FileInfo) = fileInfo.folder + ":" + fileInfo.path
private fun getIndexBrowser(folderId: String): IndexBrowser {
val latch = CountDownLatch(1)
var indexBrowser: IndexBrowser? = null
getLibraryHandler().syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folderId)
latch.countDown()
}
latch.await()
return indexBrowser!!
}
}
@@ -0,0 +1,47 @@
package net.syncthing.lite.library
import android.content.Context
import android.preference.PreferenceManager
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.util.*
class UpdateIndexTask(private val androidContext: Context, private val syncthingClient: SyncthingClient) {
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(androidContext)
fun updateIndex() {
if (sIndexUpdateInProgress)
return
sIndexUpdateInProgress = true
syncthingClient.updateIndexFromPeers { _, failures ->
sIndexUpdateInProgress = false
if (failures.isEmpty()) {
showToast(androidContext.getString(R.string.toast_index_update_successful))
} else {
showToast(androidContext.getString(R.string.toast_index_update_failed, failures.size))
}
mPreferences.edit()
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
.apply()
}
}
private fun showToast(message: String) {
doAsync {
uiThread {
androidContext.toast(message)
}
}
}
companion object {
val LAST_INDEX_UPDATE_TS_PREF = "LAST_INDEX_UPDATE_TS"
private var sIndexUpdateInProgress: Boolean = false
}
}
@@ -1,48 +1,103 @@
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import android.util.Log
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.R
import net.syncthing.lite.utils.Util
import org.apache.commons.io.IOUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.IOException
// TODO: this should be an IntentService with notification
class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
localFile: Uri, private val syncthingFolder: String,
class UploadFileTask(private val context: Context, private val syncthingClient: SyncthingClient,
private val localFile: Uri, private val syncthingFolder: String,
syncthingSubFolder: String,
private val onProgress: (BlockPusher.FileUploadObserver) -> Unit,
private val onComplete: () -> Unit,
private val onError: () -> Unit) {
private val onUploadCompleteListener: () -> Unit) {
private val TAG = "UploadFileTask"
companion object {
private val TAG = "UploadFileTask"
}
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
private val uploadStream = context.contentResolver.openInputStream(localFile)
private val fileName = Util.getContentFileName(context, localFile)
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
private var isCancelled = false
private lateinit var mProgressDialog: ProgressDialog
private var mCancelled = false
init {
fun uploadFile() {
createDialog()
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
onProgress(observer)
while (!observer.isCompleted()) {
if (isCancelled)
return@getBlockPusher
try {
val uploadStream = context.contentResolver.openInputStream(localFile)
syncthingClient.pushFile(uploadStream, syncthingFolder, syncthingPath, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted) {
if (mCancelled)
return@pushFile
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
onProgress(observer)
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = " + observer.progressMessage)
onProgress(observer)
}
} catch (e: InterruptedException) {
onError()
}
IOUtils.closeQuietly(uploadStream)
onComplete()
}, { onError() })
}, { this.onError() })
} catch (e: IOException) {
onError()
}
}
fun cancel() {
isCancelled = true
private fun createDialog() {
mProgressDialog = ProgressDialog(context)
mProgressDialog.setMessage(context.getString(R.string.dialog_uploading_file, fileName))
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
mProgressDialog.setCancelable(true)
mProgressDialog.setOnCancelListener { mCancelled = true }
mProgressDialog.isIndeterminate = true
mProgressDialog.show()
}
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
doAsync {
uiThread {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource.size.toInt()
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
}
}
}
private fun onComplete() {
if (mCancelled)
return
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
}
private fun onError() {
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_file_upload_failed)
}
}
}
}
@@ -1,82 +0,0 @@
package net.syncthing.lite.utils
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.support.v4.content.FileProvider
import android.util.Log
import android.webkit.MimeTypeMap
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import net.syncthing.lite.library.DownloadFileTask
import org.apache.commons.io.FilenameUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.newTask
import org.jetbrains.anko.toast
import java.io.File
class FileDownloadDialog(context: Context, syncthingClient: SyncthingClient,
private val fileInfo: FileInfo) : AlertDialog(context) {
private val Tag = "FileDownloadDialog"
private lateinit var progressDialog: ProgressDialog
private var downloadFileTask: DownloadFileTask? = null
init {
showDialog()
doAsync {
downloadFileTask = DownloadFileTask(context, syncthingClient, fileInfo,
this@FileDownloadDialog::onProgress, this@FileDownloadDialog::onComplete,
this@FileDownloadDialog::onError)
}
}
private fun showDialog() {
progressDialog = ProgressDialog(context)
progressDialog.setMessage(context.getString(R.string.dialog_downloading_file, fileInfo.fileName))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { downloadFileTask?.cancel() }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(downloadFileTask: DownloadFileTask, fileDownloadObserver: BlockPuller.FileDownloadObserver) {
async(UI) {
progressDialog.isIndeterminate = false
progressDialog.max = (fileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress() * fileInfo.size!!).toInt()
}
}
private fun onComplete(file: File) {
async(UI) {
progressDialog.dismiss()
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
val uri = FileProvider.getUriForFile(this@FileDownloadDialog.context, "net.syncthing.lite.fileprovider", file)
intent.setDataAndType(uri, mimeType)
intent.newTask()
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
this@FileDownloadDialog.context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
this@FileDownloadDialog.context.toast(R.string.toast_open_file_failed)
Log.w(Tag, "No handler found for file " + file.name, e)
}
}
}
private fun onError() {
async(UI) {
progressDialog.cancel()
this@FileDownloadDialog.context.toast(R.string.toast_file_download_failed)
}
}
}
@@ -1,64 +0,0 @@
package net.syncthing.lite.utils
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import net.syncthing.lite.library.UploadFileTask
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
class FileUploadDialog(private val context: Context, private val syncthingClient: SyncthingClient,
private val localFile: Uri, private val syncthingFolder: String,
syncthingSubFolder: String,
private val onUploadCompleteListener: () -> Unit) {
private lateinit var progressDialog: ProgressDialog
private var uploadFileTask: UploadFileTask? = null
init {
showDialog()
doAsync {
uploadFileTask = UploadFileTask(context, syncthingClient, localFile, syncthingFolder,
syncthingSubFolder, this@FileUploadDialog::onProgress,
this@FileUploadDialog::onComplete, this@FileUploadDialog::onError)
}
}
private fun showDialog() {
progressDialog = ProgressDialog(context)
progressDialog.setMessage(context.getString(R.string.dialog_uploading_file, Util.getContentFileName(context, localFile)))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { uploadFileTask?.cancel() }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
async(UI) {
progressDialog.isIndeterminate = false
progressDialog.progress = observer.progressPercentage()
progressDialog.max = 100
}
}
private fun onComplete() {
async(UI) {
progressDialog.dismiss()
this@FileUploadDialog.context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
private fun onError() {
async(UI) {
progressDialog.dismiss()
this@FileUploadDialog.context.toast(R.string.toast_file_upload_failed)
}
}
}
@@ -3,15 +3,18 @@ package net.syncthing.lite.utils
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
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.security.InvalidParameterException
import java.io.File
object Util {
fun getDeviceName(): String {
val manufacturer = Build.MANUFACTURER ?: ""
val model = Build.MODEL ?: ""
val manufacturer = nullToEmpty(Build.MANUFACTURER)
val model = nullToEmpty(Build.MODEL)
val deviceName =
if (model.startsWith(manufacturer)) {
capitalize(model)
@@ -21,12 +24,17 @@ object Util {
return deviceName ?: "android"
}
fun getContentFileName(context: Context, uri: Uri): String {
context.contentResolver.query(uri, null, null, null, null, null).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) {
throw InvalidParameterException("Cursor is null or empty")
fun getContentFileName(context: Context, contentUri: Uri): String {
var fileName = File(contentUri.lastPathSegment).name
if (equal(contentUri.scheme, "content")) {
context.contentResolver.query(contentUri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)!!.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
val path = cursor.getString(columnIndex)
Log.d("Main", "recovered 'content' uri real path = " + path)
fileName = File(Uri.parse(path).lastPathSegment).name
}
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
return fileName
}
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#7D000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>
@@ -6,6 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--center content BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -13,6 +14,36 @@
android:divider="?android:listDivider"
android:showDividers="middle">
<!--index loading progress BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18sp"
android:textColor="@color/white_on_primary"
android:text="@string/index_update_progress_message"
android:layout_gravity="start"
android:textAlignment="gravity"
/>
</LinearLayout>
<!--index loading progress END-->
<!--main list view BEGIN-->
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -38,9 +69,12 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone"/>
<!--main list view END-->
</LinearLayout>
<!--center content END-->
<!--upload here overlay button BEGIN-->
<android.support.design.widget.FloatingActionButton
android:id="@+id/main_list_view_upload_here_button"
android:layout_width="wrap_content"
@@ -52,6 +86,7 @@
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:src="@drawable/ic_file_upload_white_24dp"/>
<!--upload here overlay button END-->
</RelativeLayout>
@@ -0,0 +1,22 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/directory_empty" />
</FrameLayout>
</LinearLayout>
+29 -2
View File
@@ -5,12 +5,40 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18sp"
android:textColor="@color/white_on_primary"
android:text="@string/index_update_progress_message"
android:layout_gravity="start"
android:textAlignment="gravity"
/>
</LinearLayout>
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<android.support.design.widget.NavigationView
android:id="@+id/navigation"
android:layout_width="wrap_content"
@@ -18,7 +46,6 @@
android:layout_gravity="start"
android:background="@android:color/white"
app:menu="@menu/drawer_view" />
</android.support.v4.widget.DrawerLayout>
</layout>
+17 -10
View File
@@ -4,20 +4,27 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:padding="@dimen/abc_action_bar_content_inset_material"
android:theme="?alertDialogTheme"
android:orientation="horizontal"
android:gravity="center">
android:orientation="vertical">
<ProgressBar
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp" />
android:orientation="horizontal"
android:gravity="center">
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/abc_action_bar_content_inset_material" />
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
@@ -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>
+2 -2
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingTop="12dp">
<ImageView
+2 -2
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingTop="12dp">
<TextView
+6
View File
@@ -13,6 +13,12 @@
android:icon="@drawable/ic_laptop_gray_24dp"
android:title="@string/devices_label" />
<item
android:id="@+id/update_index"
android:icon="@drawable/ic_refresh_gray_24dp"
android:title="@string/update_remote_index_label"
android:checkable="false"/>
<item
android:id="@+id/clear_index"
android:icon="@drawable/ic_delete_gray_24dp"
+17 -9
View File
@@ -1,29 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<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_folder">Index aktualisierung, Ordner</string>
<string name="index_update_percent_synchronized">% synchronisiert</string>
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
<string name="last_modified">- zuletzt modifiziert</string>
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</string>
<string name="last_modified_known">zuletzt modifiziert:</string>
<string name="remove_device_title">Gerät entfernen:</string>
<string name="remove_device_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="remove_device_body_1">Gerät</string>
<string name="remove_device_body_2">von den bekannten Geräten entfernen?</string>
<string name="device_import_success">Gerät erfolgreich importiert:</string>
<string name="device_already_known">Gerät ist bereits bekannt:</string>
<string name="folders_label">Ordner</string>
<string name="devices_label">Geräte</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d Dateien, %3$d Ordner</string>
<string name="file_info">%1$s, zuletzt modifiziert %2$s</string>
</resources>
</resources>
-29
View File
@@ -1,29 +0,0 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Aucun dossier disponible</string>
<string name="clear_local_cache_index_label">Effacer le cache et l\'index local</string>
<string name="devices_list_view_empty_message">Aucun appareil disponible</string>
<string name="scan_qr_code">Scanner le QR Code</string>
<string name="enter_device_id">Entrer l\'ID de l\'appareil</string>
<string name="invalid_device_id">ID de l\'appareil invalide</string>
<string name="device_id_dialog_title">Entrer l\'ID de l\'appareil</string>
<string name="dialog_downloading_file">Téléchargement du fichier %1$s</string>
<string name="toast_file_download_failed">Le téléchargement du fichier a échoué</string>
<string name="toast_open_file_failed">Aucune appli compatible trouvée</string>
<string name="toast_file_upload_failed">Échec de l\'upload</string>
<string name="toast_upload_complete">Upload du fichier terminé</string>
<string name="dialog_uploading_file">Upload du fichier %1$s</string>
<string name="clear_cache_and_index_title">Effacer le cache local et l\'index?</string>
<string name="clear_cache_and_index_body">Effacer toutes les données du cache local et de l\'index ?</string>
<string name="loading_config_starting_syncthing_client">Chargement de la configuration, démarrage du client Syncthing</string>
<string name="last_modified_time">Dernière modification : %1$s</string>
<string name="remove_device_title">Supprimer l\'appareil %1$s\?</string>
<string name="remove_device_message">Supprimer l\'appareil %1$s de la liste des appareil connus ?</string>
<string name="device_import_success">Appareil %1$s importé avec succès</string>
<string name="device_already_known">Appareil déjà connu %1$s</string>
<string name="folders_label">Dossiers</string>
<string name="devices_label">Appareils</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d fichiers, %3$d dossiers</string>
<string name="file_info">%1$s, dernière modification %2$s</string>
</resources>
-29
View File
@@ -1,29 +0,0 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Nessuna cartella disponibile</string>
<string name="clear_local_cache_index_label">Cancella cache/indice</string>
<string name="devices_list_view_empty_message">Nessun dispositivo disponibile</string>
<string name="scan_qr_code">Scansiona codice QR</string>
<string name="enter_device_id">Inserisci ID dispositivo</string>
<string name="invalid_device_id">ID dispositivo non valido</string>
<string name="device_id_dialog_title">Inserisci ID Dispositivo</string>
<string name="dialog_downloading_file">Download del file %1$s</string>
<string name="toast_file_download_failed">Impossibile scaricare il file</string>
<string name="toast_open_file_failed">Nessuna applicazione compatibile trovata</string>
<string name="toast_file_upload_failed">Caricamento file fallito</string>
<string name="toast_upload_complete">Caricamento file completato</string>
<string name="dialog_uploading_file">Caricamento del file %1$s</string>
<string name="clear_cache_and_index_title">Cancellare la cache locale e l\'indice?</string>
<string name="clear_cache_and_index_body">Cancellare tutti i dati della cache locale e i dati dell\'indice?</string>
<string name="loading_config_starting_syncthing_client">Caricamento configurazione, avvio del client syncthing</string>
<string name="last_modified_time">Ultima modifica: %1$s</string>
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</string>
<string name="remove_device_message">Rimuovere il dispositivo %1$s dalla lista dei dispositivi noti?</string>
<string name="device_import_success">Dispositivo %1$s importato con successo</string>
<string name="device_already_known">Dispositivo %1$s già presente</string>
<string name="folders_label">Cartelle</string>
<string name="devices_label">Dispositivi</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d file, %3$d cartelle</string>
<string name="file_info">%1$s, ultima modifica %2$s</string>
</resources>
-29
View File
@@ -1,29 +0,0 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">フォルダーがありません</string>
<string name="clear_local_cache_index_label">ローカルキャッシュ/索引をクリア</string>
<string name="devices_list_view_empty_message">デバイスがありません</string>
<string name="scan_qr_code">QR コードをスキャン</string>
<string name="enter_device_id">デバイス ID を入力</string>
<string name="invalid_device_id">デバイス ID が無効です</string>
<string name="device_id_dialog_title">デバイス ID を入力</string>
<string name="dialog_downloading_file">ファイル %1$s のダウンロード中</string>
<string name="toast_file_download_failed">ファイルのダウンロードに失敗しました</string>
<string name="toast_open_file_failed">利用できるアプリが見つかりません</string>
<string name="toast_file_upload_failed">ファイルのアップロードに失敗しました</string>
<string name="toast_upload_complete">ファイルのアップロードが完了しました</string>
<string name="dialog_uploading_file">ファイル %1$s のアップロード中</string>
<string name="clear_cache_and_index_title">ローカルキャッシュと索引をクリアしますか?</string>
<string name="clear_cache_and_index_body">すべてのローカルキャッシュデータと索引データをクリアしますか?</string>
<string name="loading_config_starting_syncthing_client">設定の読み込み中、syncthing クライアントの開始中</string>
<string name="last_modified_time">最終更新: %1$s</string>
<string name="remove_device_title">デバイス %1$sを削除しますか?</string>
<string name="remove_device_message">既存のデバイスリストからデバイス %1$s を削除しますか?</string>
<string name="device_import_success">デバイス %1$s のインポートに成功しました</string>
<string name="device_already_known">デバイスは既に存在します %1$s</string>
<string name="folders_label">フォルダー</string>
<string name="devices_label">デバイス</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d ファイル, %3$d ディレクトリー</string>
<string name="file_info">%1$s, 最終更新 %2$s</string>
</resources>
-29
View File
@@ -1,29 +0,0 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Nici un director disponibil</string>
<string name="clear_local_cache_index_label">Curăță indexul/memoria locală</string>
<string name="devices_list_view_empty_message">Nici un dispozitiv disponibil</string>
<string name="scan_qr_code">Scanează cod QR</string>
<string name="enter_device_id">Introduceți ID dispozitiv</string>
<string name="invalid_device_id">ID dispozitiv invalid</string>
<string name="device_id_dialog_title">Introduceți ID dispozitiv</string>
<string name="dialog_downloading_file">Se descarcă fișierul %1$s</string>
<string name="toast_file_download_failed">Descărcarea fișierului a eșuat</string>
<string name="toast_open_file_failed">Nu a fost găsită nici o aplicație compatibilă</string>
<string name="toast_file_upload_failed">Încărcarea fișierului a eșuat</string>
<string name="toast_upload_complete">Încărcarea fișierelor finalizată</string>
<string name="dialog_uploading_file">Se încarcă fișierul %1$s</string>
<string name="clear_cache_and_index_title">Se curăță memoria locală și indexul?</string>
<string name="clear_cache_and_index_body">Se curăță datele din memoria locală și datele indexului?</string>
<string name="loading_config_starting_syncthing_client">Încărcare setări, pornire client syncthing…</string>
<string name="last_modified_time">Modificat ultima dată pe: %1$s</string>
<string name="remove_device_title">Ștergere dispozitiv %1$s\?</string>
<string name="remove_device_message">Se va șterge %1$s din lista dispozitivelor cunoscute?</string>
<string name="device_import_success">Dispozitiv importat cu succes %1$s</string>
<string name="device_already_known">Dispozitiv deja prezent %1$s</string>
<string name="folders_label">Directoare</string>
<string name="devices_label">Dispozitive</string>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d fișier(e), %3$d director(oare)</string>
<string name="file_info">%1$s, modificat ultima dată pe %2$s</string>
</resources>
+4
View File
@@ -2,7 +2,11 @@
<resources>
<color name="primary">#f43703</color>
<color name="primary_dark">#d13602</color>
<color name="white_on_primary">#fefefe</color>
<color name="accent">#FFC107</color>
<color name="divider">#1F000000</color>
<color name="device_online_active">#ff99cc00</color>
<color name="device_online_inactive">#f43703</color>
<color name="device_offline">#aaaaaa</color>
</resources>
+17 -10
View File
@@ -1,30 +1,37 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="app_name" translatable="false">Syncthing Lite</string>
<string name="index_update_progress_message">Index update…</string>
<string name="folder_list_empty_message">No folder available</string>
<string name="clear_local_cache_index_label">Clear local cache/index</string>
<string name="update_remote_index_label">Update remote index</string>
<string name="devices_list_view_empty_message">No devices available</string>
<string name="toast_write_storage_permission_required">Write storage permission is required for this functionality</string>
<string name="scan_qr_code">Scan QR code</string>
<string name="enter_device_id">Enter device ID</string>
<string name="invalid_device_id">Invalid device ID</string>
<string name="device_id_dialog_title">Enter Device ID</string>
<string name="toast_index_update_successful">Index update successful</string>
<string name="toast_index_update_failed">Index update failed for %1$d devices</string>
<string name="dialog_downloading_file">Downloading file %1$s</string>
<string name="toast_file_download_failed">Failed to download file</string>
<string name="toast_open_file_failed">No compatible app found</string>
<string name="toast_file_upload_failed">File upload failed</string>
<string name="toast_upload_complete">File upload complete</string>
<string name="dialog_uploading_file">Uploading file %1$s</string>
<string name="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="index_update_folder">Index update, folder</string>
<string name="index_update_percent_synchronized">% synchronized</string>
<string name="loading_config_starting_syncthing_client">Loading config, starting syncthing client</string>
<string name="last_modified_time">Last modified: %1$s</string>
<string name="remove_device_title">Remove device %1$s\?</string>
<string name="remove_device_message">Remove device %1$s from list of known devices?</string>
<string name="device_import_success">Successfully imported device %1$s</string>
<string name="device_already_known">Device already present %1$s</string>
<string name="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="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
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="files" path="/" />
</paths>
+4 -3
View File
@@ -1,10 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.20'
ext.kotlin_version = '1.2.0'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.0.1'
ext.anko_version = '0.10.4'
repositories {
mavenLocal()
jcenter()
@@ -14,7 +13,9 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:$build_tools_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
-54
View File
@@ -1,54 +0,0 @@
#!/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
@@ -1,37 +0,0 @@
#!/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!
"