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