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以上)的文件权限吧。