21 Commits

Author SHA1 Message Date
Felix Ableitner a3495784f7 Version 0.1.3 2018-01-18 01:36:17 +09:00
Felix Ableitner 82c92c9031 Use FileProvider to share downloaded files (fixes #13) 2018-01-18 01:15:59 +09:00
Felix Ableitner bd5d89c158 Adjust to library changes 2018-01-18 00:38:20 +09:00
Felix Ableitner 9cf96d86dd Improve LibraryHandler to prevent various crashes (fixes #8) 2018-01-09 14:51:15 +09:00
Felix Ableitner f4c1e6a0f0 Fixed crash related to loading dialog (ref #11) 2018-01-09 03:32:57 +09:00
Felix Ableitner 036d3846bc Added release scripts 2018-01-04 16:39:52 +09:00
Felix Ableitner 6696f0ff88 Version 0.1.2 2018-01-04 16:28:10 +09:00
Felix Ableitner ffff6a7617 Improve folder browser code 2018-01-04 14:39:52 +09:00
Felix Ableitner 3bd1f6e44a Better error logging 2018-01-04 14:20:27 +09:00
Patrick S a2f46b358d Externalized strings + german translation (#10)
* externalized some strings

* externalized some more strings

* Yay! More strings externalized

* externalized strings

* translated to german

* translation finished

* fixed mistake

* fixed compile error

* externalized strings
translated said strings into german

* finishing touches

* even more finishing touches

* finishing touches

* fixed missing space

* added space

* Revert "fixed compile error"

This reverts commit 0225ce6d79.
2018-01-04 14:19:32 +09:00
ImPat a0e749e879 Various fixes related to capitalization and added some question marks and a space (#9)
* fixed missing space and capitalized two word

* added capitalization

* Added Captialization
2017-12-31 13:28:04 +09:00
Felix Ableitner e566b3c58d Minor fixes 2017-12-30 01:08:45 +09:00
Felix Ableitner acff8c1f5c Simplify code with anko library 2017-12-29 14:07:05 +09:00
Felix Ableitner 4353fc5597 Move library initialization to seperate class 2017-12-29 04:35:28 +09:00
Felix Ableitner 64b2b7424b Fix screen rotation in MainActivity (fixes #7) 2017-12-29 00:24:25 +09:00
Felix Ableitner 2fa0dd2e59 Clarified build instructions 2017-12-28 17:32:43 +09:00
Felix Ableitner 2e1fa76b44 Use own activity for upload file picker 2017-12-27 13:30:23 +09:00
Felix Ableitner f447e1ecad Version 0.1.1 2017-12-27 02:32:19 +09:00
Felix Ableitner ee9098b4f1 Fix crash on initial start (fixes #5) 2017-12-27 02:31:47 +09:00
Felix Ableitner f378329da3 Added graphics 2017-12-27 00:25:52 +09:00
Felix Ableitner 0caec11826 Added google play link (fixes #3) 2017-12-22 01:43:46 +09:00
37 changed files with 865 additions and 581 deletions
+5 -2
View File
@@ -13,13 +13,16 @@ example, mobile devices with limited storage available, wishing to access a sync
This project is based on [syncthing-java][3], a java implementation of Syncthing protocols.
[<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80">](https://play.google.com/store/apps/details?id=net.syncthing.lite)
## Building
The project uses a standard Android build, and requires the Android SDK. The easiest option is if
you install [Android Studio][4] and import the project.
The syncthing-java library is not stable yet. If you encounter any build errors, you probably have
to build it from source. To do this, clone the repo and run `gradle install`.
To compile with a development version of the [syncthing-java][3] library, you have to install it to
the local maven repository. To do this, clone the repo and run `gradle install` in the
syncthing-java project folder.
## License
All code is licensed under the [MPLv2 License][5].
+21 -4
View File
@@ -10,8 +10,8 @@ android {
applicationId "net.syncthing.lite"
minSdkVersion 19
targetSdkVersion 25
versionCode 2
versionName "0.1"
versionCode 5
versionName "0.1.3"
multiDexEnabled true
}
sourceSets {
@@ -21,17 +21,34 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
release {
storeFile = {
def path = System.getenv("SYNCTHING_LITE_RELEASE_STORE_FILE")
return (path) ? file(path) : null
}()
storePassword System.getenv("SIGNING_PASSWORD") ?: ""
keyAlias System.getenv("SYNCTHING_LITE_RELEASE_KEY_ALIAS") ?: ""
keyPassword System.getenv("SIGNING_PASSWORD") ?: ""
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.anko:anko-commons:$anko_version"
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
kapt "com.android.databinding:compiler:$build_tools_version"
implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:recyclerview-v7:$support_version"
implementation "com.android.support:support-v4:$support_version"
implementation "com.android.support:design:$support_version"
implementation "com.android.support:cardview-v7:$support_version"
implementation ("com.github.Nutomic:syncthing-java:0.1") {
implementation ("com.github.Nutomic:syncthing-java:0.1.3") {
exclude group: 'commons-logging', module:'commons-logging'
exclude group: 'commons-codec'
exclude group: 'org.apache.httpcomponents', module:'httpclient'
+13 -10
View File
@@ -20,15 +20,18 @@
</activity>
<activity android:name=".activities.FolderBrowserActivity"
android:parentActivityName=".activities.MainActivity"/>
<activity
android:name=".activities.MIVFilePickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".activities.FilePickerActivity"
android:parentActivityName=".activities.FolderBrowserActivity" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="net.syncthing.lite.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>
</manifest>
@@ -0,0 +1,101 @@
package net.syncthing.lite.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import net.syncthing.lite.R
import java.io.File
import java.util.*
/**
* Activity that allows selecting a directory in the local file system.
*/
class FilePickerActivity : SyncthingActivity(), AdapterView.OnItemClickListener {
private lateinit var mListView: ListView
private lateinit var mFilesAdapter: FileAdapter
private lateinit var mLocation: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_folder_picker)
mListView = findViewById(android.R.id.list)
mListView.onItemClickListener = this
mListView.emptyView = findViewById(android.R.id.empty)
mFilesAdapter = FileAdapter(this)
mListView.adapter = mFilesAdapter
displayFolder(Environment.getExternalStorageDirectory())
}
/**
* Refreshes the ListView to show the contents of the location in ``mLocation.peek()}.
*/
private fun displayFolder(location: File) {
mLocation = location
mFilesAdapter.clear()
// In case we don't have read access to the location, just display nothing.
val contents = location.listFiles() ?: arrayOf()
Arrays.sort(contents) { f1, f2 ->
if (f1.isDirectory && f2.isFile)
return@sort -1
if (f1.isFile && f2.isDirectory)
return@sort 1
f1.name.compareTo(f2.name)
}
for (f in contents) {
mFilesAdapter.add(f)
}
mListView.adapter = mFilesAdapter
}
override fun onItemClick(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val f = mFilesAdapter.getItem(i)
if (f!!.isDirectory) {
displayFolder(f)
} else {
val intent = Intent()
intent.data = Uri.fromFile(f)
setResult(Activity.RESULT_OK, intent)
finish()
}
}
private inner class FileAdapter(context: Context) : ArrayAdapter<File>(context, R.layout.item_folder_picker) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val title = view.findViewById<TextView>(android.R.id.text1)
val f = getItem(position)!!
title.text = f.name
val icon =
if (f.isDirectory) R.drawable.ic_folder_black_24dp
else R.drawable.ic_image_black_24dp
title.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0)
return view
}
}
override fun onBackPressed() {
if (mLocation != Environment.getExternalStorageDirectory()) {
displayFolder(mLocation.parentFile)
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
}
@@ -2,32 +2,25 @@ package net.syncthing.lite.activities
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.databinding.DataBindingUtil
import android.os.AsyncTask
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import android.widget.Toast
import com.google.common.base.Objects.equal
import com.google.common.base.Preconditions.checkArgument
import com.nononsenseapps.filepicker.FilePickerActivity
import net.syncthing.java.bep.IndexBrowser
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.utils.FileInfoOrdering
import net.syncthing.java.core.utils.PathUtils
import net.syncthing.lite.R
import net.syncthing.lite.adapters.FolderContentsAdapter
import net.syncthing.lite.databinding.ActivityFolderBrowserBinding
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.utils.DownloadFileTask
import net.syncthing.lite.utils.UploadFileTask
import net.syncthing.lite.library.DownloadFileTask
import net.syncthing.lite.library.UploadFileTask
import org.jetbrains.anko.intentFor
class FolderBrowserActivity : SyncthingActivity() {
@@ -35,158 +28,120 @@ class FolderBrowserActivity : SyncthingActivity() {
private val TAG = "FolderBrowserActivity"
private val REQUEST_WRITE_STORAGE = 142
private val REQUEST_SELECT_UPLOAD_FILE = 171
val EXTRA_FOLDER_NAME = "folder_name"
}
private lateinit var binding: ActivityFolderBrowserBinding
private var indexBrowser: IndexBrowser? = null
private var loadingDialog: AlertDialog? = null
private var adapter: FolderContentsAdapter? = null
private lateinit var indexBrowser: IndexBrowser
private lateinit var adapter: FolderContentsAdapter
private var runWhenPermissionsReceived: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_folder_browser)
binding.mainListViewUploadHereButton.setOnClickListener { showUploadHereDialog() }
showFolderListView(intent.getStringExtra(EXTRA_FOLDER_NAME), null)
adapter = FolderContentsAdapter(this)
binding.listView.adapter = adapter
binding.listView.setOnItemClickListener { _, _, position, _ ->
val fileInfo = binding.listView.getItemAtPosition(position) as FileInfo
navigateToFolder(fileInfo)
}
val folder = intent.getStringExtra(EXTRA_FOLDER_NAME)
libraryHandler?.syncthingClient {
indexBrowser = it.indexHandler.newIndexBrowser(folder, true, true)
indexBrowser.setOnFolderChangedListener(this::onFolderChanged)
}
}
override fun onDestroy() {
super.onDestroy()
Thread {
indexBrowser?.close()
indexBrowser = null
indexBrowser.setOnFolderChangedListener(null)
indexBrowser.close()
}.start()
cancelLoadingDialog()
}
override fun onBackPressed() {
val listView = binding.mainFolderAndFilesListView
val listView = binding.listView
//click item '0', ie '..' (go to parent)
listView.performItemClick(adapter!!.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
listView.performItemClick(adapter.getView(0, null, listView), 0, listView.getItemIdAtPosition(0))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent) {
if (resultCode == Activity.RESULT_OK) {
UploadFileTask(this, syncthingClient(), intent.data, indexBrowser!!.folder,
indexBrowser!!.currentPath, { this.updateFolderListView() }).uploadFile()
}
}
private fun showLoadingDialog(message: String) {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
layoutInflater, R.layout.dialog_loading, null, false)
binding.loadingText.text = message
loadingDialog = android.app.AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
}
private fun cancelLoadingDialog() {
loadingDialog?.cancel()
loadingDialog = null
}
private fun showFolderListView(folder: String, previousPath: String?) {
if (indexBrowser != null && equal(folder, indexBrowser!!.folder)) {
Log.d(TAG, "reuse current index browser")
indexBrowser!!.navigateToNearestPath(previousPath)
} else {
if (indexBrowser != null) {
indexBrowser!!.close()
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (requestCode == REQUEST_SELECT_UPLOAD_FILE && resultCode == Activity.RESULT_OK) {
libraryHandler?.syncthingClient { syncthingClient ->
UploadFileTask(this@FolderBrowserActivity, syncthingClient, intent!!.data,
indexBrowser.folder, indexBrowser.currentPath,
this@FolderBrowserActivity::showUploadHereDialog).uploadFile()
}
Log.d(TAG, "open new index browser")
indexBrowser = syncthingClient().indexHandler
.newIndexBrowserBuilder()
.setOrdering(FileInfoOrdering.ALPHA_ASC_DIR_FIRST)
.includeParentInList(true).allowParentInRoot(true)
.setFolder(folder)
.buildToNearestPath(previousPath)
}
adapter = FolderContentsAdapter(this)
binding.mainFolderAndFilesListView.adapter = adapter
binding.mainFolderAndFilesListView.setOnItemClickListener { _, _, position, _ ->
val fileInfo = binding.mainFolderAndFilesListView.getItemAtPosition(position) as FileInfo
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser!!.currentPath + "'")
navigateToFolder(fileInfo)
}
navigateToFolder(indexBrowser!!.currentPathInfo)
}
private fun showFolderListView(path: String) {
indexBrowser.navigateToNearestPath(path)
navigateToFolder(indexBrowser.currentPathInfo())
}
private fun navigateToFolder(fileInfo: FileInfo) {
if (indexBrowser!!.isRoot && PathUtils.isParent(fileInfo.path)) {
Log.d(TAG, "navigate to path = '" + fileInfo.path + "' from path = '" + indexBrowser.currentPath + "'")
if (indexBrowser.isRoot() && PathUtils.isParent(fileInfo.path)) {
finish()
} else {
if (fileInfo.isDirectory) {
indexBrowser!!.navigateTo(fileInfo)
val newFileInfo = if (PathUtils.isParent(fileInfo.path)) indexBrowser!!.currentPathInfo else fileInfo
if (!indexBrowser!!.isCacheReadyAfterALittleWait) {
Log.d(TAG, "load folder cache bg")
object : AsyncTask<Void?, Void?, Void?>() {
override fun onPreExecute() {
// TODO: show ProgressBar in ListView instead of dialog
showLoadingDialog("open directory: " +
if (indexBrowser!!.isRoot) folderBrowser().getFolderInfo(indexBrowser!!.folder).label
else indexBrowser!!.currentPathFileName)
}
override fun doInBackground(vararg voids: Void?): Void? {
indexBrowser!!.waitForCacheReady()
return null
}
override fun onPostExecute(aVoid: Void?) {
Log.d(TAG, "cache ready, navigate to folder")
cancelLoadingDialog()
navigateToFolder(newFileInfo)
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
} else {
val list = indexBrowser!!.listFiles()
Log.i("navigateToFolder", "list for path = '" + indexBrowser!!.currentPath + "' list = " + list.size + " records")
Log.d("navigateToFolder", "list for path = '" + indexBrowser!!.currentPath + "' list = " + list)
checkArgument(!list.isEmpty())//list must contain at least the 'parent' path
adapter!!.clear()
adapter!!.addAll(list)
adapter!!.notifyDataSetChanged()
binding.mainFolderAndFilesListView.setSelection(0)
supportActionBar!!.setTitle(if (indexBrowser!!.isRoot)
folderBrowser().getFolderInfo(indexBrowser!!.folder).label
else
newFileInfo.fileName)
}
indexBrowser.navigateTo(fileInfo)
Log.d(TAG, "load folder cache bg")
binding.listView.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
} else {
Log.i(TAG, "pulling file = " + fileInfo)
executeWithPermissions(
Runnable { DownloadFileTask(this, syncthingClient(), fileInfo).downloadFile() })
Runnable { libraryHandler?.syncthingClient { DownloadFileTask(this, it, fileInfo).downloadFile() } })
}
}
}
private fun onFolderChanged() {
runOnUiThread {
binding.progressBar.visibility = View.GONE
binding.listView.visibility = View.VISIBLE
val list = indexBrowser.listFiles()
Log.i("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list.size + " records")
Log.d("navigateToFolder", "list for path = '" + indexBrowser.currentPath + "' list = " + list)
checkArgument(!list.isEmpty())//list must contain at least the 'parent' path
adapter.clear()
adapter.addAll(list)
adapter.notifyDataSetChanged()
binding.listView.setSelection(0)
if (indexBrowser.isRoot())
libraryHandler?.folderBrowser {
supportActionBar?.title = it.getFolderInfo(indexBrowser.folder)?.label
}
else
supportActionBar?.title = indexBrowser.currentPathInfo().fileName
}
}
private fun updateFolderListView() {
showFolderListView(indexBrowser!!.folder, indexBrowser!!.currentPath)
showFolderListView(indexBrowser.currentPath)
}
private fun showUploadHereDialog() {
executeWithPermissions(Runnable {
val i = Intent(this, MIVFilePickerActivity::class.java)
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
Log.i(TAG, "showUploadHereDialog path = " + path)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, path)
startActivityForResult(i, 0)
startActivityForResult(intentFor<FilePickerActivity>(), REQUEST_SELECT_UPLOAD_FILE)
})
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.mainIndexProgressBarLabel.text = ("index update, folder "
+ folder.label + " " + percentage + "% synchronized")
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
binding.indexUpdate.visibility = View.VISIBLE
binding.indexUpdateLabel.text = (getString(R.string.index_update_folder)
+ folder + " " + percentage + getString(R.string.index_update_percent_synchronized))
updateFolderListView()
}
override fun onIndexUpdateComplete() {
binding.mainIndexProgressBar.visibility = View.GONE
binding.indexUpdate.visibility = View.GONE
updateFolderListView()
}
@@ -1,19 +0,0 @@
package net.syncthing.lite.activities
import com.nononsenseapps.filepicker.AbstractFilePickerActivity
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import net.syncthing.lite.fragments.MIVFilePickerFragment
import java.io.File
class MIVFilePickerActivity : AbstractFilePickerActivity<File>() {
override fun getFragment(startPath: String, mode: Int, allowMultiple: Boolean,
allowCreateDir: Boolean): AbstractFilePickerFragment<File> {
// Only the fragment in this line needs to be changed
val fragment = MIVFilePickerFragment()
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
return fragment
}
}
@@ -4,22 +4,24 @@ import android.app.AlertDialog
import android.content.res.Configuration
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.ActionBarDrawerToggle
import android.view.Gravity
import android.view.MenuItem
import android.view.View
import net.syncthing.java.core.beans.FolderInfo
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import net.syncthing.lite.R
import net.syncthing.lite.databinding.ActivityMainBinding
import net.syncthing.lite.fragments.DevicesFragment
import net.syncthing.lite.fragments.FoldersFragment
import net.syncthing.lite.utils.UpdateIndexTask
import net.syncthing.lite.fragments.SyncthingFragment
import net.syncthing.lite.library.UpdateIndexTask
class MainActivity : SyncthingActivity() {
private lateinit var binding: ActivityMainBinding
private var drawerToggle: ActionBarDrawerToggle? = null
private var currentFragment: SyncthingFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -34,15 +36,19 @@ class MainActivity : SyncthingActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
/**
* Sync the toggle state and fragment after onRestoreInstanceState has occurred.
*/
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle!!.syncState()
}
override fun onLibraryLoaded() {
super.onLibraryLoaded()
setContentFragment(FoldersFragment())
drawerToggle!!.syncState()
val menu = binding.navigation.menu
val selection = (0 until menu.size())
.map { menu.getItem(it) }
.find { it.isChecked }
?: menu.getItem(0)
onNavigationItemSelectedListener(selection)
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -57,17 +63,16 @@ class MainActivity : SyncthingActivity() {
true
} else super.onOptionsItemSelected(item)
// Handle your other action bar items...
}
private fun onNavigationItemSelectedListener(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.folders -> setContentFragment(FoldersFragment())
R.id.devices -> setContentFragment(DevicesFragment())
R.id.update_index -> UpdateIndexTask(this, syncthingClient()).updateIndex()
R.id.update_index -> libraryHandler?.syncthingClient { UpdateIndexTask(this@MainActivity, it).updateIndex() }
R.id.clear_index -> AlertDialog.Builder(this)
.setTitle("clear cache and index")
.setMessage("clear all cache data and index data?")
.setTitle(getString(R.string.clear_cache_and_index_title))
.setMessage(getString(R.string.clear_cache_and_index_body))
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.yes) { _, _ -> cleanCacheAndIndex() }
.setNegativeButton(android.R.string.no, null)
@@ -77,7 +82,8 @@ class MainActivity : SyncthingActivity() {
return true
}
private fun setContentFragment(fragment: Fragment) {
private fun setContentFragment(fragment: SyncthingFragment) {
currentFragment = fragment
supportFragmentManager
.beginTransaction()
.replace(R.id.content_frame, fragment)
@@ -85,17 +91,19 @@ class MainActivity : SyncthingActivity() {
}
private fun cleanCacheAndIndex() {
syncthingClient().clearCacheAndIndex()
recreate()
async(UI) {
libraryHandler?.syncthingClient { it.clearCacheAndIndex() }
recreate()
}
}
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
binding.mainIndexProgressBar.visibility = View.VISIBLE
binding.mainIndexProgressBarLabel.text = ("index update, folder "
+ folder.label + " " + percentage + "% synchronized")
override fun onIndexUpdateProgress(folder: String, percentage: Int) {
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.mainIndexProgressBar.visibility = View.GONE
binding.indexUpdate.visibility = View.GONE
}
}
@@ -1,109 +1,52 @@
package net.syncthing.lite.activities
import android.app.AlertDialog
import android.content.Context
import android.databinding.DataBindingUtil
import android.os.AsyncTask
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.LayoutInflater
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.lite.BuildConfig
import net.syncthing.lite.R
import net.syncthing.lite.databinding.DialogLoadingBinding
import net.syncthing.lite.utils.LibraryHandler
import net.syncthing.lite.utils.UpdateIndexTask
import net.syncthing.lite.library.LibraryHandler
import org.slf4j.impl.HandroidLoggerAdapter
import java.util.*
abstract class SyncthingActivity : AppCompatActivity() {
companion object {
private val TAG = "SyncthingActivity"
private var activityCount = 0
private var libraryHandler: LibraryHandler? = null
}
fun syncthingClient(): SyncthingClient = libraryHandler!!.syncthingClient!!
fun configuration(): ConfigurationService = libraryHandler!!.configuration!!
fun folderBrowser(): FolderBrowser = libraryHandler!!.folderBrowser!!
var libraryHandler: LibraryHandler? = null
private set
private var loadingDialog: AlertDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG
activityCount++
if (libraryHandler == null) {
InitTask(this, this::onLibraryLoaded).execute()
}
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(this), R.layout.dialog_loading, null, false)
binding.loadingText.text = getString(R.string.loading_config_starting_syncthing_client)
loadingDialog = AlertDialog.Builder(this)
.setCancelable(false)
.setView(binding.root)
.show()
LibraryHandler(this, this::onLibraryLoadedInternal,
this::onIndexUpdateProgress, this::onIndexUpdateComplete)
}
override fun onDestroy() {
super.onDestroy()
activityCount--
Thread {
if (activityCount == 0) {
libraryHandler!!.destroy()
libraryHandler = null
}
}.start()
libraryHandler?.close()
loadingDialog?.dismiss()
}
private class InitTask(val context: Context, val onLibraryLoaded: () -> Unit)
: AsyncTask<Void?, Void?, Void?>() {
private var loadingDialog: AlertDialog? = null
override fun onPreExecute() {
val binding = DataBindingUtil.inflate<DialogLoadingBinding>(
LayoutInflater.from(context), R.layout.dialog_loading, null, false)
binding.loadingText.text = "loading config, starting syncthing client"
loadingDialog = android.app.AlertDialog.Builder(context)
.setCancelable(false)
.setView(binding.root)
.show()
}
override fun doInBackground(vararg voidd: Void?): Void? {
libraryHandler = LibraryHandler()
libraryHandler!!.init(context)
return null
}
override fun onPostExecute(voidd: Void?) {
loadingDialog!!.cancel()
libraryHandler!!.setOnIndexUpdatedListener(object : LibraryHandler.OnIndexUpdatedListener {
override fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {
onIndexUpdateProgress(folder, percentage)
}
override fun onIndexUpdateComplete() {
onIndexUpdateComplete()
}
})
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
val lastUpdate =
if (lastUpdateMillis < 0) null
else Date(lastUpdateMillis)
//trigger update if last was more than 10mins ago
if (lastUpdate == null || Date().time - lastUpdate.time > 10 * 60 * 1000) {
Log.d(TAG, "trigger index update, last was " + lastUpdate!!)
UpdateIndexTask(context, libraryHandler!!.syncthingClient!!).updateIndex()
}
onLibraryLoaded()
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
if (!isDestroyed) {
loadingDialog?.dismiss()
}
onLibraryLoaded()
}
open fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int) {}
open fun onIndexUpdateProgress(folder: String, percentage: Int) {}
open fun onIndexUpdateComplete() {}
@@ -32,7 +32,7 @@ class FolderContentsAdapter(context: Context) :
binding.fileIcon.setImageResource(R.drawable.ic_image_black_24dp)
binding.fileSize.visibility = View.VISIBLE
binding.fileSize.text = (FileUtils.byteCountToDisplaySize(fileInfo.size!!)
+ " - last modified "
+ context.getString(R.string.last_modified)
+ DateUtils.getRelativeDateTimeString(context, fileInfo.lastModified.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0))
}
return binding.root
@@ -13,7 +13,7 @@ import net.syncthing.lite.R
import net.syncthing.lite.databinding.ListviewFolderBinding
import org.apache.commons.lang3.tuple.Pair
class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderStats>>) :
class FoldersListAdapter(context: Context?, list: List<Pair<FolderInfo, FolderStats>>) :
ArrayAdapter<Pair<FolderInfo, FolderStats>>(context, R.layout.listview_folder, list) {
override fun getView(position: Int, v: View?, parent: ViewGroup): View {
@@ -28,8 +28,8 @@ class FoldersListAdapter(context: Context, list: List<Pair<FolderInfo, FolderSta
binding.folderName.text = "${folderInfo.label} (${folderInfo.folder})"
binding.folderLastmodInfo.text =
if (folderStats.lastUpdate == null)
"last modified: unknown"
else "last modified: " +
context.getString(R.string.last_modified_unknown)
else context.getString(R.string.last_modified_known) + " " +
DateUtils.getRelativeDateTimeString(context, folderStats.lastUpdate.time, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
binding.folderContentInfo.text = "${folderStats.describeSize()}, ${folderStats.fileCount} files, ${folderStats.dirCount} dirs"
return binding.root
@@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@@ -14,26 +13,27 @@ 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.activities.SyncthingActivity
import net.syncthing.lite.adapters.DevicesAdapter
import net.syncthing.lite.databinding.FragmentDevicesBinding
import net.syncthing.lite.utils.UpdateIndexTask
import net.syncthing.lite.library.UpdateIndexTask
import net.syncthing.lite.utils.FragmentIntentIntegrator
import org.apache.commons.lang3.StringUtils.isBlank
import uk.co.markormesher.android_fab.SpeedDialMenuAdapter
import uk.co.markormesher.android_fab.SpeedDialMenuItem
import java.security.InvalidParameterException
class DevicesFragment : Fragment() {
class DevicesFragment : SyncthingFragment() {
companion object {
private val TAG = "DevicesFragment"
}
private lateinit var syncthingActivity: SyncthingActivity
private lateinit var binding: FragmentDevicesBinding
private lateinit var adapter: DevicesAdapter
@@ -45,23 +45,22 @@ class DevicesFragment : Fragment() {
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
syncthingActivity = activity as SyncthingActivity
override fun onLibraryLoaded() {
initDeviceList()
updateDeviceList()
}
private fun initDeviceList() {
adapter = DevicesAdapter(syncthingActivity)
adapter = DevicesAdapter(context!!)
binding.list.adapter = adapter
binding.list.setOnItemLongClickListener { _, _, position, _ ->
val deviceId = (binding.list.getItemAtPosition(position) as DeviceStats).deviceId
AlertDialog.Builder(syncthingActivity)
.setTitle("remove device " + deviceId.substring(0, 7))
.setMessage("remove device" + deviceId.substring(0, 7) + " from list of known devices?")
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))
.setPositiveButton(android.R.string.yes) { _, _ ->
syncthingActivity.configuration().edit().removePeer(deviceId).persistLater() }
libraryHandler?.configuration { it.edit().removePeer(deviceId).persistLater() }
}
.setNegativeButton(android.R.string.no, null)
.show()
Log.d(TAG, "showFolderListView delete device = '$deviceId'")
@@ -70,9 +69,11 @@ class DevicesFragment : Fragment() {
}
private fun updateDeviceList() {
adapter.clear()
adapter.addAll(syncthingActivity.syncthingClient().devicesHandler.deviceStatsList)
adapter.notifyDataSetChanged()
libraryHandler?.syncthingClient { syncthingClient ->
adapter.clear()
adapter.addAll(syncthingClient.devicesHandler.deviceStatsList)
adapter.notifyDataSetChanged()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
@@ -87,21 +88,25 @@ class DevicesFragment : Fragment() {
}
private fun importDeviceId(deviceId: String) {
try {
KeystoreHandler.validateDeviceId(deviceId)
} catch (e: IllegalArgumentException) {
Toast.makeText(context, R.string.invalid_device_id, Toast.LENGTH_SHORT).show()
return
}
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 = syncthingActivity.configuration().edit().addPeers(DeviceInfo(deviceId, null))
if (modified) {
syncthingActivity.configuration().edit().persistLater()
Toast.makeText(context, "successfully imported device: " + deviceId, Toast.LENGTH_SHORT).show()
updateDeviceList()//TODO remove this if event triggered (and handler trigger update)
UpdateIndexTask(syncthingActivity, syncthingActivity.syncthingClient()).updateIndex()
} else {
Toast.makeText(context, "device already present: " + deviceId, Toast.LENGTH_SHORT).show()
val modified = configuration.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()
}
}
}
}
@@ -1,9 +1,7 @@
package net.syncthing.lite.fragments
import android.content.Intent
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v4.app.Fragment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@@ -14,19 +12,16 @@ import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.beans.FolderStats
import net.syncthing.lite.R
import net.syncthing.lite.activities.FolderBrowserActivity
import net.syncthing.lite.activities.SyncthingActivity
import net.syncthing.lite.adapters.FoldersListAdapter
import net.syncthing.lite.databinding.FragmentFoldersBinding
import org.apache.commons.lang3.tuple.Pair
import org.jetbrains.anko.intentFor
import java.util.*
class FoldersFragment : Fragment() {
class FoldersFragment : SyncthingFragment() {
companion object {
private val TAG = "FoldersFragment"
}
private val TAG = "FoldersFragment"
private lateinit var syncthingActivity: SyncthingActivity
private lateinit var binding: FragmentFoldersBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
@@ -36,24 +31,23 @@ class FoldersFragment : Fragment() {
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
syncthingActivity = activity as SyncthingActivity
override fun onLibraryLoaded() {
showAllFoldersListView()
}
private fun showAllFoldersListView() {
val list = Lists.newArrayList(syncthingActivity.folderBrowser().folderInfoAndStatsList)
Collections.sort(list, Ordering.natural<Comparable<String>>()
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
Log.i(TAG, "list folders = " + list + " (" + list.size + " records")
val adapter = FoldersListAdapter(context!!, list)
binding.list.adapter = adapter
binding.list.setOnItemClickListener { _, _, position, _ ->
val folder = adapter.getItem(position)!!.left.folder
val intent = Intent(context, FolderBrowserActivity::class.java)
intent.putExtra(FolderBrowserActivity.EXTRA_FOLDER_NAME, folder)
startActivity(intent)
libraryHandler?.folderBrowser { folderBrowser ->
val list = Lists.newArrayList(folderBrowser.folderInfoAndStatsList())
Collections.sort(list, Ordering.natural<Comparable<String>>()
.onResultOf<Pair<FolderInfo, FolderStats>> { input -> input?.left?.label })
Log.i(TAG, "list folders = " + list + " (" + list.size + " records)")
val adapter = FoldersListAdapter(context, list)
binding.list.adapter = adapter
binding.list.setOnItemClickListener { _, _, position, _ ->
val folder = adapter.getItem(position)!!.left.folder
val intent = context?.intentFor<FolderBrowserActivity>(FolderBrowserActivity.EXTRA_FOLDER_NAME to folder)
startActivity(intent)
}
}
}
}
@@ -1,31 +0,0 @@
package net.syncthing.lite.fragments
import android.view.View
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import com.nononsenseapps.filepicker.FilePickerFragment
import java.io.File
class MIVFilePickerFragment : FilePickerFragment() {
override fun onClickCheckable(v: View, vh: AbstractFilePickerFragment<File>.CheckableViewHolder) {
// auto open file on click
if (!allowMultiple) {
// Clear is necessary, in case user clicked some checkbox directly
mCheckedItems.clear()
mCheckedItems.add(vh.file)
onClickOk(null)
} else {
super.onClickCheckable(v, vh)
}
}
// private static final String EXTENSION = ".*[.](jpg|png|jpeg)";
override fun isItemVisible(file: File): Boolean {
// return isDir(file) || file.getName().toLowerCase().matches(EXTENSION);
return true
}
}
@@ -0,0 +1,33 @@
package net.syncthing.lite.fragments
import android.os.Bundle
import android.support.v4.app.Fragment
import net.syncthing.lite.library.LibraryHandler
abstract class SyncthingFragment : Fragment() {
var libraryHandler: LibraryHandler? = null
private set
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
LibraryHandler(context!!, this::onLibraryLoadedInternal, this::onIndexUpdateProgress,
this::onIndexUpdateComplete)
}
private fun onLibraryLoadedInternal(libraryHandler: LibraryHandler) {
this.libraryHandler = libraryHandler
onLibraryLoaded()
}
override fun onDestroy() {
super.onDestroy()
libraryHandler?.close()
}
open fun onLibraryLoaded() {}
open fun onIndexUpdateProgress(folder: String, percentage: Int) {}
open fun onIndexUpdateComplete() {}
}
@@ -0,0 +1,106 @@
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.support.annotation.StringRes
import android.support.v4.content.FileProvider
import android.util.Log
import android.webkit.MimeTypeMap
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.newTask
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.io.File
import java.io.IOException
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient,
private val mFileInfo: FileInfo) {
private val TAG = "DownloadFileTask"
private lateinit var progressDialog: ProgressDialog
private var cancelled = false
fun downloadFile() {
showDialog()
mSyncthingClient.pullFile(mFileInfo, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted()) {
if (cancelled)
return@pullFile
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage())
onProgress(observer)
}
val outputFile = File("${mContext.externalCacheDir}/${mFileInfo.folder}/${mFileInfo.path}")
FileUtils.copyInputStreamToFile(observer.inputStream(), outputFile)
Log.i(TAG, "downloaded file = " + mFileInfo.path)
onComplete(outputFile)
} catch (e: IOException) {
onError(R.string.toast_file_download_failed)
Log.w(TAG, "Failed to download file " + mFileInfo, e)
} catch (e: InterruptedException) {
onError(R.string.toast_file_download_failed)
Log.w(TAG, "Failed to download file " + mFileInfo, e)
}
}) { onError(R.string.toast_file_download_failed) }
}
private fun showDialog() {
progressDialog = ProgressDialog(mContext)
progressDialog.setMessage(mContext.getString(R.string.dialog_downloading_file, mFileInfo.fileName))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { cancelled = true }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
doAsync {
uiThread {
progressDialog.isIndeterminate = false
progressDialog.max = (mFileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress() * mFileInfo.size!!).toInt()
}
}
}
private fun onComplete(file: File) {
progressDialog.dismiss()
if (cancelled)
return
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
val uri = FileProvider.getUriForFile(mContext, "net.syncthing.lite.fileprovider", file)
intent.setDataAndType(uri, mimeType)
intent.newTask()
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
mContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(R.string.toast_open_file_failed)
Log.w(TAG, "No handler found for file " + file.name, e)
}
}
private fun onError(@StringRes error: Int) {
doAsync {
uiThread {
progressDialog.dismiss()
mContext.toast(error)
}
}
}
}
@@ -0,0 +1,178 @@
package net.syncthing.lite.library
import android.content.Context
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import 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 org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.io.File
import java.io.IOException
import java.util.*
class LibraryHandler(context: Context, onLibraryLoaded: (LibraryHandler) -> Unit,
onIndexUpdateProgressListener: (String, Int) -> Unit,
onIndexUpdateCompleteListener: () -> Unit) {
companion object {
private var instanceCount = 0
private var configuration: ConfigurationService? = null
private var syncthingClient: SyncthingClient? = null
private var folderBrowser: FolderBrowser? = null
private val callbacks = ArrayList<(ConfigurationService, SyncthingClient, FolderBrowser) -> Unit>()
private var isLoading = false
}
private val TAG = "LibConnectionHandler"
private val onIndexUpdateListener: Any
init {
instanceCount++
if (configuration == null && !isLoading) {
doAsync {
init(context)
//trigger update if last was more than 10mins ago
val lastUpdateMillis = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(UpdateIndexTask.LAST_INDEX_UPDATE_TS_PREF, -1)
val lastUpdateTimeAgo = Date().time - lastUpdateMillis
if (lastUpdateMillis == -1L || lastUpdateTimeAgo > 10 * 60 * 1000) {
Log.d(TAG, "trigger index update, last was " + Date(lastUpdateMillis))
syncthingClient { UpdateIndexTask(context, it).updateIndex() }
}
uiThread {
onLibraryLoaded(this@LibraryHandler)
}
}
} else {
onLibraryLoaded(this)
}
onIndexUpdateListener = object : Any() {
@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)
}
}
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 syncthingClient = SyncthingClient(configuration)
//TODO listen for device events, update device list
val folderBrowser = syncthingClient.indexHandler.newFolderBrowser()
if (instanceCount == 0) {
Log.d(TAG, "All LibraryHandler instances were closed during init")
configuration.close()
syncthingClient.close()
folderBrowser.close()
}
async(UI) {
callbacks.forEach { it(configuration, syncthingClient, folderBrowser) }
}
LibraryHandler.configuration = configuration
LibraryHandler.syncthingClient = syncthingClient
LibraryHandler.folderBrowser = folderBrowser
isLoading = false
}
fun library(callback: (ConfigurationService, SyncthingClient, FolderBrowser) -> Unit) {
val nullCount = listOf(configuration, syncthingClient, folderBrowser).count { it == null }
assert(nullCount == 0 || nullCount == 3, { "Inconsistent library state" })
// https://stackoverflow.com/a/35522422/1837158
fun <T1: Any, T2: Any, T3: Any, R: Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3)->R?): R? {
return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
}
safeLet(configuration, syncthingClient, folderBrowser) { c, s, f ->
callback(c, s, f)
} ?: run {
if (isLoading) {
callbacks.add(callback)
}
}
}
fun syncthingClient(callback: (SyncthingClient) -> Unit) {
library { _, s, _ -> callback(s) }
}
fun configuration(callback: (ConfigurationService) -> Unit) {
library { c, _, _ -> callback(c) }
}
fun folderBrowser(callback: (FolderBrowser) -> Unit) {
library { _, _, f -> callback(f) }
}
/**
* Unregisters index update listener and decreases instance count.
*
* We wait a bit before closing [[syncthingClient]] etc, in case LibraryHandler is opened again
* soon (eg in case of device rotation).
*/
fun close() {
syncthingClient {
try {
it.indexHandler.eventBus.unregister(onIndexUpdateListener)
} catch (e: IllegalArgumentException) {
// ignored, no idea why this is thrown
}
}
instanceCount--
Handler().postDelayed({
Thread {
if (instanceCount == 0) {
folderBrowser?.close()
folderBrowser = null
syncthingClient?.close()
syncthingClient = null
configuration?.close()
configuration = null
}
}.start()
}, 1000)
}
}
@@ -1,28 +1,28 @@
package net.syncthing.lite.utils
package net.syncthing.lite.library
import android.content.Context
import android.os.Handler
import android.preference.PreferenceManager
import android.widget.Toast
import net.syncthing.java.client.SyncthingClient
import net.syncthing.lite.R
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast
import org.jetbrains.anko.uiThread
import java.util.*
class UpdateIndexTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient) {
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(mContext)
private val mMainHandler = Handler()
class UpdateIndexTask(private val androidContext: Context, private val syncthingClient: SyncthingClient) {
private val mPreferences = PreferenceManager.getDefaultSharedPreferences(androidContext)
fun updateIndex() {
if (sIndexUpdateInProgress)
return
sIndexUpdateInProgress = true
mSyncthingClient.updateIndexFromPeers { _, failures ->
syncthingClient.updateIndexFromPeers { _, failures ->
sIndexUpdateInProgress = false
if (failures.isEmpty()) {
showToast(mContext.getString(R.string.toast_index_update_successful))
showToast(androidContext.getString(R.string.toast_index_update_successful))
} else {
showToast(mContext.getString(R.string.toast_index_update_failed, failures.size))
showToast(androidContext.getString(R.string.toast_index_update_failed, failures.size))
}
mPreferences.edit()
.putLong(LAST_INDEX_UPDATE_TS_PREF, Date().time)
@@ -31,7 +31,11 @@ class UpdateIndexTask(private val mContext: Context, private val mSyncthingClien
}
private fun showToast(message: String) {
mMainHandler.post { Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show() }
doAsync {
uiThread {
androidContext.toast(message)
}
}
}
companion object {
@@ -1,15 +1,17 @@
package net.syncthing.lite.utils
package net.syncthing.lite.library
import android.app.ProgressDialog
import android.content.Context
import android.net.Uri
import android.os.Handler
import android.util.Log
import android.widget.Toast
import 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
// TODO: this should be an IntentService with notification
@@ -24,7 +26,6 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
private val fileName = Util.getContentFileName(context, localFile)
private val syncthingPath = PathUtils.buildPath(syncthingSubFolder, fileName)
private val mainHandler = Handler()
private lateinit var mProgressDialog: ProgressDialog
private var mCancelled = false
@@ -37,12 +38,12 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
syncthingClient.pushFile(uploadStream, syncthingFolder, syncthingPath, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted) {
while (!observer.isCompleted()) {
if (mCancelled)
return@pushFile
observer.waitForProgressUpdate()
Log.i(TAG, "upload progress = " + observer.progressMessage)
Log.i(TAG, "upload progress = " + observer.progressMessage())
onProgress(observer)
}
} catch (e: InterruptedException) {
@@ -68,27 +69,35 @@ class UploadFileTask(private val context: Context, private val syncthingClient:
}
private fun onProgress(observer: BlockPusher.FileUploadObserver) {
mainHandler.post {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource.size.toInt()
mProgressDialog.progress = (observer.progress * observer.dataSource.size).toInt()
doAsync {
uiThread {
mProgressDialog.isIndeterminate = false
mProgressDialog.max = observer.dataSource().getSize().toInt()
mProgressDialog.progress = (observer.progress() * observer.dataSource().getSize()).toInt()
}
}
}
private fun onComplete() {
mProgressDialog.dismiss()
if (mCancelled)
return
Log.i(TAG, "Uploaded file $fileName to folder $syncthingFolder:$syncthingPath")
mainHandler.post {
Toast.makeText(context, R.string.toast_upload_complete, Toast.LENGTH_SHORT).show()
onUploadCompleteListener()
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_upload_complete)
onUploadCompleteListener()
}
}
}
private fun onError() {
mProgressDialog.dismiss()
mainHandler.post { Toast.makeText(context, R.string.toast_file_upload_failed, Toast.LENGTH_SHORT).show() }
doAsync {
uiThread {
mProgressDialog.dismiss()
context.toast(R.string.toast_file_upload_failed)
}
}
}
}
@@ -1,106 +0,0 @@
package net.syncthing.lite.utils
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.os.Handler
import android.support.annotation.StringRes
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import net.syncthing.java.bep.BlockPuller
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FileInfo
import net.syncthing.lite.R
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import java.io.File
import java.io.IOException
class DownloadFileTask(private val mContext: Context, private val mSyncthingClient: SyncthingClient, private val mFileInfo: FileInfo) {
private val mMainHandler: Handler = Handler()
private lateinit var progressDialog: ProgressDialog
private var cancelled = false
fun downloadFile() {
showDialog()
// TODO: can just pass FileInfo directly?
Thread {
mSyncthingClient.pullFile(mFileInfo.folder, mFileInfo.path, { observer ->
onProgress(observer)
try {
while (!observer.isCompleted) {
if (cancelled)
return@pullFile
observer.waitForProgressUpdate()
Log.i("pullFile", "download progress = " + observer.progressMessage)
onProgress(observer)
}
val outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val outputFile = File(outputDir, mFileInfo.fileName)
FileUtils.copyInputStreamToFile(observer.inputStream, outputFile)
Log.i(TAG, "downloaded file = " + mFileInfo.path)
onComplete(outputFile)
} catch (e: IOException) {
onError(R.string.toast_file_download_failed)
Log.w(TAG, "Failed to download file " + mFileInfo, e)
} catch (e: InterruptedException) {
onError(R.string.toast_file_download_failed)
Log.w(TAG, "Failed to download file " + mFileInfo, e)
}
}) { onError(R.string.toast_file_download_failed) }
}.start()
}
private fun showDialog() {
progressDialog = ProgressDialog(mContext)
progressDialog.setMessage(mContext.getString(R.string.dialog_downloading_file, mFileInfo.fileName))
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
progressDialog.setCancelable(true)
progressDialog.setOnCancelListener { cancelled = true }
progressDialog.isIndeterminate = true
progressDialog.show()
}
private fun onProgress(fileDownloadObserver: BlockPuller.FileDownloadObserver) {
mMainHandler.post {
progressDialog.isIndeterminate = false
progressDialog.max = (mFileInfo.size as Long).toInt()
progressDialog.progress = (fileDownloadObserver.progress * mFileInfo.size!!).toInt()
}
}
private fun onComplete(file: File) {
progressDialog.dismiss()
if (cancelled)
return
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file.name))
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.fromFile(file), mimeType)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
try {
mContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(R.string.toast_open_file_failed)
Log.w(TAG, "No handler found for file " + file.name, e)
}
}
private fun onError(@StringRes error: Int) {
progressDialog.dismiss()
mMainHandler.post { Toast.makeText(mContext, error, Toast.LENGTH_SHORT).show() }
}
companion object {
private val TAG = "DownloadFileTask"
}
}
@@ -1,4 +1,4 @@
package net.syncthing.lite.fragments
package net.syncthing.lite.utils
import android.content.Intent
import android.support.v4.app.Fragment
@@ -1,84 +0,0 @@
package net.syncthing.lite.utils
import android.content.Context
import android.util.Log
import com.google.common.eventbus.Subscribe
import net.syncthing.java.bep.FolderBrowser
import net.syncthing.java.bep.IndexHandler
import net.syncthing.java.client.SyncthingClient
import net.syncthing.java.core.beans.FolderInfo
import net.syncthing.java.core.configuration.ConfigurationService
import net.syncthing.java.core.security.KeystoreHandler
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.IOException
class LibraryHandler {
private var mOnIndexUpdatedListener: OnIndexUpdatedListener? = null
var configuration: ConfigurationService? = null
private set
var syncthingClient: SyncthingClient? = null
private set
var folderBrowser: FolderBrowser? = null
private set
interface OnIndexUpdatedListener {
fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int)
fun onIndexUpdateComplete()
}
fun init(context: Context) {
configuration = ConfigurationService.newLoader()
.setCache(File(context.externalCacheDir, "cache"))
.setDatabase(File(context.getExternalFilesDir(null), "database"))
.loadFrom(File(context.getExternalFilesDir(null), "config.properties"))
configuration!!.edit().setDeviceName(Util.getDeviceName())
try {
FileUtils.cleanDirectory(configuration!!.temp)
} catch (ex: IOException) {
Log.e(TAG, "error", ex)
destroy()
}
KeystoreHandler.newLoader().loadAndStore(configuration!!)
configuration!!.edit().persistLater()
Log.i(TAG, "loaded mConfiguration = " + configuration!!.newWriter().dumpToString())
Log.i(TAG, "storage space = " + configuration!!.storageInfo.dumpAvailableSpace())
syncthingClient = net.syncthing.java.client.SyncthingClient(configuration!!)
//TODO listen for device events, update device list
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
}
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
mOnIndexUpdatedListener = onIndexUpdatedListener
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
@Subscribe
fun handleIndexRecordAquiredEvent(event: IndexHandler.IndexRecordAquiredEvent) {
val folder = syncthingClient!!.indexHandler.getFolderInfo(event.folder)
val indexInfo = event.indexInfo
event.newRecords.size
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
mOnIndexUpdatedListener!!.onIndexUpdateProgress(folder, (indexInfo.completed * 100).toInt())
}
@Subscribe
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
}
})
}
fun destroy() {
folderBrowser!!.close()
syncthingClient!!.close()
configuration!!.close()
}
companion object {
private val TAG = "LibConnectionHandler"
}
}
@@ -22,7 +22,7 @@ object Util {
capitalize(manufacturer) + " " + model
}
return deviceName ?: "android"
}
}
fun getContentFileName(context: Context, contentUri: Uri): String {
var fileName = File(contentUri.lastPathSegment).name
@@ -19,7 +19,7 @@
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/main_index_progress_bar"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
@@ -27,9 +27,10 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"
android:paddingStart="12dp"/>
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/main_index_progress_bar_label"
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
@@ -47,12 +48,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/main_folder_and_files_list_view"
android:id="@+id/list_view"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<TextView
android:id="@+id/main_list_view_empty_element"
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
@@ -60,6 +62,13 @@
android:text="@string/folder_list_empty_message"
android:textSize="20sp"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone"/>
<!--main list view END-->
</LinearLayout>
@@ -0,0 +1,22 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/directory_empty" />
</FrameLayout>
</LinearLayout>
+2 -2
View File
@@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/main_index_progress_bar"
android:id="@+id/index_update"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
@@ -22,7 +22,7 @@
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
<TextView
android:id="@+id/main_index_progress_bar_label"
android:id="@+id/index_update_label"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
@@ -0,0 +1,13 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:gravity="center_vertical"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:minHeight="48dp" />
</layout>
+3 -3
View File
@@ -4,14 +4,14 @@
<item
android:id="@+id/folders"
android:checked="true"
android:icon="@drawable/ic_folder_gray_24dp"
android:title="Folders"
android:checked="true"/>
android:title="@string/folders_label" />
<item
android:id="@+id/devices"
android:icon="@drawable/ic_laptop_gray_24dp"
android:title="Devices" />
android:title="@string/devices_label" />
<item
android:id="@+id/update_index"
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="index_update_progress_message">Index wird aktualisiert...</string>
<string name="folder_list_empty_message">Keine Ordner verfügbar</string>
<string name="clear_local_cache_index_label">Lokalen Index und Cache löschen</string>
<string name="update_remote_index_label">Index aktualisieren</string>
<string name="devices_list_view_empty_message">Keine Geräte verfügbar</string>
<string name="toast_write_storage_permission_required">Schreibrechte werden für diese Funktion benötigt</string>
<string name="scan_qr_code">QR code scannen</string>
<string name="enter_device_id">Geräte ID eingeben</string>
<string name="invalid_device_id">Ungültige Geräte ID</string>
<string name="device_id_dialog_title">Geräte ID eingeben</string>
<string name="toast_index_update_successful">Index erfolgreich aktualisiert</string>
<string name="toast_index_update_failed">Index update für %1$d Geräte fehlgeschlagen</string>
<string name="dialog_downloading_file">Datei %1$s wird heruntergeladen</string>
<string name="toast_file_download_failed">Datei konnte nicht heruntergeladen werden</string>
<string name="toast_open_file_failed">Keine kompatible app gefunden</string>
<string name="toast_file_upload_failed">Hochladen gescheitert</string>
<string name="toast_upload_complete">Hochladen erfolgreich</string>
<string name="dialog_uploading_file">Datei %1$s wird hochgeladen</string>
<string name="directory_empty">Ordner ist leer</string>
<string name="clear_cache_and_index_title">Lokalen Cache und Index löschen?</string>
<string name="clear_cache_and_index_body">Gesamten lokalen Cache und Index löschen?</string>
<string name="index_update_folder">Index aktualisierung, Ordner</string>
<string name="index_update_percent_synchronized">% synchronisiert</string>
<string name="loading_config_starting_syncthing_client">Konfiguartion wird geladen, Syncthing wird gestartet</string>
<string name="last_modified">- zuletzt modifiziert</string>
<string name="last_modified_unknown">zuletzt modifiziert: unbekannt</string>
<string name="last_modified_known">zuletzt modifiziert:</string>
<string name="remove_device_title">Gerät entfernen:</string>
<string name="remove_device_body_1">Gerät</string>
<string name="remove_device_body_2">von den bekannten Geräten entfernen?</string>
<string name="device_import_success">Gerät erfolgreich importiert:</string>
<string name="device_already_known">Gerät ist bereits bekannt:</string>
<string name="folders_label">Ordner</string>
<string name="devices_label">Geräte</string>
</resources>
+22 -6
View File
@@ -1,10 +1,10 @@
<resources>
<string name="app_name">Syncthing Lite</string>
<string name="index_update_progress_message">index update…</string>
<string name="folder_list_empty_message">no folder available</string>
<string name="clear_local_cache_index_label">clear local cache/index</string>
<string name="update_remote_index_label">update remote index</string>
<string name="devices_list_view_empty_message">no devices available</string>
<string name="app_name" translatable="false">Syncthing Lite</string>
<string name="index_update_progress_message">Index update…</string>
<string name="folder_list_empty_message">No folder available</string>
<string name="clear_local_cache_index_label">Clear local cache/index</string>
<string name="update_remote_index_label">Update remote index</string>
<string name="devices_list_view_empty_message">No devices available</string>
<string name="toast_write_storage_permission_required">Write storage permission is required for this functionality</string>
<string name="scan_qr_code">Scan QR code</string>
<string name="enter_device_id">Enter device ID</string>
@@ -18,4 +18,20 @@
<string name="toast_file_upload_failed">File upload failed</string>
<string name="toast_upload_complete">File upload complete</string>
<string name="dialog_uploading_file">Uploading file %1$s</string>
<string name="directory_empty">Directory is empty</string>
<string name="clear_cache_and_index_title">Clear local cache and index?</string>
<string name="clear_cache_and_index_body">Clear all local cache data and index data?</string>
<string name="index_update_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="folders_label">Folders</string>
<string name="devices_label">Devices</string>
</resources>
-13
View File
@@ -6,17 +6,4 @@
<item name="colorAccent">@color/accent</item>
</style>
<style name="FilePickerTheme" parent="NNF_BaseTheme">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path name="files" path="/" />
</paths>
+1
View File
@@ -4,6 +4,7 @@ buildscript {
ext.kotlin_version = '1.2.0'
ext.support_version = '27.0.2'
ext.build_tools_version = '3.0.1'
ext.anko_version = '0.10.4'
repositories {
mavenLocal()
jcenter()

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -e
NEW_VERSION_NAME=$1
OLD_VERSION_NAME=$(grep "versionName" "app/build.gradle" | awk '{print $2}')
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
-----------------------------
"
./gradlew clean lintVitalRelease
echo "
Updating Version
-----------------------------
"
OLD_VERSION_CODE=$(grep "versionCode" "app/build.gradle" -m 1 | awk '{print $2}')
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
sed -i "s/versionCode $OLD_VERSION_CODE/versionCode $NEW_VERSION_CODE/" "app/build.gradle"
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 "
Update ready.
"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -e
version=$(git describe --tags)
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
if [[ ! ${version} =~ $regex ]]
then
echo "Current commit is not a release"
exit;
fi
echo "
Pushing to Github
-----------------------------
"
git push
git push --tags
echo "
Push to Google Play
-----------------------------
"
read -s -p "Enter signing password: " password
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
# Upload apk and listing to Google Play
SIGNING_PASSWORD=${password} ./gradlew publishRelease
echo "
Release published!
"