前几天需要快速开发一个小程序,开始使用android_studio开发,奈何学的时候已经是4年多之前,一看现在的开发完全变成了陌生的样子。
先用deepseek在python代码上修改,快速搭了一个基于compose的框架,能运行,但是生成的图片在python验证会出错。故想着使用python来开发,不想在kotlin去引入opencv库(教训:早知道还是应该使用Android_studio进行原生开发,起码资料多)。
开发流程
- 选择开发框架,百度了一下,发现有kivy,beeware,flet等,当时看见beeware有中文文档就选择beeware,哪知道这是炼狱的开始。
- 按照官网教程快速搭建了框架BeeWare教程,来到教程五,稍微花了一点时间去部署环境,这里需要科学上网。完成此步骤并将apk安装到真机上
- 界面好做,功能难写,官网基本没有安卓开发的参考,这是我的界面代码,没有参考意义
def startup(self): # 主窗口 main_box = toga.Box(style=Pack(direction=COLUMN, padding=10)) # UUID 输入 uuid_label = toga.Label('UUID:', style=Pack(padding=(0, 5))) self.uuid_input = toga.TextInput(placeholder='请输入UUID', style=Pack(flex=1)) uuid_box = toga.Box(style=Pack(direction=ROW, padding=5)) uuid_box.add(uuid_label) uuid_box.add(self.uuid_input) main_box.add(uuid_box) # 生成随机UUID按钮 self.random_uuid_button = toga.Button( '生成随机UUID', on_press=self.generate_random_uuid, style=Pack(padding=5) ) main_box.add(self.random_uuid_button) # 日期选择 date_label = toga.Label('截止日期:', style=Pack(padding=(0, 5))) self.date_input = toga.DateInput(style=Pack(flex=1)) date_box = toga.Box(style=Pack(direction=ROW, padding=5)) date_box.add(date_label) date_box.add(self.date_input) main_box.add(date_box) # 时间选择 time_label = toga.Label('截止时间:', style=Pack(padding=(0, 5))) self.time_input = toga.TimeInput(style=Pack(flex=1)) time_box = toga.Box(style=Pack(direction=ROW, padding=5)) time_box.add(time_label) time_box.add(self.time_input) main_box.add(time_box) # 时间快捷按钮 self.create_time_shortcuts(main_box) # 输出路径选择按钮 self.output_button = toga.Button( '选择输出路径', on_press=self.select_output_path, style=Pack(padding=5) ) main_box.add(self.output_button) # 生成按钮 self.generate_button = toga.Button( '生成密钥文件', on_press=self.generate_key_file, style=Pack(padding=5) ) main_box.add(self.generate_button) # 状态标签 self.status_label = toga.Label('准备就绪', style=Pack(padding=5)) main_box.add(self.status_label) # 设置主窗口内容 self.main_window = toga.MainWindow(title=self.formal_name) self.main_window.content = main_box self.main_window.show()
-
这个天杀的Google把安卓13之后文件读写权限搞得特别复杂,难以申请到写文件权限,而Beeware也没有示例可以参考,在安卓上完全不像windows上能容易的获取文件路径。通过我不懈查找,找到了能快速写文件的方法,通过Intent来开启,这是Google的java代码,
// Request code for creating a PDF document. private static final int CREATE_FILE = 1; private void createFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf"); // Optionally, specify a URI for the directory that should be opened in // the system file picker when your app creates the document. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, CREATE_FILE); }
但是beeware该怎么获取?摸索了半天找到一个开启电话的intent示例,but这个android.content是那里的python库。我找了一会也没有查找到,有大佬知道的可以说一下
from android.content import Intent from android.net import Uri intent = Intent(Intent.ACTION_DIAL) intent.setData(Uri.parse("tel:0123456789")) def number_dialed(result, data): # result is the status code (e.g., Activity.RESULT_OK) # data is the value returned by the activity. ... # Assuming your toga.App app instance is called `app` app._impl.start_activity(intent, on_complete=number_dialed)
那么我就尝试修改相关intent参数,构造了请求,我需要写入的是图片,所有Type设置png,设置了一下Flag,但是好像不需要,因为我设置的是读权限(懒得改了)在回调中拿捏住文件的uri,可以理解是句柄,用于后续写入。我们需要在那个文件夹写文件,就设置文件名intent.putExtra(Intent.EXTRA_TITLE, "xxx.png")和文件夹intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);如果只设置文件名的话默认在Download文件夹下面。
intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.setType("image/png") intent.addCategory(Intent.CATEGORY_OPENABLE) intent.putExtra(Intent.EXTRA_TITLE, "license.png") intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) def get_fileuri(result, data): self.file_uri=data.getData() self._impl.start_activity(intent, on_complete=get_fileuri)
-
但是这样写的文件只是一个空文件,我们需要处理好数据后再写入,通过在Toga-android库里面摸索,我找到了from java.io import FileOutputStream,ByteArrayOutputStream这些写文件的库。第一行pfd的获取是我翻源码然后找的,对于普通的字符串写入就很简单,data_bytes = f"Overwritten at {current_time_ms}\n".encode('utf-8'),然后fileOutputStream.write(data_bytes ),我的png_bytes是图片,处理不太一样
pfd = self._impl.native.getApplicationContext().getContentResolver().openFileDescriptor(self.file_uri, "w") fileOutputStream = FileOutputStream(pfd.getFileDescriptor()) fileOutputStream.write(png_bytes) fileOutputStream.close() pfd.close()
可以看见这个nvtive基本就是java的mainactivaty,那么我们可以做的事情就多了,这也是前面能获取pfd的前提。
def _native_checkSelfPermission(self, permission): # pragma: no cover # A wrapper around the native method so that it can be mocked during testing. return ContextCompat.checkSelfPermission( self.native.getApplicationContext(), permission ) def _native_requestPermissions(self, permissions, code): # pragma: no cover # A wrapper around the native method so that it can be mocked during testing. self.native.requestPermissions(permissions, code)
最后是相关依赖库,如opencv,numpy的打包,这个在pyproject.toml写入就好,
requires = [ "numpy", "opencv-python", "pycryptodome", ]
总结,没有参考,可恶的google权限,我真的会谢,目前尽量别用python来开发安卓,老老实实原生开发,除非你有不错的python和安卓基础。
-
动态权限申请,手动修改了beeware的安卓框架jave代码去动态权限申请,代码中就是注释的地方,在低版本安卓能正常申请,高了就不太行
protected void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate() start"); // Change away from the splash screen theme to the app theme. setTheme(R.style.AppTheme); super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); this.setContentView(layout); singletonThis = this; Python py; if (Python.isStarted()) { Log.d(TAG, "Python already started"); py = Python.getInstance(); } else { Log.d(TAG, "Starting Python"); AndroidPlatform platform = new AndroidPlatform(this); platform.redirectStdioToLogcat(); Python.start(platform); py = Python.getInstance(); String argvStr = getIntent().getStringExtra("org.beeware.ARGV"); if (argvStr != null) { try { JSONArray argvJson = new JSONArray(argvStr); List<PyObject> sysArgv = py.getModule("sys").get("argv").asList(); for (int i = 0; i < argvJson.length(); i++) { sysArgv.add(PyObject.fromJava(argvJson.getString(i))); } } catch (JSONException e) { throw new RuntimeException(e); } } Log.d(TAG, "Running main module " + argvStr); } Log.d(TAG, "Running main module " + getString(R.string.main_module)); py.getModule("runpy").callAttr( "run_module", getString(R.string.main_module), new Kwarg("run_name", "__main__"), new Kwarg("alter_sys", true) ); // if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, WRITE_EXTERNAL_STORAGE); // Log.d("Request Permission", "request permission"); // } else { // Log.d("Request Permission", "request permission ok"); // } userCode("onCreate"); Log.d(TAG, "onCreate() complete"); }