aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorLena <lena@omega>2026-06-01 00:00:00 +0000
committerLena <lena@omega>2026-06-01 00:00:00 +0000
commit72e37d2dac501faa300ca895c56c53eea43ae4ae (patch)
treeca1a3f77d46f91f14b9c713f193af755f3d1fa2f /app
parent7e04941bccb2683f8a6e3ee38a99c50129234dd1 (diff)
downloadrsend-72e37d2dac501faa300ca895c56c53eea43ae4ae.tar.gz
app: add a folder picker for local paths0.1.0
Typing absolute paths is error-prone; let the user browse real filesystem folders and pick one. Stays on real paths (no SAF) so rsync keeps operating on POSIX paths.
Diffstat (limited to 'app')
-rw-r--r--app/src/main/AndroidManifest.xml4
-rw-r--r--app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt21
-rw-r--r--app/src/main/java/invalid/lena/rsend/FolderPickerActivity.kt89
-rw-r--r--app/src/main/res/layout/activity_folder.xml26
-rw-r--r--app/src/main/res/layout/activity_picker.xml61
-rw-r--r--app/src/main/res/layout/item_dir.xml27
6 files changed, 223 insertions, 5 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 31c8b72..e146f29 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,6 +38,10 @@
android:exported="false" />
<activity
+ android:name=".FolderPickerActivity"
+ android:exported="false" />
+
+ <activity
android:name=".ScheduleActivity"
android:exported="false" />
diff --git a/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt b/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt
index 2fe2b1b..c61276c 100644
--- a/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt
+++ b/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt
@@ -1,11 +1,14 @@
package invalid.lena.rsend
+import android.app.Activity
+import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.Switch
import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import java.io.File
@@ -20,6 +23,17 @@ class FolderEditActivity : AppCompatActivity() {
private lateinit var excludes: EditText
private lateinit var delete: Switch
+ // The picker returns a real filesystem path; drop it into the local field and
+ // name the folder after its basename if the name is still blank.
+ private val pickFolder =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val path = result.data?.getStringExtra("path") ?: return@registerForActivityResult
+ local.setText(path)
+ if (name.text.isNullOrBlank()) name.setText(File(path).name)
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_folder)
@@ -43,6 +57,13 @@ class FolderEditActivity : AppCompatActivity() {
delete.isChecked = f.delete
}
+ findViewById<Button>(R.id.browse).setOnClickListener {
+ pickFolder.launch(
+ Intent(this, FolderPickerActivity::class.java)
+ .putExtra("start", local.text.toString().trim())
+ )
+ }
+
findViewById<Button>(R.id.save).setOnClickListener { saveFolder(); finish() }
val del = findViewById<Button>(R.id.removeFolder)
del.visibility = if (index < 0) View.GONE else View.VISIBLE
diff --git a/app/src/main/java/invalid/lena/rsend/FolderPickerActivity.kt b/app/src/main/java/invalid/lena/rsend/FolderPickerActivity.kt
new file mode 100644
index 0000000..1adccf3
--- /dev/null
+++ b/app/src/main/java/invalid/lena/rsend/FolderPickerActivity.kt
@@ -0,0 +1,89 @@
+package invalid.lena.rsend
+
+import android.content.Intent
+import android.os.Bundle
+import android.os.Environment
+import android.os.storage.StorageManager
+import android.widget.Button
+import android.widget.TextView
+import androidx.activity.addCallback
+import androidx.appcompat.app.AppCompatActivity
+import java.io.File
+
+// FolderPickerActivity browses the real filesystem and returns one directory's
+// absolute path in the "path" result extra. The app holds all-files access, so
+// rsync works on real paths; this picker stays on real paths too (no SAF, no
+// content URIs). Browsing is rooted at primary shared storage and cannot go
+// above it; SD cards and other volumes are still reachable by typing the path.
+class FolderPickerActivity : AppCompatActivity() {
+
+ private lateinit var pathView: TextView
+ private lateinit var list: android.widget.LinearLayout
+ private lateinit var root: File
+ private lateinit var current: File
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_picker)
+ setSupportActionBar(findViewById(R.id.toolbar))
+ title = "Pick folder"
+ fitSystemBars()
+ pathView = findViewById(R.id.currentPath)
+ list = findViewById(R.id.list)
+
+ root = getSystemService(StorageManager::class.java).primaryStorageVolume.directory
+ ?: Environment.getExternalStorageDirectory()
+
+ // Start where the field already points, if that is a directory under the
+ // root; otherwise at the root itself.
+ val start = intent.getStringExtra("start")?.let { File(it) }
+ current = if (start != null && start.isDirectory &&
+ start.absolutePath.startsWith(root.absolutePath)) start else root
+
+ findViewById<Button>(R.id.use).setOnClickListener {
+ setResult(RESULT_OK, Intent().putExtra("path", current.absolutePath))
+ finish()
+ }
+ findViewById<Button>(R.id.cancel).setOnClickListener { finish() }
+
+ // Hardware back walks up the tree until the root, then leaves the screen.
+ onBackPressedDispatcher.addCallback(this) {
+ if (current.absolutePath != root.absolutePath) {
+ current = current.parentFile ?: root
+ render()
+ } else {
+ isEnabled = false
+ onBackPressedDispatcher.onBackPressed()
+ }
+ }
+
+ render()
+ }
+
+ private fun render() {
+ pathView.text = current.absolutePath
+ list.removeAllViews()
+
+ if (current.absolutePath != root.absolutePath && current.parentFile != null) {
+ addRow("..", current.parentFile!!)
+ }
+
+ val dirs = current.listFiles { f -> f.isDirectory }
+ ?.sortedBy { it.name.lowercase() } ?: emptyList()
+ for (d in dirs) addRow(d.name, d)
+
+ if (dirs.isEmpty()) {
+ list.addView(TextView(this).apply {
+ text = "No subfolders here. Tap \"Use this folder\" to pick it."
+ setTextAppearance(R.style.TextAppearance_Rsend_Caption)
+ })
+ }
+ }
+
+ private fun addRow(label: String, dir: File) {
+ val row = layoutInflater.inflate(R.layout.item_dir, list, false)
+ row.findViewById<TextView>(R.id.dirName).text = label
+ row.setOnClickListener { current = dir; render() }
+ list.addView(row)
+ }
+}
diff --git a/app/src/main/res/layout/activity_folder.xml b/app/src/main/res/layout/activity_folder.xml
index 1a15f00..1381529 100644
--- a/app/src/main/res/layout/activity_folder.xml
+++ b/app/src/main/res/layout/activity_folder.xml
@@ -46,14 +46,30 @@
android:textAppearance="@style/TextAppearance.Rsend.Caption"
android:text="Local path" />
- <EditText
- android:id="@+id/local"
- style="@style/Widget.Rsend.EditText"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
- android:inputType="textUri"
- android:hint="/storage/emulated/0/DCIM" />
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <EditText
+ android:id="@+id/local"
+ style="@style/Widget.Rsend.EditText"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:inputType="textUri"
+ android:hint="/storage/emulated/0/DCIM" />
+
+ <Button
+ android:id="@+id/browse"
+ style="@style/Widget.Rsend.Button.Secondary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/space_s"
+ android:text="Browse" />
+ </LinearLayout>
<TextView
android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/activity_picker.xml b/app/src/main/res/layout/activity_picker.xml
new file mode 100644
index 0000000..db4a044
--- /dev/null
+++ b/app/src/main/res/layout/activity_picker.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/appRoot"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include layout="@layout/app_toolbar" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:padding="@dimen/space_m">
+
+ <TextView
+ android:id="@+id/currentPath"
+ style="@style/Widget.Rsend.Card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.Rsend.Mono"
+ android:textSize="12sp" />
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <LinearLayout
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+ </ScrollView>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginTop="@dimen/space_m">
+
+ <Button
+ android:id="@+id/use"
+ style="@style/Widget.Rsend.Button.Primary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginEnd="@dimen/space_s"
+ android:text="Use this folder" />
+
+ <Button
+ android:id="@+id/cancel"
+ style="@style/Widget.Rsend.Button.Secondary"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Cancel" />
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/app/src/main/res/layout/item_dir.xml b/app/src/main/res/layout/item_dir.xml
new file mode 100644
index 0000000..651224c
--- /dev/null
+++ b/app/src/main/res/layout/item_dir.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Widget.Rsend.Card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:foreground="?attr/selectableItemBackground">
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="@dimen/space_m"
+ android:src="@drawable/ic_folder"
+ android:contentDescription="@null" />
+
+ <TextView
+ android:id="@+id/dirName"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.Rsend.Title" />
+</LinearLayout>