From 53fd2d60b56c7c970d88ad60824997d9a014c732 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 28 Jan 2019 00:27:56 +0100 Subject: [PATCH] android recursive mutex fix, android file path resolver fix --- android/src/main/cpp/main.cpp | 9 +- .../com/omixlab/panopainter/FileUtils.java | 592 ++++++++++++++++++ .../com/omixlab/panopainter/MainActivity.java | 7 +- .../com/omixlab/panopainter/PathUtil.java | 2 + data/layout.xml | 22 +- src/asset.cpp | 2 +- 6 files changed, 617 insertions(+), 17 deletions(-) create mode 100644 android/src/main/java/com/omixlab/panopainter/FileUtils.java diff --git a/android/src/main/cpp/main.cpp b/android/src/main/cpp/main.cpp index 43aba32..902c50e 100755 --- a/android/src/main/cpp/main.cpp +++ b/android/src/main/cpp/main.cpp @@ -53,6 +53,7 @@ typedef void (*fnDebugMessageCallback)(GLDEBUGPROC callback, const void* userPar EGLDisplay g_display = EGL_NO_DISPLAY; EGLContext g_context = EGL_NO_CONTEXT; std::recursive_mutex mutex; +int mutex_count = 0; jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { @@ -127,7 +128,9 @@ int GetUnicodeChar(struct android_app* app, int eventType, int keyCode, int meta void android_async_lock(struct engine* engine) { mutex.lock(); - eglMakeCurrent(engine->display, engine->surface, engine->surface, engine->context); + if (mutex_count == 0) + eglMakeCurrent(engine->display, engine->surface, engine->surface, engine->context); + mutex_count++; } void android_async_swap(struct engine* engine) @@ -137,7 +140,9 @@ void android_async_swap(struct engine* engine) void android_async_unlock(struct engine* engine) { - eglMakeCurrent(engine->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + mutex_count--; + if (mutex_count == 0) + eglMakeCurrent(engine->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); mutex.unlock(); } diff --git a/android/src/main/java/com/omixlab/panopainter/FileUtils.java b/android/src/main/java/com/omixlab/panopainter/FileUtils.java new file mode 100644 index 0000000..f81c4ed --- /dev/null +++ b/android/src/main/java/com/omixlab/panopainter/FileUtils.java @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2018 OpenIntents.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ULR: https://github.com/coltoscosmin/FileUtils/blob/master/FileUtils.java + */ +package com.omixlab.panopainter; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.DecimalFormat; +import java.util.Comparator; + +public class FileUtils { + public static final String DOCUMENTS_DIR = "documents"; + // configured android:authorities in AndroidManifest (https://developer.android.com/reference/android/support/v4/content/FileProvider) + public static final String AUTHORITY = "YOUR_AUTHORITY.provider"; + public static final String HIDDEN_PREFIX = "."; + /** + * TAG for log messages. + */ + static final String TAG = "FileUtils"; + private static final boolean DEBUG = false; // Set to true to enable logging + /** + * File and folder comparator. TODO Expose sorting option method + */ + public static Comparator sComparator = new Comparator() { + @Override + public int compare(File f1, File f2) { + // Sort alphabetically by lower case, which is much cleaner + return f1.getName().toLowerCase().compareTo( + f2.getName().toLowerCase()); + } + }; + /** + * File (not directories) filter. + */ + public static FileFilter sFileFilter = new FileFilter() { + @Override + public boolean accept(File file) { + final String fileName = file.getName(); + // Return files only (not directories) and skip hidden files + return file.isFile() && !fileName.startsWith(HIDDEN_PREFIX); + } + }; + /** + * Folder (directories) filter. + */ + public static FileFilter sDirFilter = new FileFilter() { + @Override + public boolean accept(File file) { + final String fileName = file.getName(); + // Return directories only and skip hidden directories + return file.isDirectory() && !fileName.startsWith(HIDDEN_PREFIX); + } + }; + + private FileUtils() { + } //private constructor to enforce Singleton pattern + + /** + * Gets the extension of a file name, like ".png" or ".jpg". + * + * @param uri + * @return Extension including the dot("."); "" if there is no extension; + * null if uri was null. + */ + public static String getExtension(String uri) { + if (uri == null) { + return null; + } + + int dot = uri.lastIndexOf("."); + if (dot >= 0) { + return uri.substring(dot); + } else { + // No extension. + return ""; + } + } + + /** + * @return Whether the URI is a local one. + */ + public static boolean isLocal(String url) { + if (url != null && !url.startsWith("http://") && !url.startsWith("https://")) { + return true; + } + return false; + } + + /** + * @return True if Uri is a MediaStore Uri. + * @author paulburke + */ + public static boolean isMediaUri(Uri uri) { + return "media".equalsIgnoreCase(uri.getAuthority()); + } + + /** + * Convert File into Uri. + * + * @param file + * @return uri + */ + public static Uri getUri(File file) { + if (file != null) { + return Uri.fromFile(file); + } + return null; + } + + /** + * Returns the path only (without file name). + * + * @param file + * @return + */ + public static File getPathWithoutFilename(File file) { + if (file != null) { + if (file.isDirectory()) { + // no file to be split off. Return everything + return file; + } else { + String filename = file.getName(); + String filepath = file.getAbsolutePath(); + + // Construct path without file name. + String pathwithoutname = filepath.substring(0, + filepath.length() - filename.length()); + if (pathwithoutname.endsWith("/")) { + pathwithoutname = pathwithoutname.substring(0, pathwithoutname.length() - 1); + } + return new File(pathwithoutname); + } + } + return null; + } + + /** + * @return The MIME type for the given file. + */ + public static String getMimeType(File file) { + + String extension = getExtension(file.getName()); + + if (extension.length() > 0) + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1)); + + return "application/octet-stream"; + } + + /** + * @return The MIME type for the give Uri. + */ + public static String getMimeType(Context context, Uri uri) { + File file = new File(getPath(context, uri)); + return getMimeType(file); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is local. + */ + public static boolean isLocalStorageDocument(Uri uri) { + return AUTHORITY.equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = MediaStore.Files.FileColumns.DATA; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + if (DEBUG) + DatabaseUtils.dumpCursor(cursor); + + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders.
+ *
+ * Callers should check whether the path is local before assuming it + * represents a local file. + * + * @param context The context. + * @param uri The Uri to query. + * @see #isLocal(String) + * @see #getFile(Context, Uri) + */ + public static String getPath(final Context context, final Uri uri) { + + if (DEBUG) + Log.d(TAG + " File -", + "Authority: " + uri.getAuthority() + + ", Fragment: " + uri.getFragment() + + ", Port: " + uri.getPort() + + ", Query: " + uri.getQuery() + + ", Scheme: " + uri.getScheme() + + ", Host: " + uri.getHost() + + ", Segments: " + uri.getPathSegments().toString() + ); + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // LocalStorageProvider + if (isLocalStorageDocument(uri)) { + // The path is the id + return DocumentsContract.getDocumentId(uri); + } + // ExternalStorageProvider + else if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + + if (id != null && id.startsWith("raw:")) { + return id.substring(4); + } + + String[] contentUriPrefixesToTry = new String[]{ + "content://downloads/public_downloads", + "content://downloads/my_downloads" + }; + + for (String contentUriPrefix : contentUriPrefixesToTry) { + Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id)); + try { + String path = getDataColumn(context, contentUri, null, null); + if (path != null) { + return path; + } + } catch (Exception e) {} + } + + // path could not be retrieved using ContentResolver, therefore copy file to accessible cache using streams + String fileName = getFileName(context, uri); + File cacheDir = getDocumentCacheDir(context); + File file = generateFileName(fileName, cacheDir); + String destinationPath = null; + if (file != null) { + destinationPath = file.getAbsolutePath(); + saveFileFromUri(context, uri, destinationPath); + } + + return destinationPath; + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * Convert Uri into File, if possible. + * + * @return file A local file that the Uri was pointing to, or null if the + * Uri is unsupported or pointed to a remote resource. + * @author paulburke + * @see #getPath(Context, Uri) + */ + public static File getFile(Context context, Uri uri) { + if (uri != null) { + String path = getPath(context, uri); + if (path != null && isLocal(path)) { + return new File(path); + } + } + return null; + } + + /** + * Get the file size in a human-readable string. + * + * @param size + * @return + * @author paulburke + */ + public static String getReadableFileSize(int size) { + final int BYTES_IN_KILOBYTES = 1024; + final DecimalFormat dec = new DecimalFormat("###.#"); + final String KILOBYTES = " KB"; + final String MEGABYTES = " MB"; + final String GIGABYTES = " GB"; + float fileSize = 0; + String suffix = KILOBYTES; + + if (size > BYTES_IN_KILOBYTES) { + fileSize = size / BYTES_IN_KILOBYTES; + if (fileSize > BYTES_IN_KILOBYTES) { + fileSize = fileSize / BYTES_IN_KILOBYTES; + if (fileSize > BYTES_IN_KILOBYTES) { + fileSize = fileSize / BYTES_IN_KILOBYTES; + suffix = GIGABYTES; + } else { + suffix = MEGABYTES; + } + } + } + return String.valueOf(dec.format(fileSize) + suffix); + } + + public static File getDownloadsDir() { + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } + + public static File getDocumentCacheDir(Context context) { + File dir = new File(context.getCacheDir(), DOCUMENTS_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + logDir(context.getCacheDir()); + logDir(dir); + + return dir; + } + + private static void logDir(File dir) { + if(!DEBUG) return; + Log.d(TAG, "Dir=" + dir); + File[] files = dir.listFiles(); + for (File file : files) { + Log.d(TAG, "File=" + file.getPath()); + } + } + + public static File generateFileName(String name, File directory) { + if (name == null) { + return null; + } + + File file = new File(directory, name); + + if (file.exists()) { + String fileName = name; + String extension = ""; + int dotIndex = name.lastIndexOf('.'); + if (dotIndex > 0) { + fileName = name.substring(0, dotIndex); + extension = name.substring(dotIndex); + } + + int index = 0; + + while (file.exists()) { + index++; + name = fileName + '(' + index + ')' + extension; + file = new File(directory, name); + } + } + + try { + if (!file.createNewFile()) { + return null; + } + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + + logDir(directory); + + return file; + } + + private static void saveFileFromUri(Context context, Uri uri, String destinationPath) { + InputStream is = null; + BufferedOutputStream bos = null; + try { + is = context.getContentResolver().openInputStream(uri); + bos = new BufferedOutputStream(new FileOutputStream(destinationPath, false)); + byte[] buf = new byte[1024]; + is.read(buf); + do { + bos.write(buf); + } while (is.read(buf) != -1); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (is != null) is.close(); + if (bos != null) bos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public static byte[] readBytesFromFile(String filePath) { + + FileInputStream fileInputStream = null; + byte[] bytesArray = null; + + try { + + File file = new File(filePath); + bytesArray = new byte[(int) file.length()]; + + //read file into bytes[] + fileInputStream = new FileInputStream(file); + fileInputStream.read(bytesArray); + + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + return bytesArray; + + } + + public static File createTempImageFile(Context context, String fileName) throws IOException { + // Create an image file name + File storageDir = new File(context.getCacheDir(), DOCUMENTS_DIR); + return File.createTempFile(fileName, ".jpg", storageDir); + } + + public static String getFileName(Context context, Uri uri) { + String mimeType = context.getContentResolver().getType(uri); + String filename = null; + + if (mimeType == null && context != null) { + String path = getPath(context, uri); + if (path == null) { + filename = getName(uri.toString()); + } else { + File file = new File(path); + filename = file.getName(); + } + } else { + Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null); + if (returnCursor != null) { + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + returnCursor.moveToFirst(); + filename = returnCursor.getString(nameIndex); + returnCursor.close(); + } + } + + return filename; + } + + public static String getName(String filename) { + if (filename == null) { + return null; + } + int index = filename.lastIndexOf('/'); + return filename.substring(index + 1); + } +} diff --git a/android/src/main/java/com/omixlab/panopainter/MainActivity.java b/android/src/main/java/com/omixlab/panopainter/MainActivity.java index 3fa4207..c8e7734 100644 --- a/android/src/main/java/com/omixlab/panopainter/MainActivity.java +++ b/android/src/main/java/com/omixlab/panopainter/MainActivity.java @@ -135,10 +135,11 @@ public class MainActivity extends NativeActivity { //String path = myFile.getAbsolutePath(); String path = null; try { - path = PathUtil.getPath(this, uri); + path = FileUtils.getPath(this, uri); Log.v("PanoPainterJava", "pick selected " + path); - pickFileCallback(path); - } catch (URISyntaxException e) { + if (path != null) + pickFileCallback(path); + } catch (Exception e) { e.printStackTrace(); } } diff --git a/android/src/main/java/com/omixlab/panopainter/PathUtil.java b/android/src/main/java/com/omixlab/panopainter/PathUtil.java index 02b51a8..5697bf5 100644 --- a/android/src/main/java/com/omixlab/panopainter/PathUtil.java +++ b/android/src/main/java/com/omixlab/panopainter/PathUtil.java @@ -8,6 +8,7 @@ import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.util.Log; import java.net.URISyntaxException; @@ -60,6 +61,7 @@ public class PathUtil { return cursor.getString(column_index); } } catch (Exception e) { + Log.v("PanoPainterJava", "PathUtil error: " + e.getMessage()); } } else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); diff --git a/data/layout.xml b/data/layout.xml index ab0184e..d2cc8b0 100644 --- a/data/layout.xml +++ b/data/layout.xml @@ -88,6 +88,7 @@ + @@ -106,7 +107,6 @@ - @@ -168,13 +168,13 @@ - + - - + + - + @@ -200,23 +200,23 @@ - + - - + - - + + + - + diff --git a/src/asset.cpp b/src/asset.cpp index 8d8665d..b7ebca8 100644 --- a/src/asset.cpp +++ b/src/asset.cpp @@ -130,7 +130,7 @@ std::string Asset::absolute(const std::string& path) bool Asset::is_asset(const std::string & path) { - return path.find("data/") != std::string::npos; + return path.substr(0, 5) == "data/"; } bool Asset::open(const char* path)