From 2582f0a182a613acbc904d2b89f222f8484fde49 Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Sun, 20 Nov 2022 12:27:37 +1100 Subject: [PATCH 1/2] WIP on adding proper android content:// provider --- android/app/src/main/AndroidManifest.xml | 6 + .../java/com/classicube/CCFileProvider.java | 111 ++++++++++++++++++ .../java/com/classicube/MainActivity.java | 21 +--- 3 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 android/app/src/main/java/com/classicube/CCFileProvider.java diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7939562ee..8ace3ed22 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,12 @@ + + diff --git a/android/app/src/main/java/com/classicube/CCFileProvider.java b/android/app/src/main/java/com/classicube/CCFileProvider.java new file mode 100644 index 000000000..3fb00ced8 --- /dev/null +++ b/android/app/src/main/java/com/classicube/CCFileProvider.java @@ -0,0 +1,111 @@ +package com.classicube; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; + +public class CCFileProvider extends ContentProvider +{ + final static String[] DEFAULT_COLUMNS = { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; + File root; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public void attachInfo(Context context, ProviderInfo info) { + super.attachInfo(context, info); + root = context.getExternalFilesDir(null); // getGameDataDirectory + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + File file = getFileForUri(uri); + // can be null when caller is requesting all supported columns + if (projection == null) projection = DEFAULT_COLUMNS; + + ArrayList cols = new ArrayList(2); + ArrayList vals = new ArrayList(2); + + for (String column : projection) { + if (column.equals(OpenableColumns.DISPLAY_NAME)) { + cols.add(OpenableColumns.DISPLAY_NAME); + vals.add(file.getName()); + } else if (column.equals(OpenableColumns.SIZE)) { + cols.add(OpenableColumns.SIZE); + vals.add(file.length()); + } + } + + // https://stackoverflow.com/questions/4042434/converting-arrayliststring-to-string-in-java + MatrixCursor cursor = new MatrixCursor(cols.toArray(new String[0]), 1); + cursor.addRow(vals.toArray()); + return cursor; + } + + @Override + public String getType(Uri uri) { + String path = uri.getEncodedPath(); + int sepExt = path.lastIndexOf('.'); + + if (sepExt >= 0) { + String fileExt = path.substring(sepExt); + if (fileExt.equals(".png")) return "image/png"; + } + return "application/octet-stream"; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("Readonly access"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Readonly access"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Readonly access"); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + File file = getFileForUri(uri); + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } + + public static Uri getUriForFile(String path) { + // See AndroidManifest.xml for authority + return new Uri.Builder() + .scheme("content") + .authority("com.classicube.android.client.ccfiles") + .encodedPath(Uri.encode(path, "/")) + .build(); + } + + File getFileForUri(Uri uri) { + String path = uri.getPath(); + File file = new File(root, path); + + file = file.getAbsoluteFile(); + // security validation check + if (!file.getPath().startsWith(root.getPath())) { + throw new SecurityException("Resolved path lies outside app directory:" + path); + } + return file; + } +} diff --git a/android/app/src/main/java/com/classicube/MainActivity.java b/android/app/src/main/java/com/classicube/MainActivity.java index 59446c649..dcf9d8faf 100644 --- a/android/app/src/main/java/com/classicube/MainActivity.java +++ b/android/app/src/main/java/com/classicube/MainActivity.java @@ -26,7 +26,6 @@ import android.database.Cursor; import android.graphics.PixelFormat; import android.net.Uri; import android.os.Bundle; -import android.os.StrictMode; import android.provider.OpenableColumns; import android.provider.Settings.Secure; import android.text.Editable; @@ -159,19 +158,6 @@ public class MainActivity extends Activity runGameAsync(); } - void HACK_avoidFileUriExposedErrors() { - // StrictMode - API level 9 - // disableDeathOnFileUriExposure - API level 24 ????? - try { - Method m = StrictMode.class.getMethod("disableDeathOnFileUriExposure"); - m.invoke(null); - } catch (NoClassDefFoundError ex) { - ex.printStackTrace(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - @Override protected void onCreate(Bundle savedInstanceState) { // requestWindowFeature - API level 1 @@ -193,9 +179,6 @@ public class MainActivity extends Activity // renderOverDisplayCutouts(); // TODO: semaphore for destroyed and surfaceDestroyed - // avoid FileUriExposed exception when taking screenshots on recent Android versions - HACK_avoidFileUriExposedErrors(); - if (!gameRunning) startGameAsync(); // TODO rethink to avoid this if (gameRunning) updateInstance(); @@ -811,11 +794,11 @@ public class MainActivity extends Activity public String shareScreenshot(String path) { try { - File file = new File(getGameDataDirectory() + "/screenshots/" + path); + Uri uri = CCFileProvider.getUriForFile("screenshots", path); Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); + intent.putExtra(Intent.EXTRA_STREAM, uri); intent.setType("image/png"); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(intent, "share via")); From 57713d5c5dcd80f5ca41bc20de7abc5d38ab8d3f Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Sun, 20 Nov 2022 14:53:13 +1100 Subject: [PATCH 2/2] Still use file:// urls for devices earlier than android 6.0 --- android/app/src/main/AndroidManifest.xml | 2 +- .../main/java/com/classicube/CCFileProvider.java | 16 ++++++++++------ .../main/java/com/classicube/MainActivity.java | 12 +++++++++++- misc/buildbot.sh | 7 +++++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8ace3ed22..0f75fcf75 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ diff --git a/android/app/src/main/java/com/classicube/CCFileProvider.java b/android/app/src/main/java/com/classicube/CCFileProvider.java index 3fb00ced8..a6d011de0 100644 --- a/android/app/src/main/java/com/classicube/CCFileProvider.java +++ b/android/app/src/main/java/com/classicube/CCFileProvider.java @@ -12,11 +12,12 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; import android.provider.OpenableColumns; public class CCFileProvider extends ContentProvider { - final static String[] DEFAULT_COLUMNS = { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; + final static String[] DEFAULT_COLUMNS = { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, MediaStore.MediaColumns.DATA }; File root; @Override @@ -33,11 +34,11 @@ public class CCFileProvider extends ContentProvider @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { File file = getFileForUri(uri); - // can be null when caller is requesting all supported columns + // can be null when caller is requesting all columns if (projection == null) projection = DEFAULT_COLUMNS; - ArrayList cols = new ArrayList(2); - ArrayList vals = new ArrayList(2); + ArrayList cols = new ArrayList(3); + ArrayList vals = new ArrayList(3); for (String column : projection) { if (column.equals(OpenableColumns.DISPLAY_NAME)) { @@ -46,6 +47,9 @@ public class CCFileProvider extends ContentProvider } else if (column.equals(OpenableColumns.SIZE)) { cols.add(OpenableColumns.SIZE); vals.add(file.length()); + } else if (column.equals(MediaStore.MediaColumns.DATA)) { + cols.add(MediaStore.MediaColumns.DATA); + vals.add(file.getAbsolutePath()); } } @@ -92,7 +96,7 @@ public class CCFileProvider extends ContentProvider // See AndroidManifest.xml for authority return new Uri.Builder() .scheme("content") - .authority("com.classicube.android.client.ccfiles") + .authority("com.classicube.android.client.provider") .encodedPath(Uri.encode(path, "/")) .build(); } @@ -100,7 +104,7 @@ public class CCFileProvider extends ContentProvider File getFileForUri(Uri uri) { String path = uri.getPath(); File file = new File(root, path); - + file = file.getAbsoluteFile(); // security validation check if (!file.getPath().startsWith(root.getPath())) { diff --git a/android/app/src/main/java/com/classicube/MainActivity.java b/android/app/src/main/java/com/classicube/MainActivity.java index dcf9d8faf..f3a0a068e 100644 --- a/android/app/src/main/java/com/classicube/MainActivity.java +++ b/android/app/src/main/java/com/classicube/MainActivity.java @@ -794,7 +794,17 @@ public class MainActivity extends Activity public String shareScreenshot(String path) { try { - Uri uri = CCFileProvider.getUriForFile("screenshots", path); + Uri uri; + if (android.os.Build.VERSION.SDK_INT >= 23){ // android 6.0 + uri = CCFileProvider.getUriForFile("screenshots/" + path); + } else { + // when trying to use content:// URIs on my android 4.0.3 test device + // - 1 app crashed + // - 1 app wouldn't show image previews + // so fallback to file:// on older devices as they seem to reliably work + File file = new File(getGameDataDirectory() + "/screenshots/" + path); + uri = Uri.fromFile(file); + } Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); diff --git a/misc/buildbot.sh b/misc/buildbot.sh index 9f8d05bfe..92087f35b 100644 --- a/misc/buildbot.sh +++ b/misc/buildbot.sh @@ -179,9 +179,12 @@ build_android() { # https://github.com/skanti/Android-Manual-Build-Command-Line/blob/master/hello-jni/Makefile # https://github.com/skanti/Android-Manual-Build-Command-Line/blob/master/hello-jni/CMakeLists.txt - # compile interop java file into its multiple .class files - javac java/com/classicube/MainActivity.java -d ./obj -classpath $SDK_ROOT/android.jar + # compile java files into multiple .class files + cd $ROOT_DIR/android/app/src/main/java/com/classicube + javac *.java -d $ROOT_DIR/android/app/src/main/obj -classpath $SDK_ROOT/android.jar if [ $? -ne 0 ]; then echo "Failed to compile Android Java" >> "$ERRS_FILE"; fi + + cd $ROOT_DIR/android/app/src/main # compile the multiple .class files into one .dex file $TOOLS_ROOT/dx --dex --output=obj/classes.dex ./obj # create initial .apk with packaged version of resources