PermissionCheckpoint.java
- package net.zer0bandwidth.android.lib.security;
- import android.app.Activity;
- import android.content.Context;
- import android.content.DialogInterface;
- import android.content.Intent;
- import android.content.pm.PackageManager;
- import android.net.Uri;
- import android.os.Build;
- import android.provider.Settings;
- import android.support.annotation.NonNull;
- import android.support.annotation.RequiresApi;
- import android.support.v4.app.ActivityCompat;
- import android.support.v4.content.ContextCompat;
- import android.support.v7.app.AppCompatActivity;
- import android.util.Log;
- import net.zer0bandwidth.android.lib.R;
- import java.util.ArrayList;
- import java.util.HashMap;
- /**
- * This class checks a specified list of "dangerous" permissions, forcing the
- * user to grant each one at runtime.
- *
- * <h3>Using this Class</h3>
- *
- * <h4>Defining the List of Permissions</h4>
- *
- * <p>By default, the list of dangerous permissions is drawn from the string
- * array resource named by {@link R.array#asDangerousPermissionsRequired} in
- * this library. By default, the array is empty; the app consuming this library
- * may override the resource with its own values.</p>
- *
- * <p>The consumer of this class may, alternatively, supply a different resource
- * ID to one of the longer constructors. This will <i>replace</i> the default
- * array resource as the definitive list of required permissions.</p>
- *
- * <h4>Maintain a Persistent Instance in the Activity</h4>
- *
- * <p>The main activity of the app should construct an instance of this class in
- * its {@code onCreate} method, using itself as the operational context, and
- * optionally specifying an alternative list of permissions.</p>
- *
- * <pre>
- * protected PermissionCheckpoint m_perms = null ;
- *
- * {@literal @Override}
- * protected void onCreate( Bundle bndlState )
- * {
- * super.onCreate(bndlState) ;
- * this.setContentView( R.layout.whatever ) ;
- * if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M )
- * m_perms = new PermissionCheckpoint(this) ;
- * // ...
- * }
- * </pre>
- *
- * <h4>Refresh Permission State on Each {@code onResume}</h4>
- *
- * <p>The activity's {@code onResume} method must include a call to this class's
- * {@link #performChecks()} method, which re-evaluates the app's permissions
- * each time it is invoked.</p>
- *
- * <pre>
- * {@literal @Override}
- * protected void onResume()
- * {
- * super.onResume() ;
- * if( m_perms != null ) m_perms.performChecks() ;
- * }
- * </pre>
- *
- * <h4>Implement the Permission Request Callback</h4>
- *
- * <p>Finally, the activity must provide a callback for the Android OS in
- * response to a permission request.</p>
- *
- * <h5>{@link Activity}</h5>
- *
- * <p>Modern activities include their own callback method, which must be
- * overridden in your activity.</p>
- *
- * <pre>
- * public class MyActivity extends Activity
- * {
- * protected PermissionCheckpoint m_perms = null ; // init in onCreate()
- *
- * // etc
- *
- * {@literal @RequiresApi( api = Build.VERSION_CODES.M )}
- * {@literal @Override}
- * public void onRequestPermissionsResult( int zRequestCode,
- * {@literal @NonNull} String[] asPermissions,{@literal @NonNull} int[] azStatus )
- * {
- * super.onRequestPermissionsResult( zRequestCode, asPermissions, azStatus ) ;
- * if( m_perms != null )
- * m_perms.processRequestResults( zRequestCode, asPermissions, azStatus ) ;
- * }
- * }
- * </pre>
- *
- * <h5>{@link AppCompatActivity}</h5>
- *
- * <p>An activity from the compatibility library should implement the
- * {@link ActivityCompat.OnRequestPermissionsResultCallback} interface.</p>
- *
- * <pre>
- * public class MyCompatActivity extends AppCompatActivity
- * implements ActivityCompat.OnRequestPermissionsResultCallback
- * {
- * protected PermissionCheckpoint m_perms = null ; // init in onCreate()
- *
- * // etc
- *
- * {@literal @RequiresApi( api = Build.VERSION_CODES.M )}
- * {@literal @Override}
- * public void onRequestPermissionsResult( int zRequestCode,
- * {@literal @NonNull} String[] asPermissions,{@literal @NonNull} int[] azStatus )
- * {
- * if( m_perms != null )
- * m_perms.processRequestResults( zRequestCode, asPermissions, azStatus ) ;
- * }
- * }
- * </pre>
- *
- * <h3>Acknowledgements</h3>
- *
- * Special thanks to {@code @dapayne1} for doing the heavy lifting on the
- * research for Android 6 permissions management, and providing the original
- * reference implementation from which this class was adapted.
- *
- * @since zer0bandwidth-net/android 0.0.3 (#10)
- */
- @SuppressWarnings( "unused" ) // This is a library.
- public class PermissionCheckpoint
- {
- public static final String LOG_TAG =
- PermissionCheckpoint.class.getSimpleName() ;
- /**
- * An identifier used in permission requests.
- */
- protected static final int DANGER_REQUEST_CODE = 4 ;
- protected static final String PERMISSION_WRITE_SETTINGS =
- "android.permission.WRITE_SETTINGS" ;
- /**
- * Static method allowing any class to check the momentary status of a
- * permission.
- * @param ctx the context in which to perform the check
- * @param sPermission the qualified name of the permission to check
- * @return {@code true} iff the permission has been granted to the app
- */
- public static boolean checkPermission( Context ctx, String sPermission )
- {
- boolean bGranted ;
- if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- && PERMISSION_WRITE_SETTINGS.equals(sPermission) )
- {
- bGranted = Settings.System.canWrite( ctx ) ;
- }
- else
- {
- final int zStatus =
- ContextCompat.checkSelfPermission( ctx, sPermission ) ;
- bGranted = ( zStatus == PackageManager.PERMISSION_GRANTED ) ;
- }
- Log.d( LOG_TAG, (new StringBuilder())
- .append( "Permission [" )
- .append( sPermission )
- .append(( bGranted ? "] GRANTED" : "] DENIED" ))
- .toString()
- );
- return bGranted ;
- }
- /**
- * An {@link AppCompatActivity} which provides the context from which
- * resources are resolved and additional dialogs and activities may be
- * launched. Only one of {@code m_actCompatContext} and
- * {@link #m_actContext} should be set for any given instance.
- */
- protected AppCompatActivity m_actCompatContext = null ;
- /**
- * An {@link Activity} which provides the context from which resources
- * are resolved and additional dialogs and activities may be launched.
- * Only one of {@link #m_actCompatContext} and {@code m_actContext} should
- * be set for any given instance.
- */
- protected Activity m_actContext = null ;
- /**
- * Since both {@link Activity} and {@link AppCompatActivity} are
- * descendants of {@link Context}, this field allows whichever one we end up
- * with to represent the operational context of the instance, where we can
- * use {@code Context} for resource resolution, etc.
- */
- protected Context m_ctx = null ;
- /**
- * The default string array resource in which dangerous permissions are
- * defined. Apps that use this library may choose to override this
- * {@link R.array#asDangerousPermissionsRequired} resource, or have their
- * own resource which is passed into the constructor.
- */
- protected static final int DEFAULT_DANGER_LIST_RESOURCE =
- R.array.asDangerousPermissionsRequired ;
- /**
- * The resource ID of the string array that defines the list of dangerous
- * permissions that should be requested. This defaults to the value of
- * {@link #DEFAULT_DANGER_LIST_RESOURCE}, which is
- * {@link R.array#asDangerousPermissionsRequired}.
- */
- protected int m_resDangerList = DEFAULT_DANGER_LIST_RESOURCE ;
- /**
- * Tracks the list of "dangerous" permissions that need to be granted to the
- * app, and the current status of each.
- */
- protected HashMap<String,Boolean> m_mapGranted = null ;
- /** Indicates that any permission dialog is currently being displayed. */
- protected boolean m_bDialogVisible = false ;
- /** Indicates the "build flavor" used to build the app, if any. */
- protected String m_sFlavor = null ;
- /**
- * Initializes the instance with an {@link Activity} as the context.
- * The default string array resource,
- * {@link R.array#asDangerousPermissionsRequired}, will be used to define
- * the list of dangerous permissions required by the app.
- * @param act the activity which provides operational context
- */
- public PermissionCheckpoint( Activity act )
- { this( act, DEFAULT_DANGER_LIST_RESOURCE ) ; }
- /**
- * Initializes the instance with an {@link Activity} as the context.
- * @param act the activity which provides operational context
- * @param resDangerList the resource ID for the list of dangerous
- * permissions required by the app
- */
- public PermissionCheckpoint( Activity act, int resDangerList )
- {
- m_actCompatContext = null ;
- m_actContext = act ;
- this.init( resDangerList ) ;
- }
- /**
- * Initializes the instance with an {@link AppCompatActivity} as the context.
- * The default string array resource,
- * {@link R.array#asDangerousPermissionsRequired}, will be used to define
- * the list of dangerous permissions required by the app.
- * @param act the activity which provides operational context
- */
- public PermissionCheckpoint( AppCompatActivity act )
- { this( act, DEFAULT_DANGER_LIST_RESOURCE ) ; }
- /**
- * Initializes the instance with an {@link AppCompatActivity} as the context.
- * @param act the activity which provides operational context
- * @param resDangerList the resource ID for the list of dangerous
- * permissions required by the app
- */
- public PermissionCheckpoint( AppCompatActivity act, int resDangerList )
- {
- m_actCompatContext = act ;
- m_actContext = null ;
- this.init( resDangerList ) ;
- }
- /**
- * Initializes various fields within the instance, once context has been
- * established.
- * @param resDangerList the resource ID for the list of dangerous
- * permissions required by the app
- * @return (fluid)
- */
- protected PermissionCheckpoint init( int resDangerList )
- {
- m_resDangerList = resDangerList ;
- m_ctx = ( this.isContextAppCompat() ? m_actCompatContext : m_actContext ) ;
- String[] asDangers =
- m_ctx.getResources().getStringArray( m_resDangerList ) ;
- m_mapGranted = new HashMap<>( asDangers.length ) ;
- for( String sDanger : asDangers )
- { // Add the status of that permission to the object's map.
- m_mapGranted.put( sDanger, checkPermission( m_ctx, sDanger ) ) ;
- }
- return this ;
- }
- /**
- * Indicates whether the instance's operational context is an
- * {@link AppCompatActivity}.
- * @return {@code true} if an {@code AppCompatActivity} was used as the
- * initial context for the instance
- */
- protected boolean isContextAppCompat()
- { return ( m_actCompatContext != null && m_actContext == null ) ; }
- /**
- * This method performs all the checks necessary to determine whether the
- * app has all of its required permissions. If any permission is still
- * denied, then the method will trigger dialogs challenging the user to
- * grant those permissions to the app.
- * @return (fluid)
- */
- public PermissionCheckpoint performChecks()
- {
- if( Build.VERSION.SDK_INT < Build.VERSION_CODES.M )
- return this ; // trivially
- if( ! this.hasGrantedAll() )
- this.promptForPermissions() ;
- return this ;
- }
- /**
- * Evaluates whether all required "dangerous" permissions have been granted.
- *
- * For most permissions, we have cached the state via
- * {@link #processRequestResults}, but for the {@code WRITE_SETTINGS}
- * permission, we have to explicitly re-check it, because there's no way to
- * listen for a state-change event from the Android OS's application
- * manager.
- *
- * @return {@code true} if all permissions are granted.
- */
- protected boolean hasGrantedAll()
- {
- //this.logPermissionState() ; // Comment out this line for release!
- if( m_mapGranted.size() == 0 ) return true ; // No permissions to grant.
- if( m_mapGranted.containsKey( PERMISSION_WRITE_SETTINGS ) )
- { // Explicitly re-check whether we've been granted WRITE_SETTINGS.
- m_mapGranted.put( PERMISSION_WRITE_SETTINGS,
- checkPermission( m_ctx, PERMISSION_WRITE_SETTINGS ) ) ;
- }
- return( ! m_mapGranted.containsValue(false) ) ; // All marked "true".
- }
- /**
- * (debug only) Dumps the current state of the permission map to the Android
- * logs.
- * @return (fluid)
- * @see #hasGrantedAll()
- */
- protected PermissionCheckpoint logPermissionState()
- {
- StringBuilder sb = new StringBuilder() ;
- sb.append( "DEBUG Current permission state:\n" ) ;
- for( HashMap.Entry<String,Boolean> pair : m_mapGranted.entrySet() )
- {
- sb.append( "\t\t" )
- .append( pair.getKey() )
- .append( "\t" )
- .append( pair.getValue() )
- .append( "\n" )
- ;
- }
- Log.d( LOG_TAG, sb.toString() ) ;
- return this ;
- }
- /**
- * Prompts the user to accept all "dangerous" permission requests.
- *
- * The method tries to ask for just regularly-dangerous permissions first,
- * then asks for the "write settings" permission if all others have been
- * granted.
- *
- * @return (fluid)
- */
- protected PermissionCheckpoint promptForPermissions()
- {
- ArrayList<String> asRequired = new ArrayList<>() ;
- boolean bWriteSettings = false ;
- for( HashMap.Entry<String,Boolean> pair : m_mapGranted.entrySet() )
- {
- if( ! pair.getValue() )
- { // Found a permission that has not yet been granted.
- if( pair.getKey().equals( PERMISSION_WRITE_SETTINGS ) )
- {
- Log.w( LOG_TAG,
- "App needs special WRITE_SETTINGS permission." ) ;
- bWriteSettings = true;
- }
- else
- {
- Log.w( LOG_TAG, (new StringBuilder())
- .append( "App needs " )
- .append( pair.getKey() )
- .append( " permission." )
- .toString()
- );
- asRequired.add( pair.getKey() );
- }
- }
- }
- if( asRequired.size() > 0 )
- this.promptForOtherDangers( asRequired ) ;
- else if( bWriteSettings && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M )
- this.promptForWriteSettingsPermission();
- return this ;
- }
- /**
- * Prompts the user to enable the "write settings" permission, which can be
- * administered only on a special Android OS dialog.
- * @return (fluid)
- */
- @RequiresApi( api = Build.VERSION_CODES.M )
- protected PermissionCheckpoint promptForWriteSettingsPermission()
- {
- if( ! m_bDialogVisible )
- {
- final WriteSettingsDialogClickListener listener =
- new WriteSettingsDialogClickListener() ;
- if( this.isContextAppCompat() )
- this.promptWithAppCompatDialog( listener ) ;
- else
- this.promptWithModernDialog( listener ) ;
- }
- return this ;
- }
- /**
- * Defines a click listener for the dialog that prompts the user to grant
- * the special {@code WRITE_SETTINGS} permission.
- * @see PermissionCheckpoint#promptWithModernDialog
- * @see PermissionCheckpoint#promptWithAppCompatDialog
- * @since zer0bandwidth-net/android 0.0.3 (#10)
- */
- protected class WriteSettingsDialogClickListener
- implements DialogInterface.OnClickListener
- {
- /** An alert from the modern library. */
- public android.app.AlertDialog m_diaModernParent = null ;
- /** An alert dialog from the compatibility library. */
- public android.support.v7.app.AlertDialog m_diaCompatParent = null ;
- /** Sets the parent dialog from the modern library. */
- public WriteSettingsDialogClickListener setParent( android.app.AlertDialog dia )
- { m_diaModernParent = dia ; m_diaCompatParent = null ; return this ; }
- /** Sets the parent dialog from the compatibility library. */
- public WriteSettingsDialogClickListener setParent( android.support.v7.app.AlertDialog dia )
- { m_diaModernParent = null ; m_diaCompatParent = dia ; return this ; }
- @RequiresApi( api = Build.VERSION_CODES.M )
- @Override
- public void onClick( DialogInterface dia, int zButtonID )
- {
- Intent sig = new Intent( Settings.ACTION_MANAGE_WRITE_SETTINGS ) ;
- sig.setData( Uri.parse( "package:" + m_ctx.getPackageName() ) ) ;
- m_ctx.startActivity( sig ) ;
- if( m_diaModernParent != null ) m_diaModernParent.dismiss() ;
- else if( m_diaCompatParent != null ) m_diaCompatParent.dismiss() ;
- }
- }
- /**
- * If our parent activity is an {@link AppCompatActivity}, then we need to
- * use the corresponding {@code AlertDialog} flavor to prompt for the
- * permission.
- * @param listener the click listener for the "OK" button
- * @return (fluid)
- * @see #promptForWriteSettingsPermission
- */
- protected PermissionCheckpoint promptWithAppCompatDialog(
- WriteSettingsDialogClickListener listener )
- {
- final android.support.v7.app.AlertDialog diaAnnounce =
- new android.support.v7.app.AlertDialog.Builder( m_ctx )
- .setTitle( R.string.diaAnnounce_title )
- .setMessage( R.string.diaAnnounce_message )
- .setCancelable( false )
- .setPositiveButton( android.R.string.ok, listener )
- .create()
- ;
- listener.setParent( diaAnnounce ) ;
- diaAnnounce.show() ;
- return this ;
- }
- /**
- * If our parent activity is an {@link Activity}, then we need to use the
- * corresponding {@code AlertDialog} flavor to prompt for the permission.
- * @param listener the click listener for the "OK" button
- * @return (fluid)
- * @see #promptForWriteSettingsPermission
- */
- protected PermissionCheckpoint promptWithModernDialog(
- WriteSettingsDialogClickListener listener )
- {
- final android.app.AlertDialog diaAnnounce =
- new android.app.AlertDialog.Builder( m_ctx )
- .setTitle( R.string.diaAnnounce_title )
- .setMessage( R.string.diaAnnounce_message )
- .setCancelable( false )
- .setPositiveButton( android.R.string.ok, listener )
- .create()
- ;
- listener.setParent( diaAnnounce ) ;
- diaAnnounce.show() ;
- return this ;
- }
- /**
- * Prompts the user to enable "dangerous" permissions.
- * @param asDangers a list of dangerous permissions
- * @return (fluid)
- */
- protected PermissionCheckpoint promptForOtherDangers( ArrayList<String> asDangers )
- {
- m_bDialogVisible = true ;
- if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ! this.isContextAppCompat() )
- {
- m_actContext.requestPermissions(
- asDangers.toArray( new String[asDangers.size()] ),
- DANGER_REQUEST_CODE ) ;
- }
- else
- {
- ActivityCompat.requestPermissions( m_actCompatContext,
- asDangers.toArray( new String[asDangers.size()] ),
- DANGER_REQUEST_CODE ) ;
- }
- return this ;
- }
- /**
- * Dialogs spawned by {@link ActivityCompat#requestPermissions} will call
- * back to this method when the user chooses any option. We will use this
- * callback to re-check the permission array and determine whether we need
- * to try again.
- * @param zCode the request code (we are interested in
- * {@link #DANGER_REQUEST_CODE})
- * @param asDangers an array of permissions that were requested
- * @param azStatus an array of result indicators
- */
- @RequiresApi( api = Build.VERSION_CODES.M )
- public void processRequestResults( int zCode,
- @NonNull String[] asDangers, @NonNull int[] azStatus )
- {
- m_bDialogVisible = false ;
- Log.d( LOG_TAG, (new StringBuilder())
- .append( "Received new status for [" )
- .append( asDangers.length )
- .append(( asDangers.length == 1 ? "] permission." : "] permissions." ))
- .toString()
- );
- for( int i = 0 ; i < asDangers.length ; i++ )
- { // Update our map of permissions to grant status.
- if( azStatus[i] == PackageManager.PERMISSION_GRANTED )
- m_mapGranted.put( asDangers[i], true ) ;
- else if( azStatus[i] == PackageManager.PERMISSION_DENIED )
- m_mapGranted.put( asDangers[i], false ) ;
- }
- }
- }