Scoped storage methods for Android 10+

Please post anything related to the Hollywood APK Compiler here
Post Reply
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Scoped storage methods for Android 10+

Post by amyren »

Here are some methods for file access on devices with more recent Android versions. The main goal for me was to find a way to store files in areas so they are accessable for other apps such as file explorers etc.
I divide it into 3 methods that requires slightly different access level to use. Method 1 being the easiest way.
All of these methods involves adding custom java code so the PRO version of APK Compiler is required.
The java code is made with the aid of chatgpt.

Method 1
Will give access to create files and folders inside any of the mediastore folders on your device.
Typically existing folders like storage/emulated/0/Downloads (or /Documents, /Pictures, Movies etc.)
Add the file named HollywoodDelegate.java into custom code and the following new commands will be usable in your hollywood script:

Code: Select all

ok = CallJavaMethod("createFolder", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder"); creates a new folder under /Downloads
ok = CallJavaMethod("createFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "This is the file content from Hollywood!"); Create a new file in the specified subfolder under /Downloads
ok = CallJavaMethod("appendToFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "\nAppended line"); add test to the file
ok = CallJavaMethod("copyFileFromExternal", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "internalCopy.txt"); copy a file from the /Downloads/MyTestFolder and into the app internal storage
ok = CallJavaMethod("copyFileToExternal", {ReturnType=#BOOLEAN}, #STRING, "internalCopy.txt", #STRING, "MyTestFolder", #STRING, "hello_copy2.txt"); Copy a file from your app internal storage into /Downloads/MyTestFolder
ok = CallJavaMethod("folderExists", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder") ; True if the folder /Downloads/MyTestFolder exists
ok = CallJavaMethod("fileExists", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt")  ; True if the file /Downloads/MyTestFolder/hello.txt exists
ok = CallJavaMethod("deleteFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt") ; Delete the file in /Downloads/MyTestFolder
ok = CallJavaMethod("deleteFolder", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder") ; Delete whole folder /Downloads/MyTestFolder
numLines = CallJavaMethod("getNumberOfLines", {ReturnType=#INTEGER}, #STRING, "MyTestFolder", #STRING, "hello_copy2.txt") ; Returns number of lines in a file
list$ = CallJavaMethod("listFilesInFolder", {ReturnType=#STRING}, #STRING, "MyTestFolder") ; lists the files and folders in /Downloads/MyTestFolder
content$ = CallJavaMethod("readFile", {ReturnType=#STRING}, #STRING, "MyTestFolder", #STRING, "hello.txt"); Read whole file into a string
line$ = CallJavaMethod("readLineFromFile", {ReturnType=#STRING}, #STRING, "MyTestFolder", #STRING, "hello_copy2.txt", #INTEGER, i); Reads a single line from a file, i representing the line number (0 is fist line)
Here is a variant of the HollywoodDelegate.java file, you will need to edit the file to the correct package name. The example below will operate in the Downloads folder, you may edit this to use eg. the Documents folder instead by replacing every instance of
Environment.DIRECTORY_DOWNLOADS
into
Environment.DIRECTORY_DOCUMENTS

Code: Select all

// make sure this is identical to the package name set in the Hollywood APK Compiler
package com.amy66dev.testdir; -- replace this with your own package name

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * HollywoodDelegate - MediaStore-friendly file utilities.
 * Must be included as Custom Code in the Hollywood APK Compiler.
 */
public class HollywoodDelegate extends HollywoodActivity {

    private static final String TAG = "HollywoodDelegate";

    // Helper: find file Uri in MediaStore (Downloads/<folderName>/)
    private Uri findFileUri(String folderName, String fileName) {
        try {
            String relPath = Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/";
            Uri collection = MediaStore.Files.getContentUri("external");
            String[] projection = new String[]{ MediaStore.MediaColumns._ID };
            String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=? AND " + MediaStore.MediaColumns.DISPLAY_NAME + "=?";
            String[] selectionArgs = new String[]{ relPath, fileName };

            Cursor c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
            if (c != null) {
                try {
                    if (c.moveToFirst()) {
                        long id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                        return ContentUris.withAppendedId(collection, id);
                    }
                } finally {
                    c.close();
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "findFileUri failed", e);
        }
        return null;
    }

	// Check if a file exists in the given folder
	public boolean fileExists(String folderName, String fileName) {
		try {
			File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), folderName);
			File file = new File(dir, fileName);
			return file.exists() && file.isFile();
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error checking file existence: " + e.getMessage());
			return false;
		}
	}

	// Check if a folder exists
	public boolean folderExists(String folderName) {
		try {
			File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), folderName);
			return dir.exists() && dir.isDirectory();
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error checking folder existence: " + e.getMessage());
			return false;
		}
	}

    // Create folder by inserting a placeholder into MediaStore (shows folder)
    public boolean createFolder(String folderName) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DISPLAY_NAME, ".nomedia");
                values.put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream");
                values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/");
                values.put("is_pending", 1);

                ContentResolver resolver = getContentResolver();
                Uri uri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
                if (uri == null) {
                    Log.e(TAG, "createFolder: insert returned null");
                    return false;
                }
                // mark not pending
                ContentValues cv = new ContentValues();
                cv.put("is_pending", 0);
                resolver.update(uri, cv, null, null);
                Log.i(TAG, "createFolder (MediaStore) created: " + folderName);
                return true;
            } else {
                File docs = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                File folder = new File(docs, folderName);
                if (!folder.exists() && !folder.mkdirs()) {
                    Log.e(TAG, "createFolder: mkdirs failed: " + folder.getAbsolutePath());
                    return false;
                }
                // create .nomedia file
                File nm = new File(folder, ".nomedia");
                try {
                    if (!nm.exists()) nm.createNewFile();
                } catch (IOException e) {
                    Log.w(TAG, "createFolder: .nomedia create failed", e);
                }
                Log.i(TAG, "createFolder (legacy) created: " + folder.getAbsolutePath());
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "createFolder failed", e);
            return false;
        }
    }

    // Create or overwrite a file with text content (uses MediaStore on Q+)
    public boolean createFile(String folderName, String fileName, String content) {
        try {
            ContentResolver resolver = getContentResolver();
            Uri uri = findFileUri(folderName, fileName);

            if (uri != null) {
                // overwrite existing
                OutputStream out = null;
                try {
                    out = resolver.openOutputStream(uri, "w");
                    if (out == null) throw new IOException("openOutputStream returned null");
                    if (content != null) out.write(content.getBytes("UTF-8"));
                    out.flush();
                } finally {
                    if (out != null) try { out.close(); } catch (IOException ignored) {}
                }
                Log.i(TAG, "createFile: overwritten " + fileName);
                return true;
            }

            // insert new
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) values.put("is_pending", 1);

            Uri newUri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
            if (newUri == null) {
                Log.e(TAG, "createFile: insert returned null");
                return false;
            }

            OutputStream out = null;
            try {
                out = resolver.openOutputStream(newUri);
                if (out == null) throw new IOException("openOutputStream returned null");
                if (content != null) out.write(content.getBytes("UTF-8"));
                out.flush();
            } finally {
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues cv = new ContentValues();
                cv.put("is_pending", 0);
                resolver.update(newUri, cv, null, null);
            }
            Log.i(TAG, "createFile: created " + fileName);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFile failed", e);
            return false;
        }
    }

    // Append to file (reads existing and writes combined content)
    public boolean appendToFile(String folderName, String fileName, String content) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                // create new file
                return createFile(folderName, fileName, content);
            }

            ContentResolver resolver = getContentResolver();
            // read existing
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream in = null;
            try {
                in = resolver.openInputStream(uri);
                if (in != null) {
                    byte[] buf = new byte[4096];
                    int r;
                    while ((r = in.read(buf)) > 0) baos.write(buf, 0, r);
                }
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
            }

            byte[] existing = baos.toByteArray();
            byte[] add = (content != null) ? content.getBytes("UTF-8") : new byte[0];
            byte[] combined = new byte[existing.length + add.length];
            System.arraycopy(existing, 0, combined, 0, existing.length);
            System.arraycopy(add, 0, combined, existing.length, add.length);

            OutputStream out = null;
            try {
                out = resolver.openOutputStream(uri, "w");
                if (out == null) throw new IOException("openOutputStream returned null");
                out.write(combined);
                out.flush();
            } finally {
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            Log.i(TAG, "appendToFile success: " + fileName);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFile failed", e);
            return false;
        }
    }

    // Read file (UTF-8) - returns empty string if not found or on error
    public String readFile(String folderName, String fileName) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                Log.w(TAG, "readFile: not found: " + folderName + "/" + fileName);
                return "";
            }
            ContentResolver resolver = getContentResolver();
            InputStream in = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                in = resolver.openInputStream(uri);
                if (in == null) return "";
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) baos.write(buf, 0, r);
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
            }
            return new String(baos.toByteArray(), "UTF-8");
        } catch (Exception e) {
            Log.e(TAG, "readFile failed", e);
            return "";
        }
    }

	public String readLineFromFile(String folder, String filename, int lineNumber) {
		try {
			Uri fileUri = findFileUri(folder, filename);  // your existing helper
			if (fileUri == null) return "";
			InputStream is = getContentResolver().openInputStream(fileUri);
			BufferedReader reader = new BufferedReader(new InputStreamReader(is));
			String line = null;
			int count = 0;
			while ((line = reader.readLine()) != null) {
				if (count == lineNumber) {
					reader.close();
					return line;
				}
				count++;
			}
			reader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "";
	}

	public int getNumberOfLines(String folder, String filename) {
		int count = 0;
		try {
			Uri fileUri = findFileUri(folder, filename);  // use your existing helper
			if (fileUri == null) return 0;
			InputStream is = getContentResolver().openInputStream(fileUri);
			BufferedReader reader = new BufferedReader(new InputStreamReader(is));
			while (reader.readLine() != null) {
				count++;
			}
			reader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return count;
	}

    // Delete file via MediaStore
    public boolean deleteFile(String folderName, String fileName) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                Log.w(TAG, "deleteFile: not found");
                return false;
            }
            int deleted = getContentResolver().delete(uri, null, null);
            Log.i(TAG, "deleteFile: deleted rows = " + deleted);
            return deleted > 0;
        } catch (Exception e) {
            Log.e(TAG, "deleteFile failed", e);
            return false;
        }
    }

	// Helper: recursive delete
	private boolean deleteRecursive(File fileOrDirectory) {
		if (fileOrDirectory.isDirectory()) {
			File[] children = fileOrDirectory.listFiles();
			if (children != null) {
				for (File child : children) {
					deleteRecursive(child);
				}
			}
		}
		return fileOrDirectory.delete();
	}

	// Public method for Hollywood
	public boolean deleteFolder(String folderName) {
		try {
			File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), folderName);
			if (dir.exists() && dir.isDirectory()) {
				return deleteRecursive(dir);
			} else {
				return false; // folder does not exist
			}
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error deleting folder: " + e.getMessage());
			return false;
		}
	}

    // Copy internal (getFilesDir()/internalPath) -> external Downloads/folderName/fileName
    public boolean copyFileToExternal(String internalRelativePath, String folderName, String fileName) {
        try {
            File src = new File(getFilesDir(), internalRelativePath);
            if (!src.exists()) {
                Log.w(TAG, "copyFileToExternal: source not found: " + src.getAbsolutePath());
                return false;
            }

            ContentResolver resolver = getContentResolver();
            Uri dstUri = findFileUri(folderName, fileName);

            if (dstUri == null) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
                values.put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream");
                values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/");
                values.put("is_pending", 1);
                dstUri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
            }

            if (dstUri == null) {
                Log.e(TAG, "copyFileToExternal: insert returned null");
                return false;
            }

            FileInputStream in = null;
            OutputStream out = null;
            try {
                in = new FileInputStream(src);
                out = resolver.openOutputStream(dstUri, "w");
                if (out == null) throw new IOException("openOutputStream returned null");
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
                out.flush();
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            ContentValues cv = new ContentValues();
            cv.put("is_pending", 0);
            resolver.update(dstUri, cv, null, null);

            Log.i(TAG, "copyFileToExternal success");
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToExternal failed", e);
            return false;
        }
    }

    // Copy external Downloads/folderName/fileName -> internal getFilesDir()/internalRelativePath
    public boolean copyFileFromExternal(String folderName, String fileName, String internalRelativePath) {
        try {
            Uri srcUri = findFileUri(folderName, fileName);
            if (srcUri == null) {
                Log.w(TAG, "copyFileFromExternal: not found");
                return false;
            }

            InputStream in = null;
            FileOutputStream out = null;
            try {
                in = getContentResolver().openInputStream(srcUri);
                if (in == null) throw new IOException("openInputStream returned null");
                File dest = new File(getFilesDir(), internalRelativePath);
                File parent = dest.getParentFile();
                if (parent != null && !parent.exists()) parent.mkdirs();
                out = new FileOutputStream(dest);
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
                out.flush();
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }
            Log.i(TAG, "copyFileFromExternal success -> " + internalRelativePath);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromExternal failed", e);
            return false;
        }
    }

    // List files in Downloads/<folderName> via MediaStore; newline-separated string
    public String listFilesInFolder(String folderName) {
        try {
            String relPath = Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/";
            Uri collection = MediaStore.Files.getContentUri("external");
            String[] projection = new String[]{ MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE };
            String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
            String[] selectionArgs = new String[]{ relPath };

            Cursor c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
            if (c == null) {
                Log.w(TAG, "listFilesInFolder: cursor null");
                return "";
            }
            StringBuilder sb = new StringBuilder();
            try {
                while (c.moveToNext()) {
                    String name = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME));
                    String mime = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE));
                    sb.append(name).append(" (").append(mime).append(")").append("\n");
                }
            } finally {
                c.close();
            }
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "listFilesInFolder failed", e);
            return "";
        }
    }
}
Method 2 / 3 in separate posts
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Re: Scoped storage methods for Android 10+

Post by amyren »

Method 2 - access files on the device main storage area directly (without being tied to the Downloads or Documents folders)
This require some additional permissions, so you need to edit the AndroidManifest.xml to use.

Tick the box to use custom AndroidManifest.xml and add the following line
(I inserted it just below line containing android.permission.WRITE_EXTERNAL_STORAGE)

Code: Select all

  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Additionally we will add permission check into the HollywoodDelegate.java. The first time running the app it will open the settings to make the user enable the "enable access to all files" permission for the app.

Here is the HollywoodDelegate.java file to add to custom code:

Code: Select all

// make sure this is identical to the package name set in the Hollywood APK Compiler
package com.amy66dev.testdir;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.app.Activity;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * HollywoodDelegate - Direct external storage file utilities.
 * Must be included as Custom Code in the Hollywood APK Compiler.
 */
public class HollywoodDelegate extends HollywoodActivity {

    private static final String TAG = "HollywoodDelegate";

    // --- PERMISSION METHODS ---

    // Check if MANAGE_EXTERNAL_STORAGE is granted
    public boolean hasAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            return Environment.isExternalStorageManager();
        }
        return true; // On Android 10 and below, WRITE_EXTERNAL_STORAGE is enough
    }

    // Open settings page for granting MANAGE_EXTERNAL_STORAGE
    public void requestAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            try {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivity(intent);
            } catch (Exception e) {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);
            }
        }
    }

    // --- FOLDER METHODS ---

    // Check if folder exists
    public boolean folderExists(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            return dir.exists() && dir.isDirectory();
        } catch (Exception e) {
            Log.e(TAG, "Error checking folder existence: " + e.getMessage());
            return false;
        }
    }

    // Create folder
    public boolean createFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists()) {
                if (!dir.mkdirs()) return false;
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFolder failed: " + e.getMessage());
            return false;
        }
    }

    // Delete folder recursively
    public boolean deleteFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() || !dir.isDirectory()) return false;
            return deleteRecursive(dir);
        } catch (Exception e) {
            Log.e(TAG, "deleteFolder failed: " + e.getMessage());
            return false;
        }
    }

    private boolean deleteRecursive(File fileOrDirectory) {
        if (fileOrDirectory.isDirectory()) {
            File[] children = fileOrDirectory.listFiles();
            if (children != null) {
                for (File child : children) deleteRecursive(child);
            }
        }
        return fileOrDirectory.delete();
    }

    // --- FILE METHODS ---

    // Check if file exists
    public boolean fileExists(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            return file.exists() && file.isFile();
        } catch (Exception e) {
            Log.e(TAG, "fileExists failed: " + e.getMessage());
            return false;
        }
    }

    // Create or overwrite file
    public boolean createFile(String folderName, String fileName, String content) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() && !dir.mkdirs()) return false;
            File file = new File(dir, fileName);
            FileOutputStream fos = new FileOutputStream(file);
            if (content != null) fos.write(content.getBytes("UTF-8"));
            fos.flush();
            fos.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFile failed: " + e.getMessage());
            return false;
        }
    }

    // Read entire file
    public String readFile(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return "";
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
            reader.close();
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "readFile failed: " + e.getMessage());
            return "";
        }
    }

    // Append to file
    public boolean appendToFile(String folderName, String fileName, String content) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return createFile(folderName, fileName, content);
            FileOutputStream fos = new FileOutputStream(file, true); // append mode
            if (content != null) fos.write(content.getBytes("UTF-8"));
            fos.flush();
            fos.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFile failed: " + e.getMessage());
            return false;
        }
    }

    // Delete file
    public boolean deleteFile(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return false;
            return file.delete();
        } catch (Exception e) {
            Log.e(TAG, "deleteFile failed: " + e.getMessage());
            return false;
        }
    }

    // Read a specific line
    public String readLineFromFile(String folderName, String fileName, int lineNumber) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return "";
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            String line;
            int count = 0;
            while ((line = reader.readLine()) != null) {
                if (count == lineNumber) {
                    reader.close();
                    return line;
                }
                count++;
            }
            reader.close();
        } catch (Exception e) {
            Log.e(TAG, "readLineFromFile failed: " + e.getMessage());
        }
        return "";
    }

    // Count number of lines
    public int getNumberOfLines(String folderName, String fileName) {
        int count = 0;
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return 0;
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            while (reader.readLine() != null) count++;
            reader.close();
        } catch (Exception e) {
            Log.e(TAG, "getNumberOfLines failed: " + e.getMessage());
        }
        return count;
    }

    // List files in folder (newline-separated)
    public String listFilesInFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() || !dir.isDirectory()) return "";
            StringBuilder sb = new StringBuilder();
            File[] files = dir.listFiles();
            if (files != null) {
                for (File f : files) {
                    sb.append(f.getName());
                    if (f.isDirectory()) sb.append(" (dir)");
                    sb.append("\n");
                }
            }
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "listFilesInFolder failed: " + e.getMessage());
            return "";
        }
    }

    // Copy internal -> external
    public boolean copyFileToExternal(String internalRelativePath, String folderName, String fileName) {
        try {
            File src = new File(getFilesDir(), internalRelativePath);
            if (!src.exists()) return false;
            File dst = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!dst.getParentFile().exists()) dst.getParentFile().mkdirs();
            FileInputStream in = new FileInputStream(src);
            FileOutputStream out = new FileOutputStream(dst);
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            in.close();
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToExternal failed: " + e.getMessage());
            return false;
        }
    }

    // Copy external -> internal
    public boolean copyFileFromExternal(String folderName, String fileName, String internalRelativePath) {
        try {
            File src = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!src.exists()) return false;
            File dst = new File(getFilesDir(), internalRelativePath);
            if (!dst.getParentFile().exists()) dst.getParentFile().mkdirs();
            FileInputStream in = new FileInputStream(src);
            FileOutputStream out = new FileOutputStream(dst);
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            in.close();
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromExternal failed: " + e.getMessage());
            return false;
        }
    }
}
Usage in the hollywood script should be similar as described in method 1, exept you will need this at the start of the script to invoke the permission check (you might want to use a systemrequest instead of the Nprints I used in my tests)

Code: Select all

; --- check if we have full external storage access ---
ok = CallJavaMethod("hasAllFilesAccess", {ReturnType=#BOOLEAN})
If Not ok
	NPrint("App needs ALL FILES ACCESS. Opening settings...")
	CallJavaMethod("requestAllFilesAccess")
	NPrint("Press to continue after granting permission")
	WaitLeftMouse
EndIf
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Re: Scoped storage methods for Android 10+

Post by amyren »

Method 3 - This will handle files on the physical SDCard
This method will try to locate the proper location of the physical SDCard by using SDCard helper.
I only tested these methods on my own android 15 phone, so it could need some additional testing to verifiy it works on other devices.

The HollywoodDelegate.java file contains all the content from Method 2, but have new keywords for SDCard operation so both main storage and physical SDCard should be available here. Usage is the same as for Method 1 and 2, but SDCARD commands are:
createFolderOnSDCard, createFileOnSDCard, readFileFromSDCard, deleteFileFromSDCard, folderExistsOnSDCard, deleteFolderFromSDCard, appendToFileOnSDCard, readLineFromFileOnSDCard, getNumberOfLinesOnSDCard ,listFilesInFolderOnSDCard, copyFileToSDCard, copyFileFromSDCard, copyFromExternalToSDCard

Two new commands are available as well:

Code: Select all

ok = CallJavaMethod("copyFromExternalToSDCard", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "BackupFolder", #STRING, "hello_backup.txt"); Copies file from main storage /MyTestFolder/hello.txt into SDCard folder /BackupFolder
ok = CallJavaMethod("copyFromSDCardToExternal", {ReturnType=#BOOLEAN}, #STRING, "BackupFolder", #STRING, "hello_backup.txt", #STRING, "MyTestFolder", #STRING, "hello_from_sd.txt"); Copies from SDCard folder /BackupFolder and into folder /MyTestFolder on main storage.

Note: I made a typo in the .java script supplied for Method 1, the comments in the first line after the ; must be removed for the file to work. (I added the comment for the post, so I commented in Hollywood style instead of java)

Code: Select all

// make sure this is identical to the package name set in the Hollywood APK Compiler
package com.amy66dev.testdir;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.app.Activity;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * HollywoodDelegate - Direct external storage file utilities.
 * Must be included as Custom Code in the Hollywood APK Compiler.
 */
public class HollywoodDelegate extends HollywoodActivity {

    private static final String TAG = "HollywoodDelegate";

    // --- Add SD card helper HERE (inside the class, not before imports)
    private File getExternalSdCard() {
        File[] externalDirs = getApplicationContext().getExternalFilesDirs(null);
        if (externalDirs != null && externalDirs.length > 1) {
            for (File dir : externalDirs) {
                if (dir != null && Environment.isExternalStorageRemovable(dir)) {
                    return dir.getParentFile().getParentFile().getParentFile().getParentFile();
                }
            }
        }
        return null;
    }

    // --- PERMISSION METHODS ---

    // Check if MANAGE_EXTERNAL_STORAGE is granted
    public boolean hasAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            return Environment.isExternalStorageManager();
        }
        return true; // On Android 10 and below, WRITE_EXTERNAL_STORAGE is enough
    }

    // Open settings page for granting MANAGE_EXTERNAL_STORAGE
    public void requestAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            try {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivity(intent);
            } catch (Exception e) {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);
            }
        }
    }

    // --- FOLDER METHODS ---

    // Check if folder exists
    public boolean folderExists(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            return dir.exists() && dir.isDirectory();
        } catch (Exception e) {
            Log.e(TAG, "Error checking folder existence: " + e.getMessage());
            return false;
        }
    }

    // Create folder
    public boolean createFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists()) {
                if (!dir.mkdirs()) return false;
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFolder failed: " + e.getMessage());
            return false;
        }
    }

    // Delete folder recursively
    public boolean deleteFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() || !dir.isDirectory()) return false;
            return deleteRecursive(dir);
        } catch (Exception e) {
            Log.e(TAG, "deleteFolder failed: " + e.getMessage());
            return false;
        }
    }

    private boolean deleteRecursive(File fileOrDirectory) {
        if (fileOrDirectory.isDirectory()) {
            File[] children = fileOrDirectory.listFiles();
            if (children != null) {
                for (File child : children) deleteRecursive(child);
            }
        }
        return fileOrDirectory.delete();
    }

    // --- FILE METHODS ---

    // Check if file exists
    public boolean fileExists(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            return file.exists() && file.isFile();
        } catch (Exception e) {
            Log.e(TAG, "fileExists failed: " + e.getMessage());
            return false;
        }
    }

    // Create or overwrite file
    public boolean createFile(String folderName, String fileName, String content) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() && !dir.mkdirs()) return false;
            File file = new File(dir, fileName);
            FileOutputStream fos = new FileOutputStream(file);
            if (content != null) fos.write(content.getBytes("UTF-8"));
            fos.flush();
            fos.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFile failed: " + e.getMessage());
            return false;
        }
    }

    // Read entire file
    public String readFile(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return "";
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
            reader.close();
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "readFile failed: " + e.getMessage());
            return "";
        }
    }

    // Append to file
    public boolean appendToFile(String folderName, String fileName, String content) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return createFile(folderName, fileName, content);
            FileOutputStream fos = new FileOutputStream(file, true); // append mode
            if (content != null) fos.write(content.getBytes("UTF-8"));
            fos.flush();
            fos.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFile failed: " + e.getMessage());
            return false;
        }
    }

    // Delete file
    public boolean deleteFile(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return false;
            return file.delete();
        } catch (Exception e) {
            Log.e(TAG, "deleteFile failed: " + e.getMessage());
            return false;
        }
    }

    // Read a specific line
    public String readLineFromFile(String folderName, String fileName, int lineNumber) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return "";
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            String line;
            int count = 0;
            while ((line = reader.readLine()) != null) {
                if (count == lineNumber) {
                    reader.close();
                    return line;
                }
                count++;
            }
            reader.close();
        } catch (Exception e) {
            Log.e(TAG, "readLineFromFile failed: " + e.getMessage());
        }
        return "";
    }

    // Count number of lines
    public int getNumberOfLines(String folderName, String fileName) {
        int count = 0;
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return 0;
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
            while (reader.readLine() != null) count++;
            reader.close();
        } catch (Exception e) {
            Log.e(TAG, "getNumberOfLines failed: " + e.getMessage());
        }
        return count;
    }

    // List files in folder (newline-separated)
    public String listFilesInFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() || !dir.isDirectory()) return "";
            StringBuilder sb = new StringBuilder();
            File[] files = dir.listFiles();
            if (files != null) {
                for (File f : files) {
                    sb.append(f.getName());
                    if (f.isDirectory()) sb.append(" (dir)");
                    sb.append("\n");
                }
            }
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "listFilesInFolder failed: " + e.getMessage());
            return "";
        }
    }

    // Copy internal -> external
    public boolean copyFileToExternal(String internalRelativePath, String folderName, String fileName) {
        try {
            File src = new File(getFilesDir(), internalRelativePath);
            if (!src.exists()) return false;
            File dst = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!dst.getParentFile().exists()) dst.getParentFile().mkdirs();
            FileInputStream in = new FileInputStream(src);
            FileOutputStream out = new FileOutputStream(dst);
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            in.close();
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToExternal failed: " + e.getMessage());
            return false;
        }
    }

    // Copy external -> internal
    public boolean copyFileFromExternal(String folderName, String fileName, String internalRelativePath) {
        try {
            File src = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!src.exists()) return false;
            File dst = new File(getFilesDir(), internalRelativePath);
            if (!dst.getParentFile().exists()) dst.getParentFile().mkdirs();
            FileInputStream in = new FileInputStream(src);
            FileOutputStream out = new FileOutputStream(dst);
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            in.close();
            out.flush();
            out.close();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromExternal failed: " + e.getMessage());
            return false;
        }
    }
	
	// --- Create folder on SD card ---
	public boolean createFolderOnSDCard(String folderName) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return false;
		File folder = new File(sdRoot, folderName);
		return folder.exists() || folder.mkdirs();
	}

	// --- Create/overwrite file on SD card ---
	public boolean createFileOnSDCard(String folderName, String fileName, String content) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return false;
		try {
			File folder = new File(sdRoot, folderName);
			if (!folder.exists()) folder.mkdirs();
			File file = new File(folder, fileName);
			FileOutputStream fos = new FileOutputStream(file, false);
			fos.write(content.getBytes());
			fos.close();
			return true;
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error writing file to SD card", e);
			return false;
		}
	}

	// --- Read file from SD card ---
	public String readFileFromSDCard(String folderName, String fileName) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return "";
		try {
			File file = new File(new File(sdRoot, folderName), fileName);
			FileInputStream fis = new FileInputStream(file);
			BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
			StringBuilder sb = new StringBuilder();
			String line;
			while ((line = reader.readLine()) != null) {
				sb.append(line).append("\n");
			}
			reader.close();
			return sb.toString();
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error reading file from SD card", e);
			return "";
		}
	}

	// --- Delete file from SD card ---
	public boolean deleteFileFromSDCard(String folderName, String fileName) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return false;
		File file = new File(new File(sdRoot, folderName), fileName);
		return file.exists() && file.delete();
	}
	
    // --- SD CARD OPERATIONS ---

    // Check if folder exists on SD card
    public boolean folderExistsOnSDCard(String folderName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dir = new File(sdCard, folderName);
        return dir.exists() && dir.isDirectory();
    }

    // Recursive delete helper for SD card
    private boolean deleteRecursiveFromSDCard(File fileOrDirectory) {
        if (fileOrDirectory.isDirectory()) {
            File[] children = fileOrDirectory.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteRecursiveFromSDCard(child);
                }
            }
        }
        return fileOrDirectory.delete();
    }

    // Delete folder from SD card
    public boolean deleteFolderFromSDCard(String folderName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dir = new File(sdCard, folderName);
        if (dir.exists() && dir.isDirectory()) {
            return deleteRecursiveFromSDCard(dir);
        }
        return false;
    }

    // Check if a file exists on SD card
    public boolean fileExistsOnSDCard(String folderName, String fileName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File file = new File(new File(sdCard, folderName), fileName);
        return file.exists() && file.isFile();
    }

    // Append to file on SD card
    public boolean appendToFileOnSDCard(String folderName, String fileName, String content) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dir = new File(sdCard, folderName);
        if (!dir.exists()) dir.mkdirs();
        File file = new File(dir, fileName);
        try (FileOutputStream fos = new FileOutputStream(file, true)) {
            fos.write(content.getBytes("UTF-8"));
            fos.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFileOnSDCard failed", e);
            return false;
        }
    }

    // Read a specific line from a file on SD card
    public String readLineFromFileOnSDCard(String folderName, String fileName, int lineNumber) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return "";
        File file = new File(new File(sdCard, folderName), fileName);
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
            String line;
            int count = 0;
            while ((line = reader.readLine()) != null) {
                if (count == lineNumber) return line;
                count++;
            }
        } catch (Exception e) {
            Log.e(TAG, "readLineFromFileOnSDCard failed", e);
        }
        return "";
    }

    // Get number of lines in a file on SD card
    public int getNumberOfLinesOnSDCard(String folderName, String fileName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return 0;
        File file = new File(new File(sdCard, folderName), fileName);
        int count = 0;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
            while (reader.readLine() != null) count++;
        } catch (Exception e) {
            Log.e(TAG, "getNumberOfLinesOnSDCard failed", e);
        }
        return count;
    }

    // List files in a folder on SD card
    public String listFilesInFolderOnSDCard(String folderName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return "";
        File dir = new File(sdCard, folderName);
        if (!dir.exists() || !dir.isDirectory()) return "";
        StringBuilder sb = new StringBuilder();
        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                sb.append(f.getName());
                if (f.isDirectory()) sb.append(" (dir)");
                sb.append("\n");
            }
        }
        return sb.toString().trim();
    }

    // Copy file from internal storage -> SD card
    public boolean copyFileToSDCard(String internalRelativePath, String folderName, String fileName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File src = new File(getFilesDir(), internalRelativePath);
        if (!src.exists()) return false;
        File dst = new File(new File(sdCard, folderName), fileName);
        dst.getParentFile().mkdirs();
        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToSDCard failed", e);
            return false;
        }
    }

    // Copy file from SD card -> internal storage
    public boolean copyFileFromSDCard(String folderName, String fileName, String internalRelativePath) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File src = new File(new File(sdCard, folderName), fileName);
        if (!src.exists()) return false;
        File dst = new File(getFilesDir(), internalRelativePath);
        dst.getParentFile().mkdirs();
        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromSDCard failed", e);
            return false;
        }
    }

    // Copy file from External (primary shared storage) -> SD card
    public boolean copyFromExternalToSDCard(String externalFolder, String externalFile, String sdFolder, String sdFile) {
        File external = Environment.getExternalStorageDirectory();
        if (external == null) return false;
        File src = new File(new File(external, externalFolder), externalFile);
        if (!src.exists()) return false;

        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dst = new File(new File(sdCard, sdFolder), sdFile);
        dst.getParentFile().mkdirs();

        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFromExternalToSDCard failed", e);
            return false;
        }
    }

    // Copy file from SD card -> External (primary shared storage)
    public boolean copyFromSDCardToExternal(String sdFolder, String sdFile, String externalFolder, String externalFile) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File src = new File(new File(sdCard, sdFolder), sdFile);
        if (!src.exists()) return false;

        File external = Environment.getExternalStorageDirectory();
        if (external == null) return false;
        File dst = new File(new File(external, externalFolder), externalFile);
        dst.getParentFile().mkdirs();

        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFromSDCardToExternal failed", e);
            return false;
        }
    }
	
}
Here is a hollywood testscript I used to test these features. The initial files are cleaned up along the way, so you need to check the filestructure using a file explorer in the waitleftmouse pauses if you want to verify the folders and files. The files copied between the storages at the end are not cleaned up, so you should end up with the folder MyTestFolder on the main storage and a BackupFolder on the SDCard.

Code: Select all

@DISPLAY {Title = "Testfile", ScaleMode = #SCALEMODE_AUTO, FitScale = True, SmoothScale = True, HideTitleBar = True}

; --- check if we have full external storage access ---
ok = CallJavaMethod("hasAllFilesAccess", {ReturnType=#BOOLEAN})
If Not ok
	NPrint("App needs ALL FILES ACCESS. Opening settings...")
	CallJavaMethod("requestAllFilesAccess")
	NPrint("Press to continue after granting permission")
	WaitLeftMouse
EndIf


ta = GetSystemInfo()

; --- create folder ---
ok = CallJavaMethod("createFolder", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder")
If ok
	NPrint("Folder created!")
Else
	NPrint("Failed creating folder!")
EndIf

; --- create/overwrite file with content ---
ok = CallJavaMethod("createFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "This is the file content from Hollywood!")
If ok
	NPrint("File created!")
Else
	NPrint("Failed creating file!")
EndIf

; --- read file ---
content$ = CallJavaMethod("readFile", {ReturnType=#STRING}, #STRING, "MyTestFolder", #STRING, "hello.txt")
NPrint("File content:\n"..content$)

NPrint("Press to continue before append")
WaitLeftMouse

; --- append to file --- ( folder name, file name, string to append)
ok = CallJavaMethod("appendToFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "\nAppended line")
If ok
	NPrint("Appended to file!")
Else
	NPrint("Failed to append!")
EndIf

OpenDirectory(1, ta.InternalStorage)
e = NextDirectoryEntry(1)
While e <> Nil
	direntry$ = IIf(e.type = #DOSTYPE_FILE, "File:", "Directory:")..e.name
	NPrint(direntry$)
	e = NextDirectoryEntry(1)
Wend
CloseDirectory(1)

NPrint("InternalStorage listing done, press to list external storage")
WaitLeftMouse

; --- list files in folder (MediaStore-based) ---
list$ = CallJavaMethod("listFilesInFolder", {ReturnType=#STRING}, #STRING, "MyTestFolder")
NPrint("Files in MyTestFolder:")
NPrint(list$)

NPrint("Press to continue before copying")
WaitLeftMouse

; --- copy external -> internal (creates internal file at getFilesDir()/internalCopy.txt) ---
ok = CallJavaMethod("copyFileFromExternal", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "internalCopy.txt")
If ok
	NPrint("Copied to internal (internalCopy.txt)!")
Else
	NPrint("Failed copying to internal!")
EndIf

; --- copy internal -> external (internalRelativePath, dst folder, dst filename) ---
ok = CallJavaMethod("copyFileToExternal", {ReturnType=#BOOLEAN}, #STRING, "internalCopy.txt", #STRING, "MyTestFolder", #STRING, "hello_copy2.txt")
If ok
	NPrint("Copied back to external as hello_copy2.txt!")
Else
	NPrint("Failed copying back!")
EndIf

;test file- and folderexists command before deleting the file
; Check folder
folderOk = CallJavaMethod("folderExists", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder")
If folderOk
	NPrint("Folder exists!")
Else
	NPrint("Folder missing!")
EndIf

; Check file
fexists = CallJavaMethod("fileExists", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt")
If fexists
	NPrint("File exists!")
Else
	NPrint("File not found!")
EndIf

; --- delete original external file --- ( source folder, source filename)
ok = CallJavaMethod("deleteFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt")
If ok
	NPrint("Deleted original hello.txt!")
Else
	NPrint("Failed deleting hello.txt (maybe it didn't exist).")
EndIf

OpenDirectory(1, ta.InternalStorage)
e = NextDirectoryEntry(1)
While e <> Nil
	direntry$ = IIf(e.type = #DOSTYPE_FILE, "File:", "Directory:")..e.name
	NPrint(direntry$)
	e = NextDirectoryEntry(1)
Wend
CloseDirectory(1)

NPrint("Listing InternalStorage done, now deleting the file from internal storage")
DeleteFile(ta.InternalStorage.."/internalCopy.txt")
WaitLeftMouse

; --- final listing ---
list$ = CallJavaMethod("listFilesInFolder", {ReturnType=#STRING}, #STRING, "MyTestFolder")
NPrint("Files in MyTestFolder:")
NPrint(list$)

; Get number of lines in the file
numLines = CallJavaMethod("getNumberOfLines", {ReturnType=#INTEGER}, #STRING, "MyTestFolder", #STRING, "hello_copy2.txt")

; Loop through each line
i = 0
While i < numLines
	line$ = CallJavaMethod("readLineFromFile", {ReturnType=#STRING}, #STRING, "MyTestFolder", #STRING, "hello_copy2.txt", #INTEGER, i)
	i = i + 1
	NPrint("Line "..i..": "..line$)
Wend

NPrint("Now we will delete the folder completely! Press to proceed")
WaitLeftMouse

; Try to delete MyTestFolder
ok = CallJavaMethod("deleteFolder", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder")
If ok
	NPrint("MyTestFolder deleted!")
Else
	NPrint("Failed to delete MyTestFolder!")
EndIf

;test file- and folderexists command after deleting the file
; Check folder
folderOk = CallJavaMethod("folderExists", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder")
If folderOk
	NPrint("MyTestFolder exists!")
Else
	NPrint("MyTestFolder missing!")
EndIf

; Check file
fexists = CallJavaMethod("fileExists", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt")
If fexists
	NPrint("hello.txt exists!")
Else
	NPrint("hello.txt not found!")
EndIf

WaitLeftMouse
NPrint("Will try to create a folder MySDTest on the SDCard")

; SDCard test
ok = CallJavaMethod("createFolderOnSDCard", {ReturnType=#BOOLEAN}, #STRING, "MySDTest")
If ok
    NPrint("SD card folder created!")
Else
    NPrint("Failed to create SD card folder!")
EndIf

WaitLeftMouse
NPrint("Will try to create a file in folder MySDTest on the SDCard")

ok = CallJavaMethod("createFileOnSDCard", {ReturnType=#BOOLEAN}, #STRING, "MySDTest", #STRING, "sdtest.txt", #STRING, "Hello SD card!")
If ok
    NPrint("File created on SD card!")
Else
    NPrint("Failed creating file on SD card!")
EndIf

WaitLeftMouse
NPrint("Will try to read the file from the SDCard")

content$ = CallJavaMethod("readFileFromSDCard", {ReturnType=#STRING}, #STRING, "MySDTest", #STRING, "sdtest.txt")
NPrint("Content from SD card:\n"..content$)

WaitLeftMouse

NPrint("Will try to delete the folder MySDTest on the SDCard")
ok = CallJavaMethod("deleteFolderFromSDCard", {ReturnType=#BOOLEAN}, #STRING, "MySDTest")
If ok
    NPrint("Folder deleted from SD card!")
Else
    NPrint("Failed to delete folder from SD card!")
EndIf

ok = CallJavaMethod("deleteFileFromSDCard", {ReturnType=#BOOLEAN}, #STRING, "MySDTest", #STRING, "sdtest.txt")
If ok
    NPrint("File deleted from SD card!")
Else
    NPrint("Failed to delete file from SD card! (probably deleted with folder already")
EndIf

WaitLeftMouse

; --- create/overwrite file with content ---
ok = CallJavaMethod("createFile", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "This is the file content from Hollywood!")
If ok
	NPrint("File re-created!")
Else
	NPrint("Failed re-creating file!")
EndIf

; Copy file hello.txt from External/MyTestFolder -> SDCard/BackupFolder
ok = CallJavaMethod("copyFromExternalToSDCard", {ReturnType=#BOOLEAN}, #STRING, "MyTestFolder", #STRING, "hello.txt", #STRING, "BackupFolder", #STRING, "hello_backup.txt")
If ok
    NPrint("Copied from External to SDCard!")
Else
    NPrint("Copy failed!")
EndIf

; Copy back from SDCard/BackupFolder -> External/MyTestFolder
ok = CallJavaMethod("copyFromSDCardToExternal", {ReturnType=#BOOLEAN}, #STRING, "BackupFolder", #STRING, "hello_backup.txt", #STRING, "MyTestFolder", #STRING, "hello_from_sd.txt")
If ok
    NPrint("Copied from SDCard to External!")
Else
    NPrint("Copy failed!")
EndIf

WaitLeftMouse
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Re: Scoped storage methods for Android 10+

Post by amyren »

Found a minor issue with the HollywoodDelegate.java used in Method 1
There is a MIME_type in the createFile section that adds the .txt extension to the file name when a new file is created.
I missed that during my testing, since only tested with .txt files anyway.
Simply delete this line in the java script to avoid that.

Code: Select all

values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
New HollywoodDelegate.java for Method 1 should be like:

Code: Select all

// make sure this is identical to the package name set in the Hollywood APK Compiler
package com.amy66dev.testdir;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * HollywoodDelegate - MediaStore-friendly file utilities.
 * Must be included as Custom Code in the Hollywood APK Compiler.
 */
public class HollywoodDelegate extends HollywoodActivity {

    private static final String TAG = "HollywoodDelegate";

    // Helper: find file Uri in MediaStore (Downloads/<folderName>/)
    private Uri findFileUri(String folderName, String fileName) {
        try {
            String relPath = Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/";
            Uri collection = MediaStore.Files.getContentUri("external");
            String[] projection = new String[]{ MediaStore.MediaColumns._ID };
            String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=? AND " + MediaStore.MediaColumns.DISPLAY_NAME + "=?";
            String[] selectionArgs = new String[]{ relPath, fileName };

            Cursor c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
            if (c != null) {
                try {
                    if (c.moveToFirst()) {
                        long id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                        return ContentUris.withAppendedId(collection, id);
                    }
                } finally {
                    c.close();
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "findFileUri failed", e);
        }
        return null;
    }

	// Check if a file exists in the given folder
	public boolean fileExists(String folderName, String fileName) {
		try {
			File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), folderName);
			File file = new File(dir, fileName);
			return file.exists() && file.isFile();
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error checking file existence: " + e.getMessage());
			return false;
		}
	}

	// Check if a folder exists
	public boolean folderExists(String folderName) {
		try {
			File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), folderName);
			return dir.exists() && dir.isDirectory();
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error checking folder existence: " + e.getMessage());
			return false;
		}
	}

    // Create folder by inserting a placeholder into MediaStore (shows folder)
    public boolean createFolder(String folderName) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DISPLAY_NAME, ".nomedia");
                values.put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream");
                values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/");
                values.put("is_pending", 1);

                ContentResolver resolver = getContentResolver();
                Uri uri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
                if (uri == null) {
                    Log.e(TAG, "createFolder: insert returned null");
                    return false;
                }
                // mark not pending
                ContentValues cv = new ContentValues();
                cv.put("is_pending", 0);
                resolver.update(uri, cv, null, null);
                Log.i(TAG, "createFolder (MediaStore) created: " + folderName);
                return true;
            } else {
                File docs = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
                File folder = new File(docs, folderName);
                if (!folder.exists() && !folder.mkdirs()) {
                    Log.e(TAG, "createFolder: mkdirs failed: " + folder.getAbsolutePath());
                    return false;
                }
                // create .nomedia file
                File nm = new File(folder, ".nomedia");
                try {
                    if (!nm.exists()) nm.createNewFile();
                } catch (IOException e) {
                    Log.w(TAG, "createFolder: .nomedia create failed", e);
                }
                Log.i(TAG, "createFolder (legacy) created: " + folder.getAbsolutePath());
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "createFolder failed", e);
            return false;
        }
    }

    // Create or overwrite a file with text content (uses MediaStore on Q+)
    public boolean createFile(String folderName, String fileName, String content) {
        try {
            ContentResolver resolver = getContentResolver();
            Uri uri = findFileUri(folderName, fileName);

            if (uri != null) {
                // overwrite existing
                OutputStream out = null;
                try {
                    out = resolver.openOutputStream(uri, "w");
                    if (out == null) throw new IOException("openOutputStream returned null");
                    if (content != null) out.write(content.getBytes("UTF-8"));
                    out.flush();
                } finally {
                    if (out != null) try { out.close(); } catch (IOException ignored) {}
                }
                Log.i(TAG, "createFile: overwritten " + fileName);
                return true;
            }

            // insert new
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) values.put("is_pending", 1);

            Uri newUri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
            if (newUri == null) {
                Log.e(TAG, "createFile: insert returned null");
                return false;
            }

            OutputStream out = null;
            try {
                out = resolver.openOutputStream(newUri);
                if (out == null) throw new IOException("openOutputStream returned null");
                if (content != null) out.write(content.getBytes("UTF-8"));
                out.flush();
            } finally {
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues cv = new ContentValues();
                cv.put("is_pending", 0);
                resolver.update(newUri, cv, null, null);
            }
            Log.i(TAG, "createFile: created " + fileName);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFile failed", e);
            return false;
        }
    }

    // Append to file (reads existing and writes combined content)
    public boolean appendToFile(String folderName, String fileName, String content) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                // create new file
                return createFile(folderName, fileName, content);
            }

            ContentResolver resolver = getContentResolver();
            // read existing
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream in = null;
            try {
                in = resolver.openInputStream(uri);
                if (in != null) {
                    byte[] buf = new byte[4096];
                    int r;
                    while ((r = in.read(buf)) > 0) baos.write(buf, 0, r);
                }
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
            }

            byte[] existing = baos.toByteArray();
            byte[] add = (content != null) ? content.getBytes("UTF-8") : new byte[0];
            byte[] combined = new byte[existing.length + add.length];
            System.arraycopy(existing, 0, combined, 0, existing.length);
            System.arraycopy(add, 0, combined, existing.length, add.length);

            OutputStream out = null;
            try {
                out = resolver.openOutputStream(uri, "w");
                if (out == null) throw new IOException("openOutputStream returned null");
                out.write(combined);
                out.flush();
            } finally {
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            Log.i(TAG, "appendToFile success: " + fileName);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFile failed", e);
            return false;
        }
    }

    // Read file (UTF-8) - returns empty string if not found or on error
    public String readFile(String folderName, String fileName) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                Log.w(TAG, "readFile: not found: " + folderName + "/" + fileName);
                return "";
            }
            ContentResolver resolver = getContentResolver();
            InputStream in = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                in = resolver.openInputStream(uri);
                if (in == null) return "";
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) baos.write(buf, 0, r);
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
            }
            return new String(baos.toByteArray(), "UTF-8");
        } catch (Exception e) {
            Log.e(TAG, "readFile failed", e);
            return "";
        }
    }

	public String readLineFromFile(String folder, String filename, int lineNumber) {
		try {
			Uri fileUri = findFileUri(folder, filename);  // your existing helper
			if (fileUri == null) return "";
			InputStream is = getContentResolver().openInputStream(fileUri);
			BufferedReader reader = new BufferedReader(new InputStreamReader(is));
			String line = null;
			int count = 0;
			while ((line = reader.readLine()) != null) {
				if (count == lineNumber) {
					reader.close();
					return line;
				}
				count++;
			}
			reader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "";
	}

	public int getNumberOfLines(String folder, String filename) {
		int count = 0;
		try {
			Uri fileUri = findFileUri(folder, filename);  // use your existing helper
			if (fileUri == null) return 0;
			InputStream is = getContentResolver().openInputStream(fileUri);
			BufferedReader reader = new BufferedReader(new InputStreamReader(is));
			while (reader.readLine() != null) {
				count++;
			}
			reader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return count;
	}

    // Delete file via MediaStore
    public boolean deleteFile(String folderName, String fileName) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                Log.w(TAG, "deleteFile: not found");
                return false;
            }
            int deleted = getContentResolver().delete(uri, null, null);
            Log.i(TAG, "deleteFile: deleted rows = " + deleted);
            return deleted > 0;
        } catch (Exception e) {
            Log.e(TAG, "deleteFile failed", e);
            return false;
        }
    }

	// Helper: recursive delete
	private boolean deleteRecursive(File fileOrDirectory) {
		if (fileOrDirectory.isDirectory()) {
			File[] children = fileOrDirectory.listFiles();
			if (children != null) {
				for (File child : children) {
					deleteRecursive(child);
				}
			}
		}
		return fileOrDirectory.delete();
	}

	// Public method for Hollywood
	public boolean deleteFolder(String folderName) {
		try {
			File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), folderName);
			if (dir.exists() && dir.isDirectory()) {
				return deleteRecursive(dir);
			} else {
				return false; // folder does not exist
			}
		} catch (Exception e) {
			Log.e("HollywoodDelegate", "Error deleting folder: " + e.getMessage());
			return false;
		}
	}

    // Copy internal (getFilesDir()/internalPath) -> external Downloads/folderName/fileName
    public boolean copyFileToExternal(String internalRelativePath, String folderName, String fileName) {
        try {
            File src = new File(getFilesDir(), internalRelativePath);
            if (!src.exists()) {
                Log.w(TAG, "copyFileToExternal: source not found: " + src.getAbsolutePath());
                return false;
            }

            ContentResolver resolver = getContentResolver();
            Uri dstUri = findFileUri(folderName, fileName);

            if (dstUri == null) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
                values.put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream");
                values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/");
                values.put("is_pending", 1);
                dstUri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
            }

            if (dstUri == null) {
                Log.e(TAG, "copyFileToExternal: insert returned null");
                return false;
            }

            FileInputStream in = null;
            OutputStream out = null;
            try {
                in = new FileInputStream(src);
                out = resolver.openOutputStream(dstUri, "w");
                if (out == null) throw new IOException("openOutputStream returned null");
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
                out.flush();
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            ContentValues cv = new ContentValues();
            cv.put("is_pending", 0);
            resolver.update(dstUri, cv, null, null);

            Log.i(TAG, "copyFileToExternal success");
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToExternal failed", e);
            return false;
        }
    }

    // Copy external Downloads/folderName/fileName -> internal getFilesDir()/internalRelativePath
    public boolean copyFileFromExternal(String folderName, String fileName, String internalRelativePath) {
        try {
            Uri srcUri = findFileUri(folderName, fileName);
            if (srcUri == null) {
                Log.w(TAG, "copyFileFromExternal: not found");
                return false;
            }

            InputStream in = null;
            FileOutputStream out = null;
            try {
                in = getContentResolver().openInputStream(srcUri);
                if (in == null) throw new IOException("openInputStream returned null");
                File dest = new File(getFilesDir(), internalRelativePath);
                File parent = dest.getParentFile();
                if (parent != null && !parent.exists()) parent.mkdirs();
                out = new FileOutputStream(dest);
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
                out.flush();
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }
            Log.i(TAG, "copyFileFromExternal success -> " + internalRelativePath);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromExternal failed", e);
            return false;
        }
    }

    // List files in Downloads/<folderName> via MediaStore; newline-separated string
    public String listFilesInFolder(String folderName) {
        try {
            String relPath = Environment.DIRECTORY_DOWNLOADS + "/" + folderName + "/";
            Uri collection = MediaStore.Files.getContentUri("external");
            String[] projection = new String[]{ MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE };
            String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
            String[] selectionArgs = new String[]{ relPath };

            Cursor c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
            if (c == null) {
                Log.w(TAG, "listFilesInFolder: cursor null");
                return "";
            }
            StringBuilder sb = new StringBuilder();
            try {
                while (c.moveToNext()) {
                    String name = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME));
                    String mime = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE));
                    sb.append(name).append(" (").append(mime).append(")").append("\n");
                }
            } finally {
                c.close();
            }
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "listFilesInFolder failed", e);
            return "";
        }
    }
}
User avatar
airsoftsoftwair
Posts: 5830
Joined: Fri Feb 12, 2010 2:33 pm
Location: Germany
Contact:

Re: Scoped storage methods for Android 10+

Post by airsoftsoftwair »

Quite impressive that you managed to inject that into Hollywood using custom code, kudos :)
xabierpayet
Posts: 270
Joined: Fri Feb 24, 2012 9:34 am

Re: Scoped storage methods for Android 10+

Post by xabierpayet »

i need test it ASAP, thanks
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Re: Scoped storage methods for Android 10+

Post by amyren »

xabierpayet wrote: Sun Oct 12, 2025 12:57 am i need test it ASAP, thanks
There are most likely room for improvement. I came across a couple issues that needed adjustments. Will post a modified version of the java file tomorrow.
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Re: Scoped storage methods for Android 10+

Post by amyren »

The methods mentioned above are mostly working, but as said earlier there are room for improvement, and there also might be bugs I'm not aware of.

In my case I tried by using the method for storing files in the mediastorage area. Some of the issues I found when putting the scrips into real usage was permission. If a file is not owned by the program using it you may need additional permissions to make things work as planned. In my case I tried to import a settings file and while the file was found as existing it reported it to be empty, and also deleting it did not work before enabling more permissions.
My previous test scripts was handling normal text files with the .txt extension, and it worked well in my tests. But when I tried using other extensions the java scripts ended up adding the .txt extension to the files anyway when processing them.

Here is the modified version of my java script for handling files in mediastorage. There are some new commands in there as well used to control the screen timeout, so that you can prevent the screen going off while your program is active.

Code: Select all

package com.myproject.test;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.content.Intent;
import android.view.WindowManager;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;


/**
 * HollywoodDelegate - MediaStore-friendly file utilities.
 * Must be included as Custom Code in the Hollywood APK Compiler.
 */
public class HollywoodDelegate extends HollywoodActivity {

    private static final String TAG = "HollywoodDelegate";

    // Helper: find file Uri in MediaStore (Documents/<folderName>/)
    private Uri findFileUri(String folderName, String fileName) {
        try {
            String relPath = Environment.DIRECTORY_DOCUMENTS + "/" + folderName + "/";
            Uri collection = MediaStore.Files.getContentUri("external");
            String[] projection = new String[]{ MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME };
            String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=? AND " + MediaStore.MediaColumns.DISPLAY_NAME + "=?";
            String[] selectionArgs = new String[]{ relPath, fileName };

            Cursor c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
            if (c != null) {
                try {
                    if (c.moveToFirst()) {
                        long id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                        String actualName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME));
                        Log.i(TAG, "findFileUri: found " + actualName + " with ID " + id);
                        return ContentUris.withAppendedId(collection, id);
                    }
                } finally {
                    c.close();
                }
            }

            // Also check for files with .txt extension that might have been auto-added
            if (!fileName.toLowerCase().endsWith(".txt")) {
                selection = MediaStore.MediaColumns.RELATIVE_PATH + "=? AND " + MediaStore.MediaColumns.DISPLAY_NAME + "=?";
                selectionArgs = new String[]{ relPath, fileName + ".txt" };
                
                c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
                if (c != null) {
                    try {
                        if (c.moveToFirst()) {
                            long id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                            String actualName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME));
                            Log.w(TAG, "findFileUri: found file with .txt extension: " + actualName);
                            return ContentUris.withAppendedId(collection, id);
                        }
                    } finally {
                        c.close();
                    }
                }
            }

            // Fallback: check directly on filesystem
            File file = new File(Environment.getExternalStoragePublicDirectory(
                    Environment.DIRECTORY_DOCUMENTS + "/" + folderName), fileName);
            if (file.exists()) {
                Log.w(TAG, "findFileUri: MediaStore record not found, but file exists on filesystem: " + fileName);
                return Uri.fromFile(file);
            }

        } catch (Exception e) {
            Log.e(TAG, "findFileUri failed", e);
        }
        return null;
    }

   // --- PERMISSION METHODS ---

    // Check if MANAGE_EXTERNAL_STORAGE is granted
    public boolean hasAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            return Environment.isExternalStorageManager();
        }
        return true; // On Android 10 and below, WRITE_EXTERNAL_STORAGE is enough
    }

    // Open settings page for granting MANAGE_EXTERNAL_STORAGE
    public void requestAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            try {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivity(intent);
            } catch (Exception e) {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);
            }
        }
    }

    // Check if a file exists in the given folder
    public boolean fileExists(String folderName, String fileName) {
        try {
            // PRIMARY: Direct filesystem check (immediate, no MediaStore delay)
            File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), folderName);
            File file = new File(dir, fileName);
            if (file.exists() && file.isFile()) {
                Log.i(TAG, "fileExists: DIRECT found: " + folderName + "/" + fileName);
                return true;
            }
            
            // SECONDARY: MediaStore check (for cases where file might only be in MediaStore)
            Uri uri = findFileUri(folderName, fileName);
            if (uri != null) {
                Log.i(TAG, "fileExists: MEDIASTORE found: " + folderName + "/" + fileName);
                return true;
            }
            
            Log.i(TAG, "fileExists: not found: " + folderName + "/" + fileName);
            return false;
            
        } catch (Exception e) {
            Log.e("HollywoodDelegate", "Error checking file existence: " + e.getMessage());
            return false;
        }
    }

    // Check if a folder exists
    public boolean folderExists(String folderName) {
        try {
            File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), folderName);
            return dir.exists() && dir.isDirectory();
        } catch (Exception e) {
            Log.e("HollywoodDelegate", "Error checking folder existence: " + e.getMessage());
            return false;
        }
    }

    // Create folder by inserting a placeholder into MediaStore (shows folder)
    public boolean createFolder(String folderName) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DISPLAY_NAME, ".nomedia");
                values.put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream");
                values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + "/" + folderName + "/");
                values.put("is_pending", 1);

                ContentResolver resolver = getContentResolver();
                Uri uri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
                if (uri == null) {
                    Log.e(TAG, "createFolder: insert returned null");
                    return false;
                }
                // mark not pending
                ContentValues cv = new ContentValues();
                cv.put("is_pending", 0);
                resolver.update(uri, cv, null, null);
                Log.i(TAG, "createFolder (MediaStore) created: " + folderName);
                return true;
            } else {
                File docs = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
                File folder = new File(docs, folderName);
                if (!folder.exists() && !folder.mkdirs()) {
                    Log.e(TAG, "createFolder: mkdirs failed: " + folder.getAbsolutePath());
                    return false;
                }
                // create .nomedia file
                File nm = new File(folder, ".nomedia");
                try {
                    if (!nm.exists()) nm.createNewFile();
                } catch (IOException e) {
                    Log.w(TAG, "createFolder: .nomedia create failed", e);
                }
                Log.i(TAG, "createFolder (legacy) created: " + folder.getAbsolutePath());
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "createFolder failed", e);
            return false;
        }
    }

    // Create or overwrite a file with text content (uses MediaStore on Q+)
    public boolean createFile(String folderName, String fileName, String content) {
        try {
            ContentResolver resolver = getContentResolver();
            
            // First, delete any existing file (including .txt variants)
            Uri existingUri = findFileUri(folderName, fileName);
            if (existingUri != null) {
                int deleted = resolver.delete(existingUri, null, null);
                Log.i(TAG, "createFile: deleted existing file, rows = " + deleted);
            }
            
            // Also check and delete .txt version if it exists
            if (!fileName.toLowerCase().endsWith(".txt")) {
                Uri txtUri = findFileUri(folderName, fileName + ".txt");
                if (txtUri != null) {
                    int deleted = resolver.delete(txtUri, null, null);
                    Log.i(TAG, "createFile: deleted .txt variant, rows = " + deleted);
                }
            }

            // Create new file entry - use octet-stream to prevent auto .txt extension
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream"); // Changed from text/plain
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + "/" + folderName + "/");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                values.put("is_pending", 1);
            }

            Uri newUri = resolver.insert(MediaStore.Files.getContentUri("external"), values);
            if (newUri == null) {
                Log.e(TAG, "createFile: insert returned null");
                return false;
            }

            // Write content
            OutputStream out = null;
            try {
                out = resolver.openOutputStream(newUri);
                if (out == null) throw new IOException("openOutputStream returned null");
                if (content != null) {
                    out.write(content.getBytes("UTF-8"));
                }
                out.flush();
            } finally {
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues cv = new ContentValues();
                cv.put("is_pending", 0);
                resolver.update(newUri, cv, null, null);
            }
            
            Log.i(TAG, "createFile: successfully created " + fileName);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFile failed", e);
            return false;
        }
    }

    // Append to file (reads existing and writes combined content)
    public boolean appendToFile(String folderName, String fileName, String content) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                // create new file
                return createFile(folderName, fileName, content);
            }

            ContentResolver resolver = getContentResolver();
            // read existing
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            InputStream in = null;
            try {
                in = resolver.openInputStream(uri);
                if (in != null) {
                    byte[] buf = new byte[4096];
                    int r;
                    while ((r = in.read(buf)) > 0) baos.write(buf, 0, r);
                }
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
            }

            byte[] existing = baos.toByteArray();
            byte[] add = (content != null) ? content.getBytes("UTF-8") : new byte[0];
            byte[] combined = new byte[existing.length + add.length];
            System.arraycopy(existing, 0, combined, 0, existing.length);
            System.arraycopy(add, 0, combined, existing.length, add.length);

            OutputStream out = null;
            try {
                out = resolver.openOutputStream(uri, "w");
                if (out == null) throw new IOException("openOutputStream returned null");
                out.write(combined);
                out.flush();
            } finally {
                if (out != null) try { out.close(); } catch (IOException ignored) {}
            }

            Log.i(TAG, "appendToFile success: " + fileName);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFile failed", e);
            return false;
        }
    }

    // Read file (UTF-8) - returns empty string if not found or on error
    public String readFile(String folderName, String fileName) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                Log.w(TAG, "readFile: not found via MediaStore: " + folderName + "/" + fileName);
                return "";
            }
            
            ContentResolver resolver = getContentResolver();
            InputStream in = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                in = resolver.openInputStream(uri);
                if (in == null) {
                    Log.w(TAG, "readFile: cannot open input stream for " + fileName);
                    return "";
                }
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) baos.write(buf, 0, r);
            } finally {
                if (in != null) try { in.close(); } catch (IOException ignored) {}
            }

            byte[] data = baos.toByteArray();
            if (data.length == 0) {
                Log.w(TAG, "readFile: empty file " + fileName);
                return "";
            }

            // Use UTF-8 encoding
            String text = new String(data, "UTF-8");
            
            // Normalize line endings
            text = text.replace("\r\n", "\n");
            
            Log.i(TAG, "readFile: successfully read " + data.length + " bytes from " + fileName);
            return text;

        } catch (Exception e) {
            Log.e(TAG, "readFile failed for " + folderName + "/" + fileName, e);
            return "";
        }
    }

    public String readLineFromFile(String folder, String filename, int lineNumber) {
        try {
            String content = readFile(folder, filename);
            if (content.isEmpty()) return "";
            
            String[] lines = content.split("\n", -1); // keep trailing empty lines
            if (lineNumber >= 0 && lineNumber < lines.length) {
                return lines[lineNumber];
            }
            return "";
        } catch (Exception e) {
            Log.e(TAG, "readLineFromFile failed", e);
            return "";
        }
    }

    public int getNumberOfLines(String folder, String filename) {
        try {
            String content = readFile(folder, filename);
            if (content.isEmpty()) return 0;
            
            String[] lines = content.split("\n", -1); // keep trailing empty lines
            return lines.length;
        } catch (Exception e) {
            Log.e(TAG, "getNumberOfLines failed", e);
            return 0;
        }
    }

    // Delete file via MediaStore
    public boolean deleteFile(String folderName, String fileName) {
        try {
            Uri uri = findFileUri(folderName, fileName);
            if (uri == null) {
                Log.w(TAG, "deleteFile: not found: " + folderName + "/" + fileName);
                return false;
            }
            int deleted = getContentResolver().delete(uri, null, null);
            Log.i(TAG, "deleteFile: deleted rows = " + deleted);
            return deleted > 0;
        } catch (Exception e) {
            Log.e(TAG, "deleteFile failed", e);
            return false;
        }
    }

    // Helper: recursive delete
    private boolean deleteRecursive(File fileOrDirectory) {
        if (fileOrDirectory.isDirectory()) {
            File[] children = fileOrDirectory.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteRecursive(child);
                }
            }
        }
        return fileOrDirectory.delete();
    }

    // Public method for Hollywood
    public boolean deleteFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), folderName);
            if (dir.exists() && dir.isDirectory()) {
                return deleteRecursive(dir);
            } else {
                return false; // folder does not exist
            }
        } catch (Exception e) {
            Log.e("HollywoodDelegate", "Error deleting folder: " + e.getMessage());
            return false;
        }
    }

	// Copy internal (getFilesDir()/internalPath) -> external Documents/folderName/fileName
	public boolean copyFileToExternal(String internalRelativePath, String folderName, String fileName) {
		FileInputStream in = null;
		FileOutputStream out = null;
		try {
			File src = new File(getFilesDir(), internalRelativePath);
			if (!src.exists()) {
				Log.w(TAG, "copyFileToExternal: source not found: " + src.getAbsolutePath());
				return false;
			}

			// FIXED: Use Documents directory instead of storage root
			File dstDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), folderName);
			if (!dstDir.exists() && !dstDir.mkdirs()) {
				Log.e(TAG, "copyFileToExternal: failed to create destination directory");
				return false;
			}

			File dst = new File(dstDir, fileName);

			in = new FileInputStream(src);
			out = new FileOutputStream(dst);
			
			byte[] buf = new byte[4096];
			int r;
			while ((r = in.read(buf)) > 0) {
				out.write(buf, 0, r);
			}
			out.flush();
			
			Log.i(TAG, "copyFileToExternal: successfully copied " + src.getAbsolutePath() + " to " + dst.getAbsolutePath());
			return true;
			
		} catch (Exception e) {
			Log.e(TAG, "copyFileToExternal failed", e);
			return false;
		} finally {
			// Close streams in finally block to ensure they're always closed
			if (in != null) {
				try { in.close(); } catch (IOException ignored) {}
			}
			if (out != null) {
				try { out.close(); } catch (IOException ignored) {}
			}
		}
	}

	// Copy external Documents/folderName/fileName -> internal getFilesDir()/internalRelativePath
	public boolean copyFileFromExternal(String folderName, String fileName, String internalRelativePath) {
		FileInputStream in = null;
		FileOutputStream out = null;
		try {
			// FIXED: Use Documents directory instead of storage root
			File src = new File(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), folderName), fileName);
			if (!src.exists() || !src.canRead()) {
				Log.w(TAG, "copyFileFromExternal: source file not found or not readable");
				return false;
			}

			File dest = new File(getFilesDir(), internalRelativePath);
			File parent = dest.getParentFile();
			if (parent != null && !parent.exists()) parent.mkdirs();

			in = new FileInputStream(src);
			out = new FileOutputStream(dest);
			
			byte[] buf = new byte[4096];
			int r;
			while ((r = in.read(buf)) > 0) {
				out.write(buf, 0, r);
			}
			out.flush();
			
			Log.i(TAG, "copyFileFromExternal success -> " + internalRelativePath);
			return true;
		} catch (Exception e) {
			Log.e(TAG, "copyFileFromExternal failed", e);
			return false;
		} finally {
			// Close streams in finally block
			if (in != null) {
				try { in.close(); } catch (IOException ignored) {}
			}
			if (out != null) {
				try { out.close(); } catch (IOException ignored) {}
			}
		}
	}

    // List files in Documents/<folderName> via MediaStore; newline-separated string
    public String listFilesInFolder(String folderName) {
        try {
            String relPath = Environment.DIRECTORY_DOCUMENTS + "/" + folderName + "/";
            Uri collection = MediaStore.Files.getContentUri("external");
            String[] projection = new String[]{ MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE };
            String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
            String[] selectionArgs = new String[]{ relPath };

            Cursor c = getContentResolver().query(collection, projection, selection, selectionArgs, null);
            if (c == null) {
                Log.w(TAG, "listFilesInFolder: cursor null");
                return "";
            }
            StringBuilder sb = new StringBuilder();
            try {
                while (c.moveToNext()) {
                    String name = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME));
                    String mime = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE));
                    sb.append(name).append(" (").append(mime).append(")").append("\n");
                }
            } finally {
                c.close();
            }
            String result = sb.toString().trim();
            Log.i(TAG, "listFilesInFolder: found files: " + result.replace("\n", ", "));
            return result;
        } catch (Exception e) {
            Log.e(TAG, "listFilesInFolder failed", e);
            return "";
        }
    }

	// ================================================================
	// 🔆 KEEP SCREEN ON 
	// ================================================================

	private boolean screenShouldStayOn = false;

	// Keep screen on - call this repeatedly if needed
	public void keepScreenOn() {
		try {
			runOnUiThread(new Runnable() {
				@Override
				public void run() {
					getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
					screenShouldStayOn = true;
					Log.i(TAG, "Screen keep-on flag set");
				}
			});
		} catch (Exception e) {
			Log.e(TAG, "keepScreenOn failed", e);
		}
	}

	// Allow screen to turn off normally
	public void allowScreenOff() {
		try {
			runOnUiThread(new Runnable() {
				@Override
				public void run() {
					getWindow().clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
					screenShouldStayOn = false;
					Log.i(TAG, "Screen keep-on flag cleared");
				}
			});
		} catch (Exception e) {
			Log.e(TAG, "allowScreenOff failed", e);
		}
	}

	// Force screen on and wake up device (requires WAKE_LOCK permission)
	public void wakeUpScreen() {
		try {
			// This is more aggressive - wakes up the screen if it's off
			android.view.Window window = getWindow();
			window.addFlags(android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
						   android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
						   android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
			Log.i(TAG, "Screen woken up and kept on");
		} catch (Exception e) {
			Log.e(TAG, "wakeUpScreen failed", e);
		}
	}

	// Check if screen is currently kept on
	public boolean isScreenKeptOn() {
		try {
			int flags = getWindow().getAttributes().flags;
			return (flags & android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0;
		} catch (Exception e) {
			Log.e(TAG, "isScreenKeptOn failed", e);
			return false;
		}
	}
}
I use a custom AndoridManifest.xml. Perhaps part of this xml is not needed in this case, but at least include the storage permissions

Code: Select all

<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
        package="%PACKAGE%">

    <uses-feature android:glEsVersion="0x00020000" />
    <uses-feature android:name="android.hardware.camera" android:required="false" />    
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.VIBRATE"/> 
             
    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
        
        <!-- NB: for some reason Lint returns a wrong "MissingClass" for both HollywoodDelegate and
             FileRequest class so we disable checking for this error. As soon as this problem is
             fixed in Lint, we should remove 'tools:ignore="MissingClass"' again because obviously
             we want Lint to do its checks but as of September 2021, this seems to be broken -->
                 
        <activity android:name=".HollywoodDelegate"
                android:label="@string/app_name"
                android:configChanges="orientation|screenSize"
                android:exported="true"
		android:theme="@style/%THEME%"
		tools:ignore="MissingClass">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".FileRequest" android:configChanges="orientation|screenSize" android:theme="@style/%THEME%" tools:ignore="MissingClass"/> 
        
	<provider
        	android:name="androidx.core.content.FileProvider"
        	android:authorities="${applicationId}.fileprovider"
        	android:exported="false"
        	android:grantUriPermissions="true">
        	<meta-data
            		android:name="android.support.FILE_PROVIDER_PATHS"
            		android:resource="@xml/file_paths"></meta-data>
    	</provider>                   
    </application>

</manifest> 
<!-- END_INCLUDE(manifest) -->
I also have another version that includes Camera usage, Sharing (text or image), GPS data and vibration. Let me know if it is of any interest.
xabierpayet
Posts: 270
Joined: Fri Feb 24, 2012 9:34 am

Re: Scoped storage methods for Android 10+

Post by xabierpayet »

Yes it´s very usefull for me at least, thank you again
amyren
Posts: 410
Joined: Thu May 02, 2019 11:53 am

Re: Scoped storage methods for Android 10+

Post by amyren »

xabierpayet wrote: Thu Oct 16, 2025 11:38 pm Yes it´s very usefull for me at least, thank you again
Ok, here is a hollywood script that will first ask for the required permissions, then open the camera for you to take a picture.
Then the picture should be stored on mediastorage (Pictures/HollywoodPhotos) and the path to the picture is copied into a string.
Picture will show up in the app background
Current GPS location should be shown on screen
gps location link will be copied into your clipboard
Next it vibrate and then ask for sharing method
You must manually paste the gps link into the message you like to share

Code: Select all

@DISPLAY {Title = "Android Features Test", ScaleMode = #SCALEMODE_AUTO, FitScale = True, SmoothScale = True, HideTitleBar = True}

; Check if camera is available
If Not CallJavaMethod("isCameraAvailable", {ReturnType=#BOOLEAN})
    NPrint("No camera app available on this device!")
    WaitLeftMouse
    End
EndIf

; Check camera permission
If Not CallJavaMethod("hasCameraPermission", {ReturnType=#BOOLEAN})
    NPrint("Camera permission needed")
    NPrint("Please allow camera permission, then return and press to continue")
    WaitLeftMouse
    CallJavaMethod("requestCameraPermission", {ReturnType=#VOID})
EndIf

; Check storage permission
If Not CallJavaMethod("hasAllFilesAccess", {ReturnType=#BOOLEAN})
    NPrint("Storage permission needed for saving photos")
    NPrint("Please grant storage permission, then return and press to continue")
    WaitLeftMouse
    CallJavaMethod("requestAllFilesAccess", {ReturnType=#VOID})
EndIf

; Check position permission
If Not CallJavaMethod("hasLocationPermission", {ReturnType=#BOOLEAN})
    NPrint("Loaction permission needed")
    NPrint("Please allow loaction permission, then return and press to continue")
    WaitLeftMouse
    CallJavaMethod("requestLocationPermission", {ReturnType=#VOID})
EndIf

; Open camera
    NPrint("Take a picture and return to the app...")
    WaitLeftMouse
	result$ = CallJavaMethod("openCamera", {ReturnType=#STRING})
	NPrint("Camera result: " .. result$)
	If result$ = "camera_opened"
    
    ; Get photo path
    photoPath$ = CallJavaMethod("getLastPhotoPath", {ReturnType=#STRING})
    
    If photoPath$ <> "" And Exists(photoPath$)
	NPrint("Photo saved: " .. photoPath$)
	LoadBrush(1, photoPath$)
	DisplayBrush(1, 0, 0, {Width=640, Height=480})
	NPrint("Photo displayed!")
	WaitLeftMouse
	FreeBrush(1)
    Else
	NPrint("No photo returned or file not found")
    EndIf
Else
    NPrint("Failed to open camera: " .. result$)
EndIf

; Get basic location
location$ = CallJavaMethod("getCurrentLocation", {ReturnType=#STRING})
NPrint("Current location: " .. location$)

; Safe coordinate parsing
If location$ <> "permission_denied" And location$ <> "location_disabled" And location$ <> "no_location_available" And location$ <> "error" And location$ <> "service_unavailable"
    ; Find the comma position
    commaPos = FindStr(location$, ",")
    
    If commaPos > 0
	; Extract latitude and longitude correctly
	lat$ = LeftStr(location$, commaPos - 1)
	lon$ = MidStr(location$, commaPos + 1)
	
	; Convert to numbers (decimal degrees)
	lat_dec = ToNumber(lat$)
	lon_dec = ToNumber(lon$)
	
	NPrint("Decimal degrees:")
	NPrint("Latitude: " .. lat_dec)
	NPrint("Longitude: " .. lon_dec)

	; Convert to Degrees-Minutes-Seconds
	; Latitude conversion
	lat_deg = Int(lat_dec)
	lat_min_frac = (lat_dec - lat_deg) * 60
	lat_min = Int(lat_min_frac)
	lat_sec = (lat_min_frac - lat_min) * 60
	
	; Longitude conversion  
	lon_deg = Int(lon_dec)
	lon_min_frac = (lon_dec - lon_deg) * 60
	lon_min = Int(lon_min_frac)
	lon_sec = (lon_min_frac - lon_min) * 60
	
	; Manual decimal formatting for seconds - ensure 1 decimal place always
	lat_sec_decimal = Round(lat_sec * 10) / 10
	lon_sec_decimal = Round(lon_sec * 10) / 10
	
	; Convert to string and force one decimal place
	lat_sec_str$ = StrStr(lat_sec_decimal)
	lon_sec_str$ = StrStr(lon_sec_decimal)
	
	; Force one decimal place by checking for decimal point
	If FindStr(lat_sec_str$, ".") = 0
	    lat_sec_str$ = lat_sec_str$ .. ".0"
	EndIf
	If FindStr(lon_sec_str$, ".") = 0
	    lon_sec_str$ = lon_sec_str$ .. ".0"
	EndIf
	
	; Also handle case where we have decimal but no digit after it
	dotPos = FindStr(lat_sec_str$, ".")
	If dotPos > 0 And dotPos = StrLen(lat_sec_str$)
	    lat_sec_str$ = lat_sec_str$ .. "0"
	EndIf
	
	dotPos = FindStr(lon_sec_str$, ".")
	If dotPos > 0 And dotPos = StrLen(lon_sec_str$)
	    lon_sec_str$ = lon_sec_str$ .. "0"
	EndIf        

	; Determine hemispheres
	lat_hemisphere = "N"
	If lat_dec < 0
	    lat_hemisphere = "S"
	    lat_deg = -lat_deg
	EndIf
	
	lon_hemisphere = "E" 
	If lon_dec < 0
	    lon_hemisphere = "W"
	    lon_deg = -lon_deg
	EndIf
	
	; Format as DMS with decimal seconds
	lat_dms$ = lat_deg .. "°" .. lat_min .. "'" .. lat_sec_str$ .. "\"" .. lat_hemisphere
	lon_dms$ = lon_deg .. "°" .. lon_min .. "'" .. lon_sec_str$ .. "\"" .. lon_hemisphere
	
	NPrint("Degrees-Minutes-Seconds (with decimals):")
	NPrint("Latitude: " .. lat_dms$)
	NPrint("Longitude: " .. lon_dms$)
	
	; Also show whole seconds version for comparison
	lat_sec_whole = Round(lat_sec)
	lon_sec_whole = Round(lon_sec)
	
	lat_dms_whole$ = lat_deg .. "°" .. lat_min .. "'" .. lat_sec_whole .. "\"" .. lat_hemisphere
	lon_dms_whole$ = lon_deg .. "°" .. lon_min .. "'" .. lon_sec_whole .. "\"" .. lon_hemisphere
	
	NPrint("Whole seconds:")
	NPrint("Latitude: " .. lat_dms_whole$)
	NPrint("Longitude: " .. lon_dms_whole$)
	
    Else
	NPrint("No comma found in location string")
    EndIf
Else
    NPrint("Location not available: " .. location$)
EndIf

NPrint("GPS conversion completed!")
WaitLeftMouse

; Skip detailed location For now To test If this fixes the crash
 detailed$ = CallJavaMethod("getDetailedLocation", {ReturnType=#STRING})
 NPrint("Detailed location: " .. detailed$)

WaitLeftMouse

; Create Google Maps link from GPS coordinates
mapslink$ = "http://google.com/maps/search/?api=1&query=" .. lat_dec .. "," .. lon_dec

; Copy maps link to clipboard
CallJavaMethod("copyToClipboard", {ReturnType=#VOID}, #STRING, mapslink$)
NPrint("Location link copied to clipboard!")

;; --- Optional: test vibration ---
CallJavaMethod("vibrateNow", {ReturnType=#VOID}, #INTEGER, 300)
;

; User-friendly sharing options
sel = SystemRequest("Choose sharing option", "Share photo or link or both\n(link shared via Clipboard, paste manually)", "Photo Only|Link Only|Both|Skip")

Switch sel
Case 1:
    ; Share photo only
    CallJavaMethod("shareImage", {ReturnType=#VOID}, #STRING, photoPath$)
    NPrint("Photo shared!")
    
Case 2:
    ; Share location link only  
    CallJavaMethod("shareText", {ReturnType=#VOID}, #STRING, mapslink$)
    NPrint("Location link shared!")
    
Case 3:
    ; Share both - copy link to clipboard and share photo
    CallJavaMethod("copyToClipboard", {ReturnType=#VOID}, #STRING, mapslink$)
    NPrint("Location link copied to clipboard!")
    NPrint("Now sharing photo - you can paste the link")
    CallJavaMethod("shareImage", {ReturnType=#VOID}, #STRING, photoPath$)
    
Case 0:
    ; User cancelled
    NPrint("Sharing cancelled")
EndSwitch

NPrint("Done! Press to exit.")
WaitLeftMouse
The HollywoodDelegate.java
remember to edit package name

Code: Select all

package com.yourpackage.test; 

// Core Android functionality
import android.content.Intent;                   // Camera, GPS, Sharing, Permissions
import android.net.Uri;                          // Camera, Sharing, File operations
import android.os.Build;                         // Version checks throughout
import android.os.Environment;                   // File operations, SD card, Storage
import android.util.Log;                         // Logging throughout
import android.app.Activity;                     // Base class, context

// Permissions and hardware features
import android.Manifest;                         // Camera, Location permissions
import android.content.pm.PackageManager;        // Permission checking
import android.location.Location;                // GPS functionality
import android.location.LocationManager;         // GPS functionality
import android.media.MediaScannerConnection;     // Camera file scanning
import android.os.Vibrator;                      // Vibration (currently commented out)
import android.provider.MediaStore;              // Camera, MediaStore operations
import android.widget.Toast;                     // Toast messages (optional)
import android.content.Context;                  // System services
import android.content.ContentValues;            // MediaStore operations
import android.media.MediaScannerConnection;     // Camera file scanning (duplicate)
import android.content.ClipboardManager;         // Clipboard functions
import android.content.ClipData;                 // Clipboard functions
import android.view.WindowManager;               // Keep Screen On

// AndroidX libraries
import androidx.annotation.Nullable;             // Nullable annotations
import androidx.core.app.ActivityCompat;         // Permission requests
import androidx.core.content.ContextCompat;      // Permission checking
import androidx.core.content.FileProvider;       // File sharing

// Java utilities
import java.text.SimpleDateFormat;               // Camera timestamp
import java.util.Date;                           // Camera timestamp
import java.util.Locale;                         // String formatting
import java.util.List;                           // GPS location providers
import java.util.ArrayList;                      // Multiple permissions requests

// File I/O operations
import java.io.File;                             // All file operations
import java.io.FileInputStream;                  // File reading, copy operations
import java.io.FileOutputStream;                 // File writing, copy operations
import java.io.IOException;                      // Exception handling
import java.io.BufferedReader;                   // File reading
import java.io.InputStreamReader;                // File reading

/**
 * HollywoodDelegate - Direct external storage file utilities.
 * Must be included as Custom Code in the Hollywood APK Compiler.
 */
public class HollywoodDelegate extends HollywoodActivity {

    private static final String TAG = "HollywoodDelegate";

    // --- Add SD card helper HERE (inside the class, not before imports)
    private File getExternalSdCard() {
        File[] externalDirs = getApplicationContext().getExternalFilesDirs(null);
        if (externalDirs != null && externalDirs.length > 1) {
            for (File dir : externalDirs) {
                if (dir != null && Environment.isExternalStorageRemovable(dir)) {
                    return dir.getParentFile().getParentFile().getParentFile().getParentFile();
                }
            }
        }
        return null;
    }

    // --- PERMISSION METHODS ---

    // Check if MANAGE_EXTERNAL_STORAGE is granted
    public boolean hasAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            return Environment.isExternalStorageManager();
        }
        return true; // On Android 10 and below, WRITE_EXTERNAL_STORAGE is enough
    }

    // Open settings page for granting MANAGE_EXTERNAL_STORAGE
    public void requestAllFilesAccess() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            try {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                intent.setData(Uri.parse("package:" + getPackageName()));
                startActivity(intent);
            } catch (Exception e) {
                Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);
            }
        }
    }

    // --- FOLDER METHODS ---

    // Check if folder exists
    public boolean folderExists(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            return dir.exists() && dir.isDirectory();
        } catch (Exception e) {
            Log.e(TAG, "Error checking folder existence: " + e.getMessage());
            return false;
        }
    }

    // Create folder
    public boolean createFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists()) {
                if (!dir.mkdirs()) return false;
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFolder failed: " + e.getMessage());
            return false;
        }
    }

    // Delete folder recursively
    public boolean deleteFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() || !dir.isDirectory()) return false;
            return deleteRecursive(dir);
        } catch (Exception e) {
            Log.e(TAG, "deleteFolder failed: " + e.getMessage());
            return false;
        }
    }

    private boolean deleteRecursive(File fileOrDirectory) {
        if (fileOrDirectory.isDirectory()) {
            File[] children = fileOrDirectory.listFiles();
            if (children != null) {
                for (File child : children) deleteRecursive(child);
            }
        }
        return fileOrDirectory.delete();
    }

    // --- FILE METHODS ---

    // Check if file exists AND is readable
    public boolean fileExists(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            boolean exists = file.exists() && file.isFile();
            boolean readable = exists && file.canRead();
            Log.i(TAG, "fileExists: " + folderName + "/" + fileName + " - exists: " + exists + ", readable: " + readable);
            return exists && readable;
        } catch (Exception e) {
            Log.e(TAG, "fileExists failed: " + e.getMessage());
            return false;
        }
    }

    // Create or overwrite file
    public boolean createFile(String folderName, String fileName, String content) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() && !dir.mkdirs()) return false;
            File file = new File(dir, fileName);
            try (FileOutputStream fos = new FileOutputStream(file)) {
                if (content != null) fos.write(content.getBytes("UTF-8"));
                fos.flush();
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "createFile failed: " + e.getMessage());
            return false;
        }
    }

    // Read entire file
    public String readFile(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists() || !file.canRead()) return "";
            
            StringBuilder sb = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line).append("\n");
                }
            }
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "readFile failed: " + e.getMessage());
            return "";
        }
    }

    // Append to file
    public boolean appendToFile(String folderName, String fileName, String content) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return createFile(folderName, fileName, content);
            try (FileOutputStream fos = new FileOutputStream(file, true)) {
                if (content != null) fos.write(content.getBytes("UTF-8"));
                fos.flush();
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFile failed: " + e.getMessage());
            return false;
        }
    }

    // Delete file
    public boolean deleteFile(String folderName, String fileName) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists()) return false;
            return file.delete();
        } catch (Exception e) {
            Log.e(TAG, "deleteFile failed: " + e.getMessage());
            return false;
        }
    }

    // Read a specific line
    public String readLineFromFile(String folderName, String fileName, int lineNumber) {
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists() || !file.canRead()) return "";
            
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
                String line;
                int count = 0;
                while ((line = reader.readLine()) != null) {
                    if (count == lineNumber) {
                        return line;
                    }
                    count++;
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "readLineFromFile failed: " + e.getMessage());
        }
        return "";
    }

    // Count number of lines
    public int getNumberOfLines(String folderName, String fileName) {
        int count = 0;
        try {
            File file = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!file.exists() || !file.canRead()) return 0;
            
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
                while (reader.readLine() != null) count++;
            }
        } catch (Exception e) {
            Log.e(TAG, "getNumberOfLines failed: " + e.getMessage());
        }
        return count;
    }

    // List files in folder (newline-separated)
    public String listFilesInFolder(String folderName) {
        try {
            File dir = new File(Environment.getExternalStorageDirectory(), folderName);
            if (!dir.exists() || !dir.isDirectory()) return "";
            StringBuilder sb = new StringBuilder();
            File[] files = dir.listFiles();
            if (files != null) {
                for (File f : files) {
                    sb.append(f.getName());
                    if (f.isDirectory()) sb.append(" (dir)");
                    sb.append("\n");
                }
            }
            return sb.toString().trim();
        } catch (Exception e) {
            Log.e(TAG, "listFilesInFolder failed: " + e.getMessage());
            return "";
        }
    }

    // Copy internal -> external
    public boolean copyFileToExternal(String internalRelativePath, String folderName, String fileName) {
        try {
            File src = new File(getFilesDir(), internalRelativePath);
            if (!src.exists()) return false;
            File dst = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!dst.getParentFile().exists()) dst.getParentFile().mkdirs();
            
            try (FileInputStream in = new FileInputStream(src);
                 FileOutputStream out = new FileOutputStream(dst)) {
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
                out.flush();
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToExternal failed: " + e.getMessage());
            return false;
        }
    }

    // Copy external -> internal
    public boolean copyFileFromExternal(String folderName, String fileName, String internalRelativePath) {
        try {
            File src = new File(new File(Environment.getExternalStorageDirectory(), folderName), fileName);
            if (!src.exists() || !src.canRead()) return false;
            File dst = new File(getFilesDir(), internalRelativePath);
            if (!dst.getParentFile().exists()) dst.getParentFile().mkdirs();
            
            try (FileInputStream in = new FileInputStream(src);
                 FileOutputStream out = new FileOutputStream(dst)) {
                byte[] buf = new byte[4096];
                int r;
                while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
                out.flush();
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromExternal failed: " + e.getMessage());
            return false;
        }
    }
	
	// --- Create folder on SD card ---
	public boolean createFolderOnSDCard(String folderName) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return false;
		File folder = new File(sdRoot, folderName);
		return folder.exists() || folder.mkdirs();
	}

	// --- Create/overwrite file on SD card ---
	public boolean createFileOnSDCard(String folderName, String fileName, String content) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return false;
		try {
			File folder = new File(sdRoot, folderName);
			if (!folder.exists()) folder.mkdirs();
			File file = new File(folder, fileName);
			try (FileOutputStream fos = new FileOutputStream(file, false)) {
				fos.write(content.getBytes("UTF-8"));
				fos.flush();
			}
			return true;
		} catch (Exception e) {
			Log.e(TAG, "Error writing file to SD card", e);
			return false;
		}
	}

	// --- Read file from SD card ---
	public String readFileFromSDCard(String folderName, String fileName) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return "";
		try {
			File file = new File(new File(sdRoot, folderName), fileName);
			if (!file.exists() || !file.canRead()) return "";
			
			StringBuilder sb = new StringBuilder();
			try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
				String line;
				while ((line = reader.readLine()) != null) {
					sb.append(line).append("\n");
				}
			}
			return sb.toString().trim();
		} catch (Exception e) {
			Log.e(TAG, "Error reading file from SD card", e);
			return "";
		}
	}

	// --- Delete file from SD card ---
	public boolean deleteFileFromSDCard(String folderName, String fileName) {
		File sdRoot = getExternalSdCard();
		if (sdRoot == null) return false;
		File file = new File(new File(sdRoot, folderName), fileName);
		return file.exists() && file.delete();
	}
	
    // --- SD CARD OPERATIONS ---

    // Check if folder exists on SD card
    public boolean folderExistsOnSDCard(String folderName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dir = new File(sdCard, folderName);
        return dir.exists() && dir.isDirectory();
    }

    // Recursive delete helper for SD card
    private boolean deleteRecursiveFromSDCard(File fileOrDirectory) {
        if (fileOrDirectory.isDirectory()) {
            File[] children = fileOrDirectory.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteRecursiveFromSDCard(child);
                }
            }
        }
        return fileOrDirectory.delete();
    }

    // Delete folder from SD card
    public boolean deleteFolderFromSDCard(String folderName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dir = new File(sdCard, folderName);
        if (dir.exists() && dir.isDirectory()) {
            return deleteRecursiveFromSDCard(dir);
        }
        return false;
    }

    // Check if a file exists on SD card
    public boolean fileExistsOnSDCard(String folderName, String fileName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File file = new File(new File(sdCard, folderName), fileName);
        boolean exists = file.exists() && file.isFile();
        boolean readable = exists && file.canRead();
        Log.i(TAG, "fileExistsOnSDCard: " + folderName + "/" + fileName + " - exists: " + exists + ", readable: " + readable);
        return exists && readable;
    }

    // Append to file on SD card
    public boolean appendToFileOnSDCard(String folderName, String fileName, String content) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dir = new File(sdCard, folderName);
        if (!dir.exists()) dir.mkdirs();
        File file = new File(dir, fileName);
        try (FileOutputStream fos = new FileOutputStream(file, true)) {
            fos.write(content.getBytes("UTF-8"));
            fos.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "appendToFileOnSDCard failed", e);
            return false;
        }
    }

    // Read a specific line from a file on SD card
    public String readLineFromFileOnSDCard(String folderName, String fileName, int lineNumber) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return "";
        File file = new File(new File(sdCard, folderName), fileName);
        if (!file.exists() || !file.canRead()) return "";
        
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
            String line;
            int count = 0;
            while ((line = reader.readLine()) != null) {
                if (count == lineNumber) return line;
                count++;
            }
        } catch (Exception e) {
            Log.e(TAG, "readLineFromFileOnSDCard failed", e);
        }
        return "";
    }

    // Get number of lines in a file on SD card
    public int getNumberOfLinesOnSDCard(String folderName, String fileName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return 0;
        File file = new File(new File(sdCard, folderName), fileName);
        if (!file.exists() || !file.canRead()) return 0;
        
        int count = 0;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
            while (reader.readLine() != null) count++;
        } catch (Exception e) {
            Log.e(TAG, "getNumberOfLinesOnSDCard failed", e);
        }
        return count;
    }

    // List files in a folder on SD card
    public String listFilesInFolderOnSDCard(String folderName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return "";
        File dir = new File(sdCard, folderName);
        if (!dir.exists() || !dir.isDirectory()) return "";
        StringBuilder sb = new StringBuilder();
        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                sb.append(f.getName());
                if (f.isDirectory()) sb.append(" (dir)");
                sb.append("\n");
            }
        }
        return sb.toString().trim();
    }

    // Copy file from internal storage -> SD card
    public boolean copyFileToSDCard(String internalRelativePath, String folderName, String fileName) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File src = new File(getFilesDir(), internalRelativePath);
        if (!src.exists()) return false;
        File dst = new File(new File(sdCard, folderName), fileName);
        dst.getParentFile().mkdirs();
        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileToSDCard failed", e);
            return false;
        }
    }

    // Copy file from SD card -> internal storage
    public boolean copyFileFromSDCard(String folderName, String fileName, String internalRelativePath) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File src = new File(new File(sdCard, folderName), fileName);
        if (!src.exists() || !src.canRead()) return false;
        File dst = new File(getFilesDir(), internalRelativePath);
        dst.getParentFile().mkdirs();
        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFileFromSDCard failed", e);
            return false;
        }
    }

    // Copy file from External (primary shared storage) -> SD card
    public boolean copyFromExternalToSDCard(String externalFolder, String externalFile, String sdFolder, String sdFile) {
        File external = Environment.getExternalStorageDirectory();
        if (external == null) return false;
        File src = new File(new File(external, externalFolder), externalFile);
        if (!src.exists() || !src.canRead()) return false;

        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File dst = new File(new File(sdCard, sdFolder), sdFile);
        dst.getParentFile().mkdirs();

        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFromExternalToSDCard failed", e);
            return false;
        }
    }

    // Copy file from SD card -> External (primary shared storage)
    public boolean copyFromSDCardToExternal(String sdFolder, String sdFile, String externalFolder, String externalFile) {
        File sdCard = getExternalSdCard();
        if (sdCard == null) return false;
        File src = new File(new File(sdCard, sdFolder), sdFile);
        if (!src.exists() || !src.canRead()) return false;

        File external = Environment.getExternalStorageDirectory();
        if (external == null) return false;
        File dst = new File(new File(external, externalFolder), externalFile);
        dst.getParentFile().mkdirs();

        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[4096];
            int r;
            while ((r = in.read(buf)) > 0) out.write(buf, 0, r);
            out.flush();
            return true;
        } catch (Exception e) {
            Log.e(TAG, "copyFromSDCardToExternal failed", e);
            return false;
        }
    }

	// ================================================================
	// 📸 CAMERA CAPTURE (Complete Section)
	// ================================================================
	private String lastPhotoPath;

	// Camera permission methods
	public boolean hasCameraPermission() {
		return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 
			== PackageManager.PERMISSION_GRANTED;
	}

	public void requestCameraPermission() {
		if (!hasCameraPermission()) {
			ActivityCompat.requestPermissions(this,
				new String[]{Manifest.permission.CAMERA},
				2002
			);
		}
	}

	// Main camera method
	public String openCamera() {
		try {
			// Check camera permission first
			if (!hasCameraPermission()) {
				Log.w(TAG, "Camera permission not granted");
				return "camera_permission_denied";
			}

			// Check storage permission for saving photos
			if (!hasAllFilesAccess()) {
				Log.w(TAG, "Storage permission not granted");
				return "storage_permission_denied";
			}

			// Use Pictures directory that Hollywood can access
			File storageDir = new File(
				Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
				"HollywoodPhotos"
			);
			if (!storageDir.exists()) storageDir.mkdirs();

			// Create unique filename
			String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
			String fileName = "photo_" + timeStamp + ".jpg";
			File photoFile = new File(storageDir, fileName);
			lastPhotoPath = photoFile.getAbsolutePath();

			Log.i(TAG, "Photo will be saved to: " + lastPhotoPath);

			// Use MediaStore to create the file entry
			ContentValues values = new ContentValues();
			values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
			values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
			values.put(MediaStore.Images.Media.DATA, lastPhotoPath);
			
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
				values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/HollywoodPhotos");
				values.put(MediaStore.Images.Media.IS_PENDING, 1);
			}

			Uri imageUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
			
			if (imageUri != null) {
				Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
				intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
				
				if (intent.resolveActivity(getPackageManager()) != null) {
					startActivityForResult(intent, 2001);
					Log.i(TAG, "Camera intent started");
					return "camera_opened";
				} else {
					Log.e(TAG, "No camera app available");
					lastPhotoPath = "";
					return "no_camera_app";
				}
			} else {
				Log.e(TAG, "Failed to create MediaStore entry");
				lastPhotoPath = "";
				return "media_store_error";
			}
		} catch (Exception e) {
			Log.e(TAG, "openCamera failed: " + e.toString());
			lastPhotoPath = "";
			return "error: " + e.getMessage();
		}
	}

	// Handle camera result
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		
		if (requestCode == 2001) {
			if (resultCode == RESULT_OK) {
				Log.i(TAG, "Camera returned OK, photo path: " + lastPhotoPath);
				
				// Finalize MediaStore entry for Android Q+
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && lastPhotoPath != null) {
					try {
						ContentValues values = new ContentValues();
						values.put(MediaStore.Images.Media.IS_PENDING, 0);
						
						String selection = MediaStore.Images.Media.DATA + "=?";
						String[] selectionArgs = new String[]{lastPhotoPath};
						getContentResolver().update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
												  values, selection, selectionArgs);
						
						Log.i(TAG, "MediaStore entry finalized for: " + lastPhotoPath);
					} catch (Exception e) {
						Log.e(TAG, "Failed to finalize MediaStore entry", e);
					}
				}
				
				// Trigger media scan so file is immediately visible to Hollywood
				MediaScannerConnection.scanFile(this, new String[]{lastPhotoPath}, 
					new String[]{"image/jpeg"}, null);
					
			} else {
				// Camera was cancelled
				Log.i(TAG, "Camera was cancelled");
				lastPhotoPath = "";
				
				// Clean up pending file if camera was cancelled
				if (lastPhotoPath != null) {
					try {
						File cancelledFile = new File(lastPhotoPath);
						if (cancelledFile.exists()) {
							cancelledFile.delete();
						}
					} catch (Exception e) {
						Log.e(TAG, "Failed to delete cancelled photo", e);
					}
					lastPhotoPath = "";
				}
			}
		}
	}

	// Get the photo path for Hollywood
	public String getLastPhotoPath() {
		if (lastPhotoPath != null) {
			File photoFile = new File(lastPhotoPath);
			if (photoFile.exists()) {
				Log.i(TAG, "Returning existing photo path: " + lastPhotoPath);
				return lastPhotoPath;
			} else {
				Log.w(TAG, "Photo file doesn't exist: " + lastPhotoPath);
				return "";
			}
		}
		return "";
	}

	// Check if camera is available on the device
	public boolean isCameraAvailable() {
		try {
			Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
			return intent.resolveActivity(getPackageManager()) != null;
		} catch (Exception e) {
			Log.e(TAG, "isCameraAvailable failed", e);
			return false;
		}
	}

	// ================================================================
	// 📍 GPS LOCATION (Complete Section)
	// ================================================================

	// Location permission methods
	public boolean hasLocationPermission() {
		return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
			== PackageManager.PERMISSION_GRANTED;
	}

	public void requestLocationPermission() {
		if (!hasLocationPermission()) {
			ActivityCompat.requestPermissions(this,
				new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
				2003
			);
		}
	}

	// Check if location services are enabled
	public boolean isLocationEnabled() {
		try {
			LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
			if (lm == null) return false;
			
			return lm.isProviderEnabled(LocationManager.GPS_PROVIDER) || 
				   lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
		} catch (Exception e) {
			Log.e(TAG, "isLocationEnabled failed", e);
			return false;
		}
	}

	// Get last known location (quick, cached location)
	public String getLastKnownLocation() {
		try {
			// Check location permission
			if (!hasLocationPermission()) {
				Log.w(TAG, "Location permission not granted");
				return "permission_denied";
			}

			LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
			if (lm == null) {
				Log.e(TAG, "LocationManager is null");
				return "service_unavailable";
			}

			// Check if location services are enabled
			if (!isLocationEnabled()) {
				Log.w(TAG, "Location services are disabled");
				return "location_disabled";
			}

			// Try multiple location providers in order of preference
			Location location = null;
			List<String> providers = lm.getProviders(true);
			
			for (String provider : providers) {
				try {
					Location loc = lm.getLastKnownLocation(provider);
					if (loc != null) {
						if (location == null || loc.getAccuracy() < location.getAccuracy()) {
							location = loc;
						}
					}
				} catch (SecurityException e) {
					Log.w(TAG, "No permission for provider: " + provider);
				} catch (Exception e) {
					Log.w(TAG, "Error getting location from provider: " + provider, e);
				}
			}

			if (location != null) {
				double lat = location.getLatitude();
				double lon = location.getLongitude();
				float accuracy = location.getAccuracy();
				long time = location.getTime();
				
				String result = String.format(Locale.US, "%.6f,%.6f", lat, lon);
				Log.i(TAG, "Location found: " + result + " (accuracy: " + accuracy + "m, time: " + time + ")");
				return result;
			} else {
				Log.w(TAG, "No last known location available");
				return "no_location_available";
			}
			
		} catch (SecurityException e) {
			Log.e(TAG, "Location permission denied", e);
			return "permission_denied";
		} catch (Exception e) {
			Log.e(TAG, "getLastKnownLocation failed", e);
			return "error: " + e.getMessage();
		}
	}

	// Get location with provider preference (GPS first, then network)
	public String getCurrentLocation() {
		try {
			// Check location permission
			if (!hasLocationPermission()) {
				return "permission_denied";
			}

			LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
			if (lm == null) {
				return "service_unavailable";
			}

			// Check if location services are enabled
			if (!isLocationEnabled()) {
				return "location_disabled";
			}

			// Try GPS first for highest accuracy
			Location location = null;
			
			if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
				try {
					location = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
					if (location != null) {
						Log.i(TAG, "Using GPS location");
					}
				} catch (SecurityException e) {
					Log.w(TAG, "No permission for GPS provider");
				}
			}
			
			// Fallback to network if GPS not available
			if (location == null && lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
				try {
					location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
					if (location != null) {
						Log.i(TAG, "Using network location");
					}
				} catch (SecurityException e) {
					Log.w(TAG, "No permission for network provider");
				}
			}

			if (location != null) {
				double lat = location.getLatitude();
				double lon = location.getLongitude();
				float accuracy = location.getAccuracy();
				
				String result = String.format(Locale.US, "%.6f,%.6f", lat, lon);
				Log.i(TAG, "Current location: " + result + " (accuracy: " + accuracy + "m)");
				return result;
			} else {
				Log.w(TAG, "No current location available from any provider");
				return "location_unavailable";
			}
			
		} catch (SecurityException e) {
			Log.e(TAG, "Location permission denied", e);
			return "permission_denied";
		} catch (Exception e) {
			Log.e(TAG, "getCurrentLocation failed", e);
			return "error";
		}
	}

	// Get detailed location info (returns comma-separated: lat,lon,accuracy,time,provider)
	public String getDetailedLocation() {
		try {
			if (!hasLocationPermission()) {
				return "permission_denied";
			}

			LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
			if (lm == null) return "service_unavailable";
			if (!isLocationEnabled()) return "location_disabled";

			Location bestLocation = null;
			List<String> providers = lm.getProviders(true);
			
			for (String provider : providers) {
				try {
					Location loc = lm.getLastKnownLocation(provider);
					if (loc != null) {
						if (bestLocation == null || loc.getAccuracy() < bestLocation.getAccuracy()) {
							bestLocation = loc;
						}
					}
				} catch (Exception e) {
					// Continue with next provider
				}
			}

			if (bestLocation != null) {
				return String.format(Locale.US, "%.6f,%.6f,%.1f,%d,%s",
					bestLocation.getLatitude(),
					bestLocation.getLongitude(),
					bestLocation.getAccuracy(),
					bestLocation.getTime(),
					bestLocation.getProvider()
				);
			} else {
				return "no_location_available";
			}
			
		} catch (Exception e) {
			Log.e(TAG, "getDetailedLocation failed", e);
			return "error";
		}
	}

	// Open location settings so user can enable location services
	public void openLocationSettings() {
		try {
			Intent intent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
			startActivity(intent);
		} catch (Exception e) {
			Log.e(TAG, "openLocationSettings failed", e);
			// Fallback to general settings
			try {
				Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS);
				startActivity(intent);
			} catch (Exception e2) {
				Log.e(TAG, "Fallback settings also failed", e2);
			}
		}
	}

    // ================================================================
    // 💥 VIBRATION
    // ================================================================
//    public void vibrateDevice(int durationMs) {
//        try {
//            Vibrator v = (Vibrator) getSystemService(VIBRATOR_SERVICE);
//            if (v != null) v.vibrate(durationMs);
//        } catch (Exception e) {
//            Log.e(TAG, "vibrateDevice", e);
//        }
//    }

	public void vibrateNow(int durationMs) {
		try {
			HollywoodActivity.vibrateDevice(durationMs);
		} catch (Exception e) {
			Log.e(TAG, "vibrateNow failed", e);
		}
	}

    // ================================================================
    // 📤 SHARE TEXT
    // ================================================================
    public void shareText(String message) {
        try {
            Intent sendIntent = new Intent();
            sendIntent.setAction(Intent.ACTION_SEND);
            sendIntent.putExtra(Intent.EXTRA_TEXT, message);
            sendIntent.setType("text/plain");
            startActivity(Intent.createChooser(sendIntent, "Share via"));
        } catch (Exception e) {
            Log.e(TAG, "shareText", e);
        }
    }

	// ================================================================
	// 📤 SHARE IMAGE
	// ================================================================
	public void shareImage(String imagePath) {
		try {
			File imageFile = new File(imagePath);
			if (!imageFile.exists()) {
				Log.e(TAG, "Image file not found: " + imagePath);
				return;
			}

			Intent shareIntent = new Intent(Intent.ACTION_SEND);
			shareIntent.setType("image/jpeg");
			
			// Use YOUR existing FileProvider authority
			Uri imageUri;
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
				imageUri = FileProvider.getUriForFile(this,
					getPackageName() + ".fileprovider",  // ← Changed to match your manifest
					imageFile);
			} else {
				imageUri = Uri.fromFile(imageFile);
			}
			
			shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
			shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
			
			startActivity(Intent.createChooser(shareIntent, "Share Photo"));
			Log.i(TAG, "Sharing image: " + imagePath);
			
		} catch (Exception e) {
			Log.e(TAG, "shareImage failed", e);
		}
	}

	// ================================================================
	// 📋 CLIPBOARD FUNCTIONS
	// ================================================================

	// Copy text to clipboard
	public void copyToClipboard(String text) {
		try {
			ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
			ClipData clip = ClipData.newPlainText("Hollywood App", text);
			clipboard.setPrimaryClip(clip);
			Log.i(TAG, "Text copied to clipboard: " + text);
			
			// Optional: Show a quick toast message
			// Toast.makeText(this, "Copied to clipboard", Toast.LENGTH_SHORT).show();
			
		} catch (Exception e) {
			Log.e(TAG, "copyToClipboard failed", e);
		}
	}

	// Get text from clipboard
	public String getFromClipboard() {
		try {
			ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
			if (clipboard.hasPrimaryClip()) {
				ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
				String text = item.getText().toString();
				Log.i(TAG, "Retrieved from clipboard: " + text);
				return text;
			}
		} catch (Exception e) {
			Log.e(TAG, "getFromClipboard failed", e);
		}
		return "";
	}

	// ================================================================
	// 🔆 KEEP SCREEN ON 
	// ================================================================

	private boolean screenShouldStayOn = false;

	// Keep screen on - call this repeatedly if needed
	public void keepScreenOn() {
		try {
			runOnUiThread(new Runnable() {
				@Override
				public void run() {
					getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
					screenShouldStayOn = true;
					Log.i(TAG, "Screen keep-on flag set");
				}
			});
		} catch (Exception e) {
			Log.e(TAG, "keepScreenOn failed", e);
		}
	}

	// Allow screen to turn off normally
	public void allowScreenOff() {
		try {
			runOnUiThread(new Runnable() {
				@Override
				public void run() {
					getWindow().clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
					screenShouldStayOn = false;
					Log.i(TAG, "Screen keep-on flag cleared");
				}
			});
		} catch (Exception e) {
			Log.e(TAG, "allowScreenOff failed", e);
		}
	}

	// Force screen on and wake up device (requires WAKE_LOCK permission)
	public void wakeUpScreen() {
		try {
			// This is more aggressive - wakes up the screen if it's off
			android.view.Window window = getWindow();
			window.addFlags(android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
						   android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
						   android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
			Log.i(TAG, "Screen woken up and kept on");
		} catch (Exception e) {
			Log.e(TAG, "wakeUpScreen failed", e);
		}
	}

	// Check if screen is currently kept on
	public boolean isScreenKeptOn() {
		try {
			int flags = getWindow().getAttributes().flags;
			return (flags & android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0;
		} catch (Exception e) {
			Log.e(TAG, "isScreenKeptOn failed", e);
			return false;
		}
	}
}
Finally, add some stuff into your custom AndroidManifest.xml

Code: Select all

<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
        package="%PACKAGE%">

    <uses-feature android:glEsVersion="0x00020000" />
    <uses-feature android:name="android.hardware.camera" android:required="false" />    
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.INTERNET"/>
             
    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
        
        <!-- NB: for some reason Lint returns a wrong "MissingClass" for both HollywoodDelegate and
             FileRequest class so we disable checking for this error. As soon as this problem is
             fixed in Lint, we should remove 'tools:ignore="MissingClass"' again because obviously
             we want Lint to do its checks but as of September 2021, this seems to be broken -->
                 
        <activity android:name=".HollywoodDelegate"
                android:label="@string/app_name"
                android:configChanges="orientation|screenSize"
                android:exported="true"
		android:theme="@style/%THEME%"
		tools:ignore="MissingClass">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".FileRequest" android:configChanges="orientation|screenSize" android:theme="@style/%THEME%" tools:ignore="MissingClass"/> 
        
	<provider
        	android:name="androidx.core.content.FileProvider"
        	android:authorities="${applicationId}.fileprovider"
        	android:exported="false"
        	android:grantUriPermissions="true">
        	<meta-data
            		android:name="android.support.FILE_PROVIDER_PATHS"
            		android:resource="@xml/file_paths"></meta-data>
    	</provider>                   
    </application>

</manifest> 
<!-- END_INCLUDE(manifest) -->
Post Reply