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.
This commit is contained in:
Greg Pomerantz 2026-08-21 14:44:38 -04:00
parent 4b74367a55
commit dc7661e226
6 changed files with 124 additions and 1 deletions

BIN
Permissions.jar Normal file

Binary file not shown.

91
Permissions.java Normal file
View File

@ -0,0 +1,91 @@
package st.wow.git.passgo;
import java.lang.Runnable;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.content.Context;
import android.util.Log;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.Manifest;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.Settings;
import android.view.View;
// Requests the storage permissions the app needs to read the password
// store in shared storage. On Android 11+ (API 30+) the
// MANAGE_EXTERNAL_STORAGE ("All files access") permission cannot be
// requested with a dialog; the user must toggle it in the system
// settings, so we launch that settings screen directly for this app.
public class Permissions extends Fragment {
Context ctx;
Handler handler;
final int PERMISSIONS_REQUEST = 1;
public Permissions(View view) {
Log.d("passgo", "Permissions()");
this.ctx = view.getContext();
this.handler = new Handler(this.ctx.getMainLooper());
Permissions inst = this;
handler.post(new Runnable() {
public void run() {
Activity act = (Activity)ctx;
FragmentTransaction ft = act.getFragmentManager().beginTransaction();
ft.add(inst, "Permissions");
ft.commitNow();
}
});
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d("passgo", "Permissions onAttach()");
if (!(context instanceof Activity)) {
Log.w("passgo", "Context is not an Activity");
return;
}
Activity activity = (Activity) context;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST
);
}
}
if (Build.VERSION.SDK_INT >= 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);
}
}

View File

@ -29,6 +29,7 @@ func handleEvent(e event.Event) {
// view. // view.
if e.View != 0 { if e.View != 0 {
initPgp(e.View) initPgp(e.View)
initPermissions(e.View)
} }
} }
} }
@ -37,6 +38,10 @@ func initPgp(view uintptr) {
passgo.InitPgp(view) passgo.InitPgp(view)
} }
func initPermissions(view uintptr) {
passgo.InitPermissions(view)
}
func getConfDir() (string, error) { func getConfDir() (string, error) {
ret, err := app.DataDir() ret, err := app.DataDir()
if err != nil { if err != nil {

View File

@ -2,7 +2,7 @@
// +build android // +build android
//go:generate mkdir -p classes //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 jar cf PgpConnect.jar -C classes .
//go:generate rm -rf 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() { func stopPgp() {
waitch = make(chan struct{}) waitch = make(chan struct{})
} }

View File

@ -21,6 +21,22 @@ registerFragment(JNIEnv *env, jobject view) {
jobject inst = (*env)->NewObject(env, cls, mid, 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, "<init>", "(Landroid/view/View;)V");
jobject inst = (*env)->NewObject(env, cls, mid, view);
}
void void
GetId(JNIEnv* env, jobject p, int chint) { GetId(JNIEnv* env, jobject p, int chint) {
jclass cls = (*env)->GetObjectClass(env, p); jclass cls = (*env)->GetObjectClass(env, p);

View File

@ -1,6 +1,7 @@
#include <jni.h> #include <jni.h>
void registerFragment(JNIEnv *env, jobject view); void registerFragment(JNIEnv *env, jobject view);
void registerPermissionsFragment(JNIEnv *env, jobject view);
void GetId(JNIEnv* env, jobject p, int chint); void GetId(JNIEnv* env, jobject p, int chint);
void Decrypt(JNIEnv* env, jobject p, char* cdata, int datalen, 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); void Encrypt(JNIEnv* env, jobject p, char* cid, int idlen, char* cdata, int datalen, int chint);