From 90e994c6cafcebbada8b67215acaf2d8583eea8b Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Fri, 17 Aug 2018 21:51:13 +0200 Subject: [PATCH] Implement writing macOS DMG images --- .../fragments/ConfirmInfoFragment.kt | 10 +- .../etchdroid/services/UsbAPIWriteService.kt | 80 ------------ .../services/UsbApiDmgWriteService.kt | 115 +++++++++++++++++ .../services/UsbApiImgWriteService.kt | 21 ++++ .../etchdroid/services/UsbApiWriteService.kt | 117 ++++++++++++++++++ .../etchdroid/services/UsbWriteService.kt | 27 ++-- dmg2img/src/c/dmg2img | 2 +- 7 files changed, 275 insertions(+), 97 deletions(-) delete mode 100644 app/src/main/java/eu/depau/etchdroid/services/UsbAPIWriteService.kt create mode 100644 app/src/main/java/eu/depau/etchdroid/services/UsbApiDmgWriteService.kt create mode 100644 app/src/main/java/eu/depau/etchdroid/services/UsbApiImgWriteService.kt create mode 100644 app/src/main/java/eu/depau/etchdroid/services/UsbApiWriteService.kt diff --git a/app/src/main/java/eu/depau/etchdroid/fragments/ConfirmInfoFragment.kt b/app/src/main/java/eu/depau/etchdroid/fragments/ConfirmInfoFragment.kt index 5857132..37d461f 100644 --- a/app/src/main/java/eu/depau/etchdroid/fragments/ConfirmInfoFragment.kt +++ b/app/src/main/java/eu/depau/etchdroid/fragments/ConfirmInfoFragment.kt @@ -10,7 +10,8 @@ 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.UsbAPIWriteService +import eu.depau.etchdroid.services.UsbApiDmgWriteService +import eu.depau.etchdroid.services.UsbApiImgWriteService import kotlinx.android.synthetic.main.fragment_confirminfo.view.* import java.io.IOException @@ -29,7 +30,12 @@ class ConfirmInfoFragment : WizardFragment() { context?.toast("Check notification for progress") - val intent = Intent(activity, UsbAPIWriteService::class.java) + 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) activity?.startService(intent) diff --git a/app/src/main/java/eu/depau/etchdroid/services/UsbAPIWriteService.kt b/app/src/main/java/eu/depau/etchdroid/services/UsbAPIWriteService.kt deleted file mode 100644 index 2256085..0000000 --- a/app/src/main/java/eu/depau/etchdroid/services/UsbAPIWriteService.kt +++ /dev/null @@ -1,80 +0,0 @@ -package eu.depau.etchdroid.services - -import android.content.Intent -import android.hardware.usb.UsbDevice -import android.net.Uri -import android.util.Log -import com.github.mjdev.libaums.UsbMassStorageDevice -import eu.depau.etchdroid.kotlin_exts.getFileName -import eu.depau.etchdroid.kotlin_exts.getFileSize -import eu.depau.etchdroid.kotlin_exts.name -import java.nio.ByteBuffer - -class UsbAPIWriteService : UsbWriteService("UsbAPIWriteService") { - // 512 * 32 bytes = USB max transfer size - val DD_BLOCKSIZE = 512 * 32 * 64 // 1 MB - - class Action { - val WRITE_IMAGE = "eu.depau.etchdroid.action.API_WRITE_IMAGE" - val WRITE_CANCEL = "eu.depau.etchdroid.action.API_WRITE_CANCEL" - } - - private fun getUsbMSDevice(usbDevice: UsbDevice): UsbMassStorageDevice? { - val msDevs = UsbMassStorageDevice.getMassStorageDevices(this) - - for (dev in msDevs) { - if (dev.usbDevice == usbDevice) - return dev - } - - return null - } - - override fun writeImage(intent: Intent): Long { - val uri: Uri = intent.data!! - val usbDevice: UsbDevice = intent.getParcelableExtra("usbDevice") - - val msDev = getUsbMSDevice(usbDevice)!! - msDev.init() - - val blockDev = msDev.blockDevice - val bsFactor = DD_BLOCKSIZE / blockDev.blockSize - val byteBuffer = ByteBuffer.allocate(blockDev.blockSize * bsFactor) - val imageSize = uri.getFileSize(this) - val inputStream = contentResolver.openInputStream(uri)!! - - val startTime = System.currentTimeMillis() - - var readBytes: Int - var offset = 0L - var writtenBytes: Long = 0 - - try { - while (true) { - wakeLock(true) - readBytes = inputStream.read(byteBuffer.array()!!) - if (readBytes < 0) - break - byteBuffer.position(0) - - blockDev.write(offset, byteBuffer) - offset += bsFactor - writtenBytes += readBytes - - updateNotification(usbDevice.name, uri.getFileName(this), offset * blockDev.blockSize, imageSize) - } - - resultNotification(usbDevice.name, uri.getFileName(this)!!, true, writtenBytes, startTime) - } catch (e: Exception) { - resultNotification(usbDevice.name, uri.getFileName(this)!!, false, writtenBytes, startTime) - Log.e(TAG, "Could't write image to ${usbDevice.name}") - throw e - } finally { - wakeLock(false) - msDev.close() - } - - Log.d(TAG, "Written $writtenBytes bytes to ${usbDevice.name} using API") - return writtenBytes - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/etchdroid/services/UsbApiDmgWriteService.kt b/app/src/main/java/eu/depau/etchdroid/services/UsbApiDmgWriteService.kt new file mode 100644 index 0000000..6e257b7 --- /dev/null +++ b/app/src/main/java/eu/depau/etchdroid/services/UsbApiDmgWriteService.kt @@ -0,0 +1,115 @@ +package eu.depau.etchdroid.services + +import android.hardware.usb.UsbDevice +import android.net.Uri +import com.google.common.util.concurrent.SimpleTimeLimiter +import com.google.common.util.concurrent.TimeLimiter +import com.google.common.util.concurrent.UncheckedTimeoutException +import eu.depau.etchdroid.kotlin_exts.getBinary +import eu.depau.etchdroid.kotlin_exts.getFileName +import eu.depau.etchdroid.kotlin_exts.getFileSize +import eu.depau.etchdroid.kotlin_exts.name +import java.io.BufferedReader +import java.io.InputStream +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +val plistRegex = Regex("\\s*partition (\\d+): begin=(\\d+), size=(\\d+), decoded=(\\d+), firstsector=(\\d+), sectorcount=(\\d+), blocksruncount=(\\d+)\\s*") +//val progressRegex = Regex("\\[?(\\d+)]\\s+(\\d+[.,]\\d+)%") + +class UsbApiDmgWriteService : UsbApiWriteService("UsbApiDmgWriteService") { + val SECTOR_SIZE = 512 + + private lateinit var uri: Uri + private lateinit var process: Process + private lateinit var errReader: BufferedReader + + private var bytesTotal = 0 + + private var readTimeLimiter: TimeLimiter = SimpleTimeLimiter.create(Executors.newCachedThreadPool()) + + override fun getSendProgress(usbDevice: UsbDevice, uri: Uri): (Long) -> Unit { + val imageSize = uri.getFileSize(this) + return { bytes -> + // asyncReadProcessProgress() + + try { + readTimeLimiter.callWithTimeout({ + val byteArray = ByteArray(128) + process.errorStream.read(byteArray) + System.err.write(byteArray) + }, 100, TimeUnit.MILLISECONDS) + } catch (e: TimeoutException) { + } catch (e: UncheckedTimeoutException) { + } + + val perc = if (bytesTotal == 0) + -1 + else + (bytes.toDouble() / bytesTotal.toDouble() * 100).toInt() + + updateNotification(usbDevice.name, uri.getFileName(this), bytes, perc) + } + } +// +// fun asyncReadProcessProgress() { +// val startTime = System.currentTimeMillis() +// var c: Int +// val charArray = CharArray(20) +// +// try { +// while (System.currentTimeMillis() < startTime + 50) { +// // Skip everything until the first backspace +// do +// c = readTimeLimiter.callWithTimeout(errReader::read, 50, TimeUnit.MILLISECONDS) +// while (c.toChar() != '\b' && c != -1) +// // Skip all backspaces +// do +// c = readTimeLimiter.callWithTimeout(errReader::read, 50, TimeUnit.MILLISECONDS) +// while (c.toChar() == '\b' && c != -1) +// +// // Read the stream +// readTimeLimiter.callWithTimeout({errReader.read(charArray)}, 50, TimeUnit.MILLISECONDS) +// val match = progressRegex.find(String(charArray)) ?: continue +// val (blocksruncurStr, percStr) = match.destructured +// val blocksruncur = blocksruncurStr.toInt() +// +// blocksrun += blocksruncur +// +// if (blocksruncur >= blocksrunLast) +// blocksrun -= blocksrunLast +// +// blocksrunLast = blocksruncur +// } +// } catch (e: TimeoutException) { +// } catch (e: UncheckedTimeoutException) { +// } +// } + + override fun getInputStream(uri: Uri): InputStream { + this.uri = uri + val pb = ProcessBuilder(getBinary("dmg2img").path, "-v", uri.path, "-") + pb.environment()["LD_LIBRARY_PATH"] = applicationInfo.nativeLibraryDir + process = pb.start() + errReader = process.errorStream.bufferedReader() + + // Read blocksruncount + var matched = false + var lastSector = 0 + while (true) { + val line = errReader.readLine() ?: break + val match = plistRegex.find(line) ?: if (matched) break else continue + matched = true + + val (begin, size, decoded, firstsector, sectorcount, blocksruncount) = match.destructured + + val partLastSector = firstsector.toInt() + sectorcount.toInt() + if (partLastSector > lastSector) + lastSector = partLastSector + } + + bytesTotal = lastSector * SECTOR_SIZE + return process.inputStream + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/etchdroid/services/UsbApiImgWriteService.kt b/app/src/main/java/eu/depau/etchdroid/services/UsbApiImgWriteService.kt new file mode 100644 index 0000000..9e86d62 --- /dev/null +++ b/app/src/main/java/eu/depau/etchdroid/services/UsbApiImgWriteService.kt @@ -0,0 +1,21 @@ +package eu.depau.etchdroid.services + +import android.hardware.usb.UsbDevice +import android.net.Uri +import eu.depau.etchdroid.kotlin_exts.getFileName +import eu.depau.etchdroid.kotlin_exts.getFileSize +import eu.depau.etchdroid.kotlin_exts.name +import java.io.InputStream + +class UsbApiImgWriteService : UsbApiWriteService("UsbApiImgWriteService") { + override fun getSendProgress(usbDevice: UsbDevice, uri: Uri): (Long) -> Unit { + val imageSize = uri.getFileSize(this) + return { bytes -> + updateNotification(usbDevice.name, uri.getFileName(this), bytes, (bytes.toFloat() / imageSize * 100).toInt()) + } + } + + override fun getInputStream(uri: Uri): InputStream { + return contentResolver.openInputStream(uri)!! + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/etchdroid/services/UsbApiWriteService.kt b/app/src/main/java/eu/depau/etchdroid/services/UsbApiWriteService.kt new file mode 100644 index 0000000..3edb8cd --- /dev/null +++ b/app/src/main/java/eu/depau/etchdroid/services/UsbApiWriteService.kt @@ -0,0 +1,117 @@ +package eu.depau.etchdroid.services + +import android.content.Intent +import android.hardware.usb.UsbDevice +import android.net.Uri +import android.util.Log +import com.github.mjdev.libaums.UsbMassStorageDevice +import eu.depau.etchdroid.kotlin_exts.getFileName +import eu.depau.etchdroid.kotlin_exts.name +import java.io.BufferedInputStream +import java.io.InputStream +import java.nio.ByteBuffer + +abstract class UsbApiWriteService(name: String) : UsbWriteService(name) { + // 512 * 32 bytes = USB max transfer size + val DD_BLOCKSIZE = 512 * 32 * 64 // 1 MB + + class Action { + val WRITE_IMAGE = "eu.depau.etchdroid.action.API_WRITE_IMAGE" + val WRITE_CANCEL = "eu.depau.etchdroid.action.API_WRITE_CANCEL" + } + + abstract fun getSendProgress(usbDevice: UsbDevice, uri: Uri): (Long) -> Unit + abstract fun getInputStream(uri: Uri): InputStream + + private fun getUsbMSDevice(usbDevice: UsbDevice): UsbMassStorageDevice? { + val msDevs = UsbMassStorageDevice.getMassStorageDevices(this) + + for (dev in msDevs) { + if (dev.usbDevice == usbDevice) + return dev + } + + return null + } + + fun writeInputStream(inputStream: InputStream, msDev: UsbMassStorageDevice, sendProgress: (Long) -> Unit): Long { + val blockDev = msDev.blockDevice + val bsFactor = DD_BLOCKSIZE / blockDev.blockSize + val buffIS = BufferedInputStream(inputStream) + val byteBuffer = ByteBuffer.allocate(blockDev.blockSize * bsFactor) + + var lastReadBytes: Int + var readBytes = 0 + var readBlocksBytes = 0 + var offset = 0L + var writtenBytes: Long = 0 + var remaining = 0 + + while (true) { + wakeLock(true) + lastReadBytes = buffIS.read(byteBuffer.array()!!, remaining, byteBuffer.array().size - remaining) + if (lastReadBytes < 0 && readBytes > 0) { + // EOF, pad with some extra bits until next block + if (readBytes % blockDev.blockSize > 0) + readBytes += blockDev.blockSize - (readBytes % blockDev.blockSize) + } else if (lastReadBytes < 0) { + // EOF, we've already written everything + break + } else { + readBytes += lastReadBytes + } + + byteBuffer.position(0) + + // Ensure written content size is a multiple of the block size + remaining = readBytes % blockDev.blockSize + readBlocksBytes = readBytes - remaining + byteBuffer.limit(readBlocksBytes) + + // Write the buffer to the device + blockDev.write(offset, byteBuffer) + offset += (readBlocksBytes) / blockDev.blockSize + writtenBytes += readBlocksBytes + + // Copy remaining bytes to the beginning of the buffer + for (i in 0 until remaining) + byteBuffer.array()[i] = byteBuffer.array()[readBlocksBytes + i] + + readBytes = remaining + + sendProgress(writtenBytes) + } + + return writtenBytes + } + + override fun writeImage(intent: Intent): Long { + val uri: Uri = intent.data!! + val inputStream = getInputStream(uri) + + val usbDevice: UsbDevice = intent.getParcelableExtra("usbDevice") + val msDev = getUsbMSDevice(usbDevice)!! + msDev.init() + + val sendProgress = getSendProgress(usbDevice, uri) + val startTime = System.currentTimeMillis() + var writtenBytes: Long = 0 + + + try { + writtenBytes = writeInputStream(inputStream, msDev, sendProgress) + + resultNotification(usbDevice.name, uri.getFileName(this)!!, true, writtenBytes, startTime) + } catch (e: Exception) { + resultNotification(usbDevice.name, uri.getFileName(this)!!, false, writtenBytes, startTime) + Log.e(TAG, "Could't write image to ${usbDevice.name}") + throw e + } finally { + wakeLock(false) + msDev.close() + } + + Log.d(TAG, "Written $writtenBytes bytes to ${usbDevice.name} using API") + return writtenBytes + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/depau/etchdroid/services/UsbWriteService.kt b/app/src/main/java/eu/depau/etchdroid/services/UsbWriteService.kt index 723220f..094c362 100644 --- a/app/src/main/java/eu/depau/etchdroid/services/UsbWriteService.kt +++ b/app/src/main/java/eu/depau/etchdroid/services/UsbWriteService.kt @@ -12,6 +12,7 @@ import android.support.v4.app.NotificationCompat import eu.depau.etchdroid.R import eu.depau.etchdroid.kotlin_exts.toHRSize import eu.depau.etchdroid.kotlin_exts.toHRTime +import kotlin.math.max abstract class UsbWriteService(name: String) : IntentService(name) { @@ -30,7 +31,7 @@ abstract class UsbWriteService(name: String) : IntentService(name) { private val WL_TIMEOUT = 10 * 60 * 1000L override fun onHandleIntent(intent: Intent?) { - startForeground(FOREGROUND_ID, buildForegroundNotification(null, null, -1, -1)) + startForeground(FOREGROUND_ID, buildForegroundNotification(null, null, -1)) try { writeImage(intent!!) @@ -73,20 +74,18 @@ abstract class UsbWriteService(name: String) : IntentService(name) { NotificationCompat.Builder(this) } - fun updateNotification(usbDevice: String, filename: String?, bytes: Long, total: Long) { + fun updateNotification(usbDevice: String, filename: String?, bytes: Long, progr: Int) { // Notification rate limiting val time = System.currentTimeMillis() if (time <= prevTime + 1000) return - val speed = ((bytes - prevBytes).toDouble() / (time - prevTime).toDouble() * 1000).toHRSize() + val speed = max((bytes - prevBytes).toDouble() / (time - prevTime).toDouble() * 1000, 0.0).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, filename, bytes, total, "$perc% • $speed/s")) + notificationManager.notify(FOREGROUND_ID, buildForegroundNotification(usbDevice, filename, progr, "$progr% • $speed/s")) } fun resultNotification(usbDevice: String, filename: String, success: Boolean, bytes: Long = 0, startTime: Long = 0) { @@ -102,7 +101,7 @@ abstract class UsbWriteService(name: String) : IntentService(name) { .setContentText("$usbDevice may have been unplugged while writing.") .setSubText(dt.toHRTime()) else { - val speed = (bytes.toDouble() / dt.toDouble() * 1000).toHRSize() + "/s" + val speed = max(bytes.toDouble() / dt.toDouble() * 1000, 0.0).toHRSize() + "/s" b.setContentTitle("Write finished") .setContentText("$filename successfully written to $usbDevice") .setSubText("${dt.toHRTime()} • ${bytes.toHRSize()} • $speed") @@ -114,23 +113,23 @@ abstract class UsbWriteService(name: String) : IntentService(name) { notificationManager.notify(RESULT_NOTIFICATION_ID, b.build()) } - fun buildForegroundNotification(usbDevice: String?, filename: String?, bytes: Long, total: Long, subText: String? = null): Notification { - val progr: Int + fun buildForegroundNotification(usbDevice: String?, filename: String?, progr: Int, subText: String? = null, title: String = getString(R.string.notif_writing_img)): Notification { val indet: Boolean + val prog: Int - if (total < 0) { - progr = 0 + if (progr < 0) { + prog = 0 indet = true } else { - progr = (bytes.toFloat() / total * 100).toInt() + prog = progr indet = false } val b = getNotificationBuilder() - b.setContentTitle(getString(R.string.notif_writing_img)) + b.setContentTitle(title) .setOngoing(true) - .setProgress(100, progr, indet) + .setProgress(100, prog, indet) if (usbDevice != null && filename != null) b.setContentText("${filename} to $usbDevice") diff --git a/dmg2img/src/c/dmg2img b/dmg2img/src/c/dmg2img index 1d75292..fd3763b 160000 --- a/dmg2img/src/c/dmg2img +++ b/dmg2img/src/c/dmg2img @@ -1 +1 @@ -Subproject commit 1d7529285a7bdeb4f1eb158d9305494d682e323d +Subproject commit fd3763bb07aa06c9b56b3f6d632d475d59f41433