From 5e92baa4a0f2b4105986c489921b3e20a1348ecf Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Tue, 14 Aug 2018 01:32:02 +0200 Subject: [PATCH] Implement API writing --- app/src/main/AndroidManifest.xml | 5 +- .../main/java/eu/depau/ddroid/StateKeeper.kt | 5 + .../java/eu/depau/ddroid/abc/ClickListener.kt | 9 + .../eu/depau/ddroid/abc/UsbWriteService.kt | 99 ++++++++++ .../eu/depau/ddroid/abc/WizardFragment.kt | 7 +- .../depau/ddroid/activities/MainActivity.kt | 42 +++++ .../ddroid/fragments/ConfirmInfoFragment.kt | 87 +++++++++ .../ddroid/fragments/FlashMethodFragment.kt | 6 +- .../ddroid/fragments/ImageLocationFragment.kt | 56 +++--- .../ddroid/fragments/USBDriveFragment.kt | 70 ------- .../ddroid/fragments/UsbDriveFragment.kt | 171 ++++++++++++++++++ .../ddroid/services/UsbAPIWriteService.kt | 71 ++++++++ .../eu/depau/ddroid/utils/ContextToast.kt | 8 + .../depau/ddroid/utils/NumberToSizeString.kt | 18 ++ .../depau/ddroid/utils/UriGetDisplayName.kt | 29 +++ .../eu/depau/ddroid/utils/UriGetFileSize.kt | 39 ++++ .../depau/ddroid/utils/UsbDeviceVidPidName.kt | 17 ++ .../utils/UsbDrivesRecyclerViewAdapter.kt | 50 +++++ .../eu/depau/ddroid/utils/ViewSnackbar.kt | 8 + .../java/eu/depau/ddroid/values/WizardStep.kt | 3 +- .../main/res/drawable/ic_usb_white_24dp.xml | 5 + .../main/res/layout/fragment_confirminfo.xml | 145 +++++++++++++++ .../layout/fragment_select_flash_method.xml | 3 + .../res/layout/fragment_select_location.xml | 1 + .../res/layout/fragment_select_usb_drive.xml | 26 ++- app/src/main/res/layout/usb_device_row.xml | 37 ++++ app/src/main/res/menu/usb_devices_menu.xml | 10 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 11 ++ build.gradle | 1 + libaums | 2 +- 32 files changed, 921 insertions(+), 125 deletions(-) create mode 100644 app/src/main/java/eu/depau/ddroid/abc/ClickListener.kt create mode 100644 app/src/main/java/eu/depau/ddroid/abc/UsbWriteService.kt create mode 100644 app/src/main/java/eu/depau/ddroid/fragments/ConfirmInfoFragment.kt delete mode 100644 app/src/main/java/eu/depau/ddroid/fragments/USBDriveFragment.kt create mode 100644 app/src/main/java/eu/depau/ddroid/fragments/UsbDriveFragment.kt create mode 100644 app/src/main/java/eu/depau/ddroid/services/UsbAPIWriteService.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/ContextToast.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/NumberToSizeString.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/UriGetDisplayName.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/UriGetFileSize.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/UsbDeviceVidPidName.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/UsbDrivesRecyclerViewAdapter.kt create mode 100644 app/src/main/java/eu/depau/ddroid/utils/ViewSnackbar.kt create mode 100644 app/src/main/res/drawable/ic_usb_white_24dp.xml create mode 100644 app/src/main/res/layout/fragment_confirminfo.xml create mode 100644 app/src/main/res/layout/usb_device_row.xml create mode 100644 app/src/main/res/menu/usb_devices_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 86406c7..4906bff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + + \ No newline at end of file diff --git a/app/src/main/java/eu/depau/ddroid/StateKeeper.kt b/app/src/main/java/eu/depau/ddroid/StateKeeper.kt index bbf3656..18e0e25 100644 --- a/app/src/main/java/eu/depau/ddroid/StateKeeper.kt +++ b/app/src/main/java/eu/depau/ddroid/StateKeeper.kt @@ -1,6 +1,8 @@ package eu.depau.ddroid +import android.hardware.usb.UsbDevice import android.net.Uri +import com.github.mjdev.libaums.UsbMassStorageDevice import eu.depau.ddroid.abc.WizardFragment import eu.depau.ddroid.values.FlashMethod import eu.depau.ddroid.values.ImageLocation @@ -13,4 +15,7 @@ object StateKeeper { var imageLocation: ImageLocation? = null var streamingWrite: Boolean = false var imageFile: Uri? = null + + var usbDevice: UsbDevice? = null + var usbMassStorageDevice: UsbMassStorageDevice? = null } \ No newline at end of file diff --git a/app/src/main/java/eu/depau/ddroid/abc/ClickListener.kt b/app/src/main/java/eu/depau/ddroid/abc/ClickListener.kt new file mode 100644 index 0000000..50d4d00 --- /dev/null +++ b/app/src/main/java/eu/depau/ddroid/abc/ClickListener.kt @@ -0,0 +1,9 @@ +package eu.depau.ddroid.abc + +import android.view.View + +interface ClickListener { + fun onClick(view: View, position: Int) + + fun onLongClick(view: View, position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/ddroid/abc/UsbWriteService.kt b/app/src/main/java/eu/depau/ddroid/abc/UsbWriteService.kt new file mode 100644 index 0000000..e6e3f88 --- /dev/null +++ b/app/src/main/java/eu/depau/ddroid/abc/UsbWriteService.kt @@ -0,0 +1,99 @@ +package eu.depau.ddroid.abc + +import android.app.IntentService +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.net.Uri +import android.os.Build +import eu.depau.ddroid.R +import eu.depau.ddroid.utils.getFileName +import eu.depau.ddroid.utils.toHRSize + + +abstract class UsbWriteService(name: String) : IntentService(name) { + val TAG = name + val FOREGROUND_ID = 1931 + val WRITE_PROGRESS_CHANNEL_ID = "eu.depau.ddroid.notifications.USB_WRITE_PROGRESS" + private var prevTime = System.currentTimeMillis() + private var prevBytes = 0L + private var notifyChanRegistered = false + + fun getNotificationBuilder(): Notification.Builder { + if (!notifyChanRegistered) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channame = "USB write progress" + val description = "Displays the status of ongoing USB writes" + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(WRITE_PROGRESS_CHANNEL_ID, channame, importance) + channel.description = description + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager!!.createNotificationChannel(channel) + } + notifyChanRegistered = true + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + Notification.Builder(this, WRITE_PROGRESS_CHANNEL_ID) + else + Notification.Builder(this) + } + + fun updateNotification(usbDevice: String, uri: Uri, bytes: Long, total: Long) { + // Notification rate limiting + val time = System.currentTimeMillis() + if (time <= prevTime + 1000) + return + + val speed = ((bytes - prevBytes).toDouble() / (time - prevTime).toDouble()).toHRSize() + prevTime = time + prevBytes = bytes + + val perc: Int = (bytes.toDouble() / total * 100.0).toInt() + + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(FOREGROUND_ID, buildForegroundNotification(usbDevice, uri, bytes, total, "$perc% • $speed/s")) + } + + fun errorNotification() { + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + val b = getNotificationBuilder() + .setContentTitle("Write failed") + .setContentText("The USB drive may have been unplugged while writing.") + .setOngoing(false) + .setSmallIcon(R.drawable.ic_usb_white_24dp) + + notificationManager.notify(FOREGROUND_ID, b.build()) + } + + fun buildForegroundNotification(usbDevice: String, uri: Uri, bytes: Long, total: Long, subText: String? = null): Notification { + val progr: Int + val indet: Boolean + + if (total < 0) { + progr = 0 + indet = true + } else { + progr = (bytes.toFloat()/total * 100).toInt() + indet = false + } + + val b = getNotificationBuilder() + + b.setContentTitle("Writing image") + .setContentText("${uri.getFileName(this)} to $usbDevice") + .setOngoing(true) + .setSmallIcon(R.drawable.ic_usb_white_24dp) + .setProgress(100, progr, indet) + + if (subText != null) + b.setSubText(subText) + + return b.build() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/ddroid/abc/WizardFragment.kt b/app/src/main/java/eu/depau/ddroid/abc/WizardFragment.kt index 2e12a0a..ebe7c78 100644 --- a/app/src/main/java/eu/depau/ddroid/abc/WizardFragment.kt +++ b/app/src/main/java/eu/depau/ddroid/abc/WizardFragment.kt @@ -4,10 +4,13 @@ import android.content.Intent import android.support.v4.app.Fragment import android.view.View -abstract class WizardFragment : Fragment() { - abstract fun nextStep(view: View) +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?) {} diff --git a/app/src/main/java/eu/depau/ddroid/activities/MainActivity.kt b/app/src/main/java/eu/depau/ddroid/activities/MainActivity.kt index 44e7cc3..eea10fc 100644 --- a/app/src/main/java/eu/depau/ddroid/activities/MainActivity.kt +++ b/app/src/main/java/eu/depau/ddroid/activities/MainActivity.kt @@ -1,15 +1,49 @@ package eu.depau.ddroid.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.ddroid.R +import eu.depau.ddroid.StateKeeper import eu.depau.ddroid.abc.WizardActivity import eu.depau.ddroid.abc.WizardFragment import eu.depau.ddroid.fragments.FlashMethodFragment +import eu.depau.ddroid.fragments.UsbDriveFragment import kotlinx.android.synthetic.main.activity_main.* class MainActivity : WizardActivity() { + val TAG = "MainActivity" + val ACTION_USB_PERMISSION = "eu.depau.ddroid.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) @@ -22,14 +56,22 @@ class MainActivity : WizardActivity() { 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 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) } diff --git a/app/src/main/java/eu/depau/ddroid/fragments/ConfirmInfoFragment.kt b/app/src/main/java/eu/depau/ddroid/fragments/ConfirmInfoFragment.kt new file mode 100644 index 0000000..6ed0bd0 --- /dev/null +++ b/app/src/main/java/eu/depau/ddroid/fragments/ConfirmInfoFragment.kt @@ -0,0 +1,87 @@ +package eu.depau.ddroid.fragments + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import eu.depau.ddroid.R +import eu.depau.ddroid.StateKeeper +import eu.depau.ddroid.abc.WizardActivity +import eu.depau.ddroid.abc.WizardFragment +import eu.depau.ddroid.services.UsbAPIWriteService +import eu.depau.ddroid.utils.* +import eu.depau.ddroid.values.FlashMethod +import eu.depau.ddroid.values.WizardStep +import kotlinx.android.synthetic.main.fragment_confirminfo.view.* + +/** + * 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(activity, UsbAPIWriteService::class.java) + intent.setDataAndType(StateKeeper.imageFile, "application/octet-stream") + intent.putExtra("usbDevice", StateKeeper.usbDevice) + 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_DD -> getString(R.string.flash_dd_root) + 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 + + 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 { + view.confirm_extra_info.text = getString(R.string.tap_next_to_write) + canContinue = true + } + } else { + view.confirm_extra_info.text = getString(R.string.cant_read_usbdev) + } + + return view + } + + +} diff --git a/app/src/main/java/eu/depau/ddroid/fragments/FlashMethodFragment.kt b/app/src/main/java/eu/depau/ddroid/fragments/FlashMethodFragment.kt index df21359..e6f2ea1 100644 --- a/app/src/main/java/eu/depau/ddroid/fragments/FlashMethodFragment.kt +++ b/app/src/main/java/eu/depau/ddroid/fragments/FlashMethodFragment.kt @@ -1,7 +1,6 @@ package eu.depau.ddroid.fragments import android.os.Bundle -import android.support.design.widget.Snackbar import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,6 +8,7 @@ import eu.depau.ddroid.R import eu.depau.ddroid.StateKeeper import eu.depau.ddroid.abc.WizardActivity import eu.depau.ddroid.abc.WizardFragment +import eu.depau.ddroid.utils.snackbar import eu.depau.ddroid.values.FlashMethod import eu.depau.ddroid.values.WizardStep @@ -16,9 +16,9 @@ import eu.depau.ddroid.values.WizardStep * A placeholder fragment containing a simple view. */ class FlashMethodFragment : WizardFragment() { - override fun nextStep(view: View) { + override fun nextStep(view: View?) { if (StateKeeper.flashMethod == null) - Snackbar.make(view, "Please select writing method", Snackbar.LENGTH_LONG).show() + view?.snackbar(getString(R.string.please_select_writing_method)) else (activity as WizardActivity).goToNewFragment(ImageLocationFragment()) } diff --git a/app/src/main/java/eu/depau/ddroid/fragments/ImageLocationFragment.kt b/app/src/main/java/eu/depau/ddroid/fragments/ImageLocationFragment.kt index e5b169c..786b5ab 100644 --- a/app/src/main/java/eu/depau/ddroid/fragments/ImageLocationFragment.kt +++ b/app/src/main/java/eu/depau/ddroid/fragments/ImageLocationFragment.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle -import android.provider.OpenableColumns import android.support.design.widget.Snackbar import android.support.v4.app.ActivityCompat import android.support.v4.content.ContextCompat @@ -14,16 +13,18 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button -import android.widget.CheckBox -import android.widget.EditText import eu.depau.ddroid.R import eu.depau.ddroid.StateKeeper import eu.depau.ddroid.abc.WizardActivity import eu.depau.ddroid.abc.WizardFragment +import eu.depau.ddroid.utils.getFileName +import eu.depau.ddroid.utils.snackbar import eu.depau.ddroid.values.FlashMethod import eu.depau.ddroid.values.ImageLocation import eu.depau.ddroid.values.WizardStep +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.fragment_select_location.* +import kotlinx.android.synthetic.main.wizard_fragment_layout.* /** @@ -43,7 +44,7 @@ class ImageLocationFragment : WizardFragment() { } fun setStreamingCheckBoxAvailability(context: WizardActivity) { - val checkBox = context.findViewById(R.id.streaming_write_checkbox) + val checkBox = streaming_write_checkbox if (checkBox == null) return @@ -71,8 +72,10 @@ class ImageLocationFragment : WizardFragment() { else -> null } - activity?.findViewById