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(); }
}
}
}