Browse Source

Complete app redesign

- Switched to AndroidX namespace
- Switched from Fragments to Activities
- Automatically detects USB drives on (dis)connection
- UX is simpler, less taps are needed
tmp-new-features
Davide Depau 3 years ago
parent
commit
53dc07490c
45 changed files with 1072 additions and 1017 deletions
  1. +10
    -8
      app/build.gradle
  2. +2
    -2
      app/src/androidTest/java/eu/depau/etchdroid/ExampleInstrumentedTest.kt
  3. +30
    -7
      app/src/main/AndroidManifest.xml
  4. +0
    -7
      app/src/main/java/eu/depau/etchdroid/StateKeeper.kt
  5. +30
    -0
      app/src/main/java/eu/depau/etchdroid/activities/ActivityBase.kt
  6. +150
    -0
      app/src/main/java/eu/depau/etchdroid/activities/ConfirmationActivity.kt
  7. +3
    -6
      app/src/main/java/eu/depau/etchdroid/activities/LicensesActivity.kt
  8. +0
    -102
      app/src/main/java/eu/depau/etchdroid/activities/MainActivity.kt
  9. +128
    -0
      app/src/main/java/eu/depau/etchdroid/activities/StartActivity.kt
  10. +150
    -0
      app/src/main/java/eu/depau/etchdroid/activities/UsbDrivePickerActivity.kt
  11. +0
    -35
      app/src/main/java/eu/depau/etchdroid/activities/WizardActivity.kt
  12. +1
    -1
      app/src/main/java/eu/depau/etchdroid/adapters/LicenseRecyclerViewAdapter.kt
  13. +1
    -1
      app/src/main/java/eu/depau/etchdroid/adapters/PartitionTableRecyclerViewAdapter.kt
  14. +1
    -1
      app/src/main/java/eu/depau/etchdroid/adapters/UsbDrivesRecyclerViewAdapter.kt
  15. +0
    -115
      app/src/main/java/eu/depau/etchdroid/fragments/ConfirmInfoFragment.kt
  16. +0
    -42
      app/src/main/java/eu/depau/etchdroid/fragments/FlashMethodFragment.kt
  17. +0
    -211
      app/src/main/java/eu/depau/etchdroid/fragments/ImageLocationFragment.kt
  18. +0
    -139
      app/src/main/java/eu/depau/etchdroid/fragments/UsbDriveFragment.kt
  19. +0
    -20
      app/src/main/java/eu/depau/etchdroid/fragments/WizardFragment.kt
  20. +1
    -1
      app/src/main/java/eu/depau/etchdroid/kotlin_exts/ViewSnackbar.kt
  21. +1
    -1
      app/src/main/java/eu/depau/etchdroid/services/UsbWriteService.kt
  22. +42
    -0
      app/src/main/java/eu/depau/etchdroid/utils/EmptyRecyclerView.kt
  23. +1
    -1
      app/src/main/java/eu/depau/etchdroid/utils/RecyclerViewTouchListener.kt
  24. +34
    -0
      app/src/main/res/drawable/ic_beta.xml
  25. +16
    -0
      app/src/main/res/drawable/ic_dmg.xml
  26. +13
    -0
      app/src/main/res/drawable/ic_raw.xml
  27. +9
    -0
      app/src/main/res/drawable/ic_twotone_save_alt_24px.xml
  28. +5
    -0
      app/src/main/res/drawable/ic_usb_black_200dp.xml
  29. +219
    -0
      app/src/main/res/layout/activity_confirmation.xml
  30. +3
    -3
      app/src/main/res/layout/activity_licenses.xml
  31. +0
    -34
      app/src/main/res/layout/activity_main.xml
  32. +151
    -0
      app/src/main/res/layout/activity_start.xml
  33. +42
    -0
      app/src/main/res/layout/activity_usb_drive_picker.xml
  34. +0
    -144
      app/src/main/res/layout/fragment_confirminfo.xml
  35. +0
    -46
      app/src/main/res/layout/fragment_select_flash_method.xml
  36. +0
    -59
      app/src/main/res/layout/fragment_select_location.xml
  37. +0
    -16
      app/src/main/res/layout/fragment_select_usb_drive.xml
  38. +1
    -1
      app/src/main/res/menu/menu_main.xml
  39. +3
    -3
      app/src/main/res/values/colors.xml
  40. +1
    -0
      app/src/main/res/values/dimens.xml
  41. +12
    -1
      app/src/main/res/values/strings.xml
  42. +6
    -6
      app/src/main/res/values/styles.xml
  43. +2
    -2
      build.gradle
  44. +2
    -0
      gradle.properties
  45. +2
    -2
      gradle/wrapper/gradle-wrapper.properties

+ 10
- 8
app/build.gradle View File

@ -12,7 +12,7 @@ android {
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -26,11 +26,11 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.android.support:design:28.0.0-rc01'
implementation 'com.android.support:recyclerview-v7:28.0.0-rc01'
implementation 'com.android.support:gridlayout-v7:28.0.0-rc01'
implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
implementation 'com.google.android.material:material:1.0.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.0.0-rc02'
implementation 'androidx.gridlayout:gridlayout:1.0.0-rc02'
api 'com.google.guava:guava:26.0-android'
implementation 'com.github.codekidX:storage-chooser:2.0.4.2'
@ -39,7 +39,9 @@ dependencies {
implementation project(':dmg2img')
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
implementation 'com.android.support:design:28.0.0-rc01'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
}

+ 2
- 2
app/src/androidTest/java/eu/depau/etchdroid/ExampleInstrumentedTest.kt View File

@ -1,7 +1,7 @@
package eu.depau.etchdroid
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith


+ 30
- 7
app/src/main/AndroidManifest.xml View File

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.depau.etchdroid">
<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"/>
@ -12,17 +14,41 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".activities.MainActivity"
android:name=".activities.StartActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/MaterialAppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".activities.LicensesActivity"
android:label="@string/licenses"
android:theme="@style/MaterialAppTheme">
</activity>
<activity
android:name=".activities.UsbDrivePickerActivity"
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"/>
</activity>
<activity
android:name=".activities.ConfirmationActivity"
android:label="@string/title_activity_confirmation"
android:parentActivityName=".activities.UsbDrivePickerActivity"
android:theme="@style/MaterialAppTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="eu.depau.etchdroid.activities.UsbDrivePickerActivity"/>
</activity>
<service
android:name=".services.UsbApiImgWriteService"
@ -30,9 +56,6 @@
<service
android:name=".services.UsbApiDmgWriteService"
android:exported="false"/>
<activity android:name=".activities.LicensesActivity">
</activity>
</application>
</manifest>

+ 0
- 7
app/src/main/java/eu/depau/etchdroid/StateKeeper.kt View File

@ -3,18 +3,11 @@ package eu.depau.etchdroid
import android.hardware.usb.UsbDevice
import android.net.Uri
import com.github.mjdev.libaums.UsbMassStorageDevice
import eu.depau.etchdroid.fragments.WizardFragment
import eu.depau.etchdroid.enums.FlashMethod
import eu.depau.etchdroid.enums.ImageLocation
import eu.depau.etchdroid.enums.WizardStep
import eu.depau.etchdroid.img_types.Image
object StateKeeper {
var wizardStep: WizardStep = WizardStep.SELECT_FLASH_METHOD
var currentFragment: WizardFragment? = null
var flashMethod: FlashMethod? = null
var imageLocation: ImageLocation? = ImageLocation.LOCAL
var streamingWrite: Boolean = false
var imageFile: Uri? = null
var imageRepr: Image? = null


+ 30
- 0
app/src/main/java/eu/depau/etchdroid/activities/ActivityBase.kt View File

@ -0,0 +1,30 @@
package eu.depau.etchdroid.activities
import android.content.Intent
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import eu.depau.etchdroid.R
abstract class ActivityBase: AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_licenses -> {
val intent = Intent(this, LicensesActivity::class.java)
startActivity(intent)
return true
}
else -> super.onOptionsItemSelected(item)
}
}
}

+ 150
- 0
app/src/main/java/eu/depau/etchdroid/activities/ConfirmationActivity.kt View File

@ -0,0 +1,150 @@
package eu.depau.etchdroid.activities
import android.os.Bundle
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import eu.depau.etchdroid.R
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.adapters.PartitionTableRecyclerViewAdapter
import eu.depau.etchdroid.enums.FlashMethod
import eu.depau.etchdroid.img_types.DMGImage
import eu.depau.etchdroid.kotlin_exts.*
import eu.depau.etchdroid.services.UsbApiDmgWriteService
import eu.depau.etchdroid.services.UsbApiImgWriteService
import kotlinx.android.synthetic.main.activity_confirmation.*
import java.io.IOException
class ConfirmationActivity : ActivityBase() {
var canContinue: Boolean = false
var issuesFound: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_confirmation)
actionBar?.setDisplayHomeAsUpEnabled(true)
displayDetails()
displayImageLayout()
}
fun displayDetails() {
confirm_sel_method.text = when (StateKeeper.flashMethod) {
FlashMethod.FLASH_API -> getString(R.string.flash_dd_usb_api)
FlashMethod.FLASH_DMG_API -> getString(R.string.flash_dmg_api)
FlashMethod.FLASH_UNETBOOTIN -> getString(R.string.flash_unetbootin)
FlashMethod.FLASH_WOEUSB -> getString(R.string.flash_woeusb)
else -> null
}
confirm_sel_image.text = StateKeeper.imageFile?.getFileName(this)
if (confirm_sel_image.text == null)
confirm_sel_image.text = getString(R.string.unknown_filename)
val imgSize = StateKeeper.imageFile?.getFileSize(this)
confirm_sel_image_size.text = imgSize?.toHRSize()
confirm_sel_usbdev.text = StateKeeper.usbDevice?.name
for (trial in 0..1) {
try {
StateKeeper.usbMassStorageDevice!!.init()
val blockDev = StateKeeper.usbMassStorageDevice?.blockDevice
if (blockDev != null) {
val devSize = (blockDev.size.toLong() * blockDev.blockSize.toLong())
confirm_sel_usbdev_size.text = devSize.toHRSize()
if (imgSize!! > devSize)
confirm_extra_info.text = getString(R.string.image_bigger_than_usb)
else {
var text =
if (StateKeeper.flashMethod == FlashMethod.FLASH_DMG_API)
getString(R.string.no_image_size_check_dmg) + "\n"
else
""
text += getString(R.string.tap_next_to_write)
confirm_extra_info.text = text
canContinue = true
}
} else {
confirm_extra_info.text = getString(R.string.cant_read_usbdev)
}
} catch (e: IOException) {
if (trial == 0) {
StateKeeper.usbMassStorageDevice!!.close()
continue
} else {
confirm_extra_info.text = getString(R.string.could_not_access_usb_error)
break
}
}
}
}
fun displayImageLayout() {
val uri = StateKeeper.imageFile ?: return
val text = uri.getFileName(this)
if (StateKeeper.flashMethod == FlashMethod.FLASH_DMG_API) {
StateKeeper.imageRepr = DMGImage(uri, this)
val imgRepr = StateKeeper.imageRepr as DMGImage
if (imgRepr.tableType == null && (imgRepr.partitionTable == null || imgRepr.partitionTable?.size == 0)) {
part_table_header.text = getString(R.string.image_is_not_dmg)
issuesFound = true
return
} else {
part_table_header.text = if (imgRepr.tableType != null) getString(R.string.partition_table_title) else ""
part_table_header_side.text = imgRepr.tableType?.getString(this) ?: ""
issuesFound = false
val viewAdapter = PartitionTableRecyclerViewAdapter(imgRepr.partitionTable!!)
part_table_recycler.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
adapter = viewAdapter
}
}
}
}
fun nextStep() {
if (!canContinue) {
confirm_fab.snackbar(getString(R.string.cannot_write))
return
}
toast(getString(R.string.check_notification_progress), Toast.LENGTH_LONG)
val intent: Intent = when (StateKeeper.flashMethod) {
FlashMethod.FLASH_API -> Intent(this, UsbApiImgWriteService::class.java)
FlashMethod.FLASH_DMG_API -> Intent(this, UsbApiDmgWriteService::class.java)
else -> null!!
}
intent.setDataAndType(StateKeeper.imageFile, "application/octet-stream")
intent.putExtra("usbDevice", StateKeeper.usbDevice)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
startForegroundService(intent)
else
startService(intent)
moveTaskToBack(true);
finish()
}
fun onButtonClicked(view: View) {
when (view.id) {
R.id.confirm_fab -> nextStep()
}
}
}

+ 3
- 6
app/src/main/java/eu/depau/etchdroid/activities/LicensesActivity.kt View File

@ -3,10 +3,9 @@ package eu.depau.etchdroid.activities
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.MenuItem
import android.view.View
import eu.depau.etchdroid.R
@ -41,8 +40,6 @@ class LicensesActivity : AppCompatActivity() {
setContentView(R.layout.activity_licenses)
updateLicenses()
title = getString(R.string.licenses)
// Enable back button in action bar
supportActionBar!!.setDisplayHomeAsUpEnabled(true)


+ 0
- 102
app/src/main/java/eu/depau/etchdroid/activities/MainActivity.kt View File

@ -1,102 +0,0 @@
package eu.depau.etchdroid.activities
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import eu.depau.etchdroid.R
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.fragments.WizardFragment
import eu.depau.etchdroid.fragments.FlashMethodFragment
import eu.depau.etchdroid.fragments.UsbDriveFragment
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : WizardActivity() {
val TAG = "MainActivity"
val ACTION_USB_PERMISSION = "eu.depau.etchdroid.USB_PERMISSION"
lateinit var mUsbPermissionIntent: PendingIntent
private val mUsbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_USB_PERMISSION) {
synchronized(this) {
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
val result = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (result)
device?.apply {
StateKeeper.usbDevice = this
}
if (StateKeeper.currentFragment is UsbDriveFragment)
(StateKeeper.currentFragment as UsbDriveFragment).onUsbPermissionResult(device, result)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
fab.setOnClickListener(::nextStep)
// Create new fragment and transaction
val fragment = FlashMethodFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_layout, fragment)
transaction.commit()
fragment.onFragmentAdded(this)
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
mUsbPermissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(mUsbReceiver, filter)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(mUsbReceiver)
}
override fun goToNewFragment(fragment: WizardFragment) {
StateKeeper.currentFragment?.onFragmentRemoving(this)
val transaction = supportFragmentManager.beginTransaction()
transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left)
transaction.replace(R.id.fragment_layout, fragment)
transaction.addToBackStack(null)
transaction.commit()
fragment.onFragmentAdded(this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_licenses -> {
val intent = Intent(this, LicensesActivity::class.java)
startActivity(intent)
return true
}
else -> super.onOptionsItemSelected(item)
}
}
}

+ 128
- 0
app/src/main/java/eu/depau/etchdroid/activities/StartActivity.kt View File

@ -0,0 +1,128 @@
package eu.depau.etchdroid.activities
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
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 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: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_start)
}
fun onButtonClicked(view: View) {
StateKeeper.flashMethod = when (view.id) {
R.id.btn_image_raw -> FlashMethod.FLASH_API
R.id.btn_image_dmg -> FlashMethod.FLASH_DMG_API
else -> null
}
when (StateKeeper.flashMethod) {
FlashMethod.FLASH_API -> {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("*/*");
startActivityForResult(intent, READ_REQUEST_CODE)
}
FlashMethod.FLASH_DMG_API -> {
if (checkAndRequestStorageReadPerm()) {
val sdcard = Environment.getExternalStorageDirectory().absolutePath
val chooser = StorageChooser.Builder()
.withActivity(this)
.withFragmentManager(fragmentManager)
.withMemoryBar(true)
.allowCustomPath(true)
.setType(StorageChooser.FILE_PICKER)
.customFilter(arrayListOf("dmg"))
.build()
chooser.show()
chooser.setOnSelectListener {
StateKeeper.imageFile = Uri.fromFile(File(it))
nextStep()
}
} else {
delayedButtonClicked = view
}
}
FlashMethod.FLASH_UNETBOOTIN -> {
}
FlashMethod.FLASH_WOEUSB -> {
}
null -> {
}
}
}
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 != null)
onButtonClicked(delayedButtonClicked!!)
return
}
else -> {
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// The document selected by the user won't be returned in the intent.
// Instead, a URI to that document will be contained in the return intent
// provided to this method as a parameter.
// Pull that URI using resultData.getData().
var uri: Uri? = null
if (data != null) {
uri = data.getData()
StateKeeper.imageFile = uri
nextStep()
}
}
}
fun nextStep() {
val intent = Intent(this, UsbDrivePickerActivity::class.java)
startActivity(intent)
}
}

+ 150
- 0
app/src/main/java/eu/depau/etchdroid/activities/UsbDrivePickerActivity.kt View File

@ -0,0 +1,150 @@
package eu.depau.etchdroid.activities
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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.utils.ClickListener
import eu.depau.etchdroid.utils.EmptyRecyclerView
import eu.depau.etchdroid.utils.RecyclerViewTouchListener
import kotlinx.android.synthetic.main.activity_usb_drive_picker.*
class UsbDrivePickerActivity : ActivityBase(), SwipeRefreshLayout.OnRefreshListener {
val USB_PERMISSION = "eu.depau.etchdroid.USB_PERMISSION"
lateinit var mUsbPermissionIntent: PendingIntent
private lateinit var recyclerView: EmptyRecyclerView
private lateinit var viewAdapter: UsbDrivesRecyclerViewAdapter
private lateinit var viewManager: RecyclerView.LayoutManager
private lateinit var refreshLayout: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_usb_drive_picker)
actionBar?.setDisplayHomeAsUpEnabled(true)
mUsbPermissionIntent = PendingIntent.getBroadcast(this, 0, Intent(USB_PERMISSION), 0)
val usbPermissionFilter = IntentFilter(USB_PERMISSION)
val usbAttachedFilter = IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED)
val usbDetachedFilter = IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED)
registerReceiver(mUsbReceiver, usbPermissionFilter)
registerReceiver(mUsbReceiver, usbAttachedFilter)
registerReceiver(mUsbReceiver, usbDetachedFilter)
refreshLayout = usbdevs_swiperefreshlayout
refreshLayout.setOnRefreshListener(this)
refreshLayout.post {
refreshLayout.isRefreshing = true
loadUsbDevices()
}
viewManager = LinearLayoutManager(this)
recyclerView = usbdevs_recycler_view
recyclerView.emptyView = usbdevs_recycler_empty_view
recyclerView.addOnItemTouchListener(RecyclerViewTouchListener(this, recyclerView, object : ClickListener {
override fun onClick(view: View, position: Int) {
val device = viewAdapter.get(position)
val manager = getSystemService(Context.USB_SERVICE) as UsbManager
manager.requestPermission(device.usbDevice, mUsbPermissionIntent)
}
override fun onLongClick(view: View, position: Int) {}
}))
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(mUsbReceiver)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.usb_devices_menu, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onContextItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_refresh -> {
loadUsbDevices()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onRefresh() {
loadUsbDevices()
}
private val mUsbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
USB_PERMISSION -> synchronized(this) {
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
val result = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (result)
device?.apply {
StateKeeper.usbDevice = this
}
if (!result) {
if (device != null) {
recyclerView.snackbar(getString(R.string.usb_perm_denied) + device.name)
} else {
recyclerView.snackbar(getString(R.string.usb_perm_denied_noname))
}
return
}
StateKeeper.usbDevice = device
StateKeeper.usbMassStorageDevice = UsbMassStorageDevice.getMassStorageDevices(context).find { it.usbDevice == device }
nextStep()
}
UsbManager.ACTION_USB_DEVICE_ATTACHED, UsbManager.ACTION_USB_DEVICE_DETACHED -> loadUsbDevices()
}
}
}
fun loadUsbDevices() {
try {
viewAdapter = UsbDrivesRecyclerViewAdapter(UsbMassStorageDevice.getMassStorageDevices(this))
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
} finally {
refreshLayout.isRefreshing = false
}
}
fun nextStep() {
val intent = Intent(this, ConfirmationActivity::class.java)
startActivity(intent)
}
}

+ 0
- 35
app/src/main/java/eu/depau/etchdroid/activities/WizardActivity.kt View File

@ -1,35 +0,0 @@
package eu.depau.etchdroid.activities
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.view.View
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.fragments.WizardFragment
abstract class WizardActivity : AppCompatActivity() {
abstract fun goToNewFragment(fragment: WizardFragment)
open fun onCheckBoxClicked(view: View) {
StateKeeper.currentFragment?.onCheckBoxClicked(view)
}
open fun onButtonClicked(view: View) {
StateKeeper.currentFragment?.onButtonClicked(view)
}
open fun onRadioButtonClicked(view: View) {
StateKeeper.currentFragment?.onRadioButtonClicked(view)
}
open fun nextStep(view: View) {
StateKeeper.currentFragment?.nextStep(view)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
StateKeeper.currentFragment?.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
StateKeeper.currentFragment?.onActivityResult(requestCode, resultCode, data)
}
}

+ 1
- 1
app/src/main/java/eu/depau/etchdroid/adapters/LicenseRecyclerViewAdapter.kt View File

@ -1,7 +1,7 @@
package eu.depau.etchdroid.adapters
import android.annotation.SuppressLint
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup


+ 1
- 1
app/src/main/java/eu/depau/etchdroid/adapters/PartitionTableRecyclerViewAdapter.kt View File

@ -1,6 +1,6 @@
package eu.depau.etchdroid.adapters
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout


+ 1
- 1
app/src/main/java/eu/depau/etchdroid/adapters/UsbDrivesRecyclerViewAdapter.kt View File

@ -2,7 +2,7 @@ package eu.depau.etchdroid.adapters
import android.annotation.SuppressLint
import android.os.Build
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RelativeLayout


+ 0
- 115
app/src/main/java/eu/depau/etchdroid/fragments/ConfirmInfoFragment.kt View File

@ -1,115 +0,0 @@
package eu.depau.etchdroid.fragments
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.depau.etchdroid.R
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.enums.FlashMethod
import eu.depau.etchdroid.enums.WizardStep
import eu.depau.etchdroid.kotlin_exts.*
import eu.depau.etchdroid.services.UsbApiDmgWriteService
import eu.depau.etchdroid.services.UsbApiImgWriteService
import kotlinx.android.synthetic.main.fragment_confirminfo.view.*
import java.io.IOException
/**
* A placeholder fragment containing a simple view.
*/
class ConfirmInfoFragment : WizardFragment() {
val TAG = "ConfirmInfoFragment"
var canContinue = false
override fun nextStep(view: View?) {
if (!canContinue) {
view?.snackbar("Cannot write image to USB drive")
return
}
context?.toast("Check notification for progress")
val intent: Intent = when (StateKeeper.flashMethod) {
FlashMethod.FLASH_API -> Intent(activity, UsbApiImgWriteService::class.java)
FlashMethod.FLASH_DMG_API -> Intent(activity, UsbApiDmgWriteService::class.java)
else -> null!!
}
intent.setDataAndType(StateKeeper.imageFile, "application/octet-stream")
intent.putExtra("usbDevice", StateKeeper.usbDevice)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
activity!!.startForegroundService(intent)
else
activity!!.startService(intent)
activity!!.finish()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
StateKeeper.currentFragment = this
StateKeeper.wizardStep = WizardStep.CONFIRM
val view = inflater.inflate(R.layout.fragment_confirminfo, container, false)
view.confirm_sel_method.text = when (StateKeeper.flashMethod) {
FlashMethod.FLASH_API -> getString(R.string.flash_dd_usb_api)
FlashMethod.FLASH_DMG_API -> getString(R.string.flash_dmg_api)
FlashMethod.FLASH_UNETBOOTIN -> getString(R.string.flash_unetbootin)
FlashMethod.FLASH_WOEUSB -> getString(R.string.flash_woeusb)
else -> null
}
view.confirm_sel_image.text = StateKeeper.imageFile?.getFileName(context!!)
if (view.confirm_sel_image.text == null)
view.confirm_sel_image.text = getString(R.string.unknown_filename)
val imgSize = StateKeeper.imageFile?.getFileSize(context!!)
view.confirm_sel_image_size.text = imgSize?.toHRSize()
view.confirm_sel_usbdev.text = StateKeeper.usbDevice?.name
for (trial in 0..1) {
try {
StateKeeper.usbMassStorageDevice!!.init()
val blockDev = StateKeeper.usbMassStorageDevice?.blockDevice
if (blockDev != null) {
val devSize = (blockDev.size.toLong() * blockDev.blockSize.toLong())
view.confirm_sel_usbdev_size.text = devSize.toHRSize()
if (imgSize!! > devSize)
view.confirm_extra_info.text = getString(R.string.image_bigger_than_usb)
else {
var text =
if (StateKeeper.flashMethod == FlashMethod.FLASH_DMG_API)
getString(R.string.no_image_size_check_dmg) + "\n"
else
""
text += getString(R.string.tap_next_to_write)
view.confirm_extra_info.text = text
canContinue = true
}
} else {
view.confirm_extra_info.text = getString(R.string.cant_read_usbdev)
}
} catch (e: IOException) {
if (trial == 0) {
StateKeeper.usbMassStorageDevice!!.close()
continue
} else {
view.confirm_extra_info.text = "Could not access USB device. Maybe you ran the app previously and it crashed? Remove and reinsert the USB drive, then restart the app."
break
}
}
}
return view
}
}

+ 0
- 42
app/src/main/java/eu/depau/etchdroid/fragments/FlashMethodFragment.kt View File

@ -1,42 +0,0 @@
package eu.depau.etchdroid.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.depau.etchdroid.R
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.activities.WizardActivity
import eu.depau.etchdroid.kotlin_exts.snackbar
import eu.depau.etchdroid.enums.FlashMethod
import eu.depau.etchdroid.enums.WizardStep
/**
* A placeholder fragment containing a simple view.
*/
class FlashMethodFragment : WizardFragment() {
override fun nextStep(view: View?) {
if (StateKeeper.flashMethod == null)
view?.snackbar(getString(R.string.please_select_writing_method))
else
(activity as WizardActivity).goToNewFragment(ImageLocationFragment())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
StateKeeper.currentFragment = this
StateKeeper.wizardStep = WizardStep.SELECT_FLASH_METHOD
return inflater.inflate(R.layout.fragment_select_flash_method, container, false)
}
override fun onRadioButtonClicked(view: View) {
StateKeeper.flashMethod = when (view.id) {
R.id.flash_dmg_api_radio -> FlashMethod.FLASH_DMG_API
R.id.flash_usb_api_radio -> FlashMethod.FLASH_API
R.id.flash_unetbootin_radio -> FlashMethod.FLASH_UNETBOOTIN
R.id.flash_woeusb_radio -> FlashMethod.FLASH_WOEUSB
else -> null
}
}
}

+ 0
- 211
app/src/main/java/eu/depau/etchdroid/fragments/ImageLocationFragment.kt View File

@ -1,211 +0,0 @@
package eu.depau.etchdroid.fragments
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.codekidlabs.storagechooser.StorageChooser
import eu.depau.etchdroid.R
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.activities.WizardActivity
import eu.depau.etchdroid.adapters.PartitionTableRecyclerViewAdapter
import eu.depau.etchdroid.enums.FlashMethod
import eu.depau.etchdroid.enums.ImageLocation
import eu.depau.etchdroid.enums.WizardStep
import eu.depau.etchdroid.img_types.DMGImage
import eu.depau.etchdroid.kotlin_exts.getFileName
import eu.depau.etchdroid.kotlin_exts.snackbar
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_select_location.*
import java.io.File
/**
* A placeholder fragment containing a simple view.
*/
class ImageLocationFragment : WizardFragment() {
val READ_REQUEST_CODE = 42
val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 29
val TAG = "ImageLocationFragment"
val PICKER_DIALOG_TAG = "eu.depau.etchdroid.filepicker.DIALOG_TAG"
var issuesFound = false
fun isStreamingAvailable(): Boolean {
if (StateKeeper.imageLocation != ImageLocation.REMOTE)
return false
if (StateKeeper.flashMethod != FlashMethod.FLASH_DMG_API && StateKeeper.flashMethod != FlashMethod.FLASH_API)
return false
return true
}
override fun onRadioButtonClicked(view: View) {
StateKeeper.imageLocation = ImageLocation.LOCAL
activity?.fab?.show()
pick_file_btn?.isEnabled = StateKeeper.imageLocation == ImageLocation.LOCAL
loadImageChanges(activity as WizardActivity)
}
override fun onButtonClicked(view: View) {
if (view.id == R.id.pick_file_btn) {
when (StateKeeper.flashMethod) {
FlashMethod.FLASH_API -> {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("*/*");
activity?.startActivityForResult(intent, READ_REQUEST_CODE)
}
FlashMethod.FLASH_DMG_API -> {
if (checkAndRequestStorageReadPerm()) {
val sdcard = Environment.getExternalStorageDirectory().absolutePath
val chooser = StorageChooser.Builder()
.withActivity(activity)
.withFragmentManager(activity!!.fragmentManager)
.withMemoryBar(true)
.allowCustomPath(true)
.setType(StorageChooser.FILE_PICKER)
.customFilter(arrayListOf("dmg"))
.build()
chooser.show()
chooser.setOnSelectListener {
StateKeeper.imageFile = Uri.fromFile(File(it))
loadImageChanges(activity as WizardActivity)
activity?.fab?.show()
}
}
}
FlashMethod.FLASH_UNETBOOTIN -> {
}
FlashMethod.FLASH_WOEUSB -> {
}
null -> {
}
}
}
}
fun checkAndRequestStorageReadPerm(): Boolean {
if ((ContextCompat.checkSelfPermission(activity!!, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity!!,
Manifest.permission.READ_EXTERNAL_STORAGE)) {
view!!.snackbar("Storage permission is required to read DMG images")
} else {
ActivityCompat.requestPermissions(activity!!,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
}
} else {
// Permission granted
return true
}
return false
}
override fun nextStep(view: View?) {
if (issuesFound) {
view?.snackbar(getString(R.string.issues_found_expl))
return
}
if (StateKeeper.imageLocation == null) {
view?.snackbar(getString(R.string.select_image_location))
return
}
if (StateKeeper.imageFile == null) {
view?.snackbar(getString(R.string.provide_image_file))
return
}
(activity as WizardActivity).goToNewFragment(UsbDriveFragment())
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
onButtonClicked(pick_file_btn)
return
}
else -> {}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.fab?.show()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
StateKeeper.currentFragment = this
StateKeeper.wizardStep = WizardStep.SELECT_LOCATION
return inflater.inflate(R.layout.fragment_select_location, container, false)
}
fun loadImageChanges(context: WizardActivity) {
val button = pick_file_btn
val uri = StateKeeper.imageFile ?: return
val text = uri.getFileName(context)
if (text != null)
button.text = text
else
button.text = getString(R.string.pick_a_file)
if (StateKeeper.flashMethod == FlashMethod.FLASH_DMG_API) {
StateKeeper.imageRepr = DMGImage(uri, context)
val imgRepr = StateKeeper.imageRepr as DMGImage
if (imgRepr.tableType == null && (imgRepr.partitionTable == null || imgRepr.partitionTable?.size == 0)) {
part_table_header.text = getString(R.string.image_is_not_dmg)
issuesFound = true
return
} else {
part_table_header.text = if (imgRepr.tableType != null) "Partition table:" else ""
part_table_header_side.text = imgRepr.tableType?.getString(context) ?: ""
issuesFound = false
val viewAdapter = PartitionTableRecyclerViewAdapter(imgRepr.partitionTable!!)
part_table_recycler.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(activity)
adapter = viewAdapter
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// The document selected by the user won't be returned in the intent.
// Instead, a URI to that document will be contained in the return intent
// provided to this method as a parameter.
// Pull that URI using resultData.getData().
var uri: Uri? = null
if (data != null) {
uri = data.getData()
Log.d(TAG, "Uri: " + uri!!.toString())
StateKeeper.imageFile = uri
loadImageChanges(activity as WizardActivity)
activity?.fab?.show()
}
}
}
}

+ 0
- 139
app/src/main/java/eu/depau/etchdroid/fragments/UsbDriveFragment.kt View File

@ -1,139 +0,0 @@
package eu.depau.etchdroid.fragments
import android.content.Context
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.support.v4.widget.SwipeRefreshLayout
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
import com.github.mjdev.libaums.UsbMassStorageDevice
import eu.depau.etchdroid.R
import eu.depau.etchdroid.StateKeeper
import eu.depau.etchdroid.utils.ClickListener
import eu.depau.etchdroid.activities.WizardActivity
import eu.depau.etchdroid.activities.MainActivity
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.WizardStep
import eu.depau.etchdroid.utils.RecyclerViewTouchListener
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_select_usb_drive.view.*
/**
* A placeholder fragment containing a simple view.
*/
class UsbDriveFragment : WizardFragment(), SwipeRefreshLayout.OnRefreshListener {
val TAG = "UsbDriveFragment"
private lateinit var recyclerView: RecyclerView
private lateinit var viewAdapter: UsbDrivesRecyclerViewAdapter
private lateinit var viewManager: RecyclerView.LayoutManager
private lateinit var refreshLayout: SwipeRefreshLayout
override fun onRefresh() {
loadUsbDevices()
}
override fun nextStep(view: View?) {
(activity as WizardActivity).goToNewFragment(ConfirmInfoFragment())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
StateKeeper.currentFragment = this
StateKeeper.wizardStep = WizardStep.SELECT_USB_DRIVE
val view = inflater.inflate(R.layout.fragment_select_usb_drive, container, false)
refreshLayout = view.usbdevs_refresh_layout
refreshLayout.setOnRefreshListener(this)
refreshLayout.post {
refreshLayout.isRefreshing = true
loadUsbDevices()
}
viewManager = LinearLayoutManager(activity)
recyclerView = view.usbdevs_recycler_view
recyclerView.addOnItemTouchListener(RecyclerViewTouchListener(activity!!, recyclerView, object : ClickListener {
override fun onClick(view: View, position: Int) {
val device = viewAdapter.get(position)
val manager = activity!!.getSystemService(Context.USB_SERVICE) as UsbManager
manager.requestPermission(device.usbDevice, (activity as MainActivity).mUsbPermissionIntent)
}
override fun onLongClick(view: View, position: Int) {}
}))
return view
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
fun loadUsbDevices() {
try {
viewAdapter = UsbDrivesRecyclerViewAdapter(UsbMassStorageDevice.getMassStorageDevices(activity))
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
} finally {
refreshLayout.isRefreshing = false
}
}
override fun onFragmentAdded(activity: WizardActivity) {
activity.fab.hide()
}
override fun onFragmentRemoving(activity: WizardActivity) {
activity.fab.show()
}
override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.usb_devices_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_refresh -> {
loadUsbDevices()
true
}
else -> super.onOptionsItemSelected(item)
}
}
fun onUsbPermissionResult(usbDevice: UsbDevice?, granted: Boolean) {
if (!granted) {
if (usbDevice != null) {
recyclerView.snackbar(getString(R.string.usb_perm_denied) + usbDevice.name)
} else {
recyclerView.snackbar(getString(R.string.usb_perm_denied_noname))
}
return
}
StateKeeper.usbDevice = usbDevice
StateKeeper.usbMassStorageDevice = UsbMassStorageDevice.getMassStorageDevices(activity).find { it.usbDevice == usbDevice }
nextStep(null)
}
}

+ 0
- 20
app/src/main/java/eu/depau/etchdroid/fragments/WizardFragment.kt View File

@ -1,20 +0,0 @@
package eu.depau.etchdroid.fragments
import android.content.Intent
import android.support.v4.app.Fragment
import android.view.View
import eu.depau.etchdroid.activities.WizardActivity
abstract class WizardFragment() : Fragment() {
private lateinit var wizardActivity: WizardActivity
abstract fun nextStep(view: View?)
open fun onFragmentAdded(activity: WizardActivity) {}
open fun onFragmentRemoving(activity: WizardActivity) {}
open fun onRadioButtonClicked(view: View) {}
open fun onCheckBoxClicked(view: View) {}
open override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
open fun onButtonClicked(view: View) {}
open override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {}
}

+ 1
- 1
app/src/main/java/eu/depau/etchdroid/kotlin_exts/ViewSnackbar.kt View File

@ -1,6 +1,6 @@
package eu.depau.etchdroid.kotlin_exts
import android.support.design.widget.Snackbar
import com.google.android.material.snackbar.Snackbar
import android.view.View
fun View.snackbar(message: CharSequence, duration: Int = Snackbar.LENGTH_LONG) {


+ 1
- 1
app/src/main/java/eu/depau/etchdroid/services/UsbWriteService.kt View File

@ -8,7 +8,7 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import eu.depau.etchdroid.R
import eu.depau.etchdroid.kotlin_exts.toHRSize
import eu.depau.etchdroid.kotlin_exts.toHRTime


+ 42
- 0
app/src/main/java/eu/depau/etchdroid/utils/EmptyRecyclerView.kt View File

@ -0,0 +1,42 @@
package eu.depau.etchdroid.utils
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class EmptyRecyclerView : RecyclerView {
var emptyView: View? = null
set(value) {
field = value
checkIfEmpty()
}
private val observer: RecyclerView.AdapterDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
checkIfEmpty()
}
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
internal fun checkIfEmpty() {
if (emptyView != null)
emptyView!!.visibility = if (adapter?.itemCount ?: 0 > 0) View.GONE else View.VISIBLE
}
override fun setAdapter(adapter: RecyclerView.Adapter<*>?) {
val oldAdapter = this.adapter
oldAdapter?.unregisterAdapterDataObserver(observer)
super.setAdapter(adapter)
adapter?.registerAdapterDataObserver(observer)
checkIfEmpty()
}
}

+ 1
- 1
app/src/main/java/eu/depau/etchdroid/utils/RecyclerViewTouchListener.kt View File

@ -1,7 +1,7 @@
package eu.depau.etchdroid.utils
import android.content.Context
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.GestureDetector
import android.view.MotionEvent


+ 34
- 0
app/src/main/res/drawable/ic_beta.xml View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="m0,0 l64,64L64,34.6L29.4,0Z"
android:strokeAlpha="1"
android:strokeWidth="3.24922633"
android:fillColor="#c62828"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:strokeLineCap="square"/>
<path
android:pathData="m29.8847,6.8837l3.1187,3.1187q0.6608,0.6608 0.9275,1.5072 0.2782,0.8579 0.1043,1.7043 -0.1739,0.8463 -0.7884,1.4608 -0.6145,0.6145 -1.3796,0.7536 -0.7536,0.1507 -1.484,-0.1159l-0.0696,0.0696q0.4058,0.8463 0.2782,1.739 -0.1159,0.9043 -0.8,1.5883 -0.684,0.684 -1.5767,0.8579 -0.8811,0.1855 -1.7854,-0.1159 -0.8811,-0.3014 -1.5883,-1.0086l-3.2578,-3.2578zM29.4674,13.4225q0.5565,0.5565 1.2405,0.5449 0.6956,0 1.1478,-0.4522 0.4522,-0.4522 0.4522,-1.1246 0.0232,-0.6724 -0.5101,-1.2057l-1.8086,-1.8086l-2.2839,2.2839zM26.0473,17.2368q0.5913,0.5913 1.3101,0.5681 0.7304,-0.0116 1.2173,-0.4985 0.4985,-0.4985 0.4869,-1.2289 0.0116,-0.7304 -0.6029,-1.3449l-1.9129,-1.9129l-2.4578,2.4578z"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
<path
android:pathData="m37.176,14.175l5.0432,5.0432l-1.2405,1.2405l-3.7447,-3.7447l-2.2955,2.2955l3.3737,3.3737l-1.2289,1.2289l-3.3737,-3.3737l-2.2955,2.2955l3.7447,3.7447l-1.2405,1.2405l-5.0432,-5.0432z"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
<path
android:pathData="m44.2738,23.7538l-2.3187,-2.3187l1.2405,-1.2405l5.9359,5.9359l-1.2405,1.2405l-2.3187,-2.3187l-7.0605,7.0605l-1.2985,-1.2985z"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
<path
android:pathData="m51.972,28.9709l1.484,1.484l-5.1823,11.4197l-1.4376,-1.4376l1.3796,-2.8636l-3.3505,-3.3505l-2.8752,1.368l-1.4376,-1.4376zM48.9808,35.9271 L50.5692,32.5765 51.2068,31.2897l-0.0696,-0.0696l-1.2869,0.6376 -3.3505,1.5883z"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:fillAlpha="1"/>
</vector>

+ 16
- 0
app/src/main/res/drawable/ic_dmg.xml View File

@ -0,0 +1,16 @@
<vector android:height="96dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="0.3" android:fillColor="#FF000000"
android:pathData="m170.667,341.333h256v-256h-256z"
android:strokeAlpha="0.3" android:strokeWidth="21.33333206"/>
<path android:fillColor="#FF000000"
android:pathData="m426.667,42.667h-256c-23.467,0 -42.667,19.2 -42.667,42.667v256C128,364.8 147.2,384 170.667,384h256c23.467,0 42.667,-19.2 42.667,-42.667v-256c0,-23.467 -19.2,-42.667 -42.667,-42.667zM426.667,341.333h-256v-256h256z" android:strokeWidth="21.33333206"/>
<path android:fillColor="#FF000000"
android:pathData="M85.333,128L42.667,128v298.667c0,23.467 19.2,42.667 42.667,42.667L384,469.333L384,426.667L85.333,426.667Z" android:strokeWidth="21.33333206"/>
<path android:fillColor="#000000"
android:pathData="m277.992,209.953l11.969,0l0,41.891l17.953,0l0,-42.011l11.969,0L319.883,263.814l17.953,0L337.837,203.969C337.837,197.386 332.451,192 325.868,192l-53.86,0C265.424,192 260.038,197.386 260.038,203.969L260.038,263.814l17.953,0z" android:strokeWidth="0.83333331"/>
<path android:fillAlpha="1" android:fillColor="#000000"
android:pathData="M394.169,191.68L358.263,191.68c-7.181,0 -11.969,5.985 -11.969,11.969l0,47.876c0,5.984 4.788,11.969 11.969,11.969l35.907,0c7.181,0 11.969,-5.985 11.969,-11.969l0,-23.938l-17.953,0l0,17.953l-23.938,0l0,-35.907l41.891,0L406.139,203.649c0,-5.984 -4.788,-11.969 -11.969,-11.969z" android:strokeWidth="0.83333331"/>
<path android:fillColor="#000000"
android:pathData="M233.098,192L191.195,192L191.195,263.814l41.903,0c10.24,0 17.92,-7.815 17.92,-17.92L251.017,209.92c0,-10.105 -7.68,-17.92 -17.92,-17.92zM233.098,245.894L209.115,245.894L209.115,209.92l23.983,0z" android:strokeWidth="0.93808633"/>
</vector>

+ 13
- 0
app/src/main/res/drawable/ic_raw.xml View File

@ -0,0 +1,13 @@
<vector android:height="96dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="0.3" android:fillColor="#FF000000"
android:pathData="M171.1,340.9L427.3,340.9L427.3,84.7L171.1,84.7Z" android:strokeAlpha="0.3"/>
<path android:fillColor="#FF000000" android:pathData="M427.3,42L171.1,42C147.615,42 128.4,61.215 128.4,84.7l0,256.2c0,23.485 19.215,42.7 42.7,42.7l256.2,0c23.485,0 42.7,-19.215 42.7,-42.7L470,84.7C470,61.215 450.785,42 427.3,42ZM427.3,340.9L171.1,340.9L171.1,84.7l256.2,0z"/>
<path android:fillColor="#FF000000" android:pathData="M85.7,127.4L43,127.4l0,298.9c0,23.485 19.215,42.7 42.7,42.7L384.6,469L384.6,426.3L85.7,426.3Z"/>
<path android:fillColor="#FF000000"
android:pathData="M253.711,221.384L253.711,209.384c0,-10.247 -7.82,-17.932 -17.932,-17.932L193.847,191.451L193.847,263.316l17.932,0l0,-24l13.753,0l10.247,24l17.932,0l-10.786,-25.213c5.932,-2.966 10.786,-9.573 10.786,-16.719zM235.779,221.384L211.779,221.384L211.779,209.384l24,0z" android:strokeWidth="0.63151968"/>
<path android:fillColor="#FF000000"
android:pathData="m280.71,245.35l23.955,0l0,17.966l17.966,0L322.631,203.429c0,-6.588 -5.39,-11.977 -11.977,-11.977l-35.932,0c-6.588,0 -11.977,5.39 -11.977,11.977L262.744,263.316l17.966,0zM280.71,209.418l23.955,0L304.665,227.384l-23.955,0z" android:strokeWidth="0.56099999"/>
<path android:fillColor="#FF000000"
android:pathData="m332.835,191.451l0,59.935c0,6.546 5.383,11.929 11.929,11.929l47.861,0c6.546,0 11.929,-5.383 11.929,-11.929L404.553,191.451L389.57,191.451L389.57,245.422L376.186,245.422L376.186,203.38l-14.984,0l0,42.187l-13.529,0L347.673,191.451Z" android:strokeWidth="0.68137652"/>
</vector>

+ 9
- 0
app/src/main/res/drawable/ic_twotone_save_alt_24px.xml View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,12v7H5v-7H3v7c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-7H19zM13,12.67l2.59,-2.58L17,11.5l-5,5l-5,-5l1.41,-1.41L11,12.67V3h2V12.67z"/>
</vector>

+ 5
- 0
app/src/main/res/drawable/ic_usb_black_200dp.xml View File

@ -0,0 +1,5 @@
<vector android:alpha="0.4" android:height="200dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="200dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15,7v4h1v2h-3V5h2l-3,-4 -3,4h2v8H8v-2.07c0.7,-0.37 1.2,-1.08 1.2,-1.93 0,-1.21 -0.99,-2.2 -2.2,-2.2 -1.21,0 -2.2,0.99 -2.2,2.2 0,0.85 0.5,1.56 1.2,1.93V13c0,1.11 0.89,2 2,2h3v3.05c-0.71,0.37 -1.2,1.1 -1.2,1.95 0,1.22 0.99,2.2 2.2,2.2 1.21,0 2.2,-0.98 2.2,-2.2 0,-0.85 -0.49,-1.58 -1.2,-1.95V15h3c1.11,0 2,-0.89 2,-2v-2h1V7h-4z"/>
</vector>

+ 219
- 0
app/src/main/res/layout/activity_confirmation.xml View File

@ -0,0 +1,219 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.ConfirmationActivity">
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/relativeLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/row_padding"
>
<TextView
android:id="@+id/confirm_sel_method_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:ellipsize="end"
android:text="@string/selected_method"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/confirm_sel_method"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/confirm_sel_method_title"/>
</RelativeLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/row_padding"
>
<TextView
android:id="@+id/confirm_sel_image_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:text="@string/selected_image"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/confirm_sel_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/confirm_sel_image_title"/>
<TextView
android:id="@+id/confirm_sel_image_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:textColor="@color/info"/>
</RelativeLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/relativeLayout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/row_padding"
>
<TextView
android:id="@+id/confirm_sel_usbdev_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:text="@string/selected_usbdev"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/confirm_sel_usbdev"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/confirm_sel_usbdev_title"/>
<TextView
android:id="@+id/confirm_sel_usbdev_size"