2024-04-21 00:58:00 +02:00
import { Adb , AdbDaemonTransport } from '@yume-chan/adb' ;
import { AdbDaemonWebUsbDeviceManager , AdbDaemonWebUsbDeviceWatcher } from '@yume-chan/adb-daemon-webusb' ;
2024-04-17 01:30:25 +02:00
import AdbWebCredentialStore from '@yume-chan/adb-credential-web' ;
2024-04-19 00:34:22 +02:00
import { DecodeUtf8Stream , WrapReadableStream , WrapConsumableStream } from '@yume-chan/stream-extra' ;
import { PackageManager } from '@yume-chan/android-bin' ;
2024-04-17 01:30:25 +02:00
// TODO:
// * package manager with install/uninstall/dump, debloat tool with default list and import/export
// * fastboot shell and tools? possible?
2024-04-19 00:34:22 +02:00
// * logs for Packages section?
2024-04-15 01:30:58 +02:00
( async function ( ) {
2024-04-21 00:58:00 +02:00
$ ( 'p$javascriptWarning$' ) . remove ( ) ;
$ ( '.holo-section[data-section="about"]' ) . style = null ;
$ ( 'p$connectReminder$' ) . hidden = false ;
2024-04-15 01:30:58 +02:00
const deviceSelect = $ ( 'select$deviceSelect$' ) ;
2024-04-17 01:30:25 +02:00
const terminalOutput = $ ( 'textarea$terminalOutput$' ) ;
let Device = { } ;
2024-04-15 01:30:58 +02:00
const CredentialStore = new AdbWebCredentialStore ( ) ;
2024-04-21 00:58:00 +02:00
function popupBox ( text ) {
$ ( 'p$popupBox$' ) . hidden = null ;
$ ( 'p$popupBox$' ) . innerHTML = ` ${ text } <button>X</button> ` ;
$ ( 'p$popupBox$' ) . querySelector ( 'button' ) . onclick = ( function ( ) {
$ ( 'p$popupBox$' ) . hidden = true ;
$ ( 'p$popupBox$' ) . innerHTML = null ;
} ) ;
}
2024-04-19 00:34:22 +02:00
function resizeTerminal ( ) {
const divider = ( Device . adb ? 2 : 3 ) ;
terminalOutput . style . height = ` ${ window . innerHeight - ( ( 48 + 8 ) * divider ) } px ` ;
}
window . addEventListener ( 'resize' , resizeTerminal ) ;
resizeTerminal ( ) ;
2024-04-21 00:58:00 +02:00
const UsbManager = AdbDaemonWebUsbDeviceManager . BROWSER ;
2024-04-15 01:30:58 +02:00
if ( ! UsbManager ) {
$ ( 'div$browserWarning$' ) . innerHTML = ` <p>
< a target = "_blank" href = "https://developer.mozilla.org/en-US/docs/Web/API/USB#browser_compatibility" > WebUSB is not supported < / a > i n t h i s b r o w s e r , s o t h e a p p c a n n o t w o r k .
Consider using an < a target = "_blank" href = "https://chromium.woolyss.com" > up - to - date Chromium - based < / a > o n e .
< / p > < p >
2024-04-17 01:30:25 +02:00
Otherwise , the following alternative ADB solutions might work for you :
2024-04-15 01:30:58 +02:00
< / p > < u l >
< li > < a target = "_blank" href = "https://www.makeuseof.com/use-adb-over-wifi-android/" >
How to Set Up and Use ADB Wirelessly With Android
< / a > < / l i >
< li > < a target = "_blank" href = "https://play.google.com/store/apps/details?id=com.htetznaing.adbotg" >
ADB ⚡ OTG - Android Debug Bridge
< / a > < / l i >
< li > < a target = "_blank" href = "https://play.google.com/store/apps/details?id=com.draco.ladb" >
LADB — Local ADB Shell
< / a > < / l i >
< / u l > ` ;
2024-04-17 01:30:25 +02:00
return ; // kill the app
2024-04-15 01:30:58 +02:00
}
2024-04-21 00:58:00 +02:00
new AdbDaemonWebUsbDeviceWatcher ( ( async function ( connectedDevice ) {
2024-04-17 01:30:25 +02:00
if ( ! connectedDevice ) {
await disconnectDevice ( ) ;
}
await refreshDeviceSection ( ) ;
} ) , navigator . usb ) ;
2024-04-15 01:30:58 +02:00
async function connectAuthorizeDevice ( ) {
2024-04-17 01:30:25 +02:00
if ( deviceSelect . selectedIndex > 0 ) {
Device . device = await getDevice ( ) ;
try {
Device . connection = await Device . device . connect ( ) ;
2024-04-21 00:58:00 +02:00
Device . transport = await AdbDaemonTransport . authenticate ( { connection : Device . connection , credentialStore : CredentialStore } ) ;
Device . adb = new Adb ( Device . transport ) ;
2024-04-17 01:30:25 +02:00
} catch ( err ) {
2024-04-19 00:34:22 +02:00
$ ( 'p$deviceStatus$' ) . textContent = 'An error occurred while trying to establish a device connection. Please ensure that no other processes or browser tabs on this system are currently using the device, then retry.' ;
2024-04-17 01:30:25 +02:00
}
2024-04-19 00:34:22 +02:00
} else {
$ ( 'p$deviceStatus$' ) . textContent = null ;
2024-04-17 01:30:25 +02:00
}
}
async function getDevice ( ) {
2024-04-15 01:30:58 +02:00
const devices = await UsbManager . getDevices ( ) ;
2024-04-17 01:30:25 +02:00
const device = devices [ deviceSelect . selectedIndex - 1 ] ;
return device ;
}
function disconnectDevice ( ) {
const connection = ( Device . adb || Device . transport || Device . connection ) ;
if ( connection ) {
Device = { } ;
return connection . close ( ) ;
}
2024-04-15 01:30:58 +02:00
}
async function refreshDeviceSelect ( ) {
deviceSelect . disabled = true ;
deviceSelect . innerHTML = null ;
const devices = await UsbManager . getDevices ( ) ;
if ( devices . length ) {
2024-04-19 00:34:22 +02:00
$ ( 'p$deviceStatus$' ) . textContent = null ;
2024-04-15 01:30:58 +02:00
deviceSelect . innerHTML = '<option>[📲️ Select a connected device]</option>' ;
devices . forEach ( function ( device , index ) {
2024-04-19 00:34:22 +02:00
const deviceOption = document . createElement ( 'option' ) ;
2024-04-15 01:30:58 +02:00
deviceOption . textContent = ` ${ device . raw . productName } [ ${ device . raw . serialNumber } ] ` ;
deviceSelect . appendChild ( deviceOption ) ;
} ) ;
2024-04-17 01:30:25 +02:00
deviceSelect . onchange = onSwitchDevice ;
2024-04-15 01:30:58 +02:00
deviceSelect . disabled = false ;
} else {
2024-04-19 00:34:22 +02:00
// TODO probably put this warning elsewhere? because it seems like the browser can see even Androids with ADB disabled, obviously they won't be able to connect
$ ( 'p$deviceStatus$' ) . innerHTML = 'Connect a debuggable Android device via USB to continue. (Read "<a target="_blank" href="https://www.lifewire.com/enable-usb-debugging-android-4690927#toc-how-to-enable-usb-debugging-on-android">How to Enable USB Debugging on Android</a>" for help).' ;
2024-04-15 01:30:58 +02:00
deviceSelect . innerHTML = '<option>[📵️ No connected devices]</option>' ;
}
}
2024-04-17 01:30:25 +02:00
async function onSwitchDevice ( ) {
await disconnectDevice ( ) ;
await connectAuthorizeDevice ( ) ;
await refreshDeviceInfo ( ) ;
}
2024-04-15 01:30:58 +02:00
async function refreshDeviceInfo ( ) {
2024-04-19 00:34:22 +02:00
let onDevice = ( deviceSelect . selectedIndex > 0 ) ;
if ( onDevice ) {
2024-04-17 01:30:25 +02:00
const device = await getDevice ( ) ;
2024-04-15 01:30:58 +02:00
$ ( '$deviceOem$' ) . innerHTML = ` <b>Brand</b>: ${ device . raw . manufacturerName } ` ;
$ ( '$deviceModel$' ) . innerHTML = ` <b>Model</b>: ${ device . raw . productName } ` ;
$ ( '$deviceSerial$' ) . innerHTML = ` <b>Serial number</b>: ${ device . raw . serialNumber } ` ;
//$('[name="deviceStatus"]').innerHTML = 'Connected to device.';
2024-04-17 01:30:25 +02:00
//$('$deviceInfo$').hidden = false;
if ( Device . adb ) {
$ ( '$deviceStatus$' ) . innerHTML = null ;
2024-04-19 00:34:22 +02:00
$ ( '$deviceCpuAbis$' ) . innerHTML = ` <b>CPU ABIs</b>: ${ await Device . adb . getProp ( 'ro.system.product.cpu.abilist' ) } ` ;
2024-04-17 01:30:25 +02:00
$ ( '$androidVersion$' ) . innerHTML = ` <b>Android version</b>: ${ await Device . adb . getProp ( 'ro.build.version.release' ) } ` ;
2024-04-19 00:34:22 +02:00
$ ( '$androidApi$' ) . innerHTML = ` <b>SDK API version</b>: ${ await Device . adb . getProp ( 'ro.build.version.sdk' ) } ` ;
//$('$androidNickname$').innerHTML = `<b>Device name</b>: ${await Device.adb.getProp('persist.sys.device_name')}`;
$ ( '$androidBuildDate$' ) . innerHTML = ` <b>Build date</b>: ${ await Device . adb . getProp ( 'ro.vendor.build.date' ) } ` ;
$ ( '$androidBuildFingerprint$' ) . innerHTML = ` <b>Build fingerprint</b>: ${ await Device . adb . getProp ( 'ro.vendor.build.fingerprint' ) } ` ;
2024-04-17 01:30:25 +02:00
$ ( '$androidInfo$' ) . hidden = false ;
$ ( '$connectReminder$' ) . hidden = true ;
terminalOutput . disabled = false ;
2024-04-19 00:34:22 +02:00
terminalOutput . textContent += ( terminalOutput . textContent ? '\n> ' : '> ' ) ;
$ ( 'button$clearTerminal$' ) . disabled = false ;
2024-04-17 01:30:25 +02:00
} else {
2024-04-19 00:34:22 +02:00
onDevice = false ;
2024-04-17 01:30:25 +02:00
}
2024-04-15 01:30:58 +02:00
} else {
2024-04-19 00:34:22 +02:00
//$('$deviceStatus$').innerHTML = null;
2024-04-17 01:30:25 +02:00
$ ( '$connectReminder$' ) . hidden = false ;
terminalOutput . disabled = true ;
2024-04-15 01:30:58 +02:00
}
2024-04-19 00:34:22 +02:00
toggleDeviceElems ( onDevice ) ;
}
function toggleDeviceElems ( enabled ) {
$ ( '$deviceInfo$' ) . hidden = ! enabled ;
$ ( 'button$apkInstall$' ) . disabled = ! enabled ;
2024-04-21 00:58:00 +02:00
$ ( 'button$packagesUninstall$' ) . disabled = true ;
$ ( 'button$packagesInvertSelect$' ) . disabled = ! enabled ;
2024-04-19 00:34:22 +02:00
$ ( 'input$terminalInput$' ) . disabled = ! enabled ;
resizeTerminal ( ) ;
2024-04-21 00:58:00 +02:00
if ( ! enabled ) {
$ ( 'ul$packageList$' ) . innerHTML = null ;
}
2024-04-15 01:30:58 +02:00
}
async function refreshDeviceSection ( ) {
await refreshDeviceSelect ( ) ;
await refreshDeviceInfo ( ) ;
}
refreshDeviceSection ( ) ;
2024-04-19 00:34:22 +02:00
$ ( 'button$deviceConnect$' ) . onclick = ( async function ( ) {
2024-04-15 01:30:58 +02:00
const device = await UsbManager . requestDevice ( ) ;
if ( ! device ) {
return ;
}
2024-04-17 01:30:25 +02:00
await disconnectDevice ( ) ;
2024-04-15 01:30:58 +02:00
await refreshDeviceSection ( ) ;
deviceSelect . selectedIndex = ( deviceSelect . children . length - 1 ) ;
deviceSelect . onchange ( ) ;
} ) ;
2024-04-19 00:34:22 +02:00
$ ( 'button$deviceConnect$' ) . disabled = false ;
$ ( 'input$terminalInput$' ) . addEventListener ( 'keydown' , ( async function ( event ) {
if ( event . keyCode == 13 ) { // Enter
const cmd = $ ( 'input$terminalInput$' ) . value ;
if ( ! terminalOutput . textContent ) {
terminalOutput . textContent += '> ' ;
}
terminalOutput . textContent += ( cmd + '\n' ) ;
const process = await Device . adb . subprocess . spawn ( cmd ) ;
const processWriteToTerminal = ( ) => new WritableStream ( { write ( chunk ) {
terminalOutput . textContent += chunk ;
terminalOutput . scrollTop = terminalOutput . scrollHeight ;
$ ( 'button$clearTerminal$' ) . disabled = false ;
} } ) ;
await process . stdout . pipeThrough ( new DecodeUtf8Stream ( ) ) . pipeTo ( processWriteToTerminal ( ) ) ;
await process . stderr . pipeThrough ( new DecodeUtf8Stream ( ) ) . pipeTo ( processWriteToTerminal ( ) ) ;
terminalOutput . textContent += '\n> ' ;
terminalOutput . scrollTop = terminalOutput . scrollHeight ;
$ ( 'input$terminalInput$' ) . value = null ;
} ;
} ) ) ;
$ ( 'button$clearTerminal$' ) . onclick = ( function ( ) {
terminalOutput . textContent = '' ;
$ ( 'button$clearTerminal$' ) . disabled = true ;
} ) ;
$ ( 'button$wrapTerminal$' ) . onclick = ( function ( ) {
terminalOutput . style . textWrap = ( terminalOutput . style . textWrap ? '' : 'nowrap' ) ;
terminalOutput . scrollTop = terminalOutput . scrollHeight ;
} ) ;
2024-04-21 00:58:00 +02:00
$ ( 'button$apkInstall$' ) . onclick = ( async function ( ) {
// TODO allow installing via drag&drop on packages section
const fileInput = this . querySelector ( 'input' ) ;
fileInput . onchange = ( async function ( event ) {
const count = event . target . files . length ;
if ( ! count > 0 ) {
return ;
}
popupBox ( ` Installing ${ count } package(s)... ` ) ;
const pm = new PackageManager ( Device . adb ) ;
for ( const index in Object . keys ( event . target . files ) ) {
const file = event . target . files [ index ] ;
try {
await pm . installStream ( file . size , ( new WrapReadableStream ( file . stream ( ) ) ) . pipeThrough ( new WrapConsumableStream ( ) ) ) ;
popupBox ( ` Successfully installed package ${ Number ( index ) + 1 } of ${ count } . ` ) ;
refreshPackagesList ( ) ;
} catch ( err ) {
popupBox ( 'Operation has failed:' + err ) ;
break ;
}
}
} ) ;
fileInput . click ( ) ;
} ) ;
$ ( 'button$packagesUninstall$' ) . onclick = ( async function ( event ) {
const checkedPackagesElems = $ ( 'ul$packageList$' ) . querySelectorAll ( 'input[type="checkbox"]:checked' ) ;
const count = checkedPackagesElems . length ;
if ( ! confirm ( ` Confirm uninstalling ${ count } packages? (Currently only works on user packages, will fail on system ones.) ` ) ) {
return ;
}
$ ( 'button$packagesUninstall$' ) . disabled = true ;
const pm = new PackageManager ( Device . adb ) ;
for ( const index in Object . keys ( checkedPackagesElems ) ) {
try {
const packageElem = checkedPackagesElems [ index ] . parentElement ;
const packageName = packageElem . textContent . trim ( ) ;
await pm . uninstall ( packageName ) ;
popupBox ( ` Successfully uninstalled package ${ Number ( index ) + 1 } of ${ count } . ` ) ;
refreshPackagesList ( ) ;
} catch ( err ) {
popupBox ( 'Operation has failed:' + err ) ;
break ;
}
}
$ ( 'button$packagesUninstall$' ) . disabled = false ;
refreshPackagesList ( ) ;
} )
$ ( 'button$packagesInvertSelect$' ) . onclick = ( function ( ) {
Array . from ( $ ( 'ul$packageList$' ) . querySelectorAll ( 'input[type="checkbox"]' ) ) . forEach ( function ( packageElem ) {
packageElem . checked = ! packageElem . checked ;
} ) ;
} ) ;
2024-04-19 00:34:22 +02:00
async function refreshPackagesList ( ) {
$ ( 'ul$packageList$' ) . innerHTML = null ;
2024-04-21 00:58:00 +02:00
$ ( 'button$packagesUninstall$' ) . disabled = true ;
2024-04-19 00:34:22 +02:00
const pm = new PackageManager ( Device . adb ) ;
const list = await pm . listPackages ( ) ;
2024-04-21 00:58:00 +02:00
const checkedList = { } ;
let index = 0 ;
2024-04-19 00:34:22 +02:00
let result = await list . next ( ) ;
while ( ! result . done ) {
var packageElem = document . createElement ( 'li' ) ;
2024-04-21 00:58:00 +02:00
packageElem . index = index ;
packageElem . innerHTML = ` ${ result . value . packageName } <input type="checkbox" class="floatRight"/> ` ;
packageElem . querySelector ( 'input' ) . onchange = ( function ( ) {
checkedList [ this . parentElement . index ] = this . checked ;
$ ( 'button$packagesUninstall$' ) . disabled = ! Object . values ( checkedList ) . includes ( true ) ;
} ) ;
2024-04-19 00:34:22 +02:00
$ ( 'ul$packageList$' ) . appendChild ( packageElem ) ;
2024-04-21 00:58:00 +02:00
index ++ ;
2024-04-19 00:34:22 +02:00
result = await list . next ( ) ;
}
}
window . addEventListener ( 'hashchange' , ( async function ( ) {
const sectionHash = location . hash . slice ( 2 ) . split ( '/' ) [ 0 ] ;
2024-04-21 00:58:00 +02:00
if ( Device . adb && sectionHash === 'packages' ) {
2024-04-19 00:34:22 +02:00
refreshPackagesList ( ) ;
}
} ) ) ;
2024-04-15 01:30:58 +02:00
} ) ( ) ;