使用Beeware开发文件浏览器获取Android15的文件权限

python 复制代码
"""
My last app
"""
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW
import os
from java import jclass

class Android15FileBrowserFixed(toga.App):
    def startup(self):
        self.main_window = toga.MainWindow(title="文件浏览器", size=(400, 600))
        
        # 权限相关变量
        self.has_media_permissions = False
        self.has_all_files_permission = False
        self.permission_requested = False
        
        # 可能的根目录路径
        self.possible_roots = [
            "/storage/emulated/0/DCIM",
            "/storage/emulated/0/Pictures",
            "/storage/emulated/0/Music",
            "/storage/emulated/0/Movies",
            "/storage/emulated/0/Download",
            "/storage/emulated/0/Documents",
        ]
        
        # 当前路径和选中文件
        self.current_path = self.get_app_private_dir()
        self.selected_file = None
        
        # UI 组件
        self.path_label = toga.Label(
            f"路径: {self.current_path}",
            style=Pack(padding=10, font_size=12)
        )
        
        self.status_label = toga.Label(
            "正在检查权限...",
            style=Pack(padding=10, font_size=14, color="blue")
        )
        
        self.selected_path_label = toga.Label(
            "未选择文件",
            style=Pack(padding=10, font_size=14, color="green")
        )
        
        self.permission_label = toga.Label(
            "权限状态: 检查中...",
            style=Pack(padding=5, font_size=12, color="red")
        )
        
        # 文件列表 - 使用更简单的实现
        self.file_list = toga.Table(
            headings=['名称', '类型', '大小'],
            data=[],
            style=Pack(flex=1, padding=10),
            on_select=self.on_file_select
        )
        
        # 权限请求按钮
        self.media_permission_button = toga.Button(
            '请求媒体权限',
            on_press=self.request_media_permissions,
            style=Pack(padding=5, flex=1)
        )
        
        self.all_files_permission_button = toga.Button(
            '请求所有文件权限',
            on_press=self.request_all_files_permission,
            style=Pack(padding=5, flex=1)
        )
        
        permission_button_box = toga.Box(
            children=[
                self.media_permission_button,
                self.all_files_permission_button
            ],
            style=Pack(direction=ROW, padding=5)
        )
        
        # 快速访问按钮
        quick_access_box = toga.Box(
            children=[
                toga.Button('照片', on_press=self.browse_images, style=Pack(padding=5, flex=1)),
                toga.Button('下载', on_press=self.browse_downloads, style=Pack(padding=5, flex=1)),
                toga.Button('音频', on_press=self.browse_audio, style=Pack(padding=5, flex=1)),
            ],
            style=Pack(direction=ROW, padding=5)
        )
        
        # 导航按钮
        nav_button_box = toga.Box(
            children=[
                toga.Button('上级目录', on_press=self.go_up, style=Pack(padding=5, flex=1)),
                toga.Button('刷新', on_press=self.refresh_list, style=Pack(padding=5, flex=1)),
                toga.Button('根目录', on_press=self.go_to_root, style=Pack(padding=5, flex=1)),
            ],
            style=Pack(direction=ROW, padding=10)
        )
        
        # 文件选择按钮
        select_button_box = toga.Box(
            children=[
                toga.Button('选择文件', on_press=self.confirm_selection, style=Pack(padding=10, width=200)),
            ],
            style=Pack(direction=ROW, padding=10, alignment="center")
        )
        
        main_box = toga.Box(
            children=[
                self.path_label,
                self.status_label,
                self.selected_path_label,
                self.permission_label,
                permission_button_box,
                quick_access_box,
                self.file_list,
                nav_button_box,
                select_button_box
            ],
            style=Pack(direction=COLUMN, padding=10, flex=1)
        )
        
        self.main_window.content = main_box
        self.main_window.show()
        
        # 应用启动时检查权限
        self.check_permissions_and_refresh()
    
    def get_app_private_dir(self):
        """获取应用私有目录"""
        try:
            Python = jclass('com.chaquo.python.Python')
            context = Python.getPlatform().getApplication()
            files_dir = context.getFilesDir()
            return files_dir.getAbsolutePath() if files_dir else "/data/data/com.example.fileturn/files"
        except Exception as e:
            print(f"获取应用私有目录失败: {e}")
            return "/data/data/com.example.fileturn/files"
    
    def check_permissions_and_refresh(self):
        """检查权限并刷新文件列表"""
        self.check_all_permissions()
        self.refresh_list(None)
    
    def check_all_permissions(self):
        """检查所有权限状态"""
        try:
            # 检查媒体权限
            self.has_media_permissions = self.has_media_permissions_check()
            
            # 检查所有文件权限
            self.has_all_files_permission = self.has_all_files_permission_check()
            
            # 更新权限标签
            status_parts = []
            if self.has_all_files_permission:
                status_parts.append("所有文件权限: ✅")
            else:
                status_parts.append("所有文件权限: ❌")
                
            if self.has_media_permissions:
                status_parts.append("媒体权限: ✅")
            else:
                status_parts.append("媒体权限: ❌")
                
            self.permission_label.text = " | ".join(status_parts)
            
            # 更新按钮状态
            self.media_permission_button.enabled = not self.has_media_permissions
            self.all_files_permission_button.enabled = not self.has_all_files_permission
            
            if self.has_all_files_permission:
                self.media_permission_button.enabled = False
                self.all_files_permission_button.text = "已有所有文件权限"
                
        except Exception as e:
            print(f"检查权限失败: {e}")
            self.permission_label.text = "检查权限时出错"
    
    def has_media_permissions_check(self):
        """检查是否有媒体权限"""
        try:
            Python = jclass('com.chaquo.python.Python')
            Context = jclass('android.content.Context')
            ActivityCompat = jclass('androidx.core.app.ActivityCompat')
            PackageManager = jclass('android.content.pm.PackageManager')
            
            context = Python.getPlatform().getApplication()
            
            # Android 13+ 媒体权限
            media_permissions = [
                "android.permission.READ_MEDIA_IMAGES",
                "android.permission.READ_MEDIA_VIDEO",
                "android.permission.READ_MEDIA_AUDIO"
            ]
            
            for permission in media_permissions:
                if ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED:
                    return False
            return True
        except Exception as e:
            print(f"检查媒体权限失败: {e}")
            return False
    
    def has_all_files_permission_check(self):
        """检查是否有所有文件访问权限"""
        try:
            Environment = jclass('android.os.Environment')
            return Environment.isExternalStorageManager()
        except Exception as e:
            print(f"检查所有文件权限失败: {e}")
            return False
    
    def request_media_permissions(self, widget):
        """请求媒体权限"""
        try:
            Python = jclass('com.chaquo.python.Python')
            Context = jclass('android.content.Context')
            ActivityCompat = jclass('androidx.core.app.ActivityCompat')
            
            # 获取应用上下文
            context = Python.getPlatform().getApplication()
            
            # 尝试获取 Activity
            try:
                ActivityThread = jclass('android.app.ActivityThread')
                current_activity = ActivityThread.currentActivity()
                
                if current_activity:
                    # 请求媒体权限
                    permissions = [
                        "android.permission.READ_MEDIA_IMAGES",
                        "android.permission.READ_MEDIA_VIDEO", 
                        "android.permission.READ_MEDIA_AUDIO"
                    ]
                    ActivityCompat.requestPermissions(current_activity, permissions, 1001)
                    self.permission_requested = True
                    self.status_label.text = "已请求媒体权限,请允许权限"
                    return
            except Exception as e:
                print(f"通过 ActivityThread 获取 Activity 失败: {e}")
            
            # 如果无法获取 Activity,跳转到设置
            Intent = jclass('android.content.Intent')
            Settings = jclass('android.provider.Settings')
            
            intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
            uri = jclass('android.net.Uri').fromParts("package", context.getPackageName(), None)
            intent.setData(uri)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            
            context.startActivity(intent)
            self.status_label.text = "请在设置中授予媒体权限"
            
        except Exception as e:
            print(f"请求媒体权限失败: {e}")
            self.status_label.text = f"请求权限失败: {str(e)}"
    
    def request_all_files_permission(self, widget):
        """请求所有文件访问权限"""
        try:
            Python = jclass('com.chaquo.python.Python')
            Context = jclass('android.content.Context')
            Intent = jclass('android.content.Intent')
            Settings = jclass('android.provider.Settings')
            Uri = jclass('android.net.Uri')
            
            # 获取应用上下文
            context = Python.getPlatform().getApplication()
            
            # 跳转到所有文件访问权限设置页面
            intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
            uri = Uri.fromParts("package", context.getPackageName(), None)
            intent.setData(uri)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            
            context.startActivity(intent)
            self.status_label.text = "请在设置中授予所有文件访问权限"
            
        except Exception as e:
            print(f"请求所有文件权限失败: {e}")
            self.status_label.text = f"请求权限失败: {str(e)}"
    
    def refresh_list(self, widget):
        """刷新文件列表"""
        try:
            # 检查权限状态
            self.check_all_permissions()
            
            # 检查当前路径是否可访问
            if not self.is_path_accessible(self.current_path):
                # 如果当前路径不可访问,回退到应用私有目录
                self.current_path = self.get_app_private_dir()
                self.path_label.text = f"路径: {self.current_path}"
            
            entries = []
            
            # 添加上级目录(如果不是根目录)
            if (self.current_path != "/" and 
                self.current_path != self.get_app_private_dir() and
                os.path.dirname(self.current_path) != self.current_path):
                
                entries.append(("..", "目录", ""))
            
            # 获取目录内容
            try:
                items = os.listdir(self.current_path)
                for item in items:
                    full_path = os.path.join(self.current_path, item)
                    
                    if os.path.isdir(full_path):
                        entries.append((item, "目录", ""))
                    else:
                        try:
                            size = os.path.getsize(full_path)
                            size_str = self.format_size(size)
                            entries.append((item, "文件", size_str))
                        except:
                            entries.append((item, "文件", "未知"))
                
            except PermissionError:
                self.status_label.text = "无权限访问此目录"
                # 回退到应用私有目录
                self.current_path = self.get_app_private_dir()
                self.path_label.text = f"路径: {self.current_path}"
                self.refresh_list(None)
                return
            except Exception as e:
                self.status_label.text = f"读取目录错误: {str(e)}"
                return
            
            # 按类型排序:目录在前,文件在后
            directories = [e for e in entries if "目录" in e[1]]
            files = [e for e in entries if "文件" in e[1]]
            
            # 按名称排序
            directories.sort(key=lambda x: x[0].lower())
            files.sort(key=lambda x: x[0].lower())
            
            self.file_list.data = directories + files
            
            dir_count = len(directories) - (1 if ".." in [d[0] for d in directories] else 0)
            file_count = len(files)
            
            self.status_label.text = f"找到 {dir_count} 个目录, {file_count} 个文件"
            
        except Exception as e:
            self.status_label.text = f"刷新列表错误: {str(e)}"
    
    def is_path_accessible(self, path):
        """检查路径是否可访问"""
        if path == self.get_app_private_dir():
            return True
            
        if not os.path.exists(path):
            return False
            
        # 检查权限
        if path.startswith(self.get_app_private_dir()):
            return True
            
        if self.has_all_files_permission:
            return True
            
        if self.has_media_permissions:
            # 检查是否是媒体文件目录
            media_dirs = [
                "/storage/emulated/0/DCIM",
                "/storage/emulated/0/Pictures",
                "/storage/emulated/0/Music",
                "/storage/emulated/0/Movies"
            ]
            for media_dir in media_dirs:
                if path.startswith(media_dir):
                    return True
                    
        # 下载目录通常有更宽松的权限
        if path.startswith("/storage/emulated/0/Download"):
            return True
            
        return False
    
    def on_file_select(self, widget):
        """处理文件/目录选择 - 使用您提供的可用的实现"""
        # 获取选中的行
        if widget.selection:
            # 获取 Row 对象中的数据
            row = widget.selection
            # 通过属性名访问列数据
            entry_name = getattr(row, "名称", None)
            entry_type = getattr(row, "类型", None)
            
            if entry_name is None or entry_type is None:
                # 如果通过属性名访问失败,尝试其他方法
                try:
                    # 尝试使用 values 属性
                    if hasattr(row, 'values') and len(row.values) >= 2:
                        entry_name = row.values[0]
                        entry_type = row.values[1]
                    else:
                        # 如果以上方法都失败,使用字符串表示
                        row_str = str(row)
                        if ".." in row_str:
                            entry_name = ".."
                            entry_type = "目录"
                        else:
                            # 无法确定具体值,返回
                            return
                except:
                    # 如果所有方法都失败,返回
                    return
            
            if entry_name == "..":
                # 导航到上级目录
                self.go_up(None)
            elif entry_type == "目录":
                # 进入子目录
                self.current_path = os.path.join(self.current_path, entry_name)
                self.path_label.text = f"当前路径: {self.current_path}"
                self.refresh_list(None)
                self.selected_file = None
                self.selected_path_label.text = "未选择文件"
            else:
                # 选择文件
                self.selected_file = os.path.join(self.current_path, entry_name)
                self.selected_path_label.text = f"已选择: {entry_name}"
    
    def go_up(self, widget):
        """返回上级目录"""
        try:
            parent_dir = os.path.dirname(self.current_path)
            
            # 确保不会退到不可访问的目录
            if parent_dir and self.is_path_accessible(parent_dir):
                self.current_path = parent_dir
                self.path_label.text = f"路径: {self.current_path}"
                self.refresh_list(None)
                self.selected_file = None
                self.selected_path_label.text = "未选择文件"
            else:
                self.status_label.text = "已经是根目录或无法访问上级目录"
        except Exception as e:
            print(f"返回上级目录时出错: {e}")
            self.status_label.text = f"返回上级目录错误: {str(e)}"
    
    def go_to_root(self, widget):
        """返回可访问的根目录"""
        accessible_roots = self.get_accessible_roots()
        if accessible_roots:
            self.current_path = accessible_roots[0]
            self.path_label.text = f"路径: {self.current_path}"
            self.refresh_list(None)
            self.selected_file = None
            self.selected_path_label.text = "未选择文件"
            self.status_label.text = "已切换到可访问的根目录"
        else:
            self.status_label.text = "无可访问的根目录"
    
    def get_accessible_roots(self):
        """获取所有可访问的根目录"""
        accessible_roots = [self.get_app_private_dir()]
        
        if self.has_all_files_permission:
            # 如果有所有文件权限,尝试所有可能的根目录
            for root in self.possible_roots:
                if os.path.exists(root) and self.is_path_accessible(root):
                    accessible_roots.append(root)
        elif self.has_media_permissions:
            # 如果只有媒体权限,只添加媒体目录
            media_roots = [
                "/storage/emulated/0/DCIM",
                "/storage/emulated/0/Pictures",
                "/storage/emulated/0/Music",
                "/storage/emulated/0/Movies",
                "/storage/emulated/0/Download"  # 下载目录通常也可访问
            ]
            for root in media_roots:
                if os.path.exists(root) and self.is_path_accessible(root):
                    accessible_roots.append(root)
        
        return accessible_roots
    
    def browse_images(self, widget):
        """浏览图片文件"""
        if not self.has_media_permissions and not self.has_all_files_permission:
            self.status_label.text = "需要媒体权限或所有文件权限"
            return
        
        image_dirs = [
            "/storage/emulated/0/DCIM",
            "/storage/emulated/0/Pictures"
        ]
        
        for directory in image_dirs:
            if os.path.exists(directory) and self.is_path_accessible(directory):
                self.current_path = directory
                self.path_label.text = f"路径: {self.current_path}"
                self.refresh_list(None)
                self.selected_file = None
                self.selected_path_label.text = "未选择文件"
                self.status_label.text = "已切换到图片目录"
                return
        
        self.status_label.text = "无法访问图片目录"
    
    def browse_downloads(self, widget):
        """浏览下载文件夹"""
        # 下载文件夹通常有更宽松的权限要求
        # 在 Android 10+ 上,即使没有完整存储权限,通常也能访问下载文件夹
        
        download_dirs = [
            "/storage/emulated/0/Download",
            "/sdcard/Download"
        ]
        
        for directory in download_dirs:
            if os.path.exists(directory):
                # 尝试访问下载目录
                try:
                    # 测试是否可访问
                    test_items = os.listdir(directory)
                    
                    self.current_path = directory
                    self.path_label.text = f"路径: {self.current_path}"
                    self.refresh_list(None)
                    self.selected_file = None
                    self.selected_path_label.text = "未选择文件"
                    self.status_label.text = "已切换到下载目录"
                    return
                    
                except (PermissionError, OSError) as e:
                    print(f"无法访问下载目录 {directory}: {e}")
                    continue
        
        # 如果没有找到可访问的下载目录
        if not self.has_media_permissions and not self.has_all_files_permission:
            self.status_label.text = "无法访问下载目录,请先请求权限"
        else:
            self.status_label.text = "无法找到或访问下载目录"
    
    def browse_audio(self, widget):
        """浏览音频文件"""
        if not self.has_media_permissions and not self.has_all_files_permission:
            self.status_label.text = "需要媒体权限或所有文件权限"
            return
        
        audio_dirs = [
            "/storage/emulated/0/Music",
            "/storage/emulated/0/Notifications",
            "/storage/emulated/0/Ringtones"
        ]
        
        for directory in audio_dirs:
            if os.path.exists(directory) and self.is_path_accessible(directory):
                self.current_path = directory
                self.path_label.text = f"路径: {self.current_path}"
                self.refresh_list(None)
                self.selected_file = None
                self.selected_path_label.text = "未选择文件"
                self.status_label.text = "已切换到音频目录"
                return
        
        self.status_label.text = "无法访问音频目录"
    
    def confirm_selection(self, widget):
        """确认文件选择"""
        if self.selected_file:
            self.selected_path_label.text = f"最终选择: {self.selected_file}"
            # 这里可以添加处理选中文件的逻辑
            print(f"用户选择了文件: {self.selected_file}")
            
            # 在实际应用中,你可以在这里添加文件处理代码
            # 例如:读取文件内容、复制文件、上传文件等
        else:
            self.selected_path_label.text = "请先选择一个文件"
    
    def format_size(self, size):
        """格式化文件大小"""
        if size == 0:
            return "0 B"
        
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size < 1024.0:
                return f"{size:.1f} {unit}"
            size /= 1024.0
        return f"{size:.1f} TB"

def main():
    return Android15FileBrowserFixed("Android 15 文件浏览器", "com.example.android15filebrowser")

if __name__ == '__main__':
    app = main()
    app.main_loop()

上面代码与deepseek聊了一天,才生成的。

briefcase create android后,修改AndroidManifest.xml内容

XML 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.fileturn">
    
    <!-- 传统存储权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    
    <!-- Android 13+ 媒体权限 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    
    <!-- 所有文件访问权限 -->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    
    <application
        android:requestLegacyExternalStorage="true"
        ...>
    </application>
</manifest>

然后再briefcase build android

生成app-debug.apk,复制到手机就能用,主要还是获取安卓15(12以上)的文件权限吧。

相关推荐
柒柒钏2 小时前
VSCode 终端配置与 Python 虚拟环境使用指南
ide·vscode·python
环己酮2 小时前
py数据科学学习笔记day4-空间数据统计分析与可视化(2)
python
q***48253 小时前
基于python语言的网页设计(手把手教你设计一个个人博客网站)
开发语言·python
qq_22589174663 小时前
基于Python+Django餐饮评论大数据分析与智能推荐系统 毕业论文
开发语言·后端·python·信息可视化·数据分析·django
FreakStudio3 小时前
串口协议解析实战:以 R60ABD1 雷达为例,详解 MicroPython 驱动中数据与业务逻辑的分离设计
python·单片机·pycharm·嵌入式·面向对象·硬件·电子diy
南山安3 小时前
让 LLM 与外界对话:使用 Function Calling 实现天气查询工具
人工智能·后端·python
用户12039112947264 小时前
打破信息壁垒:手把手教你实现DeepSeek大模型的天气查询功能
python·openai
鱼骨不是鱼翅4 小时前
力扣hot100----1day
python·算法·leetcode·职场和发展
2501_941236214 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python