Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 087b0f4ec1 | |||
| a6cc38f4a1 | |||
| 96a4ec1738 | |||
| 42b77cab12 | |||
| a28e5d704d | |||
| b924b50ddc | |||
| 96503dd9c1 | |||
| c83504c155 | |||
| acea170854 | |||
| c6950493e6 | |||
| b71740d044 | |||
| dafe262c1c | |||
| acb1f75c5c | |||
| 03cc4f931d | |||
| 967d65b3f9 | |||
| ce6e7e2130 | |||
| 809eff7354 | |||
| 944cecce1f | |||
| 31abff58c1 | |||
| ef401378e2 | |||
| 2c0be54e61 | |||
| cb4b838082 | |||
| 6a6b40a89d | |||
| 1b7dbd91e6 | |||
| cb7a2d362f | |||
| ef2d7fe9d7 | |||
| 0bd96302e0 | |||
| e0a95a0314 | |||
| c2e7f7cbc2 | |||
| 631a2a4fe3 | |||
| 36e54f5d24 | |||
| 3506df6b22 | |||
| 2e1369d9a8 | |||
| a3495784f7 | |||
| 82c92c9031 | |||
| bd5d89c158 | |||
| 9cf96d86dd | |||
| f4c1e6a0f0 | |||
| 036d3846bc | |||
| 6696f0ff88 | |||
| ffff6a7617 | |||
| 3bd1f6e44a | |||
| a2f46b358d | |||
| a0e749e879 | |||
| e566b3c58d | |||
| acff8c1f5c | |||
| 4353fc5597 | |||
| 64b2b7424b | |||
| 2fa0dd2e59 | |||
| 2e1fa76b44 | |||
| f447e1ecad | |||
| ee9098b4f1 | |||
| f378329da3 | |||
| 0caec11826 |
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[syncthing-lite.stringsxml]
|
||||
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = app/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw, id:in
|
||||
@@ -13,13 +13,22 @@ example, mobile devices with limited storage available, wishing to access a sync
|
||||
|
||||
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
|
||||
|
||||
|
||||
[<img alt="Get it on F-Droid" src="https://f-droid.org/badge/get-it-on.png" height="80">](https://f-droid.org/packages/net.syncthing.lite/)
|
||||
[<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80">](https://play.google.com/store/apps/details?id=net.syncthing.lite)
|
||||
|
||||
## Translations
|
||||
|
||||
The project is translated on [Transifex](https://www.transifex.com/syncthing-android/syncthing-lite/).
|
||||
|
||||
## Building
|
||||
|
||||
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
|
||||
you install [Android Studio][4] and import the project.
|
||||
|
||||
The syncthing-java library is not stable yet. If you encounter any build errors, you probably have
|
||||
to build it from source. To do this, clone the repo and run `gradle install`.
|
||||
To compile with a development version of the [syncthing-java][3] library, you have to install it to
|
||||
the local maven repository. To do this, clone the repo and run `gradle install` in the
|
||||
syncthing-java project folder.
|
||||
|
||||
## License
|
||||
All code is licensed under the [MPLv2 License][5].
|
||||
|
||||
+32
-11
@@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
@@ -8,10 +9,10 @@ android {
|
||||
dataBinding.enabled = true
|
||||
defaultConfig {
|
||||
applicationId "net.syncthing.lite"
|
||||
minSdkVersion 19
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 2
|
||||
versionName "0.1"
|
||||
versionCode 9
|
||||
versionName "0.2.1"
|
||||
multiDexEnabled true
|
||||
}
|
||||
sourceSets {
|
||||
@@ -21,26 +22,46 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile = {
|
||||
def path = System.getenv("SYNCTHING_LITE_RELEASE_STORE_FILE")
|
||||
return (path) ? file(path) : null
|
||||
}()
|
||||
storePassword System.getenv("SIGNING_PASSWORD") ?: ""
|
||||
keyAlias System.getenv("SYNCTHING_LITE_RELEASE_KEY_ALIAS") ?: ""
|
||||
keyPassword System.getenv("SIGNING_PASSWORD") ?: ""
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/*'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko-commons:$anko_version"
|
||||
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
|
||||
kapt "com.android.databinding:compiler:$build_tools_version"
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation "com.android.support:recyclerview-v7:$support_version"
|
||||
implementation "com.android.support:support-v4:$support_version"
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:cardview-v7:$support_version"
|
||||
implementation ("com.github.Nutomic:syncthing-java:0.1") {
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
kapt "com.android.databinding:library:1.3.3"
|
||||
implementation ("com.github.Nutomic:syncthing-java:0.2.1") {
|
||||
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 'com.google.zxing:core:3.3.0'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.3'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
|
||||
<issue id="GoogleAppIndexingWarning" severity="ignore" />
|
||||
|
||||
<issue id="InvalidPackage" severity="ignore" />
|
||||
|
||||
<issue id="OldTargetApi" severity="ignore" />
|
||||
</lint>
|
||||
@@ -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"
|
||||
@@ -18,17 +17,29 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".activities.IntroActivity"
|
||||
android:theme="@style/Theme.Syncthing.NoActionBar"/>
|
||||
<activity android:name=".activities.FolderBrowserActivity"
|
||||
android:parentActivityName=".activities.MainActivity"/>
|
||||
<activity
|
||||
android:name=".activities.MIVFilePickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme">
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="net.syncthing.lite.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name=".library.SyncthingProvider"
|
||||
android:authorities="net.syncthing.lite.documents"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,221 +1,132 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.google.common.base.Objects.equal
|
||||
import com.google.common.base.Preconditions.checkArgument
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.utils.FileInfoOrdering
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.adapters.FolderContentsAdapter
|
||||
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
|
||||
import net.syncthing.lite.databinding.DialogLoadingBinding
|
||||
import net.syncthing.lite.utils.DownloadFileTask
|
||||
import net.syncthing.lite.utils.UploadFileTask
|
||||
import net.syncthing.lite.dialogs.FileDownloadDialog
|
||||
import net.syncthing.lite.dialogs.FileUploadDialog
|
||||
|
||||
class FolderBrowserActivity : SyncthingActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = "FolderBrowserActivity"
|
||||
private val REQUEST_WRITE_STORAGE = 142
|
||||
private const val TAG = "FolderBrowserActivity"
|
||||
private const val REQUEST_SELECT_UPLOAD_FILE = 171
|
||||
|
||||
val EXTRA_FOLDER_NAME = "folder_name"
|
||||
const val EXTRA_FOLDER_NAME = "folder_name"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityFolderBrowserBinding
|
||||
private var indexBrowser: IndexBrowser? = null
|
||||
private var loadingDialog: AlertDialog? = null
|
||||
private var adapter: FolderContentsAdapter? = null
|
||||
private var runWhenPermissionsReceived: Runnable? = null
|
||||
private lateinit var indexBrowser: IndexBrowser
|
||||
private lateinit var adapter: FolderContentsAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
|
||||
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
|
||||
showFolderListView(intent.getStringExtra(EXTRA_FOLDER_NAME), null)
|
||||
adapter = FolderContentsAdapter(this)
|
||||
binding.listView.adapter = adapter
|
||||
binding.listView.setOnItemClickListener { _, _, position, _ ->
|
||||
val fileInfo = binding.listView.getItemAtPosition(position) as FileInfo
|
||||
navigateToFolder(fileInfo)
|
||||
}
|
||||
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
|
||||
libraryHandler?.syncthingClient {
|
||||
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
|
||||
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Thread {
|
||||
indexBrowser?.close()
|
||||
indexBrowser = null
|
||||
indexBrowser.setOnFolderChangedListener(null)
|
||||
indexBrowser.close()
|
||||
}.start()
|
||||
cancelLoadingDialog()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val listView = binding.mainFolderAndFilesListView
|
||||
val listView = binding.listView
|
||||
//click item '0', ie '..' (go to parent)
|
||||
listView.performItemClick(adapter!!.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
|
||||
listView.performItemClick(adapter.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
UploadFileTask(this, syncthingClient(), intent.data, indexBrowser!!.folder,
|
||||
indexBrowser!!.currentPath, { this.updateFolderListView() }).uploadFile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoadingDialog(message: String) {
|
||||
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
|
||||
layoutInflater, R.layout.dialog_loading, null, false)
|
||||
binding.loadingText.text = message
|
||||
loadingDialog = android.app.AlertDialog.Builder(this)
|
||||
.setCancelable(false)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun cancelLoadingDialog() {
|
||||
loadingDialog?.cancel()
|
||||
loadingDialog = null
|
||||
}
|
||||
|
||||
private fun showFolderListView(folder: String, previousPath: String?) {
|
||||
if (indexBrowser != null && equal(folder, indexBrowser!!.folder)) {
|
||||
Log.d(TAG, "reuse current index browser")
|
||||
indexBrowser!!.navigateToNearestPath(previousPath)
|
||||
} else {
|
||||
if (indexBrowser != null) {
|
||||
indexBrowser!!.close()
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
|
||||
indexBrowser.folder, indexBrowser.currentPath,
|
||||
{ showFolderListView(indexBrowser.currentPath) }).show()
|
||||
}
|
||||
Log.d(TAG, "open new index browser")
|
||||
indexBrowser = syncthingClient().indexHandler
|
||||
.newIndexBrowserBuilder()
|
||||
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
|
||||
.includeParentInList(true).allowParentInRoot(true)
|
||||
.setFolder(folder)
|
||||
.buildToNearestPath(previousPath)
|
||||
}
|
||||
adapter = FolderContentsAdapter(this)
|
||||
binding.mainFolderAndFilesListView.adapter = adapter
|
||||
binding.mainFolderAndFilesListView.setOnItemClickListener { _, _, position, _ ->
|
||||
val fileInfo = binding.mainFolderAndFilesListView.getItemAtPosition(position) as FileInfo
|
||||
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser!!.currentPath + "'")
|
||||
navigateToFolder(fileInfo)
|
||||
}
|
||||
navigateToFolder(indexBrowser!!.currentPathInfo)
|
||||
}
|
||||
|
||||
private fun showFolderListView(path: String) {
|
||||
indexBrowser.navigateToNearestPath(path)
|
||||
navigateToFolder(indexBrowser.currentPathInfo())
|
||||
}
|
||||
|
||||
private fun navigateToFolder(fileInfo: FileInfo) {
|
||||
if (indexBrowser!!.isRoot && PathUtils.isParent(fileInfo.path)) {
|
||||
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
|
||||
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
|
||||
finish()
|
||||
} else {
|
||||
if (fileInfo.isDirectory) {
|
||||
indexBrowser!!.navigateTo(fileInfo)
|
||||
val newFileInfo = if (PathUtils.isParent(fileInfo.path)) indexBrowser!!.currentPathInfo else fileInfo
|
||||
if (!indexBrowser!!.isCacheReadyAfterALittleWait) {
|
||||
Log.d(TAG, "load folder cache bg")
|
||||
object : AsyncTask<Void?, Void?, Void?>() {
|
||||
override fun onPreExecute() {
|
||||
// TODO: show ProgressBar in ListView instead of dialog
|
||||
showLoadingDialog("open directory: " +
|
||||
if (indexBrowser!!.isRoot) folderBrowser().getFolderInfo(indexBrowser!!.folder).label
|
||||
else indexBrowser!!.currentPathFileName)
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg voids: Void?): Void? {
|
||||
indexBrowser!!.waitForCacheReady()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onPostExecute(aVoid: Void?) {
|
||||
Log.d(TAG, "cache ready, navigate to folder")
|
||||
cancelLoadingDialog()
|
||||
navigateToFolder(newFileInfo)
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
} else {
|
||||
val list = indexBrowser!!.listFiles()
|
||||
Log.i("navigateToFolder", "list for path = '" + indexBrowser!!.currentPath + "' list = " + list.size + " records")
|
||||
Log.d("navigateToFolder", "list for path = '" + indexBrowser!!.currentPath + "' list = " + list)
|
||||
checkArgument(!list.isEmpty())//list must contain at least the 'parent' path
|
||||
adapter!!.clear()
|
||||
adapter!!.addAll(list)
|
||||
adapter!!.notifyDataSetChanged()
|
||||
binding.mainFolderAndFilesListView.setSelection(0)
|
||||
supportActionBar!!.setTitle(if (indexBrowser!!.isRoot)
|
||||
folderBrowser().getFolderInfo(indexBrowser!!.folder).label
|
||||
else
|
||||
newFileInfo.fileName)
|
||||
}
|
||||
if (fileInfo.isDirectory()) {
|
||||
indexBrowser.navigateTo(fileInfo)
|
||||
Log.d(TAG, "load folder cache bg")
|
||||
binding.listView.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
} else {
|
||||
Log.i(TAG, "pulling file = " + fileInfo)
|
||||
executeWithPermissions(
|
||||
Runnable { DownloadFileTask(this, syncthingClient(), fileInfo).downloadFile() })
|
||||
libraryHandler?.syncthingClient { FileDownloadDialog(this, it, fileInfo).show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFolderChanged() {
|
||||
runOnUiThread {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.listView.visibility = View.VISIBLE
|
||||
val list = indexBrowser.listFiles()
|
||||
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
|
||||
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
|
||||
assert(!list.isEmpty())//list must contain at least the 'parent' path
|
||||
adapter.clear()
|
||||
adapter.addAll(list)
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.listView.setSelection(0)
|
||||
if (indexBrowser.isRoot())
|
||||
libraryHandler?.folderBrowser {
|
||||
supportActionBar?.title = it.getFolderInfo(indexBrowser.folder)?.label
|
||||
}
|
||||
else
|
||||
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFolderListView() {
|
||||
showFolderListView(indexBrowser!!.folder, indexBrowser!!.currentPath)
|
||||
showFolderListView(indexBrowser.currentPath)
|
||||
}
|
||||
|
||||
private fun showUploadHereDialog() {
|
||||
executeWithPermissions(Runnable {
|
||||
val i = Intent(this, MIVFilePickerActivity::class.java)
|
||||
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
|
||||
Log.i(TAG, "showUploadHereDialog path = " + path)
|
||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, path)
|
||||
startActivityForResult(i, 0)
|
||||
})
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "*/*"
|
||||
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
|
||||
}
|
||||
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
binding.mainIndexProgressBarLabel.text = ("index update, folder "
|
||||
+ folder.label + " " + percentage + "% synchronized")
|
||||
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
super.onIndexUpdateComplete(folderInfo)
|
||||
updateFolderListView()
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
binding.mainIndexProgressBar.visibility = View.GONE
|
||||
updateFolderListView()
|
||||
}
|
||||
|
||||
private fun executeWithPermissions(runnable: Runnable) {
|
||||
val permissionState = ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
if (permissionState != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
REQUEST_WRITE_STORAGE)
|
||||
runWhenPermissionsReceived = runnable
|
||||
} else {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
when (requestCode) {
|
||||
REQUEST_WRITE_STORAGE -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, R.string.toast_write_storage_permission_required,
|
||||
Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
runWhenPermissionsReceived!!.run()
|
||||
}
|
||||
runWhenPermissionsReceived = null
|
||||
}
|
||||
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.github.paolorotolo.appintro.AppIntro
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.FragmentIntroOneBinding
|
||||
import net.syncthing.lite.databinding.FragmentIntroThreeBinding
|
||||
import net.syncthing.lite.databinding.FragmentIntroTwoBinding
|
||||
import net.syncthing.lite.fragments.SyncthingFragment
|
||||
import net.syncthing.lite.utils.FragmentIntentIntegrator
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import org.jetbrains.anko.intentFor
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Shown when a user first starts the app. Shows some info and helps the user to add their first
|
||||
* device and folder.
|
||||
*/
|
||||
class IntroActivity : AppIntro() {
|
||||
|
||||
/**
|
||||
* Initialize fragments and library parameters.
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Disable continue button on second slide until a valid device ID is entered.
|
||||
nextButton.setOnClickListener {
|
||||
val fragment = fragments[pager.currentItem]
|
||||
if (fragment !is IntroFragmentTwo || fragment.isDeviceIdValid()) {
|
||||
pager.goToNextSlide()
|
||||
}
|
||||
}
|
||||
|
||||
addSlide(IntroFragmentOne())
|
||||
addSlide(IntroFragmentTwo())
|
||||
addSlide(IntroFragmentThree())
|
||||
|
||||
setSeparatorColor(ContextCompat.getColor(this, android.R.color.primary_text_dark))
|
||||
showSkipButton(true)
|
||||
isProgressButtonEnabled = true
|
||||
pager.isPagingEnabled = false
|
||||
}
|
||||
|
||||
override fun onSkipPressed(currentFragment: Fragment) {
|
||||
onDonePressed(currentFragment)
|
||||
}
|
||||
|
||||
override fun onDonePressed(currentFragment: Fragment) {
|
||||
defaultSharedPreferences.edit().putBoolean(MainActivity.PREF_IS_FIRST_START, false).apply()
|
||||
startActivity(intentFor<MainActivity>())
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Display some simple welcome text.
|
||||
*/
|
||||
class IntroFragmentOne : SyncthingFragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return DataBindingUtil.inflate<FragmentIntroOneBinding>(
|
||||
inflater, R.layout.fragment_intro_one, container, false).root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
context?.let { SyncthingActivity.checkLocalDiscoveryPort(it) }
|
||||
libraryHandler?.configuration { config ->
|
||||
config.localDeviceName = Util.getDeviceName()
|
||||
config.persistLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display device ID entry field and QR scanner option.
|
||||
*/
|
||||
class IntroFragmentTwo : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentIntroTwoBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_two, container, false)
|
||||
binding.enterDeviceId!!.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@IntroFragmentTwo).initiateScan()
|
||||
}
|
||||
binding.enterDeviceId!!.scanQrCode.setImageResource(R.drawable.ic_qr_code_white_24dp)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
|
||||
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
|
||||
binding.enterDeviceId!!.deviceId.setText(scanResult.contents)
|
||||
binding.enterDeviceId!!.deviceIdHolder.isErrorEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entered device ID is valid. If yes, imports it and returns true. If not,
|
||||
* sets an error on the textview and returns false.
|
||||
*/
|
||||
fun isDeviceIdValid(): Boolean {
|
||||
return try {
|
||||
val deviceId = binding.enterDeviceId!!.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { })
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
binding.enterDeviceId!!.deviceId.error = getString(R.string.invalid_device_id)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until remote device connects with new folder.
|
||||
*/
|
||||
class IntroFragmentThree : SyncthingFragment() {
|
||||
|
||||
private lateinit var binding: FragmentIntroThreeBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_intro_three, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
libraryHandler?.library { config, client, _ ->
|
||||
client.addOnConnectionChangedListener(this::onConnectionChanged)
|
||||
val deviceId = config.localDeviceId.deviceId
|
||||
val desc = activity?.getString(R.string.intro_page_three_description, "<b>$deviceId</b>")
|
||||
binding.description.text = Html.fromHtml(desc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConnectionChanged(deviceId: DeviceId) {
|
||||
libraryHandler?.library { config, client, _ ->
|
||||
if (config.folders.isNotEmpty()) {
|
||||
client.removeOnConnectionChangedListener(this::onConnectionChanged)
|
||||
(activity as IntroActivity?)?.onDonePressed(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package net.syncthing.lite.activities
|
||||
|
||||
import com.nononsenseapps.filepicker.AbstractFilePickerActivity
|
||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
||||
|
||||
import net.syncthing.lite.fragments.MIVFilePickerFragment
|
||||
|
||||
import java.io.File
|
||||
|
||||
class MIVFilePickerActivity : AbstractFilePickerActivity<File>() {
|
||||
|
||||
override fun getFragment(startPath: String, mode: Int, allowMultiple: Boolean,
|
||||
allowCreateDir: Boolean): AbstractFilePickerFragment<File> {
|
||||
// Only the fragment in this line needs to be changed
|
||||
val fragment = MIVFilePickerFragment()
|
||||
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,34 @@ import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.ActionBarDrawerToggle
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ActivityMainBinding
|
||||
import net.syncthing.lite.dialogs.DeviceIdDialog
|
||||
import net.syncthing.lite.fragments.DevicesFragment
|
||||
import net.syncthing.lite.fragments.FoldersFragment
|
||||
import net.syncthing.lite.utils.UpdateIndexTask
|
||||
import net.syncthing.lite.fragments.SettingsFragment
|
||||
import org.jetbrains.anko.defaultSharedPreferences
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class MainActivity : SyncthingActivity() {
|
||||
|
||||
companion object {
|
||||
const val PREF_IS_FIRST_START = "net.syncthing.lite.activities.MainActivity.IS_FIRST_START"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var drawerToggle: ActionBarDrawerToggle? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (defaultSharedPreferences.getBoolean(PREF_IS_FIRST_START, true)) {
|
||||
startActivity(intentFor<IntroActivity>())
|
||||
finish()
|
||||
}
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
|
||||
|
||||
drawerToggle = ActionBarDrawerToggle(
|
||||
@@ -34,15 +46,19 @@ class MainActivity : SyncthingActivity() {
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the toggle state and fragment after onRestoreInstanceState has occurred.
|
||||
*/
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
// Sync the toggle state after onRestoreInstanceState has occurred.
|
||||
drawerToggle!!.syncState()
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
super.onLibraryLoaded()
|
||||
setContentFragment(FoldersFragment())
|
||||
drawerToggle!!.syncState()
|
||||
val menu = binding.navigation.menu
|
||||
val selection = (0 until menu.size())
|
||||
.map { menu.getItem(it) }
|
||||
.find { it.isChecked }
|
||||
?: menu.getItem(0)
|
||||
onNavigationItemSelectedListener(selection)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -57,17 +73,19 @@ class MainActivity : SyncthingActivity() {
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
// Handle your other action bar items...
|
||||
|
||||
}
|
||||
|
||||
private fun onNavigationItemSelectedListener(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.folders -> setContentFragment(FoldersFragment())
|
||||
R.id.devices -> setContentFragment(DevicesFragment())
|
||||
R.id.update_index -> UpdateIndexTask(this, syncthingClient()).updateIndex()
|
||||
R.id.settings -> setContentFragment(SettingsFragment())
|
||||
R.id.device_id -> libraryHandler?.configuration { config ->
|
||||
DeviceIdDialog(this, config.localDeviceId).show()
|
||||
}
|
||||
R.id.clear_index -> AlertDialog.Builder(this)
|
||||
.setTitle("clear cache and index")
|
||||
.setMessage("clear all cache data and index data?")
|
||||
.setTitle(getString(R.string.clear_cache_and_index_title))
|
||||
.setMessage(getString(R.string.clear_cache_and_index_body))
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.yes) { _, _ -> cleanCacheAndIndex() }
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
@@ -85,17 +103,9 @@ class MainActivity : SyncthingActivity() {
|
||||
}
|
||||
|
||||
private fun cleanCacheAndIndex() {
|
||||
syncthingClient().clearCacheAndIndex()
|
||||
recreate()
|
||||
}
|
||||
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
binding.mainIndexProgressBar.visibility = View.VISIBLE
|
||||
binding.mainIndexProgressBarLabel.text = ("index update, folder "
|
||||
+ folder.label + " " + percentage + "% synchronized")
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
binding.mainIndexProgressBar.visibility = View.GONE
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,109 +3,80 @@ package net.syncthing.lite.activities
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.configuration.ConfigurationService
|
||||
import net.syncthing.lite.BuildConfig
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogLoadingBinding
|
||||
import net.syncthing.lite.utils.LibraryHandler
|
||||
import net.syncthing.lite.utils.UpdateIndexTask
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.jetbrains.anko.contentView
|
||||
import org.slf4j.impl.HandroidLoggerAdapter
|
||||
import java.util.*
|
||||
|
||||
abstract class SyncthingActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private val TAG = "SyncthingActivity"
|
||||
|
||||
private var activityCount = 0
|
||||
private var libraryHandler: LibraryHandler? = null
|
||||
fun checkLocalDiscoveryPort(context: Context) {
|
||||
if (LibraryHandler.isListeningPortTaken) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.other_syncthing_instance_title)
|
||||
.setMessage(R.string.other_syncthing_instance_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncthingClient(): SyncthingClient = libraryHandler!!.syncthingClient!!
|
||||
|
||||
fun configuration(): ConfigurationService = libraryHandler!!.configuration!!
|
||||
|
||||
fun folderBrowser(): FolderBrowser = libraryHandler!!.folderBrowser!!
|
||||
var libraryHandler: LibraryHandler? = null
|
||||
private set
|
||||
private var loadingDialog: AlertDialog? = null
|
||||
private var snackBar: Snackbar? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
|
||||
activityCount++
|
||||
if (libraryHandler == null) {
|
||||
InitTask(this, this::onLibraryLoaded).execute()
|
||||
}
|
||||
|
||||
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
|
||||
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
|
||||
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
|
||||
loadingDialog = AlertDialog.Builder(this)
|
||||
.setCancelable(false)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
LibraryHandler(this, this::onLibraryLoadedInternal,
|
||||
this::onIndexUpdateProgress, this::onIndexUpdateComplete)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
activityCount--
|
||||
Thread {
|
||||
if (activityCount == 0) {
|
||||
libraryHandler!!.destroy()
|
||||
libraryHandler = null
|
||||
}
|
||||
}.start()
|
||||
libraryHandler?.close()
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
|
||||
private class InitTask(val context: Context, val onLibraryLoaded: () -> Unit)
|
||||
: AsyncTask<Void?, Void?, Void?>() {
|
||||
|
||||
private var loadingDialog: AlertDialog? = null
|
||||
|
||||
override fun onPreExecute() {
|
||||
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
|
||||
LayoutInflater.from(context), R.layout.dialog_loading, null, false)
|
||||
binding.loadingText.text = "loading config, starting syncthing client"
|
||||
loadingDialog = android.app.AlertDialog.Builder(context)
|
||||
.setCancelable(false)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
if (!isDestroyed) {
|
||||
loadingDialog?.dismiss()
|
||||
}
|
||||
onLibraryLoaded()
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg voidd: Void?): Void? {
|
||||
libraryHandler = LibraryHandler()
|
||||
libraryHandler!!.init(context)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onPostExecute(voidd: Void?) {
|
||||
loadingDialog!!.cancel()
|
||||
libraryHandler!!.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
|
||||
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
|
||||
onIndexUpdateProgress(folder, percentage)
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete() {
|
||||
onIndexUpdateComplete()
|
||||
}
|
||||
})
|
||||
|
||||
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
|
||||
val lastUpdate =
|
||||
if (lastUpdateMillis < 0) null
|
||||
else Date(lastUpdateMillis)
|
||||
//trigger update if last was more than 10mins ago
|
||||
if (lastUpdate == null || Date().time - lastUpdate.time > 10 * 60 * 1000) {
|
||||
Log.d(TAG, "trigger index update, last was " + lastUpdate!!)
|
||||
UpdateIndexTask(context, libraryHandler!!.syncthingClient!!).updateIndex()
|
||||
}
|
||||
onLibraryLoaded()
|
||||
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() {}
|
||||
open fun onLibraryLoaded() {
|
||||
checkLocalDiscoveryPort(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import com.google.common.collect.Lists
|
||||
import net.syncthing.java.core.beans.DeviceStats
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewDeviceBinding
|
||||
|
||||
class DevicesAdapter(context: Context) :
|
||||
ArrayAdapter<DeviceStats>(context, R.layout.listview_device, Lists.newArrayList()) {
|
||||
ArrayAdapter<DeviceInfo>(context, R.layout.listview_device, mutableListOf()) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewDeviceBinding
|
||||
@@ -24,10 +23,10 @@ class DevicesAdapter(context: Context) :
|
||||
val deviceStats = getItem(position)
|
||||
binding.deviceName.text = deviceStats!!.name
|
||||
val icon =
|
||||
when (deviceStats.status) {
|
||||
DeviceStats.DeviceStatus.OFFLINE -> R.drawable.ic_laptop_red_24dp
|
||||
DeviceStats.DeviceStatus.ONLINE_INACTIVE,
|
||||
DeviceStats.DeviceStatus.ONLINE_ACTIVE -> R.drawable.ic_laptop_green_24dp
|
||||
if (deviceStats.isConnected!!) {
|
||||
R.drawable.ic_laptop_green_24dp
|
||||
} else {
|
||||
R.drawable.ic_laptop_red_24dp
|
||||
}
|
||||
binding.deviceIcon.setImageResource(icon)
|
||||
return binding.root
|
||||
|
||||
@@ -7,14 +7,13 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import com.google.common.collect.Lists
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewFileBinding
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
class FolderContentsAdapter(context: Context) :
|
||||
ArrayAdapter<FileInfo>(context, R.layout.listview_file, Lists.newArrayList()) {
|
||||
ArrayAdapter<FileInfo>(context, R.layout.listview_file, mutableListOf()) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
val binding: ListviewFileBinding =
|
||||
@@ -25,15 +24,15 @@ class FolderContentsAdapter(context: Context) :
|
||||
}
|
||||
val fileInfo = getItem(position)
|
||||
binding.fileLabel.text = fileInfo!!.fileName
|
||||
if (fileInfo.isDirectory) {
|
||||
if (fileInfo.isDirectory()) {
|
||||
binding.fileIcon.setImageResource(R.drawable.ic_folder_black_24dp)
|
||||
binding.fileSize.visibility = View.GONE
|
||||
} else {
|
||||
binding.fileIcon.setImageResource(R.drawable.ic_image_black_24dp)
|
||||
binding.fileSize.visibility = View.VISIBLE
|
||||
binding.fileSize.text = (FileUtils.byteCountToDisplaySize(fileInfo.size!!)
|
||||
+ " - last modified "
|
||||
+ DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
binding.fileSize.text = context.getString(R.string.file_info,
|
||||
FileUtils.byteCountToDisplaySize(fileInfo.size!!),
|
||||
DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.ListviewFolderBinding
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
|
||||
class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderStats>>) :
|
||||
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
|
||||
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
|
||||
|
||||
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
|
||||
@@ -23,15 +22,13 @@ class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderSta
|
||||
} else {
|
||||
DataBindingUtil.bind(v)
|
||||
}
|
||||
val folderInfo = getItem(position)!!.left
|
||||
val folderStats = getItem(position)!!.right
|
||||
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
|
||||
binding.folderLastmodInfo.text =
|
||||
if (folderStats.lastUpdate == null)
|
||||
"last modified: unknown"
|
||||
else "last modified: " +
|
||||
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
|
||||
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
|
||||
val folderInfo = getItem(position)!!.first
|
||||
val folderStats = getItem(position)!!.second
|
||||
binding.folderName.text = context.getString(R.string.folder_label_format, folderInfo.label, folderInfo.folderId)
|
||||
|
||||
binding.folderLastmodInfo.text = context.getString(R.string.last_modified_time,
|
||||
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
|
||||
binding.folderContentInfo.text = context.getString(R.string.folder_content_info, folderStats.describeSize(), folderStats.fileCount, folderStats.dirCount)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.core.beans.DeviceId
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.databinding.DialogDeviceIdBinding
|
||||
import org.jetbrains.anko.doAsync
|
||||
|
||||
|
||||
class DeviceIdDialog(private val context: Context, private val deviceId: DeviceId) {
|
||||
|
||||
private val Tag = "DeviceIdDialog"
|
||||
|
||||
private val binding = DataBindingUtil.inflate<DialogDeviceIdBinding>(
|
||||
LayoutInflater.from(context), R.layout.dialog_device_id, null, false)
|
||||
|
||||
fun show() {
|
||||
generateQrCode()
|
||||
binding.deviceId.text = deviceId.deviceId
|
||||
// Make QR code/progress bar views rectangular based on match_parent height.
|
||||
binding.progressBar.post {
|
||||
binding.progressBar.minimumHeight = binding.progressBar.width
|
||||
binding.qrCode.minimumHeight = binding.progressBar.width
|
||||
}
|
||||
binding.deviceId.setOnClickListener({ copyDeviceId() })
|
||||
binding.share.setOnClickListener { }
|
||||
binding.share.setOnClickListener({ shareDeviceId() })
|
||||
|
||||
val qrCodeDialog = AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.device_id))
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
|
||||
qrCodeDialog.show()
|
||||
}
|
||||
|
||||
private fun generateQrCode() {
|
||||
doAsync {
|
||||
val writer = QRCodeWriter()
|
||||
try {
|
||||
val bitMatrix = writer.encode(deviceId.deviceId, BarcodeFormat.QR_CODE, 512, 512)
|
||||
val width = bitMatrix.width
|
||||
val height = bitMatrix.height
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bmp.setPixel(x, y, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE)
|
||||
}
|
||||
}
|
||||
async(UI) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.qrCode.visibility = View.VISIBLE
|
||||
binding.qrCode.setImageBitmap(bmp)
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
Log.w(Tag, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyDeviceId() {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(context.getString(R.string.device_id), deviceId.deviceId)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(context, context.getString(R.string.device_id_copied), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun shareDeviceId() {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, deviceId.deviceId)
|
||||
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share_device_id_chooser)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
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(private val context: Context, private val syncthingClient: SyncthingClient,
|
||||
private val fileInfo: FileInfo) {
|
||||
|
||||
private val Tag = "FileDownloadDialog"
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var downloadFileTask: DownloadFileTask? = null
|
||||
|
||||
fun show() {
|
||||
showDialog()
|
||||
doAsync {
|
||||
downloadFileTask = DownloadFileTask(context, syncthingClient, fileInfo,
|
||||
this@FileDownloadDialog::onProgress, this@FileDownloadDialog::onComplete,
|
||||
this@FileDownloadDialog::onError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
progressDialog = ProgressDialog(context)
|
||||
progressDialog.setMessage(context.getString(R.string.dialog_downloading_file, fileInfo.fileName))
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
progressDialog.setCancelable(true)
|
||||
progressDialog.setOnCancelListener { downloadFileTask?.cancel() }
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(downloadFileTask: DownloadFileTask, fileDownloadObserver: BlockPuller.FileDownloadObserver) {
|
||||
async(UI) {
|
||||
progressDialog.isIndeterminate = false
|
||||
progressDialog.max = (fileInfo.size as Long).toInt()
|
||||
progressDialog.progress = (fileDownloadObserver.progress() * fileInfo.size!!).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete(file: File) {
|
||||
async(UI) {
|
||||
progressDialog.dismiss()
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val uri = FileProvider.getUriForFile(this@FileDownloadDialog.context, "net.syncthing.lite.fileprovider", file)
|
||||
intent.setDataAndType(uri, mimeType)
|
||||
intent.newTask()
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
try {
|
||||
this@FileDownloadDialog.context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
this@FileDownloadDialog.context.toast(R.string.toast_open_file_failed)
|
||||
Log.w(Tag, "No handler found for file " + file.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError() {
|
||||
async(UI) {
|
||||
progressDialog.cancel()
|
||||
this@FileDownloadDialog.context.toast(R.string.toast_file_download_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.syncthing.lite.dialogs
|
||||
|
||||
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 net.syncthing.lite.utils.Util
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.toast
|
||||
|
||||
class FileUploadDialog(private val context: Context, private val syncthingClient: SyncthingClient,
|
||||
private val localFile: Uri, private val syncthingFolder: String,
|
||||
private val syncthingSubFolder: String,
|
||||
private val onUploadCompleteListener: () -> Unit) {
|
||||
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var uploadFileTask: UploadFileTask? = null
|
||||
|
||||
fun show() {
|
||||
showDialog()
|
||||
doAsync {
|
||||
uploadFileTask = UploadFileTask(context, syncthingClient, localFile, syncthingFolder,
|
||||
syncthingSubFolder, this@FileUploadDialog::onProgress,
|
||||
this@FileUploadDialog::onComplete, this@FileUploadDialog::onError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
progressDialog = ProgressDialog(context)
|
||||
progressDialog.setMessage(context.getString(R.string.dialog_uploading_file, Util.getContentFileName(context, localFile)))
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
progressDialog.setCancelable(true)
|
||||
progressDialog.setOnCancelListener { uploadFileTask?.cancel() }
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,138 +5,119 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import net.syncthing.java.core.beans.DeviceInfo
|
||||
import net.syncthing.java.core.beans.DeviceStats
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
import net.syncthing.lite.adapters.DevicesAdapter
|
||||
import net.syncthing.lite.databinding.FragmentDevicesBinding
|
||||
import net.syncthing.lite.utils.UpdateIndexTask
|
||||
import org.apache.commons.lang3.StringUtils.isBlank
|
||||
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
|
||||
import uk.co.markormesher.android_fab.SpeedDialMenuItem
|
||||
import java.security.InvalidParameterException
|
||||
import net.syncthing.lite.databinding.ViewEnterDeviceIdBinding
|
||||
import net.syncthing.lite.utils.FragmentIntentIntegrator
|
||||
import net.syncthing.lite.utils.Util
|
||||
import java.io.IOException
|
||||
|
||||
class DevicesFragment : Fragment() {
|
||||
class DevicesFragment : SyncthingFragment() {
|
||||
|
||||
companion object {
|
||||
private val TAG = "DevicesFragment"
|
||||
}
|
||||
|
||||
private lateinit var syncthingActivity: SyncthingActivity
|
||||
private lateinit var binding: FragmentDevicesBinding
|
||||
private lateinit var adapter: DevicesAdapter
|
||||
private var addDeviceDialog: AlertDialog? = null
|
||||
private var addDeviceDialogBinding: ViewEnterDeviceIdBinding? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_devices, container, false)
|
||||
binding.list.emptyView = binding.empty
|
||||
binding.fab.speedDialMenuAdapter = FabMenuAdapter()
|
||||
binding.addDevice.setOnClickListener { showDialog() }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
syncthingActivity = activity as SyncthingActivity
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener { updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener{ updateDeviceList() } }
|
||||
}
|
||||
|
||||
override fun onLibraryLoaded() {
|
||||
initDeviceList()
|
||||
updateDeviceList()
|
||||
}
|
||||
|
||||
private fun initDeviceList() {
|
||||
adapter = DevicesAdapter(syncthingActivity)
|
||||
adapter = DevicesAdapter(context!!)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemLongClickListener { _, _, position, _ ->
|
||||
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
|
||||
AlertDialog.Builder(syncthingActivity)
|
||||
.setTitle("remove device " + deviceId.substring(0, 7))
|
||||
.setMessage("remove device" + deviceId.substring(0, 7) + " from list of known devices?")
|
||||
val device = adapter.getItem(position)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(getString(R.string.remove_device_title, device.name))
|
||||
.setMessage(getString(R.string.remove_device_message, device.deviceId.deviceId.substring(0, 7)))
|
||||
.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
syncthingActivity.configuration().edit().removePeer(deviceId).persistLater() }
|
||||
libraryHandler?.configuration { config ->
|
||||
config.peers = config.peers.filterNot { it.deviceId == device.deviceId }.toSet()
|
||||
config.persistLater()
|
||||
updateDeviceList()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDeviceList() {
|
||||
adapter.clear()
|
||||
adapter.addAll(syncthingActivity.syncthingClient().devicesHandler.deviceStatsList)
|
||||
adapter.notifyDataSetChanged()
|
||||
async(UI) {
|
||||
libraryHandler?.syncthingClient { syncthingClient ->
|
||||
adapter.clear()
|
||||
adapter.addAll(syncthingClient.getPeerStatus())
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
// Check if this was a QR code scan.
|
||||
val scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent)
|
||||
if (scanResult != null) {
|
||||
val deviceId = scanResult.contents
|
||||
if (!isBlank(deviceId)) {
|
||||
importDeviceId(deviceId)
|
||||
}
|
||||
if (scanResult?.contents != null && scanResult.contents.isNotBlank()) {
|
||||
addDeviceDialogBinding?.deviceId?.setText(scanResult.contents)
|
||||
}
|
||||
}
|
||||
|
||||
private fun importDeviceId(deviceId: String) {
|
||||
try {
|
||||
KeystoreHandler.validateDeviceId(deviceId)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Toast.makeText(context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val modified = syncthingActivity.configuration().edit().addPeers(DeviceInfo(deviceId, null))
|
||||
if (modified) {
|
||||
syncthingActivity.configuration().edit().persistLater()
|
||||
Toast.makeText(context, "successfully imported device: " + deviceId, Toast.LENGTH_SHORT).show()
|
||||
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
|
||||
UpdateIndexTask(syncthingActivity, syncthingActivity.syncthingClient()).updateIndex()
|
||||
} else {
|
||||
Toast.makeText(context, "device already present: " + deviceId, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FabMenuAdapter : SpeedDialMenuAdapter() {
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getMenuItem(context: Context, position: Int): SpeedDialMenuItem {
|
||||
when (position) {
|
||||
0 -> return SpeedDialMenuItem(context, R.drawable.ic_qr_code_white_24dp, R.string.scan_qr_code)
|
||||
1 -> return SpeedDialMenuItem(context, R.drawable.ic_edit_white_24dp, R.string.enter_device_id)
|
||||
private fun showDialog() {
|
||||
addDeviceDialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.view_enter_device_id, null, false)
|
||||
addDeviceDialogBinding?.let { binding ->
|
||||
binding.scanQrCode.setOnClickListener {
|
||||
FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
}
|
||||
binding.deviceId.post {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.deviceId, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
throw InvalidParameterException()
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(position: Int): Boolean {
|
||||
when (position) {
|
||||
0 -> FragmentIntentIntegrator(this@DevicesFragment).initiateScan()
|
||||
1 -> {
|
||||
val editText = EditText(context)
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(editText)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> importDeviceId(editText.text.toString()) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
addDeviceDialog = AlertDialog.Builder(context)
|
||||
.setTitle(R.string.device_id_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
// Use different listener to keep dialog open after button click.
|
||||
// https://stackoverflow.com/a/15619098
|
||||
addDeviceDialog?.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
?.setOnClickListener {
|
||||
try {
|
||||
val deviceId = binding.deviceId.text.toString()
|
||||
Util.importDeviceId(libraryHandler, context, deviceId, { updateDeviceList() })
|
||||
addDeviceDialog?.dismiss()
|
||||
} catch (e: IOException) {
|
||||
binding.deviceId.error = getString(R.string.invalid_device_id)
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,22 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.databinding.DataBindingUtil
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.common.collect.Lists
|
||||
import com.google.common.collect.Ordering
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.FolderBrowserActivity
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
import net.syncthing.lite.adapters.FoldersListAdapter
|
||||
import net.syncthing.lite.databinding.FragmentFoldersBinding
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import java.util.*
|
||||
import org.jetbrains.anko.intentFor
|
||||
|
||||
class FoldersFragment : Fragment() {
|
||||
class FoldersFragment : SyncthingFragment() {
|
||||
|
||||
companion object {
|
||||
private val TAG = "FoldersFragment"
|
||||
}
|
||||
private val TAG = "FoldersFragment"
|
||||
|
||||
private lateinit var syncthingActivity: SyncthingActivity
|
||||
private lateinit var binding: FragmentFoldersBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
@@ -36,24 +26,26 @@ class FoldersFragment : Fragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
syncthingActivity = activity as SyncthingActivity
|
||||
override fun onLibraryLoaded() {
|
||||
showAllFoldersListView()
|
||||
}
|
||||
|
||||
private fun showAllFoldersListView() {
|
||||
val list = Lists.newArrayList(syncthingActivity.folderBrowser().folderInfoAndStatsList)
|
||||
Collections.sort(list, Ordering.natural<Comparable<String>>()
|
||||
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records")
|
||||
val adapter = FoldersListAdapter(context!!, list)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemClickListener { _, _, position, _ ->
|
||||
val folder = adapter.getItem(position)!!.left.folder
|
||||
val intent = Intent(context, FolderBrowserActivity::class.java)
|
||||
intent.putExtra(FolderBrowserActivity.EXTRA_FOLDER_NAME, folder)
|
||||
startActivity(intent)
|
||||
libraryHandler?.folderBrowser { folderBrowser ->
|
||||
val list = folderBrowser.folderInfoAndStatsList()
|
||||
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
|
||||
val adapter = FoldersListAdapter(context, list)
|
||||
binding.list.adapter = adapter
|
||||
binding.list.setOnItemClickListener { _, _, position, _ ->
|
||||
val folder = adapter.getItem(position)!!.first.folderId
|
||||
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
|
||||
super.onIndexUpdateComplete(folderInfo)
|
||||
showAllFoldersListView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.view.View
|
||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
||||
|
||||
import com.nononsenseapps.filepicker.FilePickerFragment
|
||||
|
||||
import java.io.File
|
||||
|
||||
class MIVFilePickerFragment : FilePickerFragment() {
|
||||
|
||||
override fun onClickCheckable(v: View, vh: AbstractFilePickerFragment<File>.CheckableViewHolder) {
|
||||
// auto open file on click
|
||||
if (!allowMultiple) {
|
||||
// Clear is necessary, in case user clicked some checkbox directly
|
||||
mCheckedItems.clear()
|
||||
mCheckedItems.add(vh.file)
|
||||
onClickOk(null)
|
||||
} else {
|
||||
super.onClickCheckable(v, vh)
|
||||
}
|
||||
}
|
||||
|
||||
// private static final String EXTENSION = ".*[.](jpg|png|jpeg)";
|
||||
|
||||
|
||||
override fun isItemVisible(file: File): Boolean {
|
||||
// return isDir(file) || file.getName().toLowerCase().matches(EXTENSION);
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import net.syncthing.lite.R
|
||||
import net.syncthing.lite.activities.SyncthingActivity
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
|
||||
val localDeviceName = findPreference("local_device_name") as EditTextPreference
|
||||
val appVersion = findPreference("app_version")
|
||||
|
||||
(activity as SyncthingActivity?)?.let { activity ->
|
||||
val versionName = activity.packageManager.getPackageInfo(activity.packageName, 0)?.versionName
|
||||
appVersion.summary = versionName
|
||||
|
||||
activity.libraryHandler?.configuration { localDeviceName.text = it.localDeviceName }
|
||||
localDeviceName.setOnPreferenceChangeListener { _, _ ->
|
||||
activity.libraryHandler?.configuration { conf ->
|
||||
conf.localDeviceName = localDeviceName.text
|
||||
conf.persistLater()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.syncthing.lite.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
|
||||
abstract class SyncthingFragment : Fragment() {
|
||||
|
||||
var libraryHandler: LibraryHandler? = null
|
||||
private set
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
|
||||
this::onIndexUpdateComplete)
|
||||
}
|
||||
|
||||
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
|
||||
this.libraryHandler = libraryHandler
|
||||
onLibraryLoaded()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
libraryHandler?.close()
|
||||
}
|
||||
|
||||
open fun onLibraryLoaded() {}
|
||||
|
||||
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
|
||||
|
||||
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class DownloadFileTask(private val context: Context, syncthingClient: SyncthingClient,
|
||||
private val fileInfo: FileInfo,
|
||||
private val onProgress: (DownloadFileTask, BlockPuller.FileDownloadObserver) -> Unit,
|
||||
private val onComplete: (File) -> Unit,
|
||||
private val onError: () -> Unit) {
|
||||
|
||||
private val Tag = "DownloadFileTask"
|
||||
private var isCancelled = false
|
||||
|
||||
init {
|
||||
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
|
||||
val observer = blockPuller.pullFile(fileInfo)
|
||||
onProgress(this, observer)
|
||||
try {
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPuller
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i("pullFile", "download progress = " + observer.progressMessage())
|
||||
onProgress(this, observer)
|
||||
}
|
||||
|
||||
val outputFile = File("${context.externalCacheDir}/${fileInfo.folder}/${fileInfo.path}")
|
||||
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
|
||||
Log.i(Tag, "Downloaded file $fileInfo")
|
||||
onComplete(outputFile)
|
||||
} catch (e: IOException) {
|
||||
onError()
|
||||
Log.w(Tag, "Failed to download file $fileInfo", e)
|
||||
}
|
||||
}, { onError() })
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.experimental.android.UI
|
||||
import kotlinx.coroutines.experimental.async
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.IndexInfo
|
||||
import net.syncthing.java.core.configuration.Configuration
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.SocketException
|
||||
import java.util.*
|
||||
|
||||
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
|
||||
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
|
||||
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit) {
|
||||
|
||||
companion object {
|
||||
private var instanceCount = 0
|
||||
private var configuration: Configuration? = null
|
||||
private var syncthingClient: SyncthingClient? = null
|
||||
private var folderBrowser: FolderBrowser? = null
|
||||
private val callbacks = ArrayList<(Configuration, SyncthingClient, FolderBrowser) -> Unit>()
|
||||
private var isLoading = false
|
||||
var isListeningPortTaken = false
|
||||
}
|
||||
|
||||
private val TAG = "LibraryHandler"
|
||||
|
||||
init {
|
||||
instanceCount++
|
||||
if (configuration == null && !isLoading) {
|
||||
isLoading = true
|
||||
doAsync {
|
||||
checkIsListeningPortTaken()
|
||||
init(context)
|
||||
async(UI) {
|
||||
onLibraryLoaded(this@LibraryHandler)
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
onLibraryLoaded(this)
|
||||
}
|
||||
|
||||
syncthingClient {
|
||||
it.indexHandler.registerOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
|
||||
it.indexHandler.registerOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIndexRecordAcquired(folderInfo: FolderInfo, newRecords: List<FileInfo>, indexInfo: IndexInfo) {
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
|
||||
async(UI) {
|
||||
onIndexUpdateProgressListener(folderInfo, (indexInfo.getCompleted() * 100).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRemoteIndexAcquired(folderInfo: FolderInfo) {
|
||||
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
|
||||
|
||||
async(UI) {
|
||||
onIndexUpdateCompleteListener(folderInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun init(context: Context) {
|
||||
val configuration = Configuration(configFolder = context.filesDir)
|
||||
val syncthingClient = SyncthingClient(configuration)
|
||||
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
|
||||
}
|
||||
|
||||
fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
|
||||
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
|
||||
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
|
||||
|
||||
// https://stackoverflow.com/a/35522422/1837158
|
||||
fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? {
|
||||
return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
|
||||
}
|
||||
safeLet(configuration, syncthingClient, folderBrowser) { c, s, f ->
|
||||
callback(c, s, f)
|
||||
} ?: run {
|
||||
if (isLoading) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncthingClient(callback: (SyncthingClient) -> Unit) {
|
||||
library { _, s, _ -> callback(s) }
|
||||
}
|
||||
|
||||
fun configuration(callback: (Configuration) -> Unit) {
|
||||
library { c, _, _ -> callback(c) }
|
||||
}
|
||||
|
||||
fun folderBrowser(callback: (FolderBrowser) -> Unit) {
|
||||
library { _, _, f -> callback(f) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if listening port for local discovery is taken by another app. Do this check here to
|
||||
* avoid adding another callback.
|
||||
*/
|
||||
private fun checkIsListeningPortTaken() {
|
||||
try {
|
||||
DatagramSocket(21027, InetAddress.getByName("0.0.0.0")).close()
|
||||
} catch (e: SocketException) {
|
||||
Log.w(TAG, e)
|
||||
isListeningPortTaken = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters index update listener and decreases instance count.
|
||||
*
|
||||
* We wait a bit before closing [[syncthingClient]] etc, in case LibraryHandler is opened again
|
||||
* soon (eg in case of device rotation).
|
||||
*/
|
||||
fun close() {
|
||||
syncthingClient {
|
||||
try {
|
||||
it.indexHandler.unregisterOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
|
||||
it.indexHandler.unregisterOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// ignored, no idea why this is thrown
|
||||
}
|
||||
}
|
||||
|
||||
instanceCount--
|
||||
Handler().postDelayed({
|
||||
Thread {
|
||||
if (instanceCount == 0) {
|
||||
folderBrowser?.close()
|
||||
folderBrowser = null
|
||||
syncthingClient?.close()
|
||||
syncthingClient = null
|
||||
configuration = null
|
||||
}
|
||||
}.start()
|
||||
}, 60 * 1000)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import android.util.Log
|
||||
import net.syncthing.java.bep.IndexBrowser
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.beans.FolderStats
|
||||
import net.syncthing.lite.R
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.net.URLConnection
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class SyncthingProvider : DocumentsProvider() {
|
||||
|
||||
companion object {
|
||||
private const val Tag = "SyncthingProvider"
|
||||
private val DefaultRootProjection = arrayOf(
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID,
|
||||
Root.COLUMN_ICON)
|
||||
private val DefaultDocumentProjection = arrayOf(
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_SIZE,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_FLAGS)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
Log.d(Tag, "onCreate()")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getLibraryHandler(): LibraryHandler {
|
||||
val latch = CountDownLatch(1)
|
||||
val libraryHandler = LibraryHandler(context, { latch.countDown() }, { _, _ -> }, {})
|
||||
latch.await()
|
||||
return libraryHandler
|
||||
}
|
||||
|
||||
override fun queryRoots(projection: Array<String>?): Cursor {
|
||||
Log.d(Tag, "queryRoots($projection)")
|
||||
val latch = CountDownLatch(1)
|
||||
var folders: List<Pair<FolderInfo, FolderStats>>? = null
|
||||
getLibraryHandler().folderBrowser { folderBrowser ->
|
||||
folders = folderBrowser.folderInfoAndStatsList()
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
|
||||
val result = MatrixCursor(projection ?: DefaultRootProjection)
|
||||
folders!!.forEach { folder ->
|
||||
val row = result.newRow()
|
||||
row.add(Root.COLUMN_ROOT_ID, folder.first.folderId)
|
||||
row.add(Root.COLUMN_SUMMARY, folder.first.label)
|
||||
row.add(Root.COLUMN_FLAGS, 0)
|
||||
row.add(Root.COLUMN_TITLE, context.getString(R.string.app_name))
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(folder.first))
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?,
|
||||
sortOrder: String?): Cursor {
|
||||
Log.d(Tag, "queryChildDocuments($parentDocumentId, $projection, $sortOrder)")
|
||||
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
|
||||
getIndexBrowser(getFolderIdForDocId(parentDocumentId))
|
||||
.listFiles(getPathForDocId(parentDocumentId))
|
||||
.forEach { fileInfo ->
|
||||
includeFile(result, fileInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
|
||||
Log.d(Tag, "queryDocument($documentId, $projection)")
|
||||
val result = MatrixCursor(projection ?: DefaultDocumentProjection)
|
||||
val fileInfo = getIndexBrowser(getFolderIdForDocId(documentId))
|
||||
.getFileInfoByAbsolutePath(getPathForDocId(documentId))
|
||||
includeFile(result, fileInfo)
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?):
|
||||
ParcelFileDescriptor {
|
||||
Log.d(Tag, "openDocument($documentId, $mode, $signal)")
|
||||
val fileInfo = FileInfo(folder = getFolderIdForDocId(documentId),
|
||||
path = getPathForDocId(documentId), type = FileInfo.FileType.FILE)
|
||||
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||
if (accessMode != ParcelFileDescriptor.MODE_READ_ONLY) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
val latch = CountDownLatch(1)
|
||||
var outputFile: File? = null
|
||||
getLibraryHandler().syncthingClient { syncthingClient ->
|
||||
DownloadFileTask(context, syncthingClient, fileInfo,
|
||||
{ t, _ -> if (signal?.isCanceled == true) t.cancel() }, {
|
||||
outputFile = it
|
||||
latch.countDown()
|
||||
}, {})
|
||||
}
|
||||
latch.await()
|
||||
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
}
|
||||
|
||||
private fun includeFile(result: MatrixCursor, fileInfo: FileInfo) {
|
||||
val row = result.newRow()
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(fileInfo))
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, fileInfo.fileName)
|
||||
row.add(Document.COLUMN_SIZE, fileInfo.size)
|
||||
val mime = if (fileInfo.isDirectory()) Document.MIME_TYPE_DIR
|
||||
else URLConnection.guessContentTypeFromName(fileInfo.fileName)
|
||||
row.add(Document.COLUMN_MIME_TYPE, mime)
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, fileInfo.lastModified)
|
||||
row.add(Document.COLUMN_FLAGS, 0)
|
||||
}
|
||||
|
||||
private fun getFolderIdForDocId(docId: String) = docId.split(":")[0]
|
||||
|
||||
private fun getPathForDocId(docId: String) = docId.split(":")[1]
|
||||
|
||||
private fun getDocIdForFile(folderInfo: FolderInfo) = folderInfo.folderId + ":"
|
||||
|
||||
private fun getDocIdForFile(fileInfo: FileInfo) = fileInfo.folder + ":" + fileInfo.path
|
||||
|
||||
private fun getIndexBrowser(folderId: String): IndexBrowser {
|
||||
val latch = CountDownLatch(1)
|
||||
var indexBrowser: IndexBrowser? = null
|
||||
getLibraryHandler().syncthingClient {
|
||||
indexBrowser = it.indexHandler.newIndexBrowser(folderId)
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
return indexBrowser!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.syncthing.lite.library
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.utils.Util
|
||||
import org.apache.commons.io.IOUtils
|
||||
|
||||
// TODO: this should be an IntentService with notification
|
||||
class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
|
||||
localFile: Uri, private val syncthingFolder: String,
|
||||
syncthingSubFolder: String,
|
||||
private val onProgress: (BlockPusher.FileUploadObserver) -> Unit,
|
||||
private val onComplete: () -> Unit,
|
||||
private val onError: () -> Unit) {
|
||||
|
||||
private val TAG = "UploadFileTask"
|
||||
|
||||
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
|
||||
private val uploadStream = context.contentResolver.openInputStream(localFile)
|
||||
|
||||
private var isCancelled = false
|
||||
|
||||
init {
|
||||
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
|
||||
syncthingClient.getBlockPusher(syncthingFolder, { blockPusher ->
|
||||
val observer = blockPusher.pushFile(uploadStream, syncthingFolder, syncthingPath)
|
||||
onProgress(observer)
|
||||
while (!observer.isCompleted()) {
|
||||
if (isCancelled)
|
||||
return@getBlockPusher
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
|
||||
onProgress(observer)
|
||||
}
|
||||
IOUtils.closeQuietly(uploadStream)
|
||||
onComplete()
|
||||
}, { onError() })
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.support.annotation.StringRes
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import net.syncthing.java.bep.BlockPuller
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FileInfo
|
||||
import net.syncthing.lite.R
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient, private val mFileInfo: FileInfo) {
|
||||
private val mMainHandler: Handler = Handler()
|
||||
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private var cancelled = false
|
||||
|
||||
fun downloadFile() {
|
||||
showDialog()
|
||||
// TODO: can just pass FileInfo directly?
|
||||
Thread {
|
||||
mSyncthingClient.pullFile(mFileInfo.folder, mFileInfo.path, { observer ->
|
||||
onProgress(observer)
|
||||
try {
|
||||
while (!observer.isCompleted) {
|
||||
if (cancelled)
|
||||
return@pullFile
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i("pullFile", "download progress = " + observer.progressMessage)
|
||||
onProgress(observer)
|
||||
}
|
||||
|
||||
val outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val outputFile = File(outputDir, mFileInfo.fileName)
|
||||
FileUtils.copyInputStreamToFile(observer.inputStream, outputFile)
|
||||
Log.i(TAG, "downloaded file = " + mFileInfo.path)
|
||||
onComplete(outputFile)
|
||||
} catch (e: IOException) {
|
||||
onError(R.string.toast_file_download_failed)
|
||||
Log.w(TAG, "Failed to download file " + mFileInfo, e)
|
||||
} catch (e: InterruptedException) {
|
||||
onError(R.string.toast_file_download_failed)
|
||||
Log.w(TAG, "Failed to download file " + mFileInfo, e)
|
||||
}
|
||||
}) { onError(R.string.toast_file_download_failed) }
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
progressDialog = ProgressDialog(mContext)
|
||||
progressDialog.setMessage(mContext.getString(R.string.dialog_downloading_file, mFileInfo.fileName))
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
progressDialog.setCancelable(true)
|
||||
progressDialog.setOnCancelListener { cancelled = true }
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
|
||||
mMainHandler.post {
|
||||
progressDialog.isIndeterminate = false
|
||||
progressDialog.max = (mFileInfo.size as Long).toInt()
|
||||
progressDialog.progress = (fileDownloadObserver.progress * mFileInfo.size!!).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete(file: File) {
|
||||
progressDialog.dismiss()
|
||||
if (cancelled)
|
||||
return
|
||||
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.fromFile(file), mimeType)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
try {
|
||||
mContext.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
onError(R.string.toast_open_file_failed)
|
||||
Log.w(TAG, "No handler found for file " + file.name, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun onError(@StringRes error: Int) {
|
||||
progressDialog.dismiss()
|
||||
mMainHandler.post { Toast.makeText(mContext, error, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = "DownloadFileTask"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package net.syncthing.lite.fragments
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.Fragment
|
||||
@@ -1,84 +0,0 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.common.eventbus.Subscribe
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.bep.IndexHandler
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.configuration.ConfigurationService
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class LibraryHandler {
|
||||
|
||||
private var mOnIndexUpdatedListener: OnIndexUpdatedListener? = null
|
||||
var configuration: ConfigurationService? = null
|
||||
private set
|
||||
var syncthingClient: SyncthingClient? = null
|
||||
private set
|
||||
var folderBrowser: FolderBrowser? = null
|
||||
private set
|
||||
|
||||
interface OnIndexUpdatedListener {
|
||||
fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int)
|
||||
fun onIndexUpdateComplete()
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
configuration = ConfigurationService.newLoader()
|
||||
.setCache(File(context.externalCacheDir, "cache"))
|
||||
.setDatabase(File(context.getExternalFilesDir(null), "database"))
|
||||
.loadFrom(File(context.getExternalFilesDir(null), "config.properties"))
|
||||
configuration!!.edit().setDeviceName(Util.getDeviceName())
|
||||
try {
|
||||
FileUtils.cleanDirectory(configuration!!.temp)
|
||||
} catch (ex: IOException) {
|
||||
Log.e(TAG, "error", ex)
|
||||
destroy()
|
||||
}
|
||||
|
||||
KeystoreHandler.newLoader().loadAndStore(configuration!!)
|
||||
configuration!!.edit().persistLater()
|
||||
Log.i(TAG, "loaded mConfiguration = " + configuration!!.newWriter().dumpToString())
|
||||
Log.i(TAG, "storage space = " + configuration!!.storageInfo.dumpAvailableSpace())
|
||||
syncthingClient = net.syncthing.java.client.SyncthingClient(configuration!!)
|
||||
//TODO listen for device events, update device list
|
||||
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
|
||||
}
|
||||
|
||||
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
|
||||
mOnIndexUpdatedListener = onIndexUpdatedListener
|
||||
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
|
||||
|
||||
@Subscribe
|
||||
fun handleIndexRecordAquiredEvent(event: IndexHandler.IndexRecordAquiredEvent) {
|
||||
val folder = syncthingClient!!.indexHandler.getFolderInfo(event.folder)
|
||||
val indexInfo = event.indexInfo
|
||||
event.newRecords.size
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
mOnIndexUpdatedListener!!.onIndexUpdateProgress(folder, (indexInfo.completed * 100).toInt())
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
|
||||
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
|
||||
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
folderBrowser!!.close()
|
||||
syncthingClient!!.close()
|
||||
configuration!!.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = "LibConnectionHandler"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.widget.Toast
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.lite.R
|
||||
import java.util.*
|
||||
|
||||
class UpdateIndexTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient) {
|
||||
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||
private val mMainHandler = Handler()
|
||||
|
||||
fun updateIndex() {
|
||||
if (sIndexUpdateInProgress)
|
||||
return
|
||||
|
||||
sIndexUpdateInProgress = true
|
||||
mSyncthingClient.updateIndexFromPeers { _, failures ->
|
||||
sIndexUpdateInProgress = false
|
||||
if (failures.isEmpty()) {
|
||||
showToast(mContext.getString(R.string.toast_index_update_successful))
|
||||
} else {
|
||||
showToast(mContext.getString(R.string.toast_index_update_failed, failures.size))
|
||||
}
|
||||
mPreferences.edit()
|
||||
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
mMainHandler.post { Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val LAST_INDEX_UPDATE_TS_PREF = "LAST_INDEX_UPDATE_TS"
|
||||
|
||||
private var sIndexUpdateInProgress: Boolean = false
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import net.syncthing.java.bep.BlockPusher
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.utils.PathUtils
|
||||
import net.syncthing.lite.R
|
||||
import java.io.IOException
|
||||
|
||||
// TODO: this should be an IntentService with notification
|
||||
class UploadFileTask(private val context: Context, private val syncthingClient: SyncthingClient,
|
||||
private val localFile: Uri, private val syncthingFolder: String,
|
||||
syncthingSubFolder: String,
|
||||
private val onUploadCompleteListener: () -> Unit) {
|
||||
|
||||
companion object {
|
||||
private val TAG = "UploadFileTask"
|
||||
}
|
||||
|
||||
private val fileName = Util.getContentFileName(context, localFile)
|
||||
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
|
||||
private val mainHandler = Handler()
|
||||
|
||||
private lateinit var mProgressDialog: ProgressDialog
|
||||
private var mCancelled = false
|
||||
|
||||
fun uploadFile() {
|
||||
createDialog()
|
||||
Log.i(TAG, "Uploading file $localFile to folder $syncthingFolder:$syncthingPath")
|
||||
try {
|
||||
val uploadStream = context.contentResolver.openInputStream(localFile)
|
||||
syncthingClient.pushFile(uploadStream, syncthingFolder, syncthingPath, { observer ->
|
||||
onProgress(observer)
|
||||
try {
|
||||
while (!observer.isCompleted) {
|
||||
if (mCancelled)
|
||||
return@pushFile
|
||||
|
||||
observer.waitForProgressUpdate()
|
||||
Log.i(TAG, "upload progress = " + observer.progressMessage)
|
||||
onProgress(observer)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
onError()
|
||||
}
|
||||
|
||||
onComplete()
|
||||
}, { this.onError() })
|
||||
} catch (e: IOException) {
|
||||
onError()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun createDialog() {
|
||||
mProgressDialog = ProgressDialog(context)
|
||||
mProgressDialog.setMessage(context.getString(R.string.dialog_uploading_file, fileName))
|
||||
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
mProgressDialog.setCancelable(true)
|
||||
mProgressDialog.setOnCancelListener { mCancelled = true }
|
||||
mProgressDialog.isIndeterminate = true
|
||||
mProgressDialog.show()
|
||||
}
|
||||
|
||||
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
|
||||
mainHandler.post {
|
||||
mProgressDialog.isIndeterminate = false
|
||||
mProgressDialog.max = observer.dataSource.size.toInt()
|
||||
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onComplete() {
|
||||
mProgressDialog.dismiss()
|
||||
if (mCancelled)
|
||||
return
|
||||
|
||||
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, R.string.toast_upload_complete, Toast.LENGTH_SHORT).show()
|
||||
onUploadCompleteListener()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError() {
|
||||
mProgressDialog.dismiss()
|
||||
mainHandler.post { Toast.makeText(context, R.string.toast_file_upload_failed, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,24 @@ 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 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.lite.R
|
||||
import net.syncthing.lite.library.LibraryHandler
|
||||
import org.apache.commons.lang3.StringUtils.capitalize
|
||||
import java.io.File
|
||||
import org.jetbrains.anko.toast
|
||||
import java.io.IOException
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.*
|
||||
|
||||
object Util {
|
||||
|
||||
fun getDeviceName(): String {
|
||||
val manufacturer = nullToEmpty(Build.MANUFACTURER)
|
||||
val model = nullToEmpty(Build.MODEL)
|
||||
val manufacturer = Build.MANUFACTURER ?: ""
|
||||
val model = Build.MODEL ?: ""
|
||||
val deviceName =
|
||||
if (model.startsWith(manufacturer)) {
|
||||
capitalize(model)
|
||||
@@ -22,19 +28,34 @@ object Util {
|
||||
capitalize(manufacturer) + " " + model
|
||||
}
|
||||
return deviceName ?: "android"
|
||||
}
|
||||
}
|
||||
|
||||
fun getContentFileName(context: Context, contentUri: Uri): String {
|
||||
var fileName = File(contentUri.lastPathSegment).name
|
||||
if (equal(contentUri.scheme, "content")) {
|
||||
context.contentResolver.query(contentUri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)!!.use { cursor ->
|
||||
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||
cursor.moveToFirst()
|
||||
val path = cursor.getString(columnIndex)
|
||||
Log.d("Main", "recovered 'content' uri real path = " + path)
|
||||
fileName = File(Uri.parse(path).lastPathSegment).name
|
||||
fun getContentFileName(context: Context, uri: Uri): String {
|
||||
context.contentResolver.query(uri, null, null, null, null, null).use { cursor ->
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
throw InvalidParameterException("Cursor is null or empty")
|
||||
}
|
||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun importDeviceId(libraryHandler: LibraryHandler?, context: Context?, deviceId: String,
|
||||
onComplete: () -> Unit) {
|
||||
val deviceId2 = DeviceId(deviceId.toUpperCase(Locale.US))
|
||||
libraryHandler?.configuration { configuration ->
|
||||
if (!configuration.peerIds.contains(deviceId2)) {
|
||||
configuration.peers = configuration.peers + DeviceInfo(deviceId2, null)
|
||||
configuration.persistLater()
|
||||
async(UI) {
|
||||
context?.toast(context.getString(R.string.device_import_success, deviceId2.shortId))
|
||||
onComplete()
|
||||
}
|
||||
} else {
|
||||
async(UI) {
|
||||
context?.toast(context.getString(R.string.device_already_known, deviceId2.shortId))
|
||||
}
|
||||
}
|
||||
}
|
||||
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="#FF000000"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#7D000000"
|
||||
android:pathData="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#7D000000"
|
||||
android:pathData="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>
|
||||
@@ -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="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
|
||||
</vector>
|
||||
@@ -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="#FF000000"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
@@ -6,7 +6,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!--center content BEGIN-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -14,45 +13,17 @@
|
||||
android:divider="?android:listDivider"
|
||||
android:showDividers="middle">
|
||||
|
||||
<!--index loading progress BEGIN-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/main_index_progress_bar"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:paddingStart="12dp"/>
|
||||
<TextView
|
||||
android:id="@+id/main_index_progress_bar_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white_on_primary"
|
||||
android:text="@string/index_update_progress_message"
|
||||
android:layout_gravity="start"
|
||||
android:textAlignment="gravity"
|
||||
/>
|
||||
</LinearLayout>
|
||||
<!--index loading progress END-->
|
||||
|
||||
<!--main list view BEGIN-->
|
||||
<ListView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:id="@+id/main_folder_and_files_list_view"
|
||||
android:id="@+id/list_view"
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</ListView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_list_view_empty_element"
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
@@ -60,12 +31,16 @@
|
||||
android:text="@string/folder_list_empty_message"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
<!--main list view END-->
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
<!--center content END-->
|
||||
|
||||
<!--upload here overlay button BEGIN-->
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/main_list_view_upload_here_button"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -77,7 +52,6 @@
|
||||
app:elevation="6dp"
|
||||
app:pressedTranslationZ="12dp"
|
||||
android:src="@drawable/ic_file_upload_white_24dp"/>
|
||||
<!--upload here overlay button END-->
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -5,40 +5,12 @@
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<!-- The main content view -->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/main_index_progress_bar"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/primary"
|
||||
android:visibility="gone">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"/>
|
||||
<TextView
|
||||
android:id="@+id/main_index_progress_bar_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white_on_primary"
|
||||
android:text="@string/index_update_progress_message"
|
||||
android:layout_gravity="start"
|
||||
android:textAlignment="gravity"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
<!-- The navigation drawer -->
|
||||
|
||||
<android.support.design.widget.NavigationView
|
||||
android:id="@+id/navigation"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -46,6 +18,7 @@
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
app:menu="@menu/drawer_view" />
|
||||
|
||||
</android.support.v4.widget.DrawerLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/device_id"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:clickable="true"
|
||||
android:drawableEnd="@drawable/ic_content_copy_black_24dp"
|
||||
android:focusable="true"
|
||||
android:fontFamily="monospace"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||
tools:text="ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD-ASD1ASD"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="8dp"
|
||||
android:drawableEnd="@drawable/ic_share_black_24dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</layout>
|
||||
@@ -4,27 +4,20 @@
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/abc_action_bar_content_inset_material"
|
||||
android:padding="24dp"
|
||||
android:theme="?alertDialogTheme"
|
||||
android:orientation="vertical">
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
android:layout_marginEnd="24dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/abc_action_bar_content_inset_material" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loading_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/loading_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
android:divider="@color/divider"
|
||||
android:dividerHeight="2dp">
|
||||
</ListView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -22,12 +23,14 @@
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<uk.co.markormesher.android_fab.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:buttonIcon="@drawable/ic_add_white_24dp"
|
||||
app:buttonBackgroundColour="@color/accent"/>
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/add_device"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:useCompatPadding="true"
|
||||
android:src="@drawable/ic_add_white_24dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/intro_primary"
|
||||
android:padding="28dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/intro_page_one_title"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:src="@mipmap/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#eee"
|
||||
android:text="@string/intro_page_one_description" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/intro_primary"
|
||||
android:padding="28dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/intro_page_three_title"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#eee"
|
||||
tools:text="@string/intro_page_three_description" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/intro_primary"
|
||||
android:padding="28dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:textColor="#eee"
|
||||
android:textSize="24sp"
|
||||
android:text="@string/intro_page_two_title"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<include
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:id="@+id/enter_device_id"
|
||||
layout="@layout/view_enter_device_id" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="@dimen/appIntroBottomBarHeight"
|
||||
android:textSize="16sp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#eee"
|
||||
android:text="@string/intro_page_two_description" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
@@ -5,8 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/device_id_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/device_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minLines="3"
|
||||
android:inputType="textNoSuggestions|textMultiLine|textCapCharacters"
|
||||
tools:text="VPNPKMK-VL2SOQN-SS5I2AB-G4BV7ZK-RO5ODEE-Y2G3CZ4-C4FUW4P-ZEMJOAF"/>
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/scan_qr_code"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:padding="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_qr_code_black_24dp"
|
||||
android:background="?android:selectableItemBackgroundBorderless"/>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -4,20 +4,25 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/folders"
|
||||
android:checked="true"
|
||||
android:icon="@drawable/ic_folder_gray_24dp"
|
||||
android:title="Folders"
|
||||
android:checked="true"/>
|
||||
android:title="@string/folders_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/devices"
|
||||
android:icon="@drawable/ic_laptop_gray_24dp"
|
||||
android:title="Devices" />
|
||||
android:title="@string/devices_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/update_index"
|
||||
android:icon="@drawable/ic_refresh_gray_24dp"
|
||||
android:title="@string/update_remote_index_label"
|
||||
android:checkable="false"/>
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_settings_gray_24dp"
|
||||
android:title="@string/settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/device_id"
|
||||
android:icon="@drawable/ic_qr_code_black_24dp"
|
||||
android:title="@string/show_device_id"
|
||||
android:checkable="false" />
|
||||
|
||||
<item
|
||||
android:id="@+id/clear_index"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
|
||||
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
|
||||
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
|
||||
<string name="scan_qr_code">QR code scannen</string>
|
||||
<string name="enter_device_id">Geräte ID eingeben</string>
|
||||
<string name="device_id_dialog_title">Geräte ID eingeben</string>
|
||||
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
|
||||
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
|
||||
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
|
||||
<string name="toast_file_upload_failed">Hochladen gescheitert</string>
|
||||
<string name="toast_upload_complete">Hochladen erfolgreich</string>
|
||||
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
|
||||
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
|
||||
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
|
||||
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
|
||||
<string name="remove_device_title">Gerät entfernen:</string>
|
||||
<string name="device_import_success">Gerät %1$s erfolgreich importiert</string>
|
||||
<string name="device_already_known">Gerät ist bereits bekannt $1%s</string>
|
||||
<string name="folders_label">Ordner</string>
|
||||
<string name="devices_label">Geräte</string>
|
||||
<string name="folder_label_format">%1$s (%2$s)</string>
|
||||
<string name="folder_content_info">%1$s, %2$d Dateien, %3$d Ordner</string>
|
||||
<string name="file_info">%1$s, zuletzt modifiziert %2$s</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,26 @@
|
||||
<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="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="last_modified_time">Dernière modification : %1$s</string>
|
||||
<string name="remove_device_title">Supprimer l\'appareil %1$s\?</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>
|
||||
@@ -0,0 +1,27 @@
|
||||
<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="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="index_update_progress_label">Aggiornamento dell\'indice per la cartella %1$s, %2$d%% sincronizzato</string>
|
||||
<string name="last_modified_time">Ultima modifica: %1$s</string>
|
||||
<string name="remove_device_title">Rimuovere il dispositivo %1$s\?</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>
|
||||
@@ -0,0 +1,27 @@
|
||||
<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="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="index_update_progress_label">フォルダー %1$s の索引を更新しました。 %2$d%% 同期しました</string>
|
||||
<string name="last_modified_time">最終更新: %1$s</string>
|
||||
<string name="remove_device_title">デバイス %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>
|
||||
@@ -0,0 +1,26 @@
|
||||
<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="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="last_modified_time">Modificat ultima dată pe: %1$s</string>
|
||||
<string name="remove_device_title">Ștergere dispozitiv %1$s\?</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>
|
||||
@@ -1,12 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#f43703</color>
|
||||
<color name="primary_dark">#d13602</color>
|
||||
<color name="white_on_primary">#fefefe</color>
|
||||
<color name="primary_light">#ff6f39</color>
|
||||
<color name="primary_dark">#b90000</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>
|
||||
<color name="intro_primary">#ff5252</color>
|
||||
<color name="intro_primary_dark">#c50e29</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
<resources>
|
||||
<string name="app_name">Syncthing Lite</string>
|
||||
<string name="index_update_progress_message">index update…</string>
|
||||
<string name="folder_list_empty_message">no folder available</string>
|
||||
<string name="clear_local_cache_index_label">clear local cache/index</string>
|
||||
<string name="update_remote_index_label">update remote index</string>
|
||||
<string name="devices_list_view_empty_message">no devices available</string>
|
||||
<string name="toast_write_storage_permission_required">Write storage permission is required for this functionality</string>
|
||||
<string name="folder_list_empty_message">No folder available</string>
|
||||
<string name="clear_local_cache_index_label">Clear local cache/index</string>
|
||||
<string name="devices_list_view_empty_message">No devices available</string>
|
||||
<string name="scan_qr_code">Scan QR code</string>
|
||||
<string name="enter_device_id">Enter device ID</string>
|
||||
<string name="invalid_device_id">Invalid device ID</string>
|
||||
<string name="invalid_device_id">Error: Invalid device ID</string>
|
||||
<string name="device_id_dialog_title">Enter Device ID</string>
|
||||
<string name="toast_index_update_successful">Index update successful</string>
|
||||
<string name="toast_index_update_failed">Index update failed for %1$d devices</string>
|
||||
<string name="dialog_downloading_file">Downloading file %1$s</string>
|
||||
<string name="toast_file_download_failed">Failed to download file</string>
|
||||
<string name="toast_open_file_failed">No compatible app found</string>
|
||||
<string name="toast_file_upload_failed">File upload failed</string>
|
||||
<string name="toast_upload_complete">File upload complete</string>
|
||||
<string name="dialog_uploading_file">Uploading file %1$s</string>
|
||||
<string name="clear_cache_and_index_title">Clear local cache and index?</string>
|
||||
<string name="clear_cache_and_index_body">Clear all local cache data and index data?</string>
|
||||
<string name="index_update_progress_label">Index update for folder %1$s, %2$d%% synchronized</string>
|
||||
<string name="loading_config_starting_syncthing_client">Loading config, starting syncthing client…</string>
|
||||
<string name="last_modified_time">Last modified: %1$s</string>
|
||||
<string name="remove_device_title">Remove device %1$s\?</string>
|
||||
<string name="remove_device_message">Remove %1$s from the 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>
|
||||
<string name="show_device_id">Show device ID</string>
|
||||
<string name="device_id">Device ID</string>
|
||||
<string name="device_id_copied">Device ID copied to clipboard</string>
|
||||
<string name="share_device_id_chooser">Share device ID with</string>
|
||||
<string name="other_syncthing_instance_title">Another Syncthing instance is running</string>
|
||||
<string name="other_syncthing_instance_message">Local discovery will not work. Stop the other Syncthing instance to enable local discovery.</string>
|
||||
<string name="intro_page_one_title">Welcome to Syncthing Lite</string>
|
||||
<string name="intro_page_one_description">Syncthing replaces proprietary sync and cloud services with something open, trustworthy and decentralized. Your data is your data alone and you deserve to choose where it is stored, if it is shared with some third party and how it\'s transmitted over the Internet.</string>
|
||||
<string name="intro_page_two_title">Add a device</string>
|
||||
<string name="intro_page_three_title">Share your folders</string>
|
||||
<string name="intro_page_two_description">Enter a Syncthing device ID, or scan a device ID from a QR code</string>
|
||||
<string name="intro_page_three_description">Now accept the device with ID %1$s, and share a folder with it. It may take a few minutes until the devices connect.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_app_version_title">App version</string>
|
||||
<string name="settings_local_device_name">Local device name</string>
|
||||
<string name="settings_local_device_summary">The name that other devices will see for this device</string>
|
||||
</resources>
|
||||
|
||||
@@ -4,19 +4,15 @@
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="FilePickerTheme" parent="NNF_BaseTheme">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||
<style name="Theme.Syncthing.NoActionBar" parent="@style/Theme.AppCompat">
|
||||
<item name="colorPrimary">@color/intro_primary</item>
|
||||
<item name="colorPrimaryDark">@color/intro_primary_dark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="files" path="/" />
|
||||
</paths>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/settings">
|
||||
|
||||
<EditTextPreference
|
||||
android:key="local_device_name"
|
||||
android:title="@string/settings_local_device_name"
|
||||
android:summary="@string/settings_local_device_summary"
|
||||
android:persistent="false"/>
|
||||
|
||||
<Preference
|
||||
android:key="app_version"
|
||||
android:title="@string/settings_app_version_title"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
+3
-4
@@ -1,9 +1,10 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.0'
|
||||
ext.kotlin_version = '1.2.20'
|
||||
ext.support_version = '27.0.2'
|
||||
ext.build_tools_version = '3.0.1'
|
||||
ext.anko_version = '0.10.4'
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
@@ -13,9 +14,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$build_tools_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
NEW_VERSION_NAME=$1
|
||||
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}' | tr -d "\"")
|
||||
if [[ -z ${NEW_VERSION_NAME} ]]
|
||||
then
|
||||
echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Updating Translations
|
||||
-----------------------------
|
||||
"
|
||||
tx push -s
|
||||
# Force push/pull to make sure this is executed. Apparently tx only compares timestamps, not file
|
||||
# contents. So if a file was `touch`ed, it won't be updated by default.
|
||||
tx pull -a -f
|
||||
git add -A "app/src/main/res/values-*/strings.xml"
|
||||
if ! git diff --cached --exit-code;
|
||||
then
|
||||
git commit -m "Imported translations"
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Updating Version
|
||||
-----------------------------
|
||||
"
|
||||
OLD_VERSION_CODE=$(grep "versionCode" "app/build.gradle" -m 1 | awk '{print $2}')
|
||||
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
|
||||
sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" "app/build.gradle"
|
||||
sed -i "s/versionName \"$OLD_VERSION_NAME\"/versionName \"$NEW_VERSION_NAME\"/" "app/build.gradle"
|
||||
|
||||
LIBRARY_NAME="com.github.Nutomic:syncthing-java"
|
||||
sed -i "s/$LIBRARY_NAME:$OLD_VERSION_NAME/$LIBRARY_NAME:$NEW_VERSION_NAME/" "app/build.gradle"
|
||||
|
||||
git add "app/build.gradle"
|
||||
git commit -m "Version $NEW_VERSION_NAME"
|
||||
git tag ${NEW_VERSION_NAME}
|
||||
|
||||
echo "
|
||||
|
||||
Running Lint
|
||||
-----------------------------
|
||||
"
|
||||
./gradlew clean lintVitalRelease
|
||||
|
||||
echo "
|
||||
Update ready.
|
||||
"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
version=$(git describe --tags)
|
||||
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
if [[ ! ${version} =~ $regex ]]
|
||||
then
|
||||
echo "Current commit is not a release"
|
||||
exit;
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Pushing to Github
|
||||
-----------------------------
|
||||
"
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
echo "
|
||||
|
||||
Push to Google Play
|
||||
-----------------------------
|
||||
"
|
||||
|
||||
read -s -p "Enter signing password: " password
|
||||
|
||||
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
|
||||
|
||||
# Upload apk and listing to Google Play
|
||||
SIGNING_PASSWORD=${password} ./gradlew publishRelease
|
||||
|
||||
echo "
|
||||
|
||||
Release published!
|
||||
"
|
||||
Reference in New Issue
Block a user