From 7a2a28b6137d84db18bd46f98629fab099d9a111 Mon Sep 17 00:00:00 2001 From: tobexyz <40026159+tobexyz@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:19:04 +0200 Subject: [PATCH 01/71] chore: set release to next develop iteration --- yaacc/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yaacc/src/main/AndroidManifest.xml b/yaacc/src/main/AndroidManifest.xml index 2d48edeb..25db2d13 100644 --- a/yaacc/src/main/AndroidManifest.xml +++ b/yaacc/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="40500" + android:versionName="4.5.0-SNAPSHOT"> From a4823db8524bf826d88f4f30d7fa80ad07508cd1 Mon Sep 17 00:00:00 2001 From: sr093906 Date: Sat, 18 Oct 2025 18:51:03 +0800 Subject: [PATCH 02/71] Add files via upload --- yaacc/src/main/res/values-zh/setting_strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yaacc/src/main/res/values-zh/setting_strings.xml b/yaacc/src/main/res/values-zh/setting_strings.xml index f50d7460..d4510f5a 100644 --- a/yaacc/src/main/res/values-zh/setting_strings.xml +++ b/yaacc/src/main/res/values-zh/setting_strings.xml @@ -70,7 +70,7 @@ 停用了媒体代理 激活了媒体代理 媒体存储过滤 - 媒体商店过滤已停用 + 媒体存储过滤已停用 媒体存储过滤已激活 本地服务器使用测试内容 媒体提供方来源 From 4d253f8e3045beca5d377575fd85ceb8999cfb92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:11:57 +0000 Subject: [PATCH 03/71] Bump uri from 0.13.2 to 0.13.3 in /docs Bumps [uri](https://github.com/ruby/uri) from 0.13.2 to 0.13.3. - [Release notes](https://github.com/ruby/uri/releases) - [Commits](https://github.com/ruby/uri/compare/v0.13.2...v0.13.3) --- updated-dependencies: - dependency-name: uri dependency-version: 0.13.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index b2018cfc..0dfadc42 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -258,7 +258,7 @@ GEM unf_ext unf_ext (0.0.9.1) unicode-display_width (1.8.0) - uri (0.13.2) + uri (0.13.3) webrick (1.8.2) PLATFORMS From 4eba3c23e351d2336373f1e997579d5890781127 Mon Sep 17 00:00:00 2001 From: tobexyz <40026159+tobexyz@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:39:09 +0100 Subject: [PATCH 04/71] issue #151 init --- build.gradle | 4 +- yaacc/build.gradle | 5 +- .../java/de/yaacc/upnp/server/TreeNode.java | 9 +- .../de/yaacc/upnp/server/TreeViewHolder.java | 23 ++-- .../YaaccUpnpServerControlActivity.java | 130 +++++++++++++++--- 5 files changed, 138 insertions(+), 33 deletions(-) diff --git a/build.gradle b/build.gradle index f8bc8faa..c31c54a5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,20 @@ allprojects { buildscript { repositories { - jcenter() google() mavenLocal() maven { url "https://plugins.gradle.org/m2/" } + mavenCentral() } } repositories { - jcenter() google() mavenLocal() + mavenCentral() } diff --git a/yaacc/build.gradle b/yaacc/build.gradle index 7357cf29..06628218 100644 --- a/yaacc/build.gradle +++ b/yaacc/build.gradle @@ -1,7 +1,7 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.android.tools.build:gradle:8.13.1' } } @@ -25,7 +25,8 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' //https://developer.android.com/jetpack/androidx/migrate/artifact-mappings - + //FIXMEimplementation 'androidx.documentfile:documentfile:1.1.0' + //https://medium.com/swlh/sample-for-android-storage-access-framework-aka-scoped-storage-for-basic-use-cases-3ee4fee404fc } android { diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java b/yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java index 9edeeb10..4037345e 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java @@ -19,7 +19,6 @@ package de.yaacc.upnp.server; -import java.io.File; import java.util.LinkedList; /** @@ -27,7 +26,7 @@ */ public class TreeNode { - private File value; + private Object value; private TreeNode parent; private LinkedList children; private int layoutId; @@ -35,7 +34,7 @@ public class TreeNode { private boolean isExpanded; private boolean isSelected; - public TreeNode(File value, int layoutId) { + public TreeNode(Object value, int layoutId) { this.value = value; this.parent = null; this.children = new LinkedList<>(); @@ -52,11 +51,11 @@ public void addChild(TreeNode child) { updateNodeChildrenDepth(child); } - public void setValue(File value) { + public void setValue(Object value) { this.value = value; } - public File getValue() { + public Object getValue() { return value; } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java b/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java index a9a65a25..0e2a60e1 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java @@ -27,8 +27,10 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; import androidx.recyclerview.widget.RecyclerView; +import java.io.File; import java.util.Set; import de.yaacc.R; @@ -67,21 +69,24 @@ public void bindTreeNode(TreeNode node) { itemView.getPaddingRight(), itemView.getPaddingBottom()); - - fileName.setText(node.getValue().getName()); + String name = node.getValue() instanceof File ? ((File) node.getValue()).getName() : ((DocumentFile) node.getValue()).getName(); + fileName.setText(name); fileCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { Set pathes = MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()); + String absolutePath = node.getValue() instanceof File ? ((File) node.getValue()).getAbsolutePath() : ((DocumentFile) node.getValue()).getUri().toString(); if (isChecked) { - pathes.add(node.getValue().getAbsolutePath()); + pathes.add(absolutePath); } else { - pathes.remove(node.getValue().getAbsolutePath()); + pathes.remove(absolutePath); } MediaPathFilter.saveMediaPaths(fileCheckbox.getContext(), pathes); }); - if (node.getValue() != null && node.getValue().isDirectory()) { + //FIXME + if (node.getValue() != null && node.getValue() instanceof File && ((File) node.getValue()).isDirectory()) { Drawable icon = ThemeHelper.tintDrawable(fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_folder_open_48), fileTypeIcon.getContext().getTheme()); fileTypeIcon.setImageDrawable(icon); - fileCheckbox.setChecked(MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()).contains(node.getValue().getAbsolutePath())); + String absolutePath = node.getValue() instanceof File ? ((File) node.getValue()).getAbsolutePath() : ((DocumentFile) node.getValue()).getUri().toString(); + fileCheckbox.setChecked(MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()).contains(absolutePath)); fileCheckbox.setVisibility(View.VISIBLE); } else { fileCheckbox.setVisibility(View.INVISIBLE); @@ -103,8 +108,10 @@ public void bindTreeNode(TreeNode node) { fileName.setTextColor(typedValue.data); } fileStateIcon.setVisibility(View.INVISIBLE); - if (node.getValue() != null && node.getValue().isDirectory()) { - if (node.getValue().listFiles() != null && node.getValue().listFiles().length > 0) { + //FIXME + if (node.getValue() != null && node.getValue() instanceof File && ((File) node.getValue()).isDirectory()) { + File file = (File) node.getValue(); + if (file.listFiles() != null && file.listFiles().length > 0) { fileStateIcon.setVisibility(View.VISIBLE); int stateIcon = node.isExpanded() ? R.drawable.sharp_keyboard_arrow_down_24 : R.drawable.sharp_chevron_right_24; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java index 3ca2e528..8d65327d 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.util.Log; @@ -32,6 +33,7 @@ import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.documentfile.provider.DocumentFile; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -40,7 +42,9 @@ import java.io.File; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import de.yaacc.R; import de.yaacc.settings.SettingsActivity; @@ -56,6 +60,8 @@ */ public class YaaccUpnpServerControlActivity extends AppCompatActivity { + private static final int REQUEST_CODE_OPEN_DOCUMENT_TREE = 1001; + private TreeViewAdapter treeViewAdapter; @Override protected void onCreate(Bundle savedInstanceState) { @@ -126,7 +132,7 @@ protected void onCreate(Bundle savedInstanceState) { recyclerView.setBackgroundColor(typedValue.data); TreeViewHolderFactory factory = (v, layout) -> new TreeViewHolder(v); - TreeViewAdapter treeViewAdapter = new TreeViewAdapter(factory); + treeViewAdapter = new TreeViewAdapter(factory); recyclerView.setAdapter(treeViewAdapter); buildFileSystemTree(treeViewAdapter); } @@ -162,26 +168,73 @@ private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { if (fileRoots.isEmpty()) { Log.w(getClass().getName(), "No file system roots found or storage unavailable. Adding a placeholder."); } +// --- SAF: lade persistierte Tree URIs aus SharedPreferences und füge als DocumentFile Wurzeln hinzu --- + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + Set safUris = prefs.getStringSet("saf_tree_uris", new HashSet<>()); + if (safUris != null) { + for (String uriString : safUris) { + try { + Uri uri = Uri.parse(uriString); + DocumentFile docRoot = DocumentFile.fromTreeUri(this, uri); + if (docRoot != null && docRoot.exists()) { + TreeNode node = buildFileSystemNode(docRoot, R.layout.file_list_item); + if (node != null) { + fileRoots.add(node); + } + } else { + Log.w(getClass().getName(), "SAF root not accessible: " + uriString); + } + } catch (Exception e) { + Log.e(getClass().getName(), "Error restoring SAF uri: " + uriString, e); + } + } + } + // Wenn keine Wurzeln gefunden wurden, starte SAF-Picker, damit Benutzer USB/externen Pfad wählen kann + if (fileRoots.isEmpty()) { + Log.w(getClass().getName(), "No file system roots found or storage unavailable. Starting SAF picker."); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); + } treeViewAdapter.updateTreeNodes(fileRoots); treeViewAdapter.setTreeNodeClickListener((treeNode, nodeView) -> { Log.d(getClass().getName(), "Click on TreeNode with value " + treeNode.getValue().toString()); - File file = treeNode.getValue(); - if (file.isDirectory() && file.listFiles() != null && treeNode.getChildren().size() != file.listFiles().length) { - File[] children = file.listFiles(); - if (children != null) { - for (File childFile : children) { - TreeNode childNode = buildFileSystemNode(childFile, treeNode.getLayoutId()); - if (childNode != null) { - treeNode.addChild(childNode); + Object value = treeNode.getValue(); + if (value instanceof File) { + File file = (File) value; + + if (file.isDirectory() && file.listFiles() != null && treeNode.getChildren().size() != file.listFiles().length) { + File[] children = file.listFiles(); + if (children != null) { + for (File childFile : children) { + TreeNode childNode = buildFileSystemNode(childFile, treeNode.getLayoutId()); + if (childNode != null) { + treeNode.addChild(childNode); + } + } + } + } + Log.d(getClass().getName(), "Clicked on file: " + file.getAbsolutePath()); + } else if (value instanceof DocumentFile) { + DocumentFile doc = (DocumentFile) value; + if (doc.isDirectory()) { + DocumentFile[] children = doc.listFiles(); + if (children != null && treeNode.getChildren().size() != children.length) { + for (DocumentFile childDoc : children) { + TreeNode childNode = buildFileSystemNode(childDoc, treeNode.getLayoutId()); + if (childNode != null) { + treeNode.addChild(childNode); + } } } } + Log.d(getClass().getName(), "Clicked on document file: " + doc.getUri()); } - Log.d(getClass().getName(), "Clicked on file: " + file.getAbsolutePath()); - }); treeViewAdapter.setTreeNodeLongClickListener((treeNode, nodeView) -> { @@ -191,19 +244,32 @@ private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { } /** - * Recursively builds a TreeNode structure from the file system. + * Recursively builds a TreeNode structure from the file system or a DocumentFile. * - * @param file The current file or directory. + * @param fileObj The current File or DocumentFile. * @param layoutId The layout resource ID for the TreeNode. * @return A TreeNode representing the file/directory, or null if it should be skipped. */ - private TreeNode buildFileSystemNode(File file, int layoutId) { - if (file == null || !file.exists()) { + private TreeNode buildFileSystemNode(Object fileObj, int layoutId) { + if (fileObj == null) { return null; } - return new TreeNode(file, layoutId); + if (fileObj instanceof File) { + File file = (File) fileObj; + if (!file.exists()) { + return null; + } + return new TreeNode(file, layoutId); + } else if (fileObj instanceof DocumentFile) { + DocumentFile doc = (DocumentFile) fileObj; + if (!doc.exists()) { + return null; + } + return new TreeNode(doc, layoutId); + } + return null; } @@ -230,6 +296,38 @@ private void stop() { editor.apply(); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE && resultCode == RESULT_OK) { + if (data != null) { + Uri treeUri = data.getData(); + if (treeUri != null) { + try { + final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + getContentResolver().takePersistableUriPermission(treeUri, takeFlags); + } catch (Exception e) { + Log.w(getClass().getName(), "Could not take persistable uri permission", e); + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + Set uriSet = prefs.getStringSet("saf_tree_uris", new HashSet<>()); + if (uriSet == null) { + uriSet = new HashSet<>(); + } else { + uriSet = new HashSet<>(uriSet); + } + uriSet.add(treeUri.toString()); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet("saf_tree_uris", uriSet); + editor.apply(); + + // rebuild tree with newly added SAF root + buildFileSystemTree(treeViewAdapter); + } + } + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { From 4b0d08b2976cf5d87ce4a0b951f2324c9fd13f34 Mon Sep 17 00:00:00 2001 From: tobexyz <40026159+tobexyz@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:49:22 +0100 Subject: [PATCH 05/71] issue #151 allow selection of saf sources --- .../de/yaacc/upnp/server/TreeViewHolder.java | 87 ++++++++++++--- .../YaaccUpnpServerControlActivity.java | 105 +++++++++--------- .../contentdirectory/MediaPathFilter.java | 46 ++++++++ .../res/drawable/ic_baseline_bookmark_24.xml | 5 + .../res/drawable/ic_baseline_bookmark_32.xml | 5 + .../res/drawable/ic_baseline_bookmark_48.xml | 5 + .../activity_yaacc_upnp_server_control.xml | 10 ++ .../activity_yaacc_upnp_server_control.xml | 10 +- yaacc/src/main/res/values-de/strings.xml | 1 + yaacc/src/main/res/values-es/strings.xml | 1 + yaacc/src/main/res/values-fr/strings.xml | 1 + yaacc/src/main/res/values-nl/strings.xml | 1 + yaacc/src/main/res/values-pt/strings.xml | 1 + yaacc/src/main/res/values-zh/strings.xml | 1 + yaacc/src/main/res/values/setting_strings.xml | 2 + yaacc/src/main/res/values/strings.xml | 1 + 16 files changed, 217 insertions(+), 65 deletions(-) create mode 100644 yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml create mode 100644 yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml create mode 100644 yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java b/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java index 0e2a60e1..d651d6f4 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java @@ -27,6 +27,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; import androidx.recyclerview.widget.RecyclerView; @@ -69,24 +70,34 @@ public void bindTreeNode(TreeNode node) { itemView.getPaddingRight(), itemView.getPaddingBottom()); - String name = node.getValue() instanceof File ? ((File) node.getValue()).getName() : ((DocumentFile) node.getValue()).getName(); + String name = getName(node); fileName.setText(name); fileCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - Set pathes = MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()); - String absolutePath = node.getValue() instanceof File ? ((File) node.getValue()).getAbsolutePath() : ((DocumentFile) node.getValue()).getUri().toString(); + Set pathes; + String absolutePath = getAbsolutePath(node); + if (isSafNode(node)) { + pathes = MediaPathFilter.getSelectedSafPathes(fileCheckbox.getContext()); + } else { + pathes = MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()); + } if (isChecked) { pathes.add(absolutePath); } else { pathes.remove(absolutePath); } - MediaPathFilter.saveMediaPaths(fileCheckbox.getContext(), pathes); + if (isSafNode(node)) { + MediaPathFilter.saveSelectedSafPathes(fileCheckbox.getContext(), pathes); + } else { + MediaPathFilter.saveMediaPaths(fileCheckbox.getContext(), pathes); + } }); - //FIXME - if (node.getValue() != null && node.getValue() instanceof File && ((File) node.getValue()).isDirectory()) { - Drawable icon = ThemeHelper.tintDrawable(fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_folder_open_48), fileTypeIcon.getContext().getTheme()); + + if (isDirectory(node)) { + Drawable icon = isSafNode(node) ? fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_bookmark_48) : fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_folder_open_48); + icon = ThemeHelper.tintDrawable(icon, fileTypeIcon.getContext().getTheme()); fileTypeIcon.setImageDrawable(icon); - String absolutePath = node.getValue() instanceof File ? ((File) node.getValue()).getAbsolutePath() : ((DocumentFile) node.getValue()).getUri().toString(); - fileCheckbox.setChecked(MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()).contains(absolutePath)); + String absolutePath = getAbsolutePath(node); + fileCheckbox.setChecked(isSelected(absolutePath)); fileCheckbox.setVisibility(View.VISIBLE); } else { fileCheckbox.setVisibility(View.INVISIBLE); @@ -108,12 +119,10 @@ public void bindTreeNode(TreeNode node) { fileName.setTextColor(typedValue.data); } fileStateIcon.setVisibility(View.INVISIBLE); - //FIXME - if (node.getValue() != null && node.getValue() instanceof File && ((File) node.getValue()).isDirectory()) { - File file = (File) node.getValue(); - if (file.listFiles() != null && file.listFiles().length > 0) { - fileStateIcon.setVisibility(View.VISIBLE); + if (isDirectory(node)) { + if (isDirectoryNotEmpty(node)) { + fileStateIcon.setVisibility(View.VISIBLE); int stateIcon = node.isExpanded() ? R.drawable.sharp_keyboard_arrow_down_24 : R.drawable.sharp_chevron_right_24; Drawable icon = ThemeHelper.tintDrawable(fileStateIcon.getContext().getDrawable(stateIcon), fileStateIcon.getContext().getTheme()); fileStateIcon.setImageDrawable(icon); @@ -121,4 +130,54 @@ public void bindTreeNode(TreeNode node) { } } + private static boolean isSafNode(TreeNode node) { + return node.getValue() instanceof DocumentFile; + } + + private boolean isSelected(String absolutePath) { + return MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()).contains(absolutePath) + || MediaPathFilter.getSelectedSafPathes(fileCheckbox.getContext()).contains(absolutePath); + } + + @NonNull + private static String getAbsolutePath(TreeNode node) { + return node.getValue() instanceof File ? ((File) node.getValue()).getAbsolutePath() : ((DocumentFile) node.getValue()).getUri().toString(); + } + + private static boolean isDirectoryNotEmpty(TreeNode node) { + if (node.getValue() != null) { + if (node.getValue() instanceof File) { + File[] elements = ((File) node.getValue()).listFiles(); + return elements != null && elements.length > 0; + } else if (node.getValue() instanceof DocumentFile) { + DocumentFile[] elements = ((DocumentFile) node.getValue()).listFiles(); + return elements.length > 0; + } + } + return true; + } + + private static boolean isDirectory(TreeNode node) { + if (node.getValue() != null) { + if (node.getValue() instanceof File) { + return ((File) node.getValue()).isDirectory(); + } else if (node.getValue() instanceof DocumentFile) { + return ((DocumentFile) node.getValue()).isDirectory(); + } + } + return false; + } + + @Nullable + private static String getName(TreeNode node) { + if (isSafNode(node)) { + String result = ((DocumentFile) node.getValue()).getName(); + if (result == null) { + result = ((DocumentFile) node.getValue()).getUri().toString(); + } + return result; + } + return ((File) node.getValue()).getName(); + } + } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java index 8d65327d..cc0d9cd3 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java @@ -117,7 +117,15 @@ protected void onCreate(Bundle savedInstanceState) { } })); Button resetButton = findViewById(R.id.sharedFoldersReset); - resetButton.setOnClickListener(v -> MediaPathFilter.resetMediaPaths(getApplicationContext())); + resetButton.setOnClickListener(v -> { + MediaPathFilter.resetMediaPaths(getApplicationContext()); + MediaPathFilter.resetSelectedSafPathes(getApplicationContext()); + buildFileSystemTree(treeViewAdapter); + } + ); + + Button safButton = findViewById(R.id.sharedFoldersAddSaf); + safButton.setOnClickListener(v -> selectSafContent()); TextView localServerControlInterface = findViewById(R.id.localServerControlInterface); String[] ipConfig = YaaccUpnpServerService.getIfAndIpAddress(this); @@ -137,6 +145,14 @@ protected void onCreate(Bundle savedInstanceState) { buildFileSystemTree(treeViewAdapter); } + private void selectSafContent() { + Log.w(getClass().getName(), "No file system roots found or storage unavailable. Starting SAF picker."); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); + } private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { List fileRoots = new ArrayList<>(); @@ -168,9 +184,8 @@ private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { if (fileRoots.isEmpty()) { Log.w(getClass().getName(), "No file system roots found or storage unavailable. Adding a placeholder."); } -// --- SAF: lade persistierte Tree URIs aus SharedPreferences und füge als DocumentFile Wurzeln hinzu --- - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - Set safUris = prefs.getStringSet("saf_tree_uris", new HashSet<>()); + + Set safUris = MediaPathFilter.getSafPathes(getApplicationContext()); if (safUris != null) { for (String uriString : safUris) { try { @@ -189,16 +204,6 @@ private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { } } } - - // Wenn keine Wurzeln gefunden wurden, starte SAF-Picker, damit Benutzer USB/externen Pfad wählen kann - if (fileRoots.isEmpty()) { - Log.w(getClass().getName(), "No file system roots found or storage unavailable. Starting SAF picker."); - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); - } treeViewAdapter.updateTreeNodes(fileRoots); @@ -206,41 +211,43 @@ private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { Log.d(getClass().getName(), "Click on TreeNode with value " + treeNode.getValue().toString()); Object value = treeNode.getValue(); if (value instanceof File) { - File file = (File) value; - - if (file.isDirectory() && file.listFiles() != null && treeNode.getChildren().size() != file.listFiles().length) { - File[] children = file.listFiles(); - if (children != null) { - for (File childFile : children) { - TreeNode childNode = buildFileSystemNode(childFile, treeNode.getLayoutId()); - if (childNode != null) { - treeNode.addChild(childNode); - } - } - } - } - Log.d(getClass().getName(), "Clicked on file: " + file.getAbsolutePath()); + clickedOnFile(treeNode, (File) value); } else if (value instanceof DocumentFile) { - DocumentFile doc = (DocumentFile) value; - if (doc.isDirectory()) { - DocumentFile[] children = doc.listFiles(); - if (children != null && treeNode.getChildren().size() != children.length) { - for (DocumentFile childDoc : children) { - TreeNode childNode = buildFileSystemNode(childDoc, treeNode.getLayoutId()); - if (childNode != null) { - treeNode.addChild(childNode); - } - } + clickedOnDocument(treeNode, (DocumentFile) value); + } + }); + } + + private void clickedOnDocument(TreeNode treeNode, DocumentFile value) { + DocumentFile doc = value; + if (doc.isDirectory()) { + DocumentFile[] children = doc.listFiles(); + if (children != null && treeNode.getChildren().size() != children.length) { + for (DocumentFile childDoc : children) { + TreeNode childNode = buildFileSystemNode(childDoc, treeNode.getLayoutId()); + if (childNode != null) { + treeNode.addChild(childNode); } } - Log.d(getClass().getName(), "Clicked on document file: " + doc.getUri()); } - }); + } + Log.d(getClass().getName(), "Clicked on document file: " + doc.getUri()); + } - treeViewAdapter.setTreeNodeLongClickListener((treeNode, nodeView) -> { - Log.d(getClass().getName(), "LongClick on TreeNode with value " + treeNode.getValue().toString()); - return true; - }); + private void clickedOnFile(TreeNode treeNode, File value) { + File file = value; + if (file.isDirectory() && file.listFiles() != null && treeNode.getChildren().size() != file.listFiles().length) { + File[] children = file.listFiles(); + if (children != null) { + for (File childFile : children) { + TreeNode childNode = buildFileSystemNode(childFile, treeNode.getLayoutId()); + if (childNode != null) { + treeNode.addChild(childNode); + } + } + } + } + Log.d(getClass().getName(), "Clicked on file: " + file.getAbsolutePath()); } /** @@ -304,22 +311,20 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { Uri treeUri = data.getData(); if (treeUri != null) { try { - final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION); getContentResolver().takePersistableUriPermission(treeUri, takeFlags); } catch (Exception e) { Log.w(getClass().getName(), "Could not take persistable uri permission", e); } - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - Set uriSet = prefs.getStringSet("saf_tree_uris", new HashSet<>()); + + Set uriSet = MediaPathFilter.getSafPathes(getApplicationContext()); if (uriSet == null) { uriSet = new HashSet<>(); } else { uriSet = new HashSet<>(uriSet); } uriSet.add(treeUri.toString()); - SharedPreferences.Editor editor = prefs.edit(); - editor.putStringSet("saf_tree_uris", uriSet); - editor.apply(); + MediaPathFilter.saveSafPathes(getApplicationContext(), uriSet); // rebuild tree with newly added SAF root buildFileSystemTree(treeViewAdapter); diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java index e0d75e13..d597d271 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java @@ -61,4 +61,50 @@ public static void resetMediaPaths(Context context) { editor.putStringSet(context.getString(R.string.settings_media_paths_pref_key), new HashSet<>()); editor.apply(); } + + public static Set getSafPathes(Context context) { + Set paths = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(context.getString(R.string.settings_saf_tree_uris_pref_key), new HashSet<>()); + if (paths == null) { + return new HashSet<>(); + } + return new HashSet<>(paths); + } + + public static void saveSafPathes(Context context, Set newPathes) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_pref_key), newPathes); + editor.apply(); + } + + + public static void resetSafPathes(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_pref_key), new HashSet<>()); + editor.apply(); + } + + public static Set getSelectedSafPathes(Context context) { + Set paths = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(context.getString(R.string.settings_saf_tree_uris_selected_pref_key), new HashSet<>()); + if (paths == null) { + return new HashSet<>(); + } + return new HashSet<>(paths); + } + + public static void saveSelectedSafPathes(Context context, Set newPaths) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_selected_pref_key), newPaths); + editor.apply(); + } + + + public static void resetSelectedSafPathes(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_selected_pref_key), new HashSet<>()); + editor.apply(); + } } diff --git a/yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml b/yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml new file mode 100644 index 00000000..d728c8f4 --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml b/yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml new file mode 100644 index 00000000..c78827bf --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml b/yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml new file mode 100644 index 00000000..3c4fbc0d --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml b/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml index 0ee8e1e4..334c3d04 100644 --- a/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml +++ b/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml @@ -91,6 +91,16 @@ android:layout_below="@+id/filterEnabled" android:text="@string/reset_shared_folders" android:textAppearance="@android:style/TextAppearance.Material.Medium" /> + +