From bbfb2e00543dd5a6c9bf3baaa4be42a933a57ab4 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Mon, 1 Oct 2018 02:39:48 +0200 Subject: [PATCH] Add licenses activity --- app/src/main/AndroidManifest.xml | 97 ++++++++++++- .../etchdroid/activities/ActivityBase.kt | 56 ++++++++ .../etchdroid/activities/StartActivity.kt | 93 +++--------- .../activities/UsbDrivePickerActivity.kt | 67 ++++++++- .../exceptions/CannotGetFilePathException.kt | 3 + .../etchdroid/kotlin_exts/UriGetFileExt.kt | 16 +++ .../etchdroid/kotlin_exts/UriGetFilePath.kt | 135 ++++++++++++++++++ app/src/main/res/values-it/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 9 files changed, 397 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/eu/depau/etchdroid/exceptions/CannotGetFilePathException.kt create mode 100644 app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFileExt.kt create mode 100644 app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFilePath.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff80796..1ce4f2a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,11 +3,14 @@ xmlns:tools="http://schemas.android.com/tools" package="eu.depau.etchdroid"> - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/eu/depau/etchdroid/activities/ActivityBase.kt b/app/src/main/java/eu/depau/etchdroid/activities/ActivityBase.kt index d443cdc..cf64ba2 100644 --- a/app/src/main/java/eu/depau/etchdroid/activities/ActivityBase.kt +++ b/app/src/main/java/eu/depau/etchdroid/activities/ActivityBase.kt @@ -1,18 +1,74 @@ package eu.depau.etchdroid.activities +import android.Manifest import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem +import android.webkit.MimeTypeMap import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import eu.depau.etchdroid.R import eu.depau.etchdroid.kotlin_exts.toast +import eu.depau.etchdroid.utils.DoNotShowAgainDialogFragment import eu.depau.etchdroid.utils.NightModeHelper abstract class ActivityBase : AppCompatActivity() { protected lateinit var nightModeHelper: NightModeHelper val DISMISSED_DIALOGS_PREFS = "dismissed_dialogs" + val READ_REQUEST_CODE = 42 + val READ_EXTERNAL_STORAGE_PERMISSION = 29 + + var shouldShowAndroidPieAlertDialog: Boolean + get() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + return false + val settings = getSharedPreferences(DISMISSED_DIALOGS_PREFS, 0) + return !settings.getBoolean("Android_Pie_alert", false) + } + set(value) { + val settings = getSharedPreferences(DISMISSED_DIALOGS_PREFS, 0) + val editor = settings.edit() + editor.putBoolean("Android_Pie_alert", !value) + editor.apply() + } + + fun showAndroidPieAlertDialog(callback: () -> Unit) { + val dialogFragment = DoNotShowAgainDialogFragment(nightModeHelper.nightMode) + dialogFragment.title = getString(R.string.android_pie_bug) + dialogFragment.message = getString(R.string.android_pie_bug_dialog_text) + dialogFragment.positiveButton = getString(R.string.i_understand) + dialogFragment.listener = object : DoNotShowAgainDialogFragment.DialogListener { + override fun onDialogNegative(dialog: DoNotShowAgainDialogFragment, showAgain: Boolean) {} + override fun onDialogPositive(dialog: DoNotShowAgainDialogFragment, showAgain: Boolean) { + shouldShowAndroidPieAlertDialog = showAgain + callback() + } + } + dialogFragment.show(supportFragmentManager, "DMGBetaAlertDialogFragment") + } + + fun checkAndRequestStorageReadPerm(): Boolean { + if ((ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.READ_EXTERNAL_STORAGE)) { + toast(getString(R.string.storage_permission_required)) + } else { + ActivityCompat.requestPermissions(this, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + READ_EXTERNAL_STORAGE_PERMISSION) + } + } else { + // Permission granted + return true + } + return false + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/eu/depau/etchdroid/activities/StartActivity.kt b/app/src/main/java/eu/depau/etchdroid/activities/StartActivity.kt index a5c611a..1ba2a2b 100644 --- a/app/src/main/java/eu/depau/etchdroid/activities/StartActivity.kt +++ b/app/src/main/java/eu/depau/etchdroid/activities/StartActivity.kt @@ -1,30 +1,22 @@ package eu.depau.etchdroid.activities -import android.Manifest import android.content.Intent import android.content.pm.PackageManager import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.Environment import android.view.View import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import com.codekidlabs.storagechooser.StorageChooser import eu.depau.etchdroid.R import eu.depau.etchdroid.StateKeeper import eu.depau.etchdroid.enums.FlashMethod -import eu.depau.etchdroid.kotlin_exts.snackbar import eu.depau.etchdroid.utils.DoNotShowAgainDialogFragment -import kotlinx.android.synthetic.main.activity_start.* import java.io.File class StartActivity : ActivityBase() { val TAG = "StartActivity" - val READ_REQUEST_CODE = 42 - val READ_EXTERNAL_STORAGE_PERMISSION = 29 var delayedButtonClicked: Boolean = false var shouldShowDMGAlertDialog: Boolean @@ -39,20 +31,6 @@ class StartActivity : ActivityBase() { editor.apply() } - var shouldShowAndroidPieAlertDialog: Boolean - get() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) - return false - val settings = getSharedPreferences(DISMISSED_DIALOGS_PREFS, 0) - return !settings.getBoolean("Android_Pie_alert", false) - } - set(value) { - val settings = getSharedPreferences(DISMISSED_DIALOGS_PREFS, 0) - val editor = settings.edit() - editor.putBoolean("Android_Pie_alert", !value) - editor.apply() - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_start) @@ -60,7 +38,7 @@ class StartActivity : ActivityBase() { fun onButtonClicked(view: View) = onButtonClicked(view, true) - private fun onButtonClicked(view: View?, showDialog: Boolean = true) { + private fun onButtonClicked(view: View?, showDMGDialog: Boolean = true, showAndroidPieDialog: Boolean = true) { if (view != null) StateKeeper.flashMethod = when (view.id) { R.id.btn_image_raw -> FlashMethod.FLASH_API @@ -68,13 +46,20 @@ class StartActivity : ActivityBase() { else -> null } - if (StateKeeper.flashMethod != FlashMethod.FLASH_DMG_API || !shouldShowDMGAlertDialog || !showDialog) - showFilePicker() - else - showDMGBetaAlertDialog() + if (showAndroidPieDialog && shouldShowAndroidPieAlertDialog) { + showAndroidPieAlertDialog { onButtonClicked(view, showDMGDialog, false) } + return + } + + if (showDMGDialog && shouldShowDMGAlertDialog && StateKeeper.flashMethod == FlashMethod.FLASH_DMG_API) { + showDMGBetaAlertDialog {onButtonClicked(view, false, showAndroidPieDialog)} + return + } + + showFilePicker() } - fun showDMGBetaAlertDialog() { + fun showDMGBetaAlertDialog(callback: () -> Unit) { val dialogFragment = DoNotShowAgainDialogFragment(nightModeHelper.nightMode) dialogFragment.title = getString(R.string.here_be_dragons) dialogFragment.message = getString(R.string.dmg_alert_dialog_text) @@ -83,32 +68,14 @@ class StartActivity : ActivityBase() { override fun onDialogNegative(dialog: DoNotShowAgainDialogFragment, showAgain: Boolean) {} override fun onDialogPositive(dialog: DoNotShowAgainDialogFragment, showAgain: Boolean) { shouldShowDMGAlertDialog = showAgain - showFilePicker() + callback() } } dialogFragment.show(supportFragmentManager, "DMGBetaAlertDialogFragment") } - fun showAndroidPieAlertDialog() { - val dialogFragment = DoNotShowAgainDialogFragment(nightModeHelper.nightMode) - dialogFragment.title = getString(R.string.android_pie_bug) - dialogFragment.message = getString(R.string.android_pie_bug_dialog_text) - dialogFragment.positiveButton = getString(R.string.i_understand) - dialogFragment.listener = object : DoNotShowAgainDialogFragment.DialogListener { - override fun onDialogNegative(dialog: DoNotShowAgainDialogFragment, showAgain: Boolean) {} - override fun onDialogPositive(dialog: DoNotShowAgainDialogFragment, showAgain: Boolean) { - shouldShowAndroidPieAlertDialog = showAgain - showFilePicker(false) - } - } - dialogFragment.show(supportFragmentManager, "DMGBetaAlertDialogFragment") - } - fun showFilePicker(showDialog: Boolean = true) { - if (showDialog && shouldShowAndroidPieAlertDialog) { - showAndroidPieAlertDialog() - return - } + fun showFilePicker() { when (StateKeeper.flashMethod) { FlashMethod.FLASH_API -> { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) @@ -147,31 +114,14 @@ class StartActivity : ActivityBase() { } } - private fun checkAndRequestStorageReadPerm(): Boolean { - if ((ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { - if (ActivityCompat.shouldShowRequestPermissionRationale(this, - Manifest.permission.READ_EXTERNAL_STORAGE)) { - btn_image_dmg.snackbar("Storage permission is required to read DMG images") - } else { - ActivityCompat.requestPermissions(this, - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - READ_EXTERNAL_STORAGE_PERMISSION) - } - } else { - // Permission granted - return true - } - return false - } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { READ_EXTERNAL_STORAGE_PERMISSION -> { - if (delayedButtonClicked) - onButtonClicked(null, showDialog = false) - return - } - else -> { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (delayedButtonClicked) + onButtonClicked(null, showDMGDialog = false, showAndroidPieDialog = false) + return + } } } } @@ -184,8 +134,7 @@ class StartActivity : ActivityBase() { // Pull that URI using resultData.getData(). var uri: Uri? = null if (data != null) { - uri = data.getData() - StateKeeper.imageFile = uri + StateKeeper.imageFile = data.data nextStep() } diff --git a/app/src/main/java/eu/depau/etchdroid/activities/UsbDrivePickerActivity.kt b/app/src/main/java/eu/depau/etchdroid/activities/UsbDrivePickerActivity.kt index 18c4639..6463dd5 100644 --- a/app/src/main/java/eu/depau/etchdroid/activities/UsbDrivePickerActivity.kt +++ b/app/src/main/java/eu/depau/etchdroid/activities/UsbDrivePickerActivity.kt @@ -5,8 +5,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager +import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -18,12 +20,13 @@ import com.github.mjdev.libaums.UsbMassStorageDevice import eu.depau.etchdroid.R import eu.depau.etchdroid.StateKeeper import eu.depau.etchdroid.adapters.UsbDrivesRecyclerViewAdapter -import eu.depau.etchdroid.kotlin_exts.name -import eu.depau.etchdroid.kotlin_exts.snackbar +import eu.depau.etchdroid.enums.FlashMethod +import eu.depau.etchdroid.kotlin_exts.* import eu.depau.etchdroid.utils.ClickListener import eu.depau.etchdroid.utils.EmptyRecyclerView import eu.depau.etchdroid.utils.RecyclerViewTouchListener import kotlinx.android.synthetic.main.activity_usb_drive_picker.* +import java.io.File class UsbDrivePickerActivity : ActivityBase(), SwipeRefreshLayout.OnRefreshListener { val USB_PERMISSION = "eu.depau.etchdroid.USB_PERMISSION" @@ -35,8 +38,68 @@ class UsbDrivePickerActivity : ActivityBase(), SwipeRefreshLayout.OnRefreshListe private lateinit var refreshLayout: SwipeRefreshLayout + fun handleIntent(intent: Intent) { + if (intent.action == Intent.ACTION_VIEW) { + val uri = intent.data + val ext = uri?.getExtension(contentResolver) + + when (ext) { + in listOf("iso", "img") -> { + StateKeeper.flashMethod = FlashMethod.FLASH_API + StateKeeper.imageFile = uri + } + "dmg" -> { + val path: String? + try { + path = uri.getFilePath(this) + } catch (e: Exception) { + toast(getString(R.string.cannot_find_file_in_storage)) + // Rethrow exception so it's logged in Google Developer Console + throw e + } + if (path == null) { + toast(getString(R.string.cannot_find_file_in_storage)) + finish() + } + + StateKeeper.flashMethod = FlashMethod.FLASH_DMG_API + StateKeeper.imageFile = Uri.fromFile(File(path)) + + checkAndRequestStorageReadPerm() + } + null -> { + return + } + else -> { + toast(getString(R.string.file_type_not_supported)) + finish() + } + } + + if (shouldShowAndroidPieAlertDialog) + showAndroidPieAlertDialog {} + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + READ_EXTERNAL_STORAGE_PERMISSION -> { + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + toast(getString(R.string.storage_permission_required)) + finish() + } + return + } + else -> { + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + handleIntent(intent) + setContentView(R.layout.activity_usb_drive_picker) actionBar?.setDisplayHomeAsUpEnabled(true) diff --git a/app/src/main/java/eu/depau/etchdroid/exceptions/CannotGetFilePathException.kt b/app/src/main/java/eu/depau/etchdroid/exceptions/CannotGetFilePathException.kt new file mode 100644 index 0000000..7e5ccd6 --- /dev/null +++ b/app/src/main/java/eu/depau/etchdroid/exceptions/CannotGetFilePathException.kt @@ -0,0 +1,3 @@ +package eu.depau.etchdroid.exceptions + +class CannotGetFilePathException(cause: Exception) : Exception(cause) \ No newline at end of file diff --git a/app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFileExt.kt b/app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFileExt.kt new file mode 100644 index 0000000..bc55af2 --- /dev/null +++ b/app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFileExt.kt @@ -0,0 +1,16 @@ +package eu.depau.etchdroid.kotlin_exts + +import android.content.ContentResolver +import android.net.Uri +import android.webkit.MimeTypeMap +import java.io.File + +fun Uri.getExtension(contentResolver: ContentResolver): String { + return when (scheme) { + ContentResolver.SCHEME_CONTENT -> { + val mime = MimeTypeMap.getSingleton() + mime.getExtensionFromMimeType(contentResolver.getType(this))!! + } + else -> MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(path)).toString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFilePath.kt b/app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFilePath.kt new file mode 100644 index 0000000..714eeda --- /dev/null +++ b/app/src/main/java/eu/depau/etchdroid/kotlin_exts/UriGetFilePath.kt @@ -0,0 +1,135 @@ +package eu.depau.etchdroid.kotlin_exts + +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import eu.depau.etchdroid.exceptions.CannotGetFilePathException + + +/** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders. + * + * @param context The context. + * @author paulburke + * + * https://stackoverflow.com/a/27271131/1124621 + */ +fun Uri.getFilePath(context: Context): String? { + val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + + try { + if (isKitKat && DocumentsContract.isDocumentUri(context, this)) { + // DocumentProvider + + if (isExternalStorageDocument) { + // ExternalStorageProvider + + val docId = DocumentsContract.getDocumentId(this) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val type = split[0] + + if (type.equals("primary", ignoreCase = true)) { + return Environment.getExternalStorageDirectory().path + "/" + split[1] + } + + // TODO handle non-primary volumes + + } else if (isDownloadsDocument) { + // DownloadsProvider + + val id = DocumentsContract.getDocumentId(this) + + if (id.startsWith("raw:/")) + return Uri.parse(id).path + + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) + + + return contentUri.getDataColumn(context, null, null) + + } else if (isMediaDocument) { + // MediaProvider + + val docId = DocumentsContract.getDocumentId(this) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val type = split[0] + + val contentUri = when (type) { + "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> null + } + + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + + return contentUri?.getDataColumn(context, selection, selectionArgs) + } + + } else if ("content".equals(scheme, ignoreCase = true)) { + // MediaStore (and general) + + return getDataColumn(context, null, null) + + } else if ("file".equals(scheme, ignoreCase = true)) { + // File + + return path + } + } catch (e: Exception) { + // Wrap into own exception to make debugging easier + throw CannotGetFilePathException(e) + } + + return null +} + +/** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ +fun Uri.getDataColumn(context: Context, selection: String?, selectionArgs: Array?): String? { + val column = "_data" + val projection = arrayOf(column) + + context.contentResolver.query(this, projection, selection, selectionArgs, null)?.use { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndexOrThrow(column) + return it.getString(columnIndex) + } + } + + return null +} + + +/** + * Whether the Uri authority is ExternalStorageProvider. + */ +val Uri.isExternalStorageDocument: Boolean + get() = "com.android.externalstorage.documents" == authority + +/** + * Whether the Uri authority is DownloadsProvider. + */ +val Uri.isDownloadsDocument: Boolean + get() = "com.android.providers.downloads.documents" == authority + +/** + * Whether the Uri authority is MediaProvider. + */ +val Uri.isMediaDocument: Boolean + get() = "com.android.providers.media.documents" == authority \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0ed0bd0..b08b104 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -108,4 +108,8 @@ A causa di un bug di Android 9, alcune scritture potrebbero fallire.\nSe appare un messaggio di errore \"Scrittura fallita\", riavvia il dispositivo e prova di nuovo. Reimposta tutti gli avvisi Tutti gli avvisi sono stati ripristinati + Scrivi ISO su USB + Tipo di file non supportato + Impossibile trovare file nella memoria interna. Prova ad aprirlo da dentro l\'app. + Il permesso per l\'archiviazione รจ richiesto per leggere i file DMG \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f7a439..7638618 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,4 +107,8 @@ There is a bug on Android 9 which causes some writes to fail.\nIf it says \"Write failed\", reboot your device and try again. Reset all warnings All warning dialogs restored + Flash ISO to USB + File type not supported + Cannot find file in internal storage. Try opening it from within the app. + Storage permission is required to read DMG images