From dc7661e226676fd3ac5e20eea7e4d7372d6088fd Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Fri, 21 Aug 2026 14:44:38 -0400 Subject: [PATCH] Android: launch the 'All files access' settings screen at startup. MANAGE_EXTERNAL_STORAGE (needed to read the store at /storage/emulated/0/...) cannot be requested with a runtime permission dialog; the user must toggle it in system settings. Port the mechanism from the pad app: - Permissions.java: a Fragment registered at window attach that launches Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION (per-app settings screen) when Environment.isExternalStorageManager() is false on API 30+ (runtime READ_EXTERNAL_STORAGE request on older APIs). The guard means the screen appears only until the user grants it. - The compiled classes ship in PgpConnect.jar's directory jar (Permissions.jar); gogio picks *.jar up from the package dir and dexes them. go:generate now rebuilds it. - jni_android.c/h: registerPermissionsFragment(), mirroring registerFragment() for PgpConnect. - Impl wiring: call it from the AndroidViewEvent handler right after InitPgp. Verified on the x86_64 emulator: with the appop denied the system settings screen opens automatically on launch; after granting it, launch is clean and list + OpenKeychain decryption from /sdcard/Pass work end to end. --- Permissions.jar | Bin 0 -> 3386 bytes Permissions.java | 91 +++++++++++++++++++++++++++++++++ cmd/passgo-gui/impl_android.go | 5 ++ impl_android.go | 12 ++++- jni_android.c | 16 ++++++ jni_android.h | 1 + 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 Permissions.jar create mode 100644 Permissions.java diff --git a/Permissions.jar b/Permissions.jar new file mode 100644 index 0000000000000000000000000000000000000000..b04befc63db358e4e01c0e9ae5b3138725b3abb5 GIT binary patch literal 3386 zcmai%2UJtp7KTF$z1IjqC4$tD&;kOZRDlp6^q~l$i1ZLzq=`sT1Vu_fKplD$rASAm zgY+thz@v;HVo3xkD)17W5uK;=_F8wXv)29pea_u?pLM=tf~2A40WdHy02Tvy?E&8f zBY+lw(lOVN(KphS-RlJaXaFWidIpMu`Y)NuFU>q;#Wyob!$@CO$IKjx(p^XO_ZcE( zpd+kE8Og!^p=wjuxcvMJ9Mk|Mt&bWBETv`2SOdKTWlEi^&lhtQTXuhsBjKpN%v=#X zi&rM+MUWrL%uE0Pc{TrBHVyf$e}L?-1@vD9!M?%&B4R!exw-#aHm)CqSd72Fo9~yz zKd-9j&-Jc2Z+CxxcV8d>V{oXm7dcCenY}T$29G*TjCcA3fxYZ<YAFj-VuFUO_*4GdKS+qWe zWr>YTSPb^0LP+p&SyO~~;)UKN_-f?q_6ooD*T&9oF@Q%j|1 z3j@vOm40x#p;lgYs~cx~vWaY4S#~jdO&_l?u&h2#GRU-IS&CUiGmT~V!PYQrlLCCQ ziF&Cm+3^0!dJkz9GZT*1*_?la>)7uYBosgYtL&6uEky9Z}f=<7TSn zCRON+tKY+miZ#rcvx`18 z`OhZp=>-F>(=@Pq7vLXC2-Gq|y3R&7;5(9vUjg+CJWa3c29-f`+yApzLIaZYndW@c zou|b6TKHuU&t{LV35d1#zFdaqI;b z9SQFNAKaPBG77el3Q#F=k&AJ=9M_2%Fb&@~jJH1+7m~7vMIQ~^dgMUaxUX)E@T15( z7f$NJW?eeZOic;R}&Ln2yjbWlyJFSCkk6ONZED=g2>eOQVRo$*#lXa%3JTg@V@NewOCDj3(a4niT?60k;SI_2#Y}~2 zS*!Yx=3&<^LwubXcAOEZZ+8=4v|Pr{vw?yap+Kv_2T#^-O$>{z$L1uuo%fy3cJ#WQ za7M@#o2A?1_h5rZE9(M^gBs$)5gcp|UM=(5pg%U#6u*`8AaCf^ARXeG{A;KuX|;9; z2EKE4TXHQbcUMj%>W(&uD+Q zW;&PpEj^K*k-b-M;izVzB#_hDvd%EiJUD1I$YpMUkmG96osa8~pjNQyGzb_>^kXur z3V@Ytx<9+?t2u;wUQ{qPQ!DCH`oId$Df8%a^*2 z=!mziSiPQmSUWFJWi&F_rTOZXcxA~U!jJ(@^-e**XOX>UsBvX!1x~wfdFOMJBD2gxzYZITagLHx zAB+JS#4Rrsu1;Mmc(FKhQ%-(!nRUD4k}>IK^ke3&{np{i*9|`XnPbgDk){!>CJCsk z6?=aiim!6icQ28e29N6(tv8&gyIP~f=Fmeru{go&hyAQ(tSV(%e^bap!` z2X5sQ$nL*%hvk!HU={Dw&hLM8pLXm)x6fNDBi|6r;M>L%;L=f#VqO`=f`dD?_woXgq zFUkM}9mQWc>-CW@^kWpWqsAn!-kjLGWgRaNYY<<>I@9!}N$i+4G4ug{#RaO1Qp!9D zWAAc2mDTvv9z@zQ*N1OZ&CBf>F8wA)J4M4kJ*-#gb=29Wr`gD6wJky@PgV>nB1)5? z3A|ebb^xmKV-etp>&ngR^c(h3hL3Y}qEE{P?;Q>&`ND^rSGZrl-C*OxeiW&twcqc& zSs6H|F=5ei&Tq##L3VKuiI1%H&>U0D?`}?sV;a8i1hnpGXU;$djC4Tg)JPB|U=tI5 z**}1f&cV6`seX@daVi#|iy7x)wacFZ zp!3&i!>j`qA)nLcpvfl;SIU5)4DW zjEIx6KGk=ha$53x`I2UV0KidJ%4tbO%>(%Hy!;!1DU|$Ue8XM}Gyl{2J;PEM`5@HE z!*AB_h?$as3Gi#yJ;+c;9)2O;UmGawdk|CPA=&Vignw= Build.VERSION_CODES.R + && !Environment.isExternalStorageManager()) { + Log.d("passgo", "Requesting all files access"); + Intent intent = new Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + Uri.parse("package:" + activity.getPackageName()) + ); + if (intent.resolveActivity(activity.getPackageManager()) != null) { + startActivity(intent); + } else { + Intent fallback = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + if (fallback.resolveActivity(activity.getPackageManager()) != null) { + startActivity(fallback); + } else { + Log.e("passgo", "No activity found for all files access settings"); + } + } + } + } + + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d("passgo", "Permissions onActivityResult(" + requestCode + "): " + resultCode); + super.onActivityResult(requestCode, resultCode, data); + } +} diff --git a/cmd/passgo-gui/impl_android.go b/cmd/passgo-gui/impl_android.go index e1b296a..6bd83d6 100644 --- a/cmd/passgo-gui/impl_android.go +++ b/cmd/passgo-gui/impl_android.go @@ -29,6 +29,7 @@ func handleEvent(e event.Event) { // view. if e.View != 0 { initPgp(e.View) + initPermissions(e.View) } } } @@ -37,6 +38,10 @@ func initPgp(view uintptr) { passgo.InitPgp(view) } +func initPermissions(view uintptr) { + passgo.InitPermissions(view) +} + func getConfDir() (string, error) { ret, err := app.DataDir() if err != nil { diff --git a/impl_android.go b/impl_android.go index aac5da8..dc5b806 100644 --- a/impl_android.go +++ b/impl_android.go @@ -2,7 +2,7 @@ // +build android //go:generate mkdir -p classes -//go:generate javac -nowarn -classpath $ANDROID_HOME/platforms/android-35/android.jar:openpgp-api.jar -d classes PgpConnect.java +//go:generate javac -nowarn -classpath $ANDROID_HOME/platforms/android-35/android.jar:openpgp-api.jar -d classes PgpConnect.java Permissions.java //go:generate jar cf PgpConnect.jar -C classes . //go:generate rm -rf classes @@ -58,6 +58,16 @@ func InitPgp(view uintptr) { }) } +// InitPermissions registers the Permissions fragment, which requests the +// storage permissions (launching the system "All files access" settings +// screen on Android 11+) so the app can read the store in shared storage. +func InitPermissions(view uintptr) { + log.Printf("InitPermissions()") + RunInJVM(func(env *JNIEnv) { + C.registerPermissionsFragment(env, (C.jobject)(unsafe.Pointer(view))) + }) +} + func stopPgp() { waitch = make(chan struct{}) } diff --git a/jni_android.c b/jni_android.c index c1c63d3..3a37a05 100644 --- a/jni_android.c +++ b/jni_android.c @@ -21,6 +21,22 @@ registerFragment(JNIEnv *env, jobject view) { jobject inst = (*env)->NewObject(env, cls, mid, view); } +void +registerPermissionsFragment(JNIEnv *env, jobject view) { + jclass cls = (*env)->GetObjectClass(env, view); + jmethodID mid = (*env)->GetMethodID(env, cls, "getContext", "()Landroid/content/Context;"); + jobject ctx = (*env)->CallObjectMethod(env, view, mid); + cls = (*env)->GetObjectClass(env, ctx); + mid = (*env)->GetMethodID(env, cls, "getClassLoader", "()Ljava/lang/ClassLoader;"); + jobject loader = (*env)->CallObjectMethod(env, ctx, mid); + cls = (*env)->GetObjectClass(env, loader); + mid = (*env)->GetMethodID(env, cls, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + jstring str = (*env)->NewStringUTF(env, "st/wow/git/passgo/Permissions"); + cls = (*env)->CallObjectMethod(env, loader, mid, str); + mid = (*env)->GetMethodID(env, cls, "", "(Landroid/view/View;)V"); + jobject inst = (*env)->NewObject(env, cls, mid, view); +} + void GetId(JNIEnv* env, jobject p, int chint) { jclass cls = (*env)->GetObjectClass(env, p); diff --git a/jni_android.h b/jni_android.h index ee5eb43..861bca4 100644 --- a/jni_android.h +++ b/jni_android.h @@ -1,6 +1,7 @@ #include void registerFragment(JNIEnv *env, jobject view); +void registerPermissionsFragment(JNIEnv *env, jobject view); void GetId(JNIEnv* env, jobject p, int chint); void Decrypt(JNIEnv* env, jobject p, char* cdata, int datalen, int chint); void Encrypt(JNIEnv* env, jobject p, char* cid, int idlen, char* cdata, int datalen, int chint);