package org.moire.ultrasonic.util import android.Manifest import android.content.Context import android.content.Intent import android.net.Uri import android.os.Handler import android.os.Looper import androidx.core.content.PermissionChecker import com.karumi.dexter.Dexter import com.karumi.dexter.MultiplePermissionsReport import com.karumi.dexter.PermissionToken import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.multi.MultiplePermissionsListener import org.moire.ultrasonic.R import org.moire.ultrasonic.util.FileUtil.defaultMusicDirectory import timber.log.Timber /** * Contains static functions for Permission handling */ class PermissionUtil(private val applicationContext: Context) { private var activityContext: Context? = null fun onForegroundApplicationStarted(context: Context?) { activityContext = context } fun onForegroundApplicationStopped() { activityContext = null } /** * This function can be used to handle file access permission failures. * * It will check if the failure is because the necessary permissions aren't available, * and it will request them, if necessary. * * @param callback callback function to execute after the permission request is finished */ fun handlePermissionFailed(callback: ((Boolean) -> Unit)?) { val currentCachePath = Settings.cacheLocation val defaultCachePath = defaultMusicDirectory.path // Ultrasonic can do nothing about this error when the Music Directory is already set to the default. if (currentCachePath.compareTo(defaultCachePath) == 0) return if (PermissionChecker.checkSelfPermission( applicationContext, Manifest.permission.WRITE_EXTERNAL_STORAGE ) == PermissionChecker.PERMISSION_DENIED || PermissionChecker.checkSelfPermission( applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE ) == PermissionChecker.PERMISSION_DENIED ) { // While we request permission, the Music Directory is temporarily reset to its default location Settings.cacheLocation = defaultMusicDirectory.path // If the application is not running, we can't notify the user if (activityContext == null) return requestFailedPermission(activityContext!!, currentCachePath, callback) } else { Settings.cacheLocation = defaultMusicDirectory.path // If the application is not running, we can't notify the user if (activityContext != null) { Handler(Looper.getMainLooper()).post { showWarning( activityContext!!, activityContext!!.getString(R.string.permissions_message_box_title), activityContext!!.getString(R.string.permissions_access_error), null ) } } callback?.invoke(false) } } companion object { /** * This function requests permission to access the filesystem. * It can be used to request the permission initially, e.g. when the user decides to * use a non-default folder for the cache * @param context context for the operation * @param callback callback function to execute after the permission request is finished */ @JvmStatic fun requestInitialPermission( context: Context, callback: ((Boolean) -> Unit)? ) { Dexter.withContext(context) .withPermissions( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ) .withListener(object : MultiplePermissionsListener { override fun onPermissionsChecked(report: MultiplePermissionsReport) { if (report.areAllPermissionsGranted()) { Timber.i("R/W permission granted for external storage") callback?.invoke(true) return } if (report.isAnyPermissionPermanentlyDenied) { Timber.i( "R/W permission is permanently denied for external storage" ) showSettingsDialog(context) callback?.invoke(false) return } Timber.i("R/W permission is missing for external storage") showWarning( context, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_rationale_description_initial), null ) callback?.invoke(false) } override fun onPermissionRationaleShouldBeShown( permissions: List, token: PermissionToken ) { showWarning( context, context.getString(R.string.permissions_rationale_title), context.getString(R.string.permissions_rationale_description_initial), token ) } }).withErrorListener { error -> Timber.e( "An error has occurred during checking permissions with Dexter: %s", error.toString() ) } .check() } private fun requestFailedPermission( context: Context, cacheLocation: String?, callback: ((Boolean) -> Unit)? ) { Dexter.withContext(context) .withPermissions( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ) .withListener(object : MultiplePermissionsListener { override fun onPermissionsChecked(report: MultiplePermissionsReport) { if (report.areAllPermissionsGranted()) { Timber.i("Permission granted to use cache directory %s", cacheLocation) if (cacheLocation != null) { Settings.cacheLocation = cacheLocation } callback?.invoke(true) return } if (report.isAnyPermissionPermanentlyDenied) { Timber.i( "R/W permission for cache directory %s was permanently denied", cacheLocation ) showSettingsDialog(context) callback?.invoke(false) return } Timber.i( "At least one permission is missing to use directory %s ", cacheLocation ) Settings.cacheLocation = defaultMusicDirectory.path showWarning( context, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_permission_missing), null ) callback?.invoke(false) } override fun onPermissionRationaleShouldBeShown( permissions: List, token: PermissionToken ) { showWarning( context, context.getString(R.string.permissions_rationale_title), context.getString(R.string.permissions_rationale_description_failed), token ) } }).withErrorListener { error -> Timber.e( "An error has occurred during checking permissions with Dexter: %s", error.toString() ) } .check() } private fun showSettingsDialog(ctx: Context) { val builder = Util.createDialog( context = ctx, android.R.drawable.ic_dialog_alert, ctx.getString(R.string.permissions_permanent_denial_title), ctx.getString(R.string.permissions_permanent_denial_description) ) builder.setPositiveButton(ctx.getString(R.string.permissions_open_settings)) { dialog, _ -> dialog.cancel() openSettings(ctx) } builder.setNegativeButton(ctx.getString(R.string.common_cancel)) { dialog, _ -> Settings.cacheLocation = defaultMusicDirectory.path dialog.cancel() } builder.show() } private fun openSettings(context: Context) { val i = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) i.addCategory(Intent.CATEGORY_DEFAULT) i.data = Uri.parse("package:" + context.packageName) context.startActivity(i) } private fun showWarning( context: Context, title: String, text: String, token: PermissionToken? ) { val builder = Util.createDialog( context = context, android.R.drawable.ic_dialog_alert, title, text ) builder.setPositiveButton(context.getString(R.string.common_ok)) { dialog, _ -> dialog.cancel() token?.continuePermissionRequest() } builder.show() } } }