package waymaker.top.android; // Copyright © 2015 Michael Allan. Licence MIT. import android.content.*; import android.database.*; import android.net.Uri; import android.os.*; import android.provider.DocumentsContract; // grep DocumentsContract-TS import waymaker.gen.*; import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; import static android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID; import static android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE; import static android.provider.DocumentsContract.Document.MIME_TYPE_DIR; /** A tool for reading from the user’s local wayrepo. */ public final class WayrepoReader implements java.io.Closeable { /** Constructs a WayrepoReader. Call {@linkplain #close close}() when done with it. * * @see #wayrepoTreeUri() * @throws WayrepoAccessFailure if access to the wayrepo is denied by a security exception. * See {@linkplain WaykitUI#wayrepoTreeLoc_message(String) wayrepoTreeLoc_message}. */ public @ThreadSafe WayrepoReader( final Uri wayrepoTreeUri, final ContentResolver contentResolver ) throws WayrepoAccessFailure { this.wayrepoTreeUri = wayrepoTreeUri; this.contentResolver = contentResolver; try { provider = contentResolver.acquireContentProviderClient( wayrepoTreeUri ); } // Fails with "Permission Denial" on x86 Atom system image, level 24 rev 3-4. // https://code.google.com/p/android/issues/detail?id=210861 catch( final SecurityException x ) { throw new WayrepoAccessFailure( WaykitUI.wayrepoTreeLoc_message(wayrepoTreeUri.toString()), x ); } } // -------------------------------------------------------------------------------------------------- /** Returns the document ID of the named directory, or null if the directory is not found. * * @param parentID The document ID of the parent. */ public String findDirectory( final String name, final String parentID ) throws WayrepoAccessFailure, InterruptedException { try( final Cursor c/*proID_NAME_TYPE*/ = queryChildren( parentID ); ) { while( c.moveToNext() ) { if( !name.equals( c.getString(1) )) continue; if( !MIME_TYPE_DIR.equals( c.getString(2) )) continue; return c.getString( 0 ); } } return null; // directory not found } /** A query projection of three formal parameters: * document identity tag (ID), * display name (NAME) and * MIME type (TYPE). * Do not modify it. */ public static final String[] proID_NAME_TYPE = new String[] { COLUMN_DOCUMENT_ID, COLUMN_DISPLAY_NAME, COLUMN_MIME_TYPE }; /** The device that gives this reader access to the wayrepo. */ public ContentProviderClient provider() { return provider; } private final ContentProviderClient provider; // grep ContentProviderClient-TS /** Returns an {@linkplain #proID_NAME_TYPE ID_NAME_TYPE} cursor over the children of the given * parent document. Close the cursor when done with it. * * @param parentID The document ID of the parent. */ public Cursor queryChildren( final String parentID ) throws WayrepoAccessFailure, InterruptedException { return queryChildren( parentID, /*retryCount*/0 ); } /** The access location of the user’s wayrepo in the form of a "tree URI" * * @see Android 5.0 § Directory selection */ public Uri wayrepoTreeUri() { return wayrepoTreeUri; } private final Uri wayrepoTreeUri; // - A u t o - C l o s e a b l e -------------------------------------------------------------------- public @SuppressWarnings("deprecation") void close() { if( Build.VERSION.SDK_INT >= 24 ) provider.close(); // if not already closed else provider.release(); } //// P r i v a t e ///////////////////////////////////////////////////////////////////////////////////// private final ContentResolver contentResolver; // grep ContentResolver-TS private static final java.util.logging.Logger logger = LoggerX.getLogger( WayrepoReader.class ); private static final long MS_TIMEOUT_MIN = 4500; private static final long MS_TIMEOUT_INTERVAL = 500; private Cursor queryChildren( final String parentID, int retryCount ) throws WayrepoAccessFailure, InterruptedException { final Cursor c; try { c = provider.query( DocumentsContract.buildChildDocumentsUriUsingTree(wayrepoTreeUri,parentID), proID_NAME_TYPE, /*selector, unsupported*/null, /*selectorArgs*/null, /*order*/null ); // selector unsupported in base impl (DocumentsProvider.queryChildDocuments) } catch( final RemoteException x ) { throw new WayrepoAccessFailure( x ); } if( c == null ) throw new WayrepoAccessFailure( "Cannot read wayrepo directory: " + parentID ); // Return response if fully loaded. // - - - - - - - - - - - - - - - - - if( !c.getExtras().getBoolean( DocumentsContract.EXTRA_LOADING )) return c; if( retryCount > 0 ) { final String s = "Incomplete response from documents provider after retries, count " + retryCount; if( retryCount > 1 ) throw new WayrepoAccessFailure( s ); // Else retry again. Maybe the provider isn't at fault, but some intermediary is forcing // the retry. It's accompanied by a noticeable delay. logger.info( s ); } // Else wait for response to fully load. // - - - - - - - - - - - - - - - - - - - - Uri nUri = c.getNotificationUri(); final boolean nUriDescendentsToo; if( nUri == null ) // probable bug, https://code.google.com/p/android/issues/detail?id=182258 { nUri = topmostUri(); // default nUriDescendentsToo = true; } else nUriDescendentsToo = false; final Observer o = new Observer(); contentResolver.registerContentObserver( nUri, nUriDescendentsToo, o ); // should eventually set o.isFullyLoaded, then call WayrepoReader.this.notify() try { final long msStart = System.currentTimeMillis(); synchronized( WayrepoReader.this ) { WayrepoReader.this.wait( MS_TIMEOUT_MIN + MS_TIMEOUT_INTERVAL ); while( !o.isFullyLoaded ) { final long msElapsed = System.currentTimeMillis() - msStart; if( msElapsed > MS_TIMEOUT_MIN ) { throw new WayrepoAccessFailure( "Wayrepo timeout after " + msElapsed + " ms" ); } WayrepoReader.this.wait( MS_TIMEOUT_INTERVAL ); } } } finally{ contentResolver.unregisterContentObserver( o ); } // Retry query now that response is fully loaded. // - - - - - - - - - - - - - - - - - - - - - - - - return queryChildren( parentID, ++retryCount ); } private Uri topmostUri() // topmost ancestor of wayrepoTreeUri { if( topmostUri == null ) { topmostUri = uriBuilderSA().scheme(wayrepoTreeUri.getScheme()) .authority(wayrepoTreeUri.getAuthority()).build(); } return topmostUri; } private Uri topmostUri; private Uri.Builder uriBuilderSA() // scheme + authority only; cannot clear Uri.Builder { if( uriBuilderSA == null ) uriBuilderSA = new Uri.Builder(); return uriBuilderSA; } private Uri.Builder uriBuilderSA; // ================================================================================================== private final class Observer extends ContentObserver { Observer() { super( WaykitUI.i().handler() ); } volatile boolean isFullyLoaded; public @Override void onChange( boolean _selfChange, Uri _n ) { isFullyLoaded = true; synchronized( WayrepoReader.this ) { WayrepoReader.this.notify(); } } } }