Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3495784f7 | |||
| 82c92c9031 | |||
| bd5d89c158 | |||
| 9cf96d86dd | |||
| f4c1e6a0f0 | |||
| 036d3846bc | |||
| 6696f0ff88 | |||
| ffff6a7617 | |||
| 3bd1f6e44a | |||
| a2f46b358d | |||
| a0e749e879 | |||
| e566b3c58d | |||
| acff8c1f5c | |||
| 4353fc5597 | |||
| 64b2b7424b | |||
| 2fa0dd2e59 | |||
| 2e1fa76b44 | |||
| f447e1ecad | |||
| ee9098b4f1 | |||
| f378329da3 | |||
| 0caec11826 |
@@ -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
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
+14
-10
@@ -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 {
|
||||
+25
-16
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package net.syncthing.lite.fragments
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.Fragment
|
||||
@@ -1,84 +0,0 @@
|
||||
package net.syncthing.lite.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.common.eventbus.Subscribe
|
||||
import net.syncthing.java.bep.FolderBrowser
|
||||
import net.syncthing.java.bep.IndexHandler
|
||||
import net.syncthing.java.client.SyncthingClient
|
||||
import net.syncthing.java.core.beans.FolderInfo
|
||||
import net.syncthing.java.core.configuration.ConfigurationService
|
||||
import net.syncthing.java.core.security.KeystoreHandler
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class LibraryHandler {
|
||||
|
||||
private var mOnIndexUpdatedListener: OnIndexUpdatedListener? = null
|
||||
var configuration: ConfigurationService? = null
|
||||
private set
|
||||
var syncthingClient: SyncthingClient? = null
|
||||
private set
|
||||
var folderBrowser: FolderBrowser? = null
|
||||
private set
|
||||
|
||||
interface OnIndexUpdatedListener {
|
||||
fun onIndexUpdateProgress(folder: FolderInfo, percentage: Int)
|
||||
fun onIndexUpdateComplete()
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
configuration = ConfigurationService.newLoader()
|
||||
.setCache(File(context.externalCacheDir, "cache"))
|
||||
.setDatabase(File(context.getExternalFilesDir(null), "database"))
|
||||
.loadFrom(File(context.getExternalFilesDir(null), "config.properties"))
|
||||
configuration!!.edit().setDeviceName(Util.getDeviceName())
|
||||
try {
|
||||
FileUtils.cleanDirectory(configuration!!.temp)
|
||||
} catch (ex: IOException) {
|
||||
Log.e(TAG, "error", ex)
|
||||
destroy()
|
||||
}
|
||||
|
||||
KeystoreHandler.newLoader().loadAndStore(configuration!!)
|
||||
configuration!!.edit().persistLater()
|
||||
Log.i(TAG, "loaded mConfiguration = " + configuration!!.newWriter().dumpToString())
|
||||
Log.i(TAG, "storage space = " + configuration!!.storageInfo.dumpAvailableSpace())
|
||||
syncthingClient = net.syncthing.java.client.SyncthingClient(configuration!!)
|
||||
//TODO listen for device events, update device list
|
||||
folderBrowser = syncthingClient!!.indexHandler.newFolderBrowser()
|
||||
}
|
||||
|
||||
fun setOnIndexUpdatedListener(onIndexUpdatedListener: OnIndexUpdatedListener) {
|
||||
mOnIndexUpdatedListener = onIndexUpdatedListener
|
||||
syncthingClient!!.indexHandler.eventBus.register(object : Any() {
|
||||
|
||||
@Subscribe
|
||||
fun handleIndexRecordAquiredEvent(event: IndexHandler.IndexRecordAquiredEvent) {
|
||||
val folder = syncthingClient!!.indexHandler.getFolderInfo(event.folder)
|
||||
val indexInfo = event.indexInfo
|
||||
event.newRecords.size
|
||||
Log.i(TAG, "handleIndexRecordEvent trigger folder list update from index record acquired")
|
||||
mOnIndexUpdatedListener!!.onIndexUpdateProgress(folder, (indexInfo.completed * 100).toInt())
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun handleRemoteIndexAquiredEvent(event: IndexHandler.FullIndexAquiredEvent) {
|
||||
Log.i(TAG, "handleIndexAquiredEvent trigger folder list update from index acquired")
|
||||
mOnIndexUpdatedListener!!.onIndexUpdateComplete()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
folderBrowser!!.close()
|
||||
syncthingClient!!.close()
|
||||
configuration!!.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = "LibConnectionHandler"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-cache-path name="files" path="/" />
|
||||
</paths>
|
||||
@@ -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 |
Executable
+39
@@ -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.
|
||||
"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
version=$(git describe --tags)
|
||||
regex='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
if [[ ! ${version} =~ $regex ]]
|
||||
then
|
||||
echo "Current commit is not a release"
|
||||
exit;
|
||||
fi
|
||||
|
||||
echo "
|
||||
|
||||
Pushing to Github
|
||||
-----------------------------
|
||||
"
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
echo "
|
||||
|
||||
Push to Google Play
|
||||
-----------------------------
|
||||
"
|
||||
|
||||
read -s -p "Enter signing password: " password
|
||||
|
||||
SIGNING_PASSWORD=${password} ./gradlew assembleRelease
|
||||
|
||||
# Upload apk and listing to Google Play
|
||||
SIGNING_PASSWORD=${password} ./gradlew publishRelease
|
||||
|
||||
echo "
|
||||
|
||||
Release published!
|
||||
"
|
||||
Reference in New Issue
Block a user