Add licenses activity

This commit is contained in:
Davide Depau 2018-10-01 02:39:48 +02:00
parent 1d0c1e82fa
commit bbfb2e0054
Signed by: depau
GPG key ID: C7D999B6A55EFE86
9 changed files with 397 additions and 78 deletions

View file

@ -3,11 +3,14 @@
xmlns:tools="http://schemas.android.com/tools"
package="eu.depau.etchdroid">
<uses-feature android:name="android.hardware.usb.host" />
<uses-feature android:name="android.hardware.usb.host"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove"/>
<application
android:allowBackup="false"
@ -37,9 +40,95 @@
android:label="@string/title_activity_usb_drive_picker"
android:parentActivityName=".activities.StartActivity"
android:theme="@style/MaterialAppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="eu.depau.etchdroid.activities.StartActivity"/>
<!-- Open ISO files by mimetype -->
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="application/x-iso9660-image"/>
<data android:host="*"/>
</intent-filter>
<!-- Open ISO files by extension -->
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<!--
Work around Android's ugly primitive PatternMatcher
implementation that can't cope with finding a . early in
the path unless it's explicitly matched.
https://stackoverflow.com/a/31028507/1124621
-->
<data android:pathPattern=".*\\.iso"/>
<data android:pathPattern=".*\\..*\\.iso"/>
<data android:pathPattern=".*\\..*\\..*\\.iso"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.iso"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.iso"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.iso"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.iso"/>
<data android:pathPattern=".*\\.img"/>
<data android:pathPattern=".*\\..*\\.img"/>
<data android:pathPattern=".*\\..*\\..*\\.img"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.img"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.img"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.img"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.img"/>
</intent-filter>
<!-- Open DMG files by mimetype -->
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="application/x-apple-diskimage"/>
<data android:host="*"/>
</intent-filter>
<!-- Open DMG files by extension -->
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="*/*"/>
<data android:host="*"/>
<!--
Work around Android's ugly primitive PatternMatcher
implementation that can't cope with finding a . early in
the path unless it's explicitly matched.
https://stackoverflow.com/a/31028507/1124621
-->
<data android:pathPattern=".*\\.dmg"/>
<data android:pathPattern=".*\\..*\\.dmg"/>
<data android:pathPattern=".*\\..*\\..*\\.dmg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.dmg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.dmg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.dmg"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.dmg"/>
</intent-filter>
</activity>
<activity
android:name=".activities.ConfirmationActivity"
@ -53,10 +142,10 @@
<activity
android:name=".activities.ErrorActivity"
android:excludeFromRecents="true"
android:label="@string/write_failed"
android:launchMode="singleTask"
android:taskAffinity=""
android:label="@string/write_failed"
android:excludeFromRecents="true"
>
</activity>

View file

@ -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)

View file

@ -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<out String>, 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()
}

View file

@ -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<out String>, 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)

View file

@ -0,0 +1,3 @@
package eu.depau.etchdroid.exceptions
class CannotGetFilePathException(cause: Exception) : Exception(cause)

View file

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

View file

@ -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>?): 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

View file

@ -108,4 +108,8 @@
<string name="android_pie_bug_dialog_text">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.</string>
<string name="reset_warnings">Reimposta tutti gli avvisi</string>
<string name="warnings_reset">Tutti gli avvisi sono stati ripristinati</string>
<string name="flash_iso_image">Scrivi ISO su USB</string>
<string name="file_type_not_supported">Tipo di file non supportato</string>
<string name="cannot_find_file_in_storage">Impossibile trovare file nella memoria interna. Prova ad aprirlo da dentro l\'app.</string>
<string name="storage_permission_required">Il permesso per l\'archiviazione è richiesto per leggere i file DMG</string>
</resources>

View file

@ -107,4 +107,8 @@
<string name="android_pie_bug_dialog_text">There is a bug on Android 9 which causes some writes to fail.\nIf it says \"Write failed\", reboot your device and try again.</string>
<string name="reset_warnings">Reset all warnings</string>
<string name="warnings_reset">All warning dialogs restored</string>
<string name="flash_iso_image">Flash ISO to USB</string>
<string name="file_type_not_supported">File type not supported</string>
<string name="cannot_find_file_in_storage">Cannot find file in internal storage. Try opening it from within the app.</string>
<string name="storage_permission_required">Storage permission is required to read DMG images</string>
</resources>