diff --git a/android/src/org/ppsspp/ppsspp/ContentUri.java b/android/src/org/ppsspp/ppsspp/ContentUri.java index eb11d21129..b31b01e796 100644 --- a/android/src/org/ppsspp/ppsspp/ContentUri.java +++ b/android/src/org/ppsspp/ppsspp/ContentUri.java @@ -1,6 +1,7 @@ package org.ppsspp.ppsspp; import android.app.Activity; +import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -44,6 +45,61 @@ public class ContentUri { private static final int STORAGE_ERROR_DISK_FULL = -3; private static final int STORAGE_ERROR_ALREADY_EXISTS = -4; + // Without a client of our own, every query and DocumentsContract call acquires the storage provider and + // releases it again, and when our reference count drops to zero, each of those is another binder call, + // to the system. Holding one keeps the count up. Unstable, so the provider dying doesn't take our process + // with it: queries that then fail drop it, and the next one acquires a new one. + private static final Object clientLock = new Object(); + private static ContentProviderClient client; + private static String clientAuthority; + + private static ContentProviderClient getClient(Context context, Uri uri) { + final String authority = uri.getAuthority(); + if (authority == null) { + return null; + } + synchronized (clientLock) { + if (client != null && authority.equals(clientAuthority)) { + return client; + } + dropClientLocked(); + // The application's resolver, so the client doesn't keep the activity alive. + client = context.getApplicationContext().getContentResolver().acquireUnstableContentProviderClient(authority); + clientAuthority = authority; + return client; + } + } + + private static void dropClientLocked() { + if (client == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + client.close(); + } else { + client.release(); + } + client = null; + clientAuthority = null; + } + + private static Cursor query(Context context, Uri uri, String[] projection) throws Exception { + final ContentProviderClient c = getClient(context, uri); + if (c != null) { + try { + return c.query(uri, projection, null, null, null); + } catch (android.os.DeadObjectException e) { + Log.w(TAG, "Storage provider died, acquiring it again"); + synchronized (clientLock) { + if (client == c) { + dropClientLocked(); + } + } + } + } + return context.getContentResolver().query(uri, projection, null, null, null); + } + @Keep @SuppressWarnings("unused") public static int openContentUri(Activity activity, String uriString, String mode) { @@ -110,10 +166,9 @@ public class ContentUri { DocumentsContract.Document.COLUMN_SIZE, DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR }; - final ContentResolver resolver = activity.getContentResolver(); final String documentId = DocumentsContract.getDocumentId(uri); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId); - c = resolver.query(childrenUri, columns, null, null, null); + c = query(activity, childrenUri, columns); long sizeSum = 0; // Buffer the URIs so we only have one cursor active at once. I don't trust the storage framework @@ -177,7 +232,6 @@ public class ContentUri { public static String[] listContentUriDir(Activity activity, String uriString, String prefix) { try { Uri uri = Uri.parse(uriString); - final ContentResolver resolver = activity.getContentResolver(); final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( uri, DocumentsContract.getDocumentId(uri)); final ArrayList listing = new ArrayList<>(); @@ -194,7 +248,7 @@ public class ContentUri { prefix = prefix.toLowerCase(Locale.ROOT); } - try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) { + try (Cursor c = query(activity, childrenUri, projection)) { if (c == null) { return new String[]{"X"}; } @@ -355,7 +409,7 @@ public class ContentUri { Cursor c = null; try { Uri uri = Uri.parse(fileUri); - c = activity.getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + c = query(activity, uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }); if (c != null) { return c.getCount() > 0; } else { @@ -380,7 +434,7 @@ public class ContentUri { DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4 }; - try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) { + try (Cursor c = query(activity, Uri.parse(fileName), projection)) { if (c != null && c.moveToFirst()) { return cursorToString(c, null); } else {