33 Commits

Author SHA1 Message Date
Felix Ableitner 087b0f4ec1 Version 0.2.1 2018-02-20 14:58:06 +09:00
Felix Ableitner a6cc38f4a1 Imported translations 2018-02-20 14:58:06 +09:00
Felix Ableitner 96a4ec1738 Added preferences with device name and app version 2018-02-20 14:29:26 +09:00
Felix Ableitner 42b77cab12 Added app intro 2018-02-20 03:27:40 +09:00
Felix Ableitner a28e5d704d Reworked add device dialog 2018-02-15 04:05:16 +09:00
Felix Ableitner b924b50ddc Show warning if another Syncthing instance is running (fixes 18) 2018-02-12 16:47:11 +09:00
Felix Ableitner 96503dd9c1 Added device ID and qr code (fixes #14) 2018-02-09 00:38:51 +09:00
Licaon_Kter c83504c155 Some typos (#27) 2018-02-08 17:51:26 +09:00
Felix Ableitner acea170854 Version 0.2.0 2018-02-08 02:52:43 +09:00
Felix Ableitner c6950493e6 Fixed lint issues 2018-02-08 02:43:14 +09:00
Felix Ableitner b71740d044 Imported translations 2018-02-08 02:28:32 +09:00
Felix Ableitner dafe262c1c Fixed crash when cancelling file upload/download 2018-02-08 02:25:36 +09:00
Felix Ableitner acb1f75c5c Rewrite SyncthingClient API 2018-02-08 02:12:30 +09:00
Poussinou 03cc4f931d Update README.md (#26) 2018-02-07 02:47:27 +09:00
Felix Ableitner 967d65b3f9 Change min sdk to 21 2018-02-05 02:38:52 +09:00
Felix Ableitner ce6e7e2130 Move index updates to library, some bug fixes 2018-02-02 13:14:35 +09:00
Felix Ableitner 809eff7354 Fixed bug that prevented devices from being deleted 2018-02-01 16:33:52 +09:00
Felix Ableitner 944cecce1f Use snackbar to show index updates 2018-02-01 14:20:16 +09:00
Felix Ableitner 31abff58c1 Implement Storage Provider 2018-02-01 11:11:01 +09:00
Felix Ableitner ef401378e2 Version 0.1.5 2018-01-29 23:08:08 +09:00
Felix Ableitner 2c0be54e61 Imported translations 2018-01-29 23:08:08 +09:00
Felix Ableitner cb4b838082 Rewrite configuration 2018-01-29 21:53:54 +09:00
Felix Ableitner 6a6b40a89d Fix file uploads, use system chooser for uploads 2018-01-28 21:30:11 +09:00
Felix Ableitner 1b7dbd91e6 Version 0.1.4 2018-01-27 17:31:27 +09:00
Felix Ableitner cb7a2d362f Imported translations 2018-01-27 17:31:27 +09:00
Felix Ableitner ef2d7fe9d7 Updated release script 2018-01-27 17:30:51 +09:00
Felix Ableitner 0bd96302e0 Adjust for library changes 2018-01-27 17:10:31 +09:00
Felix Ableitner e0a95a0314 Added transifex link to readme 2018-01-27 05:36:48 +09:00
Felix Ableitner c2e7f7cbc2 Add Transifex integration 2018-01-27 00:09:38 +09:00
Felix Ableitner 631a2a4fe3 Fixed some crashes during index update 2018-01-25 17:23:29 +09:00
Felix Ableitner 36e54f5d24 Implement proper string formatting 2018-01-23 16:40:11 +09:00
Felix Ableitner 3506df6b22 Code cleanup 2018-01-22 22:06:47 +09:00
Felix Ableitner 2e1369d9a8 Update dependencies 2018-01-22 01:52:13 +09:00
57 changed files with 1431 additions and 795 deletions
+9
View File
@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[syncthing-lite.stringsxml]
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en
type = ANDROID
lang_map = af_ZA: af-rZA, am_ET: am-rET, ar_AE: ar-rAE, ar_BH: ar-rBH, ar_DZ: ar-rDZ, ar_EG: ar-rEG, ar_IQ: ar-rIQ, ar_JO: ar-rJO, ar_KW: ar-rKW, ar_LB: ar-rLB, ar_LY: ar-rLY, ar_MA: ar-rMA, ar_OM: ar-rOM, ar_QA: ar-rQA, ar_SA: ar-rSA, ar_SY: ar-rSY, ar_TN: ar-rTN, ar_YE: ar-rYE, arn_CL: arn-rCL, as_IN: as-rIN, az_AZ: az-rAZ, ba_RU: ba-rRU, be_BY: be-rBY, bg_BG: bg-rBG, bn_BD: bn-rBD, bn_IN: bn-rIN, bo_CN: bo-rCN, br_FR: br-rFR, bs_BA: bs-rBA, ca_ES: ca-rES, co_FR: co-rFR, cs_CZ: cs-rCZ, cy_GB: cy-rGB, da_DK: da-rDK, de_AT: de-rAT, de_CH: de-rCH, de_DE: de-rDE, de_LI: de-rLI, de_LU: de-rLU, dsb_DE: dsb-rDE, dv_MV: dv-rMV, el_GR: el-rGR, en_AU: en-rAU, en_BZ: en-rBZ, en_CA: en-rCA, en_GB: en-rGB, en_IE: en-rIE, en_IN: en-rIN, en_JM: en-rJM, en_MY: en-rMY, en_NZ: en-rNZ, en_PH: en-rPH, en_SG: en-rSG, en_TT: en-rTT, en_US: en-rUS, en_ZA: en-rZA, en_ZW: en-rZW, es_AR: es-rAR, es_BO: es-rBO, es_CL: es-rCL, es_CO: es-rCO, es_CR: es-rCR, es_DO: es-rDO, es_EC: es-rEC, es_ES: es-rES, es_GT: es-rGT, es_HN: es-rHN, es_MX: es-rMX, es_NI: es-rNI, es_PA: es-rPA, es_PE: es-rPE, es_PR: es-rPR, es_PY: es-rPY, es_SV: es-rSV, es_US: es-rUS, es_UY: es-rUY, es_VE: es-rVE, et_EE: et-rEE, eu_ES: eu-rES, fa_IR: fa-rIR, fi_FI: fi-rFI, fil_PH: fil-rPH, fo_FO: fo-rFO, fr_BE: fr-rBE, fr_CA: fr-rCA, fr_CH: fr-rCH, fr_FR: fr-rFR, fr_LU: fr-rLU, fr_MC: fr-rMC, fy_NL: fy-rNL, ga_IE: ga-rIE, gd_GB: gd-rGB, gl_ES: gl-rES, gsw_FR: gsw-rFR, gu_IN: gu-rIN, ha_NG: ha-rNG, hi_IN: hi-rIN, hr_BA: hr-rBA, hr_HR: hr-rHR, hsb_DE: hsb-rDE, hu_HU: hu-rHU, hy_AM: hy-rAM, id_ID: id-rID, ig_NG: ig-rNG, ii_CN: ii-rCN, is_IS: is-rIS, it_CH: it-rCH, it_IT: it-rIT, iu_CA: iu-rCA, ja_JP: ja-rJP, ka_GE: ka-rGE, kk_KZ: kk-rKZ, kl_GL: kl-rGL, km_KH: km-rKH, kn_IN: kn-rIN, ko_KR: ko-rKR, kok_IN: kok-rIN, ky_KG: ky-rKG, lb_LU: lb-rLU, lo_LA: lo-rLA, lt_LT: lt-rLT, lv_LV: lv-rLV, mi_NZ: mi-rNZ, mk_MK: mk-rMK, ml_IN: ml-rIN, mn_CN: mn-rCN, mn_MN: mn-rMN, moh_CA: moh-rCA, mr_IN: mr-rIN, ms_BN: ms-rBN, ms_MY: ms-rMY, mt_MT: mt-rMT, nb_NO: nb-rNO, ne_NP: ne-rNP, nl_BE: nl-rBE, nl_NL: nl-rNL, nn_NO: nn-rNO, nso_ZA: nso-rZA, oc_FR: oc-rFR, or_IN: or-rIN, pa_IN: pa-rIN, pl_PL: pl-rPL, prs_AF: prs-rAF, ps_AF: ps-rAF, pt_BR: pt-rBR, pt_PT: pt-rPT, qut_GT: qut-rGT, quz_BO: quz-rBO, quz_EC: quz-rEC, quz_PE: quz-rPE, rm_CH: rm-rCH, ro_RO: ro-rRO, ru_RU: ru-rRU, rw_RW: rw-rRW, sa_IN: sa-rIN, sah_RU: sah-rRU, se_FI: se-rFI, se_NO: se-rNO, se_SE: se-rSE, si_LK: si-rLK, sk_SK: sk-rSK, sl_SI: sl-rSI, sma_NO: sma-rNO, sma_SE: sma-rSE, smj_NO: smj-rNO, smj_SE: smj-rSE, smn_FI: smn-rFI, sms_FI: sms-rFI, sq_AL: sq-rAL, sr_BA: sr-rBA, sr_CS: sr-rCS, sr_ME: sr-rME, sr_RS: sr-rRS, sv_FI: sv-rFI, sv_SE: sv-rSE, sw_KE: sw-rKE, syr_SY: syr-rSY, ta_IN: ta-rIN, te_IN: te-rIN, tg_TJ: tg-rTJ, th_TH: th-rTH, tk_TM: tk-rTM, tn_ZA: tn-rZA, tr_TR: tr-rTR, tt_RU: tt-rRU, tzm_DZ: tzm-rDZ, ug_CN: ug-rCN, uk_UA: uk-rUA, ur_PK: ur-rPK, uz_UZ: uz-rUZ, vi_VN: vi-rVN, wo_SN: wo-rSN, xh_ZA: xh-rZA, yo_NG: yo-rNG, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_MO: zh-rMO, zh_SG: zh-rSG, zh_TW: zh-rTW, zu_ZA: zu-rZA, no_NO: no-rNO, he_IL: iw-rIL, he: iw, id:in
+6
View File
@@ -13,8 +13,14 @@ 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
+14 -10
View File
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.ben-manes.versions'
android {
compileSdkVersion 27
@@ -8,10 +9,10 @@ android {
dataBinding.enabled = true
defaultConfig {
applicationId "net.syncthing.lite"
minSdkVersion 19
minSdkVersion 21
targetSdkVersion 25
versionCode 5
versionName "0.1.3"
versionCode 9
versionName "0.2.1"
multiDexEnabled true
}
sourceSets {
@@ -37,6 +38,9 @@ android {
signingConfig signingConfigs.release
}
}
packagingOptions {
exclude 'META-INF/*'
}
}
dependencies {
@@ -44,20 +48,20 @@ dependencies {
implementation "org.jetbrains.anko:anko-commons:$anko_version"
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
kapt "com.android.databinding:compiler:$build_tools_version"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:support-v4:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:cardview-v7:$support_version"
implementation ("com.github.Nutomic:syncthing-java:0.1.3") {
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'
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="GoogleAppIndexingWarning" severity="ignore" />
<issue id="InvalidPackage" severity="ignore" />
<issue id="OldTargetApi" severity="ignore" />
</lint>
+12 -4
View File
@@ -2,7 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.syncthing.lite">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
@@ -18,10 +17,10 @@
<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.FilePickerActivity"
android:parentActivityName=".activities.FolderBrowserActivity" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="net.syncthing.lite.fileprovider"
@@ -31,7 +30,16 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
<provider
android:name=".library.SyncthingProvider"
android:authorities="net.syncthing.lite.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>
@@ -1,101 +0,0 @@
package net.syncthing.lite.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import net.syncthing.lite.R
import java.io.File
import java.util.*
/**
* Activity that allows selecting a directory in the local file system.
*/
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
private lateinit var mListView: ListView
private lateinit var mFilesAdapter: FileAdapter
private lateinit var mLocation: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_folder_picker)
mListView = findViewById(android.R.id.list)
mListView.onItemClickListener = this
mListView.emptyView = findViewById(android.R.id.empty)
mFilesAdapter = FileAdapter(this)
mListView.adapter = mFilesAdapter
displayFolder(Environment.getExternalStorageDirectory())
}
/**
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
*/
private fun displayFolder(location: File) {
mLocation = location
mFilesAdapter.clear()
// In case we don't have read access to the location, just display nothing.
val contents = location.listFiles() ?: arrayOf()
Arrays.sort(contents) { f1, f2 ->
if (f1.isDirectory && f2.isFile)
return@sort -1
if (f1.isFile && f2.isDirectory)
return@sort 1
f1.name.compareTo(f2.name)
}
for (f in contents) {
mFilesAdapter.add(f)
}
mListView.adapter = mFilesAdapter
}
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val f = mFilesAdapter.getItem(i)
if (f!!.isDirectory) {
displayFolder(f)
} else {
val intent = Intent()
intent.data = Uri.fromFile(f)
setResult(Activity.RESULT_OK, intent)
finish()
}
}
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val title = view.findViewById<TextView>(android.R.id.text1)
val f = getItem(position)!!
title.text = f.name
val icon =
if (f.isDirectory) R.drawable.ic_folder_black_24dp
else R.drawable.ic_image_black_24dp
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
return view
}
}
override fun onBackPressed() {
if (mLocation != Environment.getExternalStorageDirectory()) {
displayFolder(mLocation.parentFile)
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
@@ -1,42 +1,34 @@
package net.syncthing.lite.activities
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import android.widget.Toast
import com.google.common.base.Preconditions.checkArgument
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.R
import net.syncthing.lite.adapters.FolderContentsAdapter
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
import net.syncthing.lite.library.DownloadFileTask
import net.syncthing.lite.library.UploadFileTask
import org.jetbrains.anko.intentFor
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 val REQUEST_SELECT_UPLOAD_FILE = 171
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 lateinit var indexBrowser: IndexBrowser
private lateinit var adapter: FolderContentsAdapter
private var runWhenPermissionsReceived: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -72,9 +64,9 @@ class FolderBrowserActivity : SyncthingActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
libraryHandler?.syncthingClient { syncthingClient ->
UploadFileTask(this@FolderBrowserActivity, syncthingClient, intent!!.data,
FileUploadDialog(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
this@FolderBrowserActivity::showUploadHereDialog).uploadFile()
{ showFolderListView(indexBrowser.currentPath) }).show()
}
}
}
@@ -89,15 +81,14 @@ class FolderBrowserActivity : SyncthingActivity() {
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
finish()
} else {
if (fileInfo.isDirectory) {
if (fileInfo.isDirectory()) {
indexBrowser.navigateTo(fileInfo)
Log.d(TAG, "load folder cache bg")
binding.listView.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
} else {
Log.i(TAG, "pulling file = " + fileInfo)
executeWithPermissions(
Runnable { libraryHandler?.syncthingClient { DownloadFileTask(this, it, fileInfo).downloadFile() } })
libraryHandler?.syncthingClient { FileDownloadDialog(this, it, fileInfo).show() }
}
}
}
@@ -109,7 +100,7 @@ class FolderBrowserActivity : SyncthingActivity() {
val list = indexBrowser.listFiles()
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
checkArgument(!list.isEmpty())//list must contain at least the 'parent' path
assert(!list.isEmpty())//list must contain at least the 'parent' path
adapter.clear()
adapter.addAll(list)
adapter.notifyDataSetChanged()
@@ -128,49 +119,14 @@ class FolderBrowserActivity : SyncthingActivity() {
}
private fun showUploadHereDialog() {
executeWithPermissions(Runnable {
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
})
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
startActivityForResult(intent, REQUEST_SELECT_UPLOAD_FILE)
}
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder)
+ folder + " " + percentage + getString(R.string.index_update_percent_synchronized))
override fun onIndexUpdateComplete(folderInfo: FolderInfo) {
super.onIndexUpdateComplete(folderInfo)
updateFolderListView()
}
override fun onIndexUpdateComplete() {
binding.indexUpdate.visibility = View.GONE
updateFolderListView()
}
private fun executeWithPermissions(runnable: Runnable) {
val permissionState = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permissionState != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_STORAGE)
runWhenPermissionsReceived = runnable
} else {
runnable.run()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
when (requestCode) {
REQUEST_WRITE_STORAGE -> {
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.toast_write_storage_permission_required,
Toast.LENGTH_LONG).show()
} else {
runWhenPermissionsReceived!!.run()
}
runWhenPermissionsReceived = null
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
@@ -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)
}
}
}
}
}
@@ -4,28 +4,38 @@ import android.app.AlertDialog
import android.content.res.Configuration
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.ActionBarDrawerToggle
import android.view.Gravity
import android.view.MenuItem
import android.view.View
import 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.fragments.SyncthingFragment
import net.syncthing.lite.library.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
private var currentFragment: SyncthingFragment? = 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(
@@ -69,7 +79,10 @@ class MainActivity : SyncthingActivity() {
when (menuItem.itemId) {
R.id.folders -> setContentFragment(FoldersFragment())
R.id.devices -> setContentFragment(DevicesFragment())
R.id.update_index -> libraryHandler?.syncthingClient { UpdateIndexTask(this@MainActivity, it).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(getString(R.string.clear_cache_and_index_title))
.setMessage(getString(R.string.clear_cache_and_index_body))
@@ -82,8 +95,7 @@ class MainActivity : SyncthingActivity() {
return true
}
private fun setContentFragment(fragment: SyncthingFragment) {
currentFragment = fragment
private fun setContentFragment(fragment: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(R.id.content_frame, fragment)
@@ -96,14 +108,4 @@ class MainActivity : SyncthingActivity() {
recreate()
}
}
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder) + " "
+ folder + " " + percentage + getString(R.string.index_update_percent_synchronized))
}
override fun onIndexUpdateComplete() {
binding.indexUpdate.visibility = View.GONE
}
}
@@ -1,21 +1,38 @@
package net.syncthing.lite.activities
import android.app.AlertDialog
import android.content.Context
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.library.LibraryHandler
import org.jetbrains.anko.contentView
import org.slf4j.impl.HandroidLoggerAdapter
abstract class SyncthingActivity : AppCompatActivity() {
companion object {
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()
}
}
}
var libraryHandler: LibraryHandler? = null
private set
private var loadingDialog: AlertDialog? = null
private var snackBar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -46,9 +63,20 @@ abstract class SyncthingActivity : AppCompatActivity() {
onLibraryLoaded()
}
open fun onIndexUpdateProgress(folder: String, percentage: Int) {}
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 onIndexUpdateComplete() {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {
snackBar?.dismiss()
snackBar = null
}
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!!)
+ context.getString(R.string.last_modified)
+ DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
binding.fileSize.text = context.getString(R.string.file_info,
FileUtils.byteCountToDisplaySize(fileInfo.size!!),
DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
}
return binding.root
}
@@ -11,7 +11,6 @@ import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewFolderBinding
import org.apache.commons.lang3.tuple.Pair
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
@@ -23,15 +22,13 @@ class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderSt
} 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)
context.getString(R.string.last_modified_unknown)
else context.getString(R.string.last_modified_known) + " " +
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
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,46 +5,46 @@ import android.content.Context
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.core.beans.DeviceInfo
import net.syncthing.java.core.beans.DeviceStats
import net.syncthing.java.core.security.KeystoreHandler
import net.syncthing.lite.R
import net.syncthing.lite.adapters.DevicesAdapter
import net.syncthing.lite.databinding.FragmentDevicesBinding
import net.syncthing.lite.library.UpdateIndexTask
import net.syncthing.lite.databinding.ViewEnterDeviceIdBinding
import net.syncthing.lite.utils.FragmentIntentIntegrator
import org.apache.commons.lang3.StringUtils.isBlank
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
import uk.co.markormesher.android_fab.SpeedDialMenuItem
import java.security.InvalidParameterException
import net.syncthing.lite.utils.Util
import java.io.IOException
class DevicesFragment : SyncthingFragment() {
companion object {
private val TAG = "DevicesFragment"
}
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 onResume() {
super.onResume()
libraryHandler?.syncthingClient { it.addOnConnectionChangedListener { updateDeviceList() } }
}
override fun onPause() {
super.onPause()
libraryHandler?.syncthingClient { it.removeOnConnectionChangedListener{ updateDeviceList() } }
}
override fun onLibraryLoaded() {
initDeviceList()
updateDeviceList()
@@ -54,94 +54,70 @@ class DevicesFragment : SyncthingFragment() {
adapter = DevicesAdapter(context!!)
binding.list.adapter = adapter
binding.list.setOnItemLongClickListener { _, _, position, _ ->
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
val device = adapter.getItem(position)
AlertDialog.Builder(context)
.setTitle(getString(R.string.remove_device_title) + " " + deviceId.substring(0, 7) + "?")
.setMessage(getString(R.string.remove_device_body_1) + " " + deviceId.substring(0, 7) + " " + getString(R.string.remove_device_body_2))
.setTitle(getString(R.string.remove_device_title, device.name))
.setMessage(getString(R.string.remove_device_message, device.deviceId.deviceId.substring(0, 7)))
.setPositiveButton(android.R.string.yes) { _, _ ->
libraryHandler?.configuration { it.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() {
libraryHandler?.syncthingClient { syncthingClient ->
adapter.clear()
adapter.addAll(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) {
libraryHandler?.library { configuration, syncthingClient, _ ->
async(UI) {
try {
KeystoreHandler.validateDeviceId(deviceId)
} catch (e: IllegalArgumentException) {
Toast.makeText(this@DevicesFragment.context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
return@async
}
val modified = configuration.edit().addPeers(DeviceInfo(deviceId, null))
if (modified) {
configuration.edit().persistLater()
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_import_success) + " " + deviceId, Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
UpdateIndexTask(this@DevicesFragment.context!!, syncthingClient).updateIndex()
} else {
Toast.makeText(this@DevicesFragment.context, getString(R.string.device_already_known) + " " + deviceId, Toast.LENGTH_SHORT).show()
}
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()
}
}
}
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)
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
}
}
}
@@ -6,17 +6,12 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.common.collect.Lists
import com.google.common.collect.Ordering
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.activities.FolderBrowserActivity
import net.syncthing.lite.adapters.FoldersListAdapter
import net.syncthing.lite.databinding.FragmentFoldersBinding
import org.apache.commons.lang3.tuple.Pair
import org.jetbrains.anko.intentFor
import java.util.*
class FoldersFragment : SyncthingFragment() {
@@ -37,17 +32,20 @@ class FoldersFragment : SyncthingFragment() {
private fun showAllFoldersListView() {
libraryHandler?.folderBrowser { folderBrowser ->
val list = Lists.newArrayList(folderBrowser.folderInfoAndStatsList())
Collections.sort(list, Ordering.natural<Comparable<String>>()
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
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)!!.left.folder
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()
}
}
@@ -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
}
}
}
}
@@ -2,6 +2,7 @@ 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() {
@@ -27,7 +28,7 @@ abstract class SyncthingFragment : Fragment() {
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folder: String, percentage: Int) {}
open fun onIndexUpdateProgress(folderInfo: FolderInfo, percentage: Int) {}
open fun onIndexUpdateComplete() {}
open fun onIndexUpdateComplete(folderInfo: FolderInfo) {}
}
@@ -1,106 +1,49 @@
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.support.annotation.StringRes
import android.support.v4.content.FileProvider
import android.util.Log
import android.webkit.MimeTypeMap
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.newTask
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.File
import java.io.IOException
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient,
private val mFileInfo: FileInfo) {
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 lateinit var progressDialog: ProgressDialog
private var cancelled = false
private val Tag = "DownloadFileTask"
private var isCancelled = false
fun downloadFile() {
showDialog()
mSyncthingClient.pullFile(mFileInfo, { observer ->
onProgress(observer)
init {
syncthingClient.getBlockPuller(fileInfo.folder, { blockPuller ->
val observer = blockPuller.pullFile(fileInfo)
onProgress(this, observer)
try {
while (!observer.isCompleted()) {
if (cancelled)
return@pullFile
if (isCancelled)
return@getBlockPuller
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage())
onProgress(observer)
onProgress(this, observer)
}
val outputFile = File("${mContext.externalCacheDir}/${mFileInfo.folder}/${mFileInfo.path}")
val outputFile = File("${context.externalCacheDir}/${fileInfo.folder}/${fileInfo.path}")
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
Log.i(TAG, "downloaded file = " + mFileInfo.path)
Log.i(Tag, "Downloaded file $fileInfo")
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()
Log.w(Tag, "Failed to download file $fileInfo", e)
}
}) { onError(R.string.toast_file_download_failed) }
}, { onError() })
}
private fun showDialog() {
progressDialog = ProgressDialog(mContext)
progressDialog.setMessage(mContext.getString(R.string.dialog_downloading_file, mFileInfo.fileName))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { cancelled = true }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
doAsync {
uiThread {
progressDialog.isIndeterminate = false
progressDialog.max = (mFileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress() * mFileInfo.size!!).toInt()
}
}
}
private fun onComplete(file: File) {
progressDialog.dismiss()
if (cancelled)
return
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
val uri = FileProvider.getUriForFile(mContext, "net.syncthing.lite.fileprovider", file)
intent.setDataAndType(uri, mimeType)
intent.newTask()
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
mContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(R.string.toast_open_file_failed)
Log.w(TAG, "No handler found for file " + file.name, e)
}
}
private fun onError(@StringRes error: Int) {
doAsync {
uiThread {
progressDialog.dismiss()
mContext.toast(error)
}
}
fun cancel() {
isCancelled = true
}
}
@@ -2,107 +2,82 @@ package net.syncthing.lite.library
import android.content.Context
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import com.google.common.eventbus.Subscribe
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.bep.IndexHandler
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.java.core.security.KeystoreHandler
import net.syncthing.lite.utils.Util
import org.apache.commons.io.FileUtils
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 org.jetbrains.anko.uiThread
import java.io.File
import java.io.IOException
import java.net.DatagramSocket
import java.net.InetAddress
import java.net.SocketException
import java.util.*
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
onIndexUpdateProgressListener: (String, Int) -> Unit,
onIndexUpdateCompleteListener: () -> Unit) {
private val onIndexUpdateProgressListener: (FolderInfo, Int) -> Unit,
private val onIndexUpdateCompleteListener: (FolderInfo) -> Unit) {
companion object {
private var instanceCount = 0
private var configuration: ConfigurationService? = null
private var configuration: Configuration? = null
private var syncthingClient: SyncthingClient? = null
private var folderBrowser: FolderBrowser? = null
private val callbacks = ArrayList<(ConfigurationService, SyncthingClient, FolderBrowser) -> Unit>()
private val callbacks = ArrayList<(Configuration, SyncthingClient, FolderBrowser) -> Unit>()
private var isLoading = false
var isListeningPortTaken = false
}
private val TAG = "LibConnectionHandler"
private val onIndexUpdateListener: Any
private val TAG = "LibraryHandler"
init {
instanceCount++
if (configuration == null && !isLoading) {
isLoading = true
doAsync {
checkIsListeningPortTaken()
init(context)
//trigger update if last was more than 10mins ago
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
val lastUpdateTimeAgo = Date().time - lastUpdateMillis
if (lastUpdateMillis == -1L || lastUpdateTimeAgo > 10 * 60 * 1000) {
Log.d(TAG, "trigger index update, last was " + Date(lastUpdateMillis))
syncthingClient { UpdateIndexTask(context, it).updateIndex() }
}
uiThread {
async(UI) {
onLibraryLoaded(this@LibraryHandler)
}
isLoading = false
}
} else {
onLibraryLoaded(this)
}
onIndexUpdateListener = object : Any() {
@Subscribe
fun handleIndexRecordAquiredEvent(event: IndexHandler.IndexRecordAquiredEvent) {
val indexInfo = event.indexInfo()
event.newRecords().size
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
onIndexUpdateProgressListener(event.folder(), (indexInfo.completed * 100).toInt())
}
@Subscribe
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
Log.i(TAG, "handleIndexAcquiredEvent trigger folder list update from index acquired")
onIndexUpdateCompleteListener()
}
}
syncthingClient {
it.indexHandler.eventBus.register(onIndexUpdateListener)
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) {
isLoading = true
val 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 (e: IOException) {
Log.e(TAG, "Failed to delete temporary files", e)
close()
}
KeystoreHandler.newLoader().loadAndStore(configuration)
configuration.edit().persistLater()
Log.i(TAG, "loaded mConfiguration = " + configuration.newWriter().dumpToString())
Log.i(TAG, "storage space = " + configuration.storageInfo.dumpAvailableSpace())
val configuration = Configuration(configFolder = context.filesDir)
val syncthingClient = SyncthingClient(configuration)
//TODO listen for device events, update device list
val folderBrowser = syncthingClient.indexHandler.newFolderBrowser()
if (instanceCount == 0) {
Log.d(TAG, "All LibraryHandler instances were closed during init")
configuration.close()
syncthingClient.close()
folderBrowser.close()
}
@@ -113,10 +88,9 @@ class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit
LibraryHandler.configuration = configuration
LibraryHandler.syncthingClient = syncthingClient
LibraryHandler.folderBrowser = folderBrowser
isLoading = false
}
fun library(callback: (ConfigurationService, SyncthingClient, FolderBrowser) -> Unit) {
fun library(callback: (Configuration, SyncthingClient, FolderBrowser) -> Unit) {
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
@@ -137,7 +111,7 @@ class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit
library { _, s, _ -> callback(s) }
}
fun configuration(callback: (ConfigurationService) -> Unit) {
fun configuration(callback: (Configuration) -> Unit) {
library { c, _, _ -> callback(c) }
}
@@ -145,6 +119,19 @@ class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> 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.
*
@@ -154,7 +141,8 @@ class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit
fun close() {
syncthingClient {
try {
it.indexHandler.eventBus.unregister(onIndexUpdateListener)
it.indexHandler.unregisterOnIndexRecordAcquiredListener(this::onIndexRecordAcquired)
it.indexHandler.unregisterOnFullIndexAcquiredListenersListener(this::onRemoteIndexAcquired)
} catch (e: IllegalArgumentException) {
// ignored, no idea why this is thrown
}
@@ -168,11 +156,10 @@ class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit
folderBrowser = null
syncthingClient?.close()
syncthingClient = null
configuration?.close()
configuration = null
}
}.start()
}, 1000)
}, 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!!
}
}
@@ -1,47 +0,0 @@
package net.syncthing.lite.library
import android.content.Context
import android.preference.PreferenceManager
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.util.*
class UpdateIndexTask(private val androidContext: Context, private val syncthingClient: SyncthingClient) {
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(androidContext)
fun updateIndex() {
if (sIndexUpdateInProgress)
return
sIndexUpdateInProgress = true
syncthingClient.updateIndexFromPeers { _, failures ->
sIndexUpdateInProgress = false
if (failures.isEmpty()) {
showToast(androidContext.getString(R.string.toast_index_update_successful))
} else {
showToast(androidContext.getString(R.string.toast_index_update_failed, failures.size))
}
mPreferences.edit()
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
.apply()
}
}
private fun showToast(message: String) {
doAsync {
uiThread {
androidContext.toast(message)
}
}
}
companion object {
val LAST_INDEX_UPDATE_TS_PREF = "LAST_INDEX_UPDATE_TS"
private var sIndexUpdateInProgress: Boolean = false
}
}
@@ -1,103 +1,48 @@
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import android.util.Log
import net.syncthing.java.bep.BlockPusher
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.R
import net.syncthing.lite.utils.Util
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.IOException
import org.apache.commons.io.IOUtils
// 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,
class UploadFileTask(context: Context, syncthingClient: SyncthingClient,
localFile: Uri, private val syncthingFolder: String,
syncthingSubFolder: String,
private val onUploadCompleteListener: () -> Unit) {
private val onProgress: (BlockPusher.FileUploadObserver) -> Unit,
private val onComplete: () -> Unit,
private val onError: () -> Unit) {
companion object {
private val TAG = "UploadFileTask"
}
private val TAG = "UploadFileTask"
private val fileName = Util.getContentFileName(context, localFile)
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, Util.getContentFileName(context, localFile))
private val uploadStream = context.contentResolver.openInputStream(localFile)
private lateinit var mProgressDialog: ProgressDialog
private var mCancelled = false
private var isCancelled = false
fun uploadFile() {
createDialog()
init {
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
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.progressMessage())
onProgress(observer)
}
} catch (e: InterruptedException) {
onError()
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = ${observer.progressPercentage()}%")
onProgress(observer)
}
IOUtils.closeQuietly(uploadStream)
onComplete()
}, { this.onError() })
} catch (e: IOException) {
onError()
}
}, { 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()
fun cancel() {
isCancelled = true
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
doAsync {
uiThread {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource().getSize().toInt()
mProgressDialog.progress = (observer.progress() * observer.dataSource().getSize()).toInt()
}
}
}
private fun onComplete() {
if (mCancelled)
return
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
}
private fun onError() {
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_file_upload_failed)
}
}
}
}
}
@@ -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)
@@ -24,17 +30,32 @@ object Util {
return deviceName ?: "android"
}
fun getContentFileName(context: Context, contentUri: Uri): String {
var fileName = File(contentUri.lastPathSegment).name
if (equal(contentUri.scheme, "content")) {
context.contentResolver.query(contentUri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)!!.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
val path = cursor.getString(columnIndex)
Log.d("Main", "recovered 'content' uri real path = " + path)
fileName = File(Uri.parse(path).lastPathSegment).name
fun getContentFileName(context: Context, uri: Uri): String {
context.contentResolver.query(uri, null, null, null, null, null).use { cursor ->
if (cursor == null || !cursor.moveToFirst()) {
throw InvalidParameterException("Cursor is null or empty")
}
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
@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,36 +13,6 @@
android:divider="?android:listDivider"
android:showDividers="middle">
<!--index loading progress BEGIN-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18sp"
android:textColor="@color/white_on_primary"
android:text="@string/index_update_progress_message"
android:layout_gravity="start"
android:textAlignment="gravity"
/>
</LinearLayout>
<!--index loading progress END-->
<!--main list view BEGIN-->
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -69,12 +38,9 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone"/>
<!--main list view END-->
</LinearLayout>
<!--center content END-->
<!--upload here overlay button BEGIN-->
<android.support.design.widget.FloatingActionButton
android:id="@+id/main_list_view_upload_here_button"
android:layout_width="wrap_content"
@@ -86,7 +52,6 @@
app:elevation="6dp"
app:pressedTranslationZ="12dp"
android:src="@drawable/ic_file_upload_white_24dp"/>
<!--upload here overlay button END-->
</RelativeLayout>
@@ -1,22 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/directory_empty" />
</FrameLayout>
</LinearLayout>
+2 -29
View File
@@ -5,40 +5,12 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18sp"
android:textColor="@color/white_on_primary"
android:text="@string/index_update_progress_message"
android:layout_gravity="start"
android:textAlignment="gravity"
/>
</LinearLayout>
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<android.support.design.widget.NavigationView
android:id="@+id/navigation"
android:layout_width="wrap_content"
@@ -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>
+10 -17
View File
@@ -4,27 +4,20 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/abc_action_bar_content_inset_material"
android:padding="24dp"
android:theme="?alertDialogTheme"
android:orientation="vertical">
android:orientation="horizontal"
android:gravity="center">
<LinearLayout
android:layout_width="match_parent"
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
android:layout_marginEnd="24dp" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/abc_action_bar_content_inset_material" />
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
+9 -6
View File
@@ -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>
@@ -1,13 +0,0 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:gravity="center_vertical"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:minHeight="48dp" />
</layout>
+2 -2
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="12dp">
<ImageView
+2 -2
View File
@@ -5,8 +5,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="12dp">
<TextView
@@ -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>
+9 -4
View File
@@ -14,10 +14,15 @@
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"
+8 -19
View File
@@ -1,37 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="index_update_progress_message">Index wird aktualisiert...</string>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
<string name="update_remote_index_label">Index aktualisieren</string>
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
<string name="toast_write_storage_permission_required">Schreibrechte werden für diese Funktion benötigt</string>
<string name="scan_qr_code">QR code scannen</string>
<string name="enter_device_id">Geräte ID eingeben</string>
<string name="invalid_device_id">Ungültige Geräte ID</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
<string name="toast_index_update_successful">Index erfolgreich aktualisiert</string>
<string name="toast_index_update_failed">Index update für %1$d Geräte fehlgeschlagen</string>
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
<string name="toast_file_upload_failed">Hochladen gescheitert</string>
<string name="toast_upload_complete">Hochladen erfolgreich</string>
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
<string name="directory_empty">Ordner ist leer</string>
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
<string name="index_update_folder">Index aktualisierung, Ordner</string>
<string name="index_update_percent_synchronized">% synchronisiert</string>
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
<string name="last_modified">- zuletzt modifiziert</string>
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</string>
<string name="last_modified_known">zuletzt modifiziert:</string>
<string name="last_modified_time">Zuletzt modifiziert: %1$s</string>
<string name="remove_device_title">Gerät entfernen:</string>
<string name="remove_device_body_1">Gerät</string>
<string name="remove_device_body_2">von den bekannten Geräten entfernen?</string>
<string name="device_import_success">Gerät erfolgreich importiert:</string>
<string name="device_already_known">Gerät ist bereits bekannt:</string>
<string name="device_import_success">Gerät %1$s erfolgreich importiert</string>
<string name="device_already_known">Gerät ist bereits bekannt $1%s</string>
<string name="folders_label">Ordner</string>
<string name="devices_label">Geräte</string>
</resources>
<string name="folder_label_format">%1$s (%2$s)</string>
<string name="folder_content_info">%1$s, %2$d Dateien, %3$d Ordner</string>
<string name="file_info">%1$s, zuletzt modifiziert %2$s</string>
</resources>
+26
View File
@@ -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>
+27
View File
@@ -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>
+27
View File
@@ -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>
+26
View File
@@ -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>
+4 -6
View File
@@ -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>
+28 -19
View File
@@ -1,37 +1,46 @@
<resources>
<string name="app_name" translatable="false">Syncthing Lite</string>
<string name="index_update_progress_message">Index update…</string>
<string name="app_name">Syncthing Lite</string>
<string name="folder_list_empty_message">No folder available</string>
<string name="clear_local_cache_index_label">Clear local cache/index</string>
<string name="update_remote_index_label">Update remote index</string>
<string name="devices_list_view_empty_message">No devices available</string>
<string name="toast_write_storage_permission_required">Write storage permission is required for this functionality</string>
<string name="scan_qr_code">Scan QR code</string>
<string name="enter_device_id">Enter device ID</string>
<string name="invalid_device_id">Invalid device ID</string>
<string name="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="directory_empty">Directory is empty</string>
<string name="clear_cache_and_index_title">Clear local cache and index?</string>
<string name="clear_cache_and_index_body">Clear all local cache data and index data?</string>
<string name="index_update_folder">Index update, folder</string>
<string name="index_update_percent_synchronized">% synchronized</string>
<string name="loading_config_starting_syncthing_client">Loading config, starting syncthing client</string>
<string name="last_modified">- last modified</string>
<string name="last_modified_unknown">last modified: unknown</string>
<string name="last_modified_known">last modified:</string>
<string name="remove_device_title">Remove device:</string>
<string name="remove_device_body_1">Remove device</string>
<string name="remove_device_body_2">from list of known devices?</string>
<string name="device_import_success">Successfully imported device:</string>
<string name="device_already_known">Device already present:</string>
<string name="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>
+9
View File
@@ -4,6 +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="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>
+19
View File
@@ -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>
+2 -4
View File
@@ -1,7 +1,7 @@
// 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'
@@ -14,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'
}
}
+22 -7
View File
@@ -3,21 +3,27 @@
set -e
NEW_VERSION_NAME=$1
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}')
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 "
Running Lint
Updating Translations
-----------------------------
"
./gradlew clean lintVitalRelease
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 "
@@ -27,13 +33,22 @@ 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"
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}')
sed -i "s/$OLD_VERSION_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.
"