Implement API writing

This commit is contained in:
Davide Depau 2018-08-14 01:32:02 +02:00
parent 4e861c7c09
commit 5e92baa4a0
32 changed files with 921 additions and 125 deletions

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.depau.ddroid">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
@ -21,6 +21,9 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".services.UsbAPIWriteService"
android:exported="false"/>
</application>
</manifest>

View File

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

View File

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

View File

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

View File

@ -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?) {}

View File

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

View File

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

View File

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

View File

@ -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<CheckBox>(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<Button>(R.id.pick_file_btn)?.isEnabled = StateKeeper.imageLocation == ImageLocation.LOCAL
activity?.findViewById<EditText>(R.id.img_url_textview)?.isEnabled = StateKeeper.imageLocation == ImageLocation.REMOTE
fab?.show()
pick_file_btn?.isEnabled = StateKeeper.imageLocation == ImageLocation.LOCAL
img_url_textview?.isEnabled = StateKeeper.imageLocation == ImageLocation.REMOTE
setStreamingCheckBoxAvailability(activity as WizardActivity)
updateFileButtonLabel(activity as WizardActivity)
@ -87,9 +90,9 @@ class ImageLocationFragment : WizardFragment() {
}
}
override fun nextStep(view: View) {
override fun nextStep(view: View?) {
if (StateKeeper.imageLocation == null) {
Snackbar.make(view, getString(R.string.select_image_location), Snackbar.LENGTH_LONG).show()
view?.snackbar(getString(R.string.select_image_location))
return
}
@ -98,13 +101,13 @@ class ImageLocationFragment : WizardFragment() {
StateKeeper.imageFile = getRemoteImageUri(activity as WizardActivity)
} catch (e: RuntimeException) {
Log.e(TAG, "Invalid URI specified", e)
Snackbar.make(view, getString(R.string.provided_url_invalid), Snackbar.LENGTH_LONG).show()
view?.snackbar(getString(R.string.provided_url_invalid))
return
}
}
if (StateKeeper.imageFile == null) {
Snackbar.make(view, getString(R.string.provide_image_file), Snackbar.LENGTH_LONG).show()
view?.snackbar(getString(R.string.provide_image_file))
return
}
@ -122,7 +125,7 @@ class ImageLocationFragment : WizardFragment() {
}
(activity as WizardActivity).goToNewFragment(USBDriveFragment())
(activity as WizardActivity).goToNewFragment(UsbDriveFragment())
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
@ -130,9 +133,9 @@ class ImageLocationFragment : WizardFragment() {
MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE -> {
// If request is cancelled, the result arrays are empty.
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
nextStep(activity!!.findViewById(R.id.fragment_layout))
nextStep(fragment_layout)
} else {
Snackbar.make(activity!!.findViewById(R.id.fragment_layout), getString(R.string.storage_perm_required_explaination), Snackbar.LENGTH_LONG).show()
Snackbar.make(fragment_layout, getString(R.string.storage_perm_required_explaination), Snackbar.LENGTH_LONG).show()
}
return
}
@ -157,31 +160,20 @@ class ImageLocationFragment : WizardFragment() {
}
fun getRemoteImageUri(context: WizardActivity): Uri {
val text = context.findViewById<EditText>(R.id.img_url_textview).text.toString()
val text = img_url_textview.text.toString()
return Uri.parse(text)
}
fun updateFileButtonLabel(context: WizardActivity) {
val button = context.findViewById<Button>(R.id.pick_file_btn)
val button = pick_file_btn
val uri = StateKeeper.imageFile
if (uri != null && uri.scheme != null && !uri.scheme!!.startsWith("http")) {
val cursor = context.contentResolver.query(uri, null, null, null, null, null)
val text = uri?.getFileName(context)
try {
if (cursor != null && cursor.moveToFirst()) {
button.text = cursor.getString(
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
return
} catch (e: RuntimeException) {
Log.e(TAG, "Error retrieving file name", e)
} finally {
cursor?.close()
}
}
button.text = getString(R.string.pick_a_file)
if (text != null)
button.text = text
else
button.text = getString(R.string.pick_a_file)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@ -1,70 +0,0 @@
package eu.depau.ddroid.fragments
import android.os.Build
import android.os.Bundle
import android.support.annotation.RequiresApi
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.github.mjdev.libaums.UsbMassStorageDevice
import eu.depau.ddroid.R
import eu.depau.ddroid.StateKeeper
import eu.depau.ddroid.abc.WizardFragment
import eu.depau.ddroid.values.WizardStep
/**
* A placeholder fragment containing a simple view.
*/
class USBDriveFragment : WizardFragment() {
val TAG = "USBDriveFragment"
val ACTION_USB_PERMISSION = "eu.depau.ddroid.USB_PERMISSION"
override fun nextStep(view: View) {
// if (StateKeeper.flashMethod == null)
// Snackbar.make(view, "Please select writing method", Snackbar.LENGTH_LONG).show()
// else
// (activity as WizardActivity).goToNewFragment(ImageLocationFragment())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
StateKeeper.currentFragment = this
StateKeeper.wizardStep = WizardStep.SELECT_USB_DRIVE
return inflater.inflate(R.layout.fragment_select_usb_drive, container, false)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onButtonClicked(view: View) {
// val usbManager = activity!!.getSystemService(Context.USB_SERVICE) as UsbManager
val devices = UsbMassStorageDevice.getMassStorageDevices(activity)
for (device in devices) {
Log.d(TAG, """USB device ${device.usbDevice.deviceName}
| proto ${device.usbDevice.deviceProtocol}
| id ${device.usbDevice.deviceId}
| class ${device.usbDevice.deviceClass}
| subclass ${device.usbDevice.deviceSubclass}
| iface count ${device.usbDevice.interfaceCount}
| manuf name ${device.usbDevice.manufacturerName}
| product name ${device.usbDevice.productName}
| id ${device.usbDevice.vendorId}:${device.usbDevice.productId}
""".trimMargin())
// val permissionIntent = PendingIntent.getBroadcast(activity, 0, Intent(MY_PERMISSIONS_REQUEST_USB_DRIVE), 0)
// usbManager.requestPermission(device.usbDevice, permissionIntent)
// // before interacting with a device you need to call init()!
// device.init()
//
// // Only uses the first partition on the device
// val currentFs = device.partitions[0].fileSystem
// Log.d(TAG, "Capacity: " + currentFs.capacity)
// Log.d(TAG, "Occupied Space: " + currentFs.occupiedSpace)
// Log.d(TAG, "Free Space: " + currentFs.freeSpace)
// Log.d(TAG, "Chunk size: " + currentFs.chunkSize)
}
}
}

View File

@ -0,0 +1,171 @@
package eu.depau.ddroid.fragments
import android.content.Context
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.support.design.widget.Snackbar
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.ddroid.R
import eu.depau.ddroid.StateKeeper
import eu.depau.ddroid.abc.ClickListener
import eu.depau.ddroid.abc.WizardActivity
import eu.depau.ddroid.abc.WizardFragment
import eu.depau.ddroid.activities.MainActivity
import eu.depau.ddroid.utils.UsbDrivesRecyclerViewAdapter
import eu.depau.ddroid.utils.name
import eu.depau.ddroid.utils.snackbar
import eu.depau.ddroid.values.WizardStep
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
class RecyclerViewTouchListener(context: Context, val recyclerView: RecyclerView, val clickListener: ClickListener) : RecyclerView.OnItemTouchListener {
private var gestureDetector: GestureDetector
init {
gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return true
}
override fun onLongPress(e: MotionEvent) {
val child = recyclerView.findChildViewUnder(e.x, e.y)
if (child != null)
clickListener.onLongClick(child, recyclerView.getChildAdapterPosition(child))
}
})
}
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
val child = rv.findChildViewUnder(e.x, e.y)
if (child != null && gestureDetector.onTouchEvent(e)) {
clickListener.onClick(child, rv.getChildAdapterPosition(child))
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
}
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)
}
}

View File

@ -0,0 +1,71 @@
package eu.depau.ddroid.services
import android.content.Intent
import android.hardware.usb.UsbDevice
import android.net.Uri
import com.github.mjdev.libaums.UsbMassStorageDevice
import eu.depau.ddroid.abc.UsbWriteService
import eu.depau.ddroid.utils.getFileSize
import eu.depau.ddroid.utils.name
import java.nio.ByteBuffer
class UsbAPIWriteService : UsbWriteService("UsbAPIWriteService") {
class Action {
val WRITE_IMAGE = "eu.depau.ddroid.action.API_WRITE_IMAGE"
val WRITE_CANCEL = "eu.depau.ddroid.action.API_WRITE_CANCEL"
}
override fun onHandleIntent(intent: Intent?) {
val uri: Uri = intent!!.data!!
val usbDevice: UsbDevice = intent.getParcelableExtra("usbDevice")
startForeground(FOREGROUND_ID, buildForegroundNotification(usbDevice.name, uri, -1, -1))
try {
val notify = { bytes: Long, total: Long -> updateNotification(usbDevice.name, uri, bytes, total) }
writeImage(usbDevice, uri, notify)
} finally {
stopForeground(true)
}
}
private fun getUsbMSDevice(usbDevice: UsbDevice): UsbMassStorageDevice? {
val msDevs = UsbMassStorageDevice.getMassStorageDevices(this)
for (dev in msDevs) {
if (dev.usbDevice == usbDevice)
return dev
}
return null
}
private fun writeImage(usbDevice: UsbDevice, uri: Uri, notify: (Long, Long) -> Unit): Long {
val msDev = getUsbMSDevice(usbDevice)!!
msDev.init()
val blockDev = msDev.blockDevice
val byteBuffer = ByteBuffer.allocate(blockDev.blockSize)
val imageSize = uri.getFileSize(this)
val inputStream = contentResolver.openInputStream(uri)!!
var readBytes: Int
var offset = 0L
while (true) {
readBytes = inputStream.read(byteBuffer.array()!!)
if (readBytes < 0)
break
byteBuffer.position(readBytes)
blockDev.write(offset, byteBuffer)
offset++
notify(offset * blockDev.blockSize, imageSize)
}
msDev.close()
return offset * blockDev.blockSize
}
}

View File

@ -0,0 +1,8 @@
package eu.depau.ddroid.utils
import android.content.Context
import android.widget.Toast
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, message, duration).show()
}

View File

@ -0,0 +1,18 @@
package eu.depau.ddroid.utils
// https://stackoverflow.com/a/3758880/1124621
private fun <T> humanReadableByteCount(bytes: T, si: Boolean = false): String where T : Comparable<T>, T : Number {
val unit: Long = if (si) 1000 else 1024
if (bytes.toLong() < unit) return bytes.toString() + " B"
val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
return String.format("%.1f %sB", bytes.toDouble() / Math.pow(unit.toDouble(), exp.toDouble()), pre)
}
fun Long.toHRSize(si: Boolean = false) = humanReadableByteCount(this, si)
fun Float.toHRSize(si: Boolean = false) = humanReadableByteCount(this, si)
fun Double.toHRSize(si: Boolean = false) = humanReadableByteCount(this, si)
fun Int.toHRSize(si: Boolean = false) = humanReadableByteCount(this, si)
fun Byte.toHRSize(si: Boolean = false) = humanReadableByteCount(this, si)
fun Short.toHRSize(si: Boolean = false) = humanReadableByteCount(this, si)

View File

@ -0,0 +1,29 @@
package eu.depau.ddroid.utils
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
fun Uri.getFileName(context: Context): String? {
val TAG = "UriGetFileNameExt"
var result: String? = null
if (this.scheme == "content") {
val cursor = context.getContentResolver().query(this, null, null, null, null)
cursor.use {
if (it != null && it.moveToFirst()) {
result = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
if (result == null) {
result = this.getPath()
val cut = result!!.lastIndexOf('/')
if (cut != -1) {
result = result!!.substring(cut + 1)
}
}
return result
}

View File

@ -0,0 +1,39 @@
package eu.depau.ddroid.utils
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.database.SQLException
import android.net.Uri
import android.provider.OpenableColumns
import java.io.File
// https://github.com/android-rcs/rcsjta/blob/master/RI/src/com/gsma/rcs/ri/utils/FileUtils.java#L214
fun Uri.getFileSize(context: Context): Long {
when (this.scheme) {
ContentResolver.SCHEME_FILE -> {
val f = File(this.path)
return f.length()
}
ContentResolver.SCHEME_CONTENT -> {
val cursor: Cursor? = context.contentResolver.query(this, null, null, null, null)
cursor.use {
if (it == null) {
throw SQLException("Failed to query file $this")
}
return if (it.moveToFirst()) {
java.lang.Long.valueOf(it.getString(it
.getColumnIndexOrThrow(OpenableColumns.SIZE)))
} else {
throw IllegalArgumentException(
"Error in retrieving this size form the URI")
}
}
}
else -> throw IllegalArgumentException("Unsupported URI scheme")
}
}

View File

@ -0,0 +1,17 @@
package eu.depau.ddroid.utils
import android.hardware.usb.UsbDevice
import android.os.Build
fun formatID(id: Int): String = Integer.toHexString(id).padStart(4,'0')
val UsbDevice.vidpid: String
get() = "${formatID(this.vendorId)}:${formatID(this.productId)}"
val UsbDevice.name: String
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
"${this.manufacturerName} ${this.productName}"
} else {
this.deviceName
}

View File

@ -0,0 +1,50 @@
package eu.depau.ddroid.utils
import android.annotation.SuppressLint
import android.os.Build
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import com.github.mjdev.libaums.UsbMassStorageDevice
import eu.depau.ddroid.R
import kotlinx.android.synthetic.main.usb_device_row.view.*
import java.lang.Integer
class UsbDrivesRecyclerViewAdapter(private val dataset: Array<UsbMassStorageDevice>) : RecyclerView.Adapter<UsbDrivesRecyclerViewAdapter.ViewHolder>() {
class ViewHolder(val relLayout: RelativeLayout) : RecyclerView.ViewHolder(relLayout)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
UsbDrivesRecyclerViewAdapter.ViewHolder {
val relLayout = LayoutInflater.from(parent.context)
.inflate(R.layout.usb_device_row, parent, false) as RelativeLayout
return ViewHolder(relLayout)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val usbDevice = dataset[position].usbDevice
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
holder.relLayout.name.text = "${usbDevice.manufacturerName} ${usbDevice.productName}"
holder.relLayout.devpath.text = usbDevice.deviceName
holder.relLayout.vidpid.text = usbDevice.vidpid
} else {
holder.relLayout.name.text = usbDevice.deviceName
holder.relLayout.devpath.text = usbDevice.vidpid
}
}
override fun getItemCount(): Int = dataset.size
fun get(position: Int): UsbMassStorageDevice {
return dataset[position]
}
}

View File

@ -0,0 +1,8 @@
package eu.depau.ddroid.utils
import android.support.design.widget.Snackbar
import android.view.View
fun View.snackbar(message: CharSequence, duration: Int = Snackbar.LENGTH_LONG) {
Snackbar.make(this, message, duration).show()
}

View File

@ -3,5 +3,6 @@ package eu.depau.ddroid.values
enum class WizardStep {
SELECT_FLASH_METHOD,
SELECT_LOCATION,
SELECT_USB_DRIVE
SELECT_USB_DRIVE,
CONFIRM
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" 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>

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".fragments.ConfirmInfoFragment">
<!--tools:showIn="@layout/activity_main"-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="@dimen/row_padding_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding_vertical">
<TextView
android:id="@+id/confirm_sel_method_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"
android:ellipsize="end"
android:text="@string/selected_method"/>
<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="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="@dimen/row_padding_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding_vertical">
<TextView
android:id="@+id/confirm_sel_image_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"
android:ellipsize="end"
android:text="@string/selected_image"/>
<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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="@dimen/row_padding_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding_vertical">
<TextView
android:id="@+id/confirm_sel_usbdev_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"
android:ellipsize="end"
android:text="@string/selected_usbdev"/>
<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"
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="@dimen/row_padding_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding_vertical">
<TextView
android:id="@+id/confirm_extra_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textColor="@color/name"
android:textSize="16sp"
android:ellipsize="end"/>
</RelativeLayout>
</LinearLayout>

View File

@ -24,6 +24,7 @@
android:id="@+id/flash_dd_root_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/flash_dd_root"
android:onClick="onRadioButtonClicked"/>
@ -31,6 +32,7 @@
android:id="@+id/flash_unetbootin_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/flash_unetbootin"
android:onClick="onRadioButtonClicked"/>
@ -38,6 +40,7 @@
android:id="@+id/flash_woeusb_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/flash_woeusb"
android:onClick="onRadioButtonClicked"/>

View File

@ -31,6 +31,7 @@
android:id="@+id/download_img_radio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:onClick="onRadioButtonClicked"
android:text="@string/download_image_from_url"/>

View File

@ -1,20 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<android.support.v4.widget.SwipeRefreshLayout
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/usbdevs_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".fragments.USBDriveFragment"
tools:showIn="@layout/activity_main">
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/usbdevs_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onButtonClicked"
android:text="Do USB stuff"/>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="@dimen/row_padding_vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/row_padding_vertical">
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textColor="@color/name"
android:textSize="16sp"
android:textStyle="bold"
android:ellipsize="end"/>
<TextView
android:id="@+id/devpath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"/>
<TextView
android:id="@+id/vidpid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:textColor="@color/info"/>
</RelativeLayout>

View File

@ -0,0 +1,10 @@
<menu 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"
tools:context="eu.depau.ddroid.fragments.UsbDriveFragment">
<item
android:id="@+id/action_refresh"
android:orderInCategory="100"
android:title="@string/action_refresh"
app:showAsAction="never"/>
</menu>

View File

@ -3,4 +3,6 @@
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="info">#999999</color>
<color name="name">#222222</color>
</resources>

View File

@ -1,3 +1,6 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="row_padding_vertical">10dp</dimen>
</resources>

View File

@ -14,4 +14,15 @@
<string name="select_image_location">Please select image location</string>
<string name="provide_image_file">Please provide an image file</string>
<string name="storage_perm_required_explaination">Storage permission required to download images</string>
<string name="action_refresh">Refresh</string>
<string name="usb_perm_denied">Permission denied for</string>
<string name="usb_perm_denied_noname">Permission denied for USB device</string>
<string name="selected_image">Selected image:</string>
<string name="selected_method">Selected writing method:</string>
<string name="selected_usbdev">Selected USB device:</string>
<string name="unknown_filename">Unknown filename</string>
<string name="please_select_writing_method">Please select writing method</string>
<string name="image_bigger_than_usb">Image is bigger than the USB drive, so it can\'t be written</string>
<string name="cant_read_usbdev">Cannot read USB device</string>
<string name="tap_next_to_write">Tap Next to write the image to the USB drive</string>
</resources>

View File

@ -32,6 +32,7 @@ task clean(type: Delete) {
}
// For libaums
ext {
bintrayRepo = 'maven'

@ -1 +1 @@
Subproject commit a57ae278535946db69e2e1ea7f02171d002198a5
Subproject commit 310bc4680581c845ab2f30474eee02b7b238669f