🌴 Sync 2026-03-05 23:51:42

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 97c049ed0a
commit 0acff03804
14253 files changed
+3893239

No files matched your search

@@ -0,0 +1,275 @@
/* This is free software, licensed under the Apache License, Version 2.0
*
* Copyright (C) 2024 Hilman Maulana <hilman0.0maulana@gmail.com>
*/
'use strict';
'require view';
'require uci';
'require fs';
'require ui';
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return uci.load('droidnet').then(function() {
var device = uci.get('droidnet', 'device', 'id');
return Promise.all([
fs.exec('adb', ['-s', device, 'shell', 'getprop']).then(function(result) {
var deviceInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
var properties = {
'ro.serialno': 'device_id',
'ro.product.brand': 'device_brand',
'ro.product.model': 'device_model',
'ro.product.device': 'device_code',
'ro.board.platform': 'device_soc',
'dalvik.vm.isa.arm.variant': 'device_cpu',
'ro.build.version.release': 'device_version',
'ro.build.version.sdk': 'device_sdk',
'ro.build.version.security_patch': 'device_security'
};
if (stderr) {
deviceInfo['device_section'] = stderr.trim();
} else {
var lines = stdout.split('\n');
for (var i = 0; i < lines.length; i++) {
for (var property in properties) {
if (lines[i].includes('[' + property + ']')) {
var parts = lines[i].split(']: [');
var value = parts[1].substring(0, parts[1].length - 1);
if (value.includes(',')) {
value = value.split(',').map(function(item) {return item.trim();});
};
deviceInfo[properties[property]] = value;
break;
};
};
};
};
return deviceInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'uptime']).then(function(result) {
var uptimeInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
uptimeInfo['uptime_section'] = stderr.trim();
} else {
var parts = stdout.trim().split(/\s+/);
var uptimeParts = parts.slice(0, 4);
uptimeInfo['device_uptime'] = uptimeParts.join(' ').replace(/,$/, '');
};
return uptimeInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'uname', '-a']).then(function(result) {
var kernelInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
kernelInfo['uname_section'] = stderr.trim();
} else {
var parts = stdout.trim().split(/\s+/);
kernelInfo['device_uname'] = {'kernel': parts[2], 'arch': parts[12]};
};
return kernelInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'cat', '/proc/meminfo']).then(function(result) {
var memmoryInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
memmoryInfo['memory_section'] = stderr.trim();
} else {
var parts = stdout.trim().split(/\s+/);
var kbValue = parseInt(parts[1]) + parseInt(parts[4]);
var gbValue = kbValue / (1024 * 1024);
memmoryInfo['device_memory'] = gbValue.toFixed(0) + ' GB';
};
return memmoryInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'dumpsys', 'battery']).then(function(result) {
var batteryInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
var properties = {
'level': 'battery_level',
'voltage': 'battery_voltage',
'technology': 'battery_technology',
'temperature': 'battery_temperature',
'Charge counter': 'battery_counter'
};
if (stderr) {
batteryInfo['battery_section'] = stderr.trim();
} else {
var lines = stdout.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
for (var property in properties) {
if (line.startsWith(property + ':')) {
var parts = line.split(':');
var value = parts[1].trim();
if (property === 'level') {
value = parseInt(value) + ' %';
} else if (property === 'voltage') {
value = (parseInt(value) / 1000).toFixed(2) + ' V';
} else if (property === 'temperature') {
value = (parseInt(value) / 10) + ' °C';
} else if (property === 'Charge counter') {
value = parseInt(value) / 1000 + ' μAh';
};
batteryInfo[properties[property]] = value;
};
};
};
};
return batteryInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'su', '-v']).then(function(result) {
var rootInfo = {};
var stdout = result.stdout ? result.stdout.trim() : '';
var stderr = result.stderr;
if (stderr || stdout === '/system/bin/sh: su: not found' || stdout === '/system/bin/sh: su: inaccessible or not found') {
rootInfo['device_root'] = false;
} else {
var parts = stdout.split(':');
rootInfo['device_root'] = {'version': parts[0], 'name': parts[1]};
};
return rootInfo;
}).catch(function(error) {
throw new Error(error);
})
]).then(function(results) {
var deviceInfo = results[0];
var uptimeInfo = results[1];
var kernelInfo = results[2];
var memmoryInfo = results[3];
var batteryInfo = results[4];
var rootInfo = results[5];
if (device && deviceInfo && uptimeInfo && kernelInfo && memmoryInfo && batteryInfo && rootInfo) {
return Object.assign({}, {device: device}, deviceInfo, uptimeInfo, kernelInfo, memmoryInfo, batteryInfo, rootInfo)
} else {
throw new Error(_('Failed to get complete device information.'));
};
}).catch(function(error) {
throw new Error(error);
});
});
},
render: function(data) {
var header = [
E('h2', {'class': 'section-title'}, _('DroidNet')),
E('div', {'class': 'cbi-map-descr'}, _('Manage Android modem and optimize network settings.'))
];
if (data.device_section) {
ui.addNotification(_('Error: Device conflict!'),
E('p', _('Please check your settings, the configured device and ADB devices are conflicting.')), 'danger'
);
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('div', {'class': 'cbi-value', 'style': 'text-align: center; display: block;'}, [
E('em', _('No device detected or connected.'))
])
])
]);
} else {
var device = [
E('h3', {'class': 'section-title'}, _('Device Information')),
E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr table-title', 'style': 'display: none;'}),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('Device ID')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_id || '-'),
E('td', {'class': 'td left', 'width': '25%'}, _('Processors')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_cpu ? (data.device_cpu.toUpperCase()) : '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('Root status')),
E('td', {'class': 'td left', 'width': '25%'},
data.device_root === false ? _('Non-root') :
data.device_root ? (_('Root with %s (%s)').format(data.device_root.name, data.device_root.version)) :
'-'
),
E('td', {'class': 'td left', 'width': '25%'}, _('RAM')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_memory || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('Brand name')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_brand ? (data.device_brand.charAt(0).toUpperCase() + data.device_brand.slice(1)) : '-'),
E('td', {'class': 'td left', 'width': '25%'}, _('Architecture')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_uname ? (data.device_uname.arch) : '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('Code name')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_code || '-'),
E('td', {'class': 'td left', 'width': '25%'}, _('Android version')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_version || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('Model number')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_model ? (data.device_model.charAt(0).toUpperCase() + data.device_model.slice(1)) : '-'),
E('td', {'class': 'td left', 'width': '25%'}, _('SDK version')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_sdk || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('Used time')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_uptime || '-'),
E('td', {'class': 'td left', 'width': '25%'}, _('Security patch level')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_security || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '25%'}, _('System on Chip (SoC)')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_soc ? (data.device_soc.toUpperCase()) : '-'),
E('td', {'class': 'td left', 'width': '25%'}, _('Kernel version')),
E('td', {'class': 'td left', 'width': '25%'}, data.device_uname ? (data.device_uname.kernel) : '-')
])
])
];
var battery = [
E('h3', {'class': 'section-title'}, _('Battery Information')),
E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr table-title', 'style': 'display: none;'}),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Level')),
E('td', {'class': 'td left', 'width': '50%'}, data.battery_level || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Charge counter')),
E('td', {'class': 'td left', 'width': '50%'}, data.battery_counter || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Voltage')),
E('td', {'class': 'td left', 'width': '50%'}, data.battery_voltage || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Temperature')),
E('td', {'class': 'td left', 'width': '50%'}, data.battery_temperature || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Technology')),
E('td', {'class': 'td left', 'width': '50%'}, data.battery_technology || '-')
]),
])
];
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, device),
E('div', {'class': 'cbi-section'}, battery)
]);
};
}
});
@@ -0,0 +1,228 @@
/* This is free software, licensed under the Apache License, Version 2.0
*
* Copyright (C) 2024 Hilman Maulana <hilman0.0maulana@gmail.com>
*/
'use strict';
'require view';
'require uci';
'require fs';
'require ui';
var page = 1;
function renderTable(data, display) {
var startIndex = (page - 1) * display;
var endIndex = Math.min(startIndex + display, data.length);
var currentPageData = data.slice(startIndex, endIndex);
var tableRows = currentPageData.map(function(inbox, index) {
var rowClass = index % 2 === 0 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2';
var message = inbox.message.replace(/\n/g, '<br>');
return E('tr', {'class': 'tr ' + rowClass }, [
E('td', {'class': 'td'}, inbox.received.date),
E('td', {'class': 'td'}, inbox.address),
E('td', {'class': 'td'}, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(inbox.address, [
E('p', [
E('em', `${inbox.received.date} - ${inbox.received.time}`)
]),
E('p', message),
E('div', {'class': 'right'}, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('OK'))
])
]);
}
}, _('View'))
])
]);
});
return E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr table-titles'}, [
E('th', {'class': 'th'}, _('Date')),
E('th', {'class': 'th'}, _('Address')),
E('th', {'class': 'th'})
]),
E(tableRows)
]);
};
function updateTable(data, display) {
var container = document.getElementsByClassName('table-container');
var prev = document.querySelector('.prev');
var next = document.querySelector('.next');
for (var i = 0; i < container.length; i++) {
container[i].innerHTML = '';
var table = renderTable(data, display);
container[i].appendChild(table);
};
var total = data.length;
var pages = Math.ceil(total / display);
if (pages <= 1) {
prev.disabled = true;
next.disabled = true;
} else if (page <= 1) {
prev.disabled = true;
next.disabled = false;
} else if (page >= pages) {
prev.disabled = false;
next.disabled = true;
} else {
prev.disabled = false;
next.disabled = false;
};
var start = (page - 1) * display + 1;
var end = Math.min(start + display - 1, total);
document.getElementById('page-info').innerText = _('Displaying %s - %s of %s').format(start, end, total);
};
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return uci.load('droidnet').then(function() {
var device = uci.get('droidnet', 'device', 'id');
var display = uci.get('droidnet', 'device', 'display_msg');
return Promise.all([
fs.exec('adb', ['-s', device, 'shell', 'content', 'query', '--uri', 'content://sms', '--projection', 'date_sent,date,address,body']).then(function(result) {
var messages = [];
var stdout = result.stdout;
var stderr = result.stderr;
if (stderr) {
messages['messages_section'] = stderr.trim();
} else if (stdout.includes('Error')) {
messages['messages_info'] = stdout.trim();
} else {
var lines = stdout.trim().split('Row: ');
lines.shift();
var properties = {
'body': 'message',
'address': 'address',
'date': 'received'
};
lines.forEach(function(line) {
var pairs = line.split(',');
var inbox = [];
pairs.forEach(function(pair) {
var keyValue = pair.split('=');
if (keyValue.length === 2) {
var key = keyValue[0].trim();
var value = keyValue[1].trim();
if (properties.hasOwnProperty(key)) {
if (key === 'body') {
var start = line.indexOf('body=') + 5;
var message = line.substring(start);
inbox[properties[key]] = message;
} else if (key === 'date') {
var timestamp = parseInt(value);
var date = new Date(timestamp).toLocaleDateString(undefined, {
weekday: 'short',
month: 'short',
day: '2-digit'
});
var time = new Date(timestamp).toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit'
});
inbox[properties[key]] = {'date': date, 'time': time}
} else {
inbox[properties[key]] = value;
};
};
};
});
messages.push(inbox);
});
};
return messages;
}).catch(function(error) {
throw new Error(error);
})
]).then(function(result) {
var messages = result[0];
if (device && display && messages) {
return Object.assign({device: device}, {display: display}, messages);
} else {
throw new Error(_('Failed to get complete device information.'));
};
}).catch(function(error) {
throw new Error(error);
});
});
},
render: function(data) {
var header = [
E('h2', {'class': 'section-title'}, _('DroidNet')),
E('div', {'class': 'cbi-map-descr'}, _('Manage Android modem and optimize network settings.'))
];
if (data.messages_section) {
ui.addNotification(_('Error: Device conflict!'),
E('p', _('Please check your settings, the configured device and ADB devices are conflicting.')), 'danger'
);
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('div', {'class': 'cbi-value', 'style': 'text-align: center; display: block;'}, [
E('em', _('No device detected or connected.'))
])
])
]);
} else if (data.messages_info) {
ui.addNotification(_('Error: Device not supported!'),
E('p', _('Unable to read message because the device version cannot execute the command. Please ensure the device is rooted or has Android version 10 or above.')), 'danger'
);
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('h3', {'class': 'section-title'}, _('Error Information')),
E('textarea', {
'id': 'syslog',
'class': 'cbi-input-textarea',
'style': 'height: 500px; overflow-y: scroll;',
'readonly': 'readonly',
'wrap': 'off',
'rows': 1
}, data.messages_info)
])
]);
} else {
var value = Object.values(data).filter(function(obj) {
return typeof obj === 'object' && !('device' in obj);
});
var display = parseInt(data.display);
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('h3', {'class': 'section-title'}, _('Inbox Messages')),
E('div', {'class': 'cbi-section-descr'}, _('Displays incoming messages with sender, subject, content, and date and time of receipt.')),
E('div', {'class': 'controls', 'style': 'display: flex; flex-wrap: wrap; justify-content: space-around; padding: 1em 0;'}, [
E('button', {
'class': 'btn cbi-button-neutral prev',
'style': 'flex-basis: 20%; text-align: center;',
'disabled': true,
'click': function() {
page--;
updateTable(value, display);
}
}, '«'),
E('div', {'class': 'text', 'id': 'page-info', 'style': 'flex-grow: 1; align-self: center; text-align: center;'}, _('Displaying 1-%s of %s').format(display, value.length)),
E('button', {
'class': 'btn cbi-button-neutral next',
'style': 'flex-basis: 20%; text-align: center;',
'click': function() {
page++;
updateTable(value, display);
}
}, '»')
]),
E('div', {'class': 'table-container'}, renderTable(value, display))
])
]);
};
}
});
@@ -0,0 +1,126 @@
/* This is free software, licensed under the Apache License, Version 2.0
*
* Copyright (C) 2024 Hilman Maulana <hilman0.0maulana@gmail.com>
*/
'use strict';
'require view';
'require fs';
'require ui';
'require poll';
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
var notification = false;
return Promise.all([
poll.add(function() {
fs.read('/var/log/droidnet.log').then(function(result) {
if (!result || result.trim() === '') {
ui.addNotification(_('Error: Read log file!'),
E('p', _('Unable to read the interface info from /var/log/droidnet.log.')), 'danger'
);
notification = true;
} else {
var data = result.trim();
var syslog = document.getElementById('syslog');
var filter = document.getElementById('log-filter').value;
var direction = document.getElementById('log-direction').value;
if (filter !== 'all') {
var value = data.split('\n').filter(function(log) {
return log.includes(filter);
}).join('\n');
data = value;
};
if (direction === 'up') {
var value = data.split('\n').reverse().join('\n');
syslog.innerHTML = value;
} else {
syslog.innerHTML = data;
};
notification = false;
};
}).catch(function(error) {
ui.addNotification(_('Error: Read log file!'),
E('p', _('An error occurred while reading the file') + `: ${error}`), 'danger'
);
notification = true;
});
})
]);
},
render: function() {
var header = [
E('h2', {'class': 'section-title'}, _('DroidNet')),
E('div', {'class': 'cbi-map-descr'}, _('Manage Android modem and optimize network settings.'))
];
var menu = [
E('label', {'for': 'log-filter', 'style': 'margin-right: 8px;'}, _('Filter by service') + ' : '),
E('select', {'id': 'log-filter', 'style': 'margin: 8px 8px 8px 0;'}, [
E('option', {'value': 'all', 'selected': 'selected'}, _('All')),
E('option', {'value': 'Application' }, _('Application')),
E('option', {'value': 'Monitoring' }, _('Monitoring')),
E('option', {'value': 'Network' }, _('Network')),
E('option', {'value': 'Power' }, _('Power'))
]),
E('label', {'for': 'log-direction', 'style': 'margin-right: 8px;'}, _('Log direction') + ' : '),
E('select', {'id': 'log-direction', 'style': 'margin: 8px 8px 8px 0;'}, [
E('option', {'value': 'down', 'selected': 'selected'}, _('Down')),
E('option', {'value': 'up' }, _('Up'))
]),
E('div', {'class': 'log-button', 'style': 'display: inline-block; margin: 0 8px 8px 0;'}, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'style': 'margin-right: 10px',
'click': function() {
fs.read('/var/log/droidnet.log').then(function(result) {
var message = _('DroidNet logs have been successfully cleared.');
var date = new Date().toLocaleDateString(undefined, {
weekday: 'short',
month: 'short',
day: '2-digit'
});
var time = new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit'
});
var notif = `${date}, ${time} - ${message}\n`;
fs.write('/var/log/droidnet.log', notif);
});
}
}, _('Clear')),
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': function() {
var logs = document.getElementById('syslog').value;
var blob = new Blob([logs], {type: 'text/plain'});
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'droidnet.log';
link.click();
}
}, _('Download'))
])
];
var body = [
E('textarea', {
'id': 'syslog',
'class': 'cbi-input-textarea',
'style': 'height: 500px; overflow-y: scroll;',
'readonly': 'readonly',
'wrap': 'off',
'rows': 1
})
];
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('div', {'class': 'cbi-control'}, menu),
E('div', {'class': 'cbi-body'}, body)
])
]);
}
});
@@ -0,0 +1,589 @@
/* This is free software, licensed under the Apache License, Version 2.0
*
* Copyright (C) 2024 Hilman Maulana <hilman0.0maulana@gmail.com>, Anas Fanani <anas@anasfanani.com>
*/
'use strict';
'require view';
'require uci';
'require fs';
'require ui';
function closeUi(type) {
if (type === 'OK') {
return E('button', {
'class': 'btn',
'click': function() {
return window.location.reload();
}
}, _('OK'));
} else {
return E('button', {
'class': 'btn cbi-button cbi-button-remove',
'style': 'margin-right: 10px',
'click': ui.hideModal
}, _('Cancel'));
};
};
function writeLog(message) {
var logFile = '/var/log/droidnet.log';
fs.read(logFile).then(function(result) {
var service = _('Network service');
var date = new Date().toLocaleDateString(undefined, {
weekday: 'short',
month: 'short',
day: '2-digit'
});
var time = new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit'
});
var notif = `${date}, ${time} - ${service}: ${message}`;
var newData = result.trim() + '\n' + notif;
return fs.write(logFile, newData);
});
};
function renderTableCell(data, id) {
if (id === 'sim-1') {
var tab = 1;
var sim = 0;
var display = 'table';
var imei = data.imei_sim01;
} else {
var tab = 2;
var sim = 1;
var display = 'none';
var imei = data.imei_sim02;
};
return E('table', {'class': 'table cbi-section-table', 'id': `sim-${tab}`, 'style': `display: ${display}`}, [
E('tr', {'class': 'tr table-titles', 'style': 'display: none;'}),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Operator name')),
E('td', {'class': 'td left', 'width': '50%'}, data.operator[sim] || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Network type')),
E('td', {'class': 'td left', 'width': '50%'}, data.signal[sim] || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Roaming mode')),
E('td', {'class': 'td left', 'width': '50%'}, data.roaming[sim] === 'true' ? _('On') :
_('Off')
)
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('MCC')),
E('td', {'class': 'td left', 'width': '50%'}, data.mcc[sim] || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('IMEI')),
E('td', {'class': 'td left', 'width': '50%'}, imei || '-')
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Driver')),
E('td', {'class': 'td left', 'width': '50%'}, data.driver || '-')
])
]);
};
function renderTableWiFi(device) {
fs.exec('adb', ['-s', device, 'shell', 'dumpsys', 'wifi', '|', 'grep', 'mWifiInfo SSID']).then(function(result) {
var table, wifiInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
var properties = {
'mWifiInfo SSID': 'ssid',
'BSSID': 'bssid',
'MAC': 'mac',
'RSSI': 'rssi',
'Link speed': 'speed',
'Frequency': 'frequency'
};
if (stderr) {
table = E('p', stderr);
} else {
var lines = stdout.trim().split('\n')[0];
var parts = lines.split(', ');
parts.forEach(function(part) {
var keyValue = part.split(': ');
var key = keyValue[0].trim();
var value = keyValue[1].replace(/"/g, '');
if (properties[key]) {
if (key === 'Frequency' || key === 'Link speed') {
value = value.replace(/(\d+)([A-Za-z]+)/g, '$1 $2');
};
if (key === 'RSSI') {
value += ' dBm';
};
wifiInfo[properties[key]] = value;
};
});
};
table = [
E('h3', {'class': 'section-title'}, _('Wireless Information')),
E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr table-titles', 'style': 'display: none;'}),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('SSID')),
E('td', {'class': 'td left', 'width': '50%'}, wifiInfo.ssid)
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('BSSID')),
E('td', {'class': 'td left', 'width': '50%'}, wifiInfo.bssid)
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('MAC address')),
E('td', {'class': 'td left', 'width': '50%'}, wifiInfo.mac)
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('RSSI')),
E('td', {'class': 'td left', 'width': '50%'}, wifiInfo.rssi)
]),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Link speed')),
E('td', {'class': 'td left', 'width': '50%'}, wifiInfo.speed)
]),
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Frequency')),
E('td', {'class': 'td left', 'width': '50%'}, wifiInfo.frequency)
])
])
];
return document.getElementById('wirelessMenu').append(...table);
}).catch(function(error) {
var message = E('p', error);
return document.getElementById('wirelessMenu').append(...message);
});
};
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return uci.load('droidnet').then(function() {
var device = uci.get('droidnet', 'device', 'id');
return Promise.all([
fs.exec('adb', ['-s', device, 'shell', 'getprop']).then(function(result) {
var networkInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
var properties = {
'gsm.operator.alpha': 'operator',
'gsm.network.type': 'signal',
'gsm.version.ril-impl': 'driver',
'gsm.operator.isroaming': 'roaming',
'gsm.sim.operator.numeric': 'mcc'
};
if (stderr) {
networkInfo['network_section'] = stderr.trim();
} else {
var lines = stdout.split('\n');
for (var i = 0; i < lines.length; i++) {
for (var property in properties) {
if (lines[i].includes('[' + property + ']')) {
var parts = lines[i].split(']: [');
var value = parts[1].substring(0, parts[1].length - 1);
if (value.includes(',')) {
value = value.split(',').map(function(item) {
return item.trim();
});
}else {
value = [value.trim(), ''];
};
networkInfo[properties[property]] = value;
break;
};
if (!networkInfo.hasOwnProperty(properties[property])) {
networkInfo[properties[property]] = false;
};
};
};
};
return networkInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'service', 'call', 'iphonesubinfo', '1', 's16', 'com.android.shell']).then(function(result) {
var imeisim01Info = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
imeisim01Info['imei_sim01_section'] = stderr.trim();
} else {
var matches = stdout.match(/'([^']+)'/g);
var value = matches.map(function(match) {
return match.slice(1, -1);
});
var combined = value.join('');
var imei = combined.replace(/[.\s]/g, '');
imeisim01Info['imei_sim01'] = imei;
};
return imeisim01Info;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'ip', 'route']).then(function(result) {
var ipInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (result && stderr) {
ipInfo['ip_section'] = stderr.trim();
} else if (result && stdout) {
var parts = stdout.trim().split(/\s+/);
var value = parts.slice(-1)[0];
ipInfo['ip'] = value;
} else {
ipInfo['ip'] = false;
};
return ipInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'dumpsys', 'wifi', '|', 'grep', 'Wi-Fi is']).then(function(result) {
var wifiInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
wifiInfo['wifi_section'] = stderr.trim();
} else {
var status = stdout.trim() === 'Wi-Fi is enabled' ? true : false;
wifiInfo['wifi'] = status;
};
return wifiInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'dumpsys', 'telephony.registry', '|', 'grep', 'mDataConnectionState=']).then(function(result) {
var dataInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
dataInfo['data_section'] = stderr.trim();
} else {
if (stdout.includes('mDataConnectionState=2')) {
var dataStatus = true;
} else {
var dataStatus = false;
};
dataInfo['data'] = dataStatus;
};
return dataInfo;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'settings', 'get', 'global', 'airplane_mode_on']).then(function(result) {
var airplaneInfo = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
airplaneInfo['airplane_section'] = stderr.trim();
} else {
var status = stdout.trim() === '1' ? true : false;
airplaneInfo['airplane'] = status;
};
return airplaneInfo;
}).catch(function(error) {
throw new Error(error);
}),
]).then(function(results) {
var networkInfo = results[0];
var imeisim01Info = results[1];
var ipInfo = results[2];
var wifiInfo = results[3];
var dataInfo = results[4];
var airplaneInfo = results[5];
if (device && networkInfo && imeisim01Info && ipInfo && wifiInfo && dataInfo && airplaneInfo) {
return Object.assign({device: device}, networkInfo, imeisim01Info, ipInfo, wifiInfo, dataInfo, airplaneInfo);
} else {
throw new Error(_('Failed to get complete device information.'));
};
}).catch(function(error) {
throw new Error(error);
});
});
},
render: function(data) {
var header = [
E('h2', {'class': 'section-title'}, _('DroidNet')),
E('div', {'class': 'cbi-map-descr'}, _('Manage Android modem and optimize network settings.'))
];
if (data.network_section) {
ui.addNotification(_('Error: Device conflict!'),
E('p', _('Please check your settings, the configured device and ADB devices are conflicting.')), 'danger'
);
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('div', {'class': 'cbi-value', 'style': 'text-align: center; display: block;'}, [
E('em', _('No device detected or connected.'))
])
])
]);
} else {
var networkMenu = [
E('h3', {'class': 'section-title'}, _('Mobile Network')),
E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr table-titles', 'style': 'display: none;'}),
E('tr', {'class': 'tr cbi-rowstyle-1'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('IP address')),
E('td', {'class': 'td left'}, data.ip || '-'),
E('td', {'class': 'td'})
]),
E('tr', {'class': 'tr cbi-rowstyle-2', 'style': 'display: table-row;'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Wireless connection')),
E('td', {'class': 'td left', 'width': '25%'}, data.wifi === true ? _('On') :
_('Off')
),
E('td', {'class': 'td center', 'width': '25%'}, [
data.wifi === false ? E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Wireless network'), [
E('p', _('Are you sure want to switched on wireless connection?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Turning on wireless connection '), [
E('p', {'class': 'spinning'}, _('Waiting for wireless connection to be turned on…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'svc', 'wifi', 'enable']);
writeLog(_('Wireless connection has been switched on.'));
setTimeout(function() {
ui.showModal(_('Wireless connection has been switched on'), [
E('p', _('Wireless connection has been successfully switched on.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
}, 5000);
}
}, _('Yes'))
])
]);
}
}, _('Enable')) :
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': function() {
ui.showModal(_('Wireless network'), [
E('p', _('Are you sure want to switched off wireless connection?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Turning off wireless connection '), [
E('p', {'class': 'spinning'}, _('Waiting for wireless connection to be turned off…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'svc', 'wifi', 'disable']);
writeLog(_('Wireless connection has been switched off.'));
setTimeout(function() {
ui.showModal(_('Wireless connection has been switched off'), [
E('p', _('Wireless connection has been successfully switched off.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
}, 5000);
}
}, _('Yes'))
])
]);
}
}, _('Disable'))
])
]),
E('tr', {'class': 'tr cbi-rowstyle-1', 'style': 'display: table-row;'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Mobile data')),
E('td', {'class': 'td left', 'width': '25%'}, data.data === true ? _('On') :
_('Off')
),
E('td', {'class': 'td center', 'width': '25%'}, [
data.data === false ? E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Mobile network'), [
E('p', _('Are you sure want to switched on mobile data?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
if (data.airplane === true) {
ui.showModal(_('Failed to switch on mobile data'), [
E('p', _('Failed to switch on mobile data because the airplane mode is active.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
writeLog(_('Failed to swich on mobile data because the airplane mode is active.'));
} else {
ui.showModal(_('Turning on mobile data'), [
E('p', {'class': 'spinning'}, _('Waiting for mobile data to be turned on…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'svc', 'data', 'enable']);
writeLog(_('Mobile data has been switched on.'));
setTimeout(function() {
ui.showModal(_('Mobile data has been switched on'), [
E('p', _('Mobile data has been successfully switched on.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
}, 5000);
};
}
}, _('Yes'))
])
]);
}
}, _('Enable')) :
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': function() {
ui.showModal(_('Mobile network'), [
E('p', _('Are you sure want to switched off mobile data?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Turning off mobile data'), [
E('p', {'class': 'spinning'}, _('Waiting for mobile data to be turned off…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'svc', 'data', 'disable']);
writeLog(_('Mobile data has been switched off.'));
setTimeout(function() {
ui.showModal(_('Mobile data has been switched off'), [
E('p', _('Mobile data has been successfully switched off.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
}, 5000);
}
}, _('Yes'))
])
]);
}
}, _('Disable'))
])
]),
E('tr', {'class': 'tr cbi-rowstyle-2', 'style': 'display: table-row;'}, [
E('td', {'class': 'td left', 'width': '50%'}, _('Airplane mode')),
E('td', {'class': 'td left', 'width': '25%'}, data.airplane === true ? _('On') :
_('Off')
),
E('td', {'class': 'td center', 'width': '25%'}, [
data.airplane === false ? E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Airplane mode'), [
E('p', _('Are you sure want to switched on airplane mode?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
fs.exec('adb', ['-s', data.device, 'shell', 'cmd', 'connectivity', 'airplane-mode', 'enable']).then(function(result) {
if (result.stdout) {
ui.showModal(_('Failed to switch on aiplane mode'), [
E('p', _('Failed to switch on airplane mode because device version cannot execute the command, make sure device is rooted or android version is 10 or above.')),
E('em', {'style': 'color: red;'}, result.stdout),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
writeLog(_('Failed to switch on airplane mode because android version is below 10.'));
} else {
ui.showModal(_('Turning on airplane mode'), [
E('p', {'class': 'spinning'}, _('Waiting for airplane mode to be turned on…'))
]);
writeLog(_('Airplane mode has been switched on.'));
setTimeout(function() {
ui.showModal(_('Airplane mode has been switched on'), [
E('p', _('Airplane mode has been successfully switched on.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
}, 5000);
};
});
}
}, _('Yes'))
])
]);
}
}, _('Enable')) :
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': function() {
ui.showModal(_('Airplane mode'), [
E('p', _('Are you sure want to switched off airplane mode?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
fs.exec('adb', ['-s', data.device, 'shell', 'cmd', 'connectivity', 'airplane-mode', 'disable']).then(function(result) {
if (result.stdout) {
ui.showModal(_('Failed to switch off aiplane mode'), [
E('p', _('Failed to switch off airplane mode because device version cannot execute the command, make sure device is rooted or android version is 10 or above.')),
E('em', {'style': 'color: red;'}, result.stdout),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
writeLog(_('Failed to switch off airplane mode because android version is below 10.'));
} else {
ui.showModal(_('Turning off Airplane mode'), [
E('p', {'class': 'spinning'}, _('Waiting for airplane mode to be turned off…'))
]);
writeLog(_('Airplane mode has been switched off.'));
setTimeout(function() {
ui.showModal(_('Airplane mode has been switched off'), [
E('p', _('Airplane mode has been successfully switched off.')),
E('div', {'class': 'right'}, [E(closeUi('OK'))])
]);
}, 5000);
};
});
}
}, _('Yes'))
])
]);
}
}, _('Disable'))
])
])
])
];
var cellularMenu = [
E('h3', {'class': 'section-title'}, _('Cellular Information')),
E('ul', {'class': 'cbi-tabmenu'}, [
data.operator && data.operator[0] ? E('li', {'class': 'cbi-tab', 'id': 'tab-1'}, [
E('a', {
'href': '#',
'click': function() {
document.getElementById('tab-1').className = 'cbi-tab';
document.getElementById('tab-2').className = 'cbi-tab-disabled';
document.getElementById('sim-1').style.display = 'table';
document.getElementById('sim-2').style.display = 'none';
}
}, _('SIM 1'))
]) : '',
data.operator && data.operator[1] ? E('li', {'class': 'cbi-tab-disabled', 'id': 'tab-2'}, [
E('a', {
'href': '#',
'click': function() {
document.getElementById('tab-1').className = 'cbi-tab-disabled';
document.getElementById('tab-2').className = 'cbi-tab';
document.getElementById('sim-1').style.display = 'none';
document.getElementById('sim-2').style.display = 'table';
}
}, _('SIM 2'))
]) : ''
]),
E(renderTableCell(data, 'sim-1')),
E(renderTableCell(data, 'sim-2'))
];
var wirelessMenu = [
data.wifi === true ? E('div', {'id': 'wirelessMenu'}, renderTableWiFi(data.device)) : ''
];
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, networkMenu),
E('div', {'class': 'cbi-section'}, wirelessMenu),
E('div', {'class': 'cbi-section'}, cellularMenu)
]);
};
}
});
@@ -0,0 +1,546 @@
/* This is free software, licensed under the Apache License, Version 2.0
*
* Copyright (C) 2024 Hilman Maulana <hilman0.0maulana@gmail.com>
*/
'use strict';
'require view';
'require uci';
'require fs';
'require ui';
var page = 1;
var filter = '';
var apkFile = '/tmp/upload.apk';
function closeUi(type) {
if (type === 'OK') {
return E('button', {
'class': 'btn',
'click': function() {
return window.location.reload();
}
}, _('OK'));
} else {
return E('button', {
'class': 'btn cbi-button cbi-button-remove',
'style': 'margin-right: 10px',
'click': ui.hideModal
}, _('Cancel'));
};
};
function writeLog(message, service, error) {
var logFile = '/var/log/droidnet.log';
if (service === 'power') {
var serviceName = _('Power service');
} else {
var serviceName = _('Application service');
};
fs.read(logFile).then(function(result) {
var date = new Date().toLocaleDateString(undefined, {
weekday: 'short',
month: 'short',
day: '2-digit'
});
var time = new Date().toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit'
});
if (error === false) {
var notif = `${date}, ${time} - ${serviceName}: ${message}`;
} else {
var notif = `${date}, ${time} - ${serviceName}: ${message}: ${error}`;
};
var newData = result.trim() + '\n' + notif;
return fs.write(logFile, newData);
});
};
function renderTable(data, display) {
var tableRows, value = data.application;
var start = (page - 1) * display;
var packages = value.filter(function(item) {
return item.toLowerCase().includes(filter.toLowerCase());
});
var end = Math.min(start + display, packages.length);
var current = packages.slice(start, end);
if (packages.length === 0) {
tableRows = [
E('tr', {'class': 'tr cbi-rowstyle-2'}, [
E('td', {'class': 'td center', 'colspan': '2'}, [
E('em', _('Application not found'))
])
])
];
} else {
tableRows = current.map(function(value, index) {
var rowClass = index % 2 === 0 ? 'cbi-rowstyle-1' : 'cbi-rowstyle-2';
return E('tr', {'class': 'tr ' + rowClass }, [
E('td', {'class': 'td left'}, value),
E('td', {'class': 'td right'}, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'click': function() {
ui.showModal(_('Remove application') + ` ${value}`, [
E('p', _('Are you sure want to remove this application?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Removing application'), [
E('p', {'class': 'spinning'}, _('Waiting for application removal to complete…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'pm', 'uninstall', '-k', '--user', '0', value]).then(function(result) {
var stdout = result.stdout;
if (stdout.trim() !== 'Success') {
writeLog(_('Failed to remove %s application.').format(value), 'application', stdout);
ui.showModal(_('Package removal failed'), [
E('p', _('Failed to remove <em>%s</em> application.').format(value)),
E('em', {'style': 'color: red;'}, stdout),
E('div', {'class': 'right'}, [
E('button', {'class': 'btn', 'click': ui.hideModal}, _('OK'))
])
]);
} else {
writeLog(_('Removing %s application successfully.').format(value), 'application', false);
setTimeout(function() {
ui.showModal(_('Removing application completed'), [
E('p', _('Application <em>%s</em> has been successfully removed.').format(value)),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
}, 10000);
};
});
}
}, _('Yes'))
])
]);
}
}, _('Remove'))
])
]);
});
};
return E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr table-titles'}, [
E('th', {'class': 'th left'}, _('Application name')),
E('th', {'class': 'th'})
]),
E(tableRows)
]);
};
function updateTable(data, display) {
var value = data.application;
var container = document.getElementsByClassName('table-container');
var prev = document.querySelector('.prev');
var next = document.querySelector('.next');
for (var i = 0; i < container.length; i++) {
container[i].innerHTML = '';
var table = renderTable(data, display);
container[i].appendChild(table);
};
var packages = value.filter(function(item) {
return item.toLowerCase().includes(filter.toLowerCase());
});
var total = packages.length;
var pages = Math.ceil(total / display);
if (pages <= 1) {
prev.disabled = true;
next.disabled = true;
} else if (page <= 1) {
prev.disabled = true;
next.disabled = false;
} else if (page >= pages) {
prev.disabled = false;
next.disabled = true;
} else {
prev.disabled = false;
next.disabled = false;
};
var start = (page - 1) * display + 1;
var end = Math.min(start + display - 1, total);
document.getElementById('page-info').innerText = _('Displaying %s - %s of %s').format(start, end, total);
};
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return uci.load('droidnet').then(function() {
var device = uci.get('droidnet', 'device', 'id');
var display = uci.get('droidnet', 'device', 'display_app');
return Promise.all([
fs.exec('adb', ['-s', device, 'shell', 'df', 'sdcard', '-h']).then(function(result) {
var storage = {};
var stderr = result.stderr;
var stdout = result.stdout;
var properties = {
'Size': 'size',
'Used': 'use',
'Avail': 'free',
'Use%': 'percentage',
'Mounted': 'mounted'
};
if (stderr) {
storage['storage_section'] = stderr.trim();
} else {
var data = [];
var lines = stdout.split('\n');
var header = lines[0].split(/\s+/);
var value = lines[1].split(/\s+/);
for (var i = 0; i < header.length; i++) {
var property = properties[header[i]];
if (property) {
data[property] = value[i];
};
};
storage['storage'] = data;
};
return storage;
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('adb', ['-s', device, 'shell', 'pm', 'list', 'packages']).then(function(result) {
var application = {};
var stderr = result.stderr;
var stdout = result.stdout;
if (stderr) {
application['application_section'] = stderr.trim();
} else {
var parts = stdout.trim().split('\n').map(function(line) {
return line.replace('package:', '');
});
application['application'] = parts;
};
return application;
}).catch(function(error) {
throw new Error(error);
})
]).then(function(results) {
var storage = results[0];
var application = results[1];
if (device && display && storage && application) {
return Object.assign({device: device}, {display: display}, storage, application)
} else {
throw new Error(_('Failed to get complete device information.'));
};
}).catch(function(error) {
throw new Error(error);
});
});
},
render: function(data) {
var display = parseInt(data.display);
var application = data.application;
var header = [
E('h2', {'class': 'section-title'}, _('DroidNet')),
E('div', {'class': 'cbi-map-descr'}, _('Manage Android modem and optimize network settings.'))
];
if (data.storage_section) {
ui.addNotification(_('Error: Device conflict!'),
E('p', _('Please check your settings, the configured device and ADB devices are conflicting.')), 'danger'
);
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, [
E('div', {'class': 'cbi-value', 'style': 'text-align: center; display: block;'}, [
E('em', _('No device detected or connected.'))
])
])
]);
} else {
var powerMenu = [
E('h3', {'class': 'section-title'}, _('Power Options')),
E('div', {'class': 'cbi-section-descr'}, _('Let you shutdown, restart, access fastboot mode or recovery mode, all in one place.')),
E('table', {'class': 'table cbi-section-table'}, [
E('tr', {'class': 'tr'}, [
E('td', {'class': 'td center', 'style': 'border: none;'}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'style': 'margin: 10px 0!important;',
'click': function() {
ui.showModal(_('Fastboot mode'), [
E('p', _('Are you sure want rebooting device to fastboot mode?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Rebooting to fastboot mode'), [
E('p', {'class': 'spinning'}, _('Waiting for device to reboot into fastboot mode…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'reboot', 'bootloader']);
writeLog(_('Device entered fastboot mode.'), 'power', false);
setTimeout(function() {
ui.showModal(_('Fastboot mode completed'), [
E('p', _('Device has been successfully to fastboot mode.')),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
}, 10000);
}
}, _('Yes'))
])
]);
}
}, _('Fastboot mode'))
]),
E('td', {'class': 'td center', 'style': 'border: none;'}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'style': 'margin: 10px 0!important;',
'click': function() {
ui.showModal(_('Recovery mode'), [
E('p', _('Are you sure want rebooting device to recovery mode?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Rebooting to recovery mode'), [
E('p', {'class': 'spinning'}, _('Waiting for device to reboot into recovery mode…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'reboot', 'recovery']);
writeLog(_('Device entered recovery mode.'), 'power', false);
setTimeout(function() {
ui.showModal(_('Recovery mode completed'), [
E('p', _('Device has been successfully to recovery mode.')),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
}, 10000);
}
}, _('Yes'))
])
]);
}
}, _('Recovery mode'))
]),
E('td', {'class': 'td center', 'style': 'border: none;'}, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'style': 'margin: 10px 0!important;',
'click': function() {
ui.showModal(_('Restart device'), [
E('p', _('Are you sure want to restart device?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Restarting device'), [
E('p', {'class': 'spinning'}, _('Waiting for device restart to complete…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'reboot']);
writeLog(_('Device restarted successfully.'), 'power', false);
setTimeout(function() {
ui.showModal(_('Device restart completed'), [
E('p', _('Device has been successfully restarted.')),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
}, 30000);
}
}, _('Yes'))
])
]);
}
}, _('Restart'))
]),
E('td', {'class': 'td center', 'style': 'border: none;'}, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'style': 'margin: 10px 0!important;',
'click': function() {
ui.showModal(_('Shutdown device'), [
E('p', _('Are you sure want to shutdown device?')),
E('div', {'class': 'right'}, [
E(closeUi('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Shutting down device'), [
E('p', {'class': 'spinning'}, _('Waiting for device to shut down…'))
]);
fs.exec('adb', ['-s', data.device, 'shell', 'reboot', '-p']);
writeLog(_('Device powered off successfully.'), 'power', false);
setTimeout(function() {
ui.showModal(_('Device shutdown completed'), [
E('p', _('Device has been successfully shut down.')),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
}, 15000);
}
}, _('Yes'))
])
]);
}
}, _('Shutdown'))
])
])
])
];
var applicationMenu = [
E('h3', {'class': 'section-title'}, _('Application Manager')),
E('div', {'class': 'cbi-section-descr'}, _('Lists installed apps on device.')),
E('div', {'class': 'controls', 'style': 'display: flex; flex-wrap: wrap;'}, [
E('div', {'class': 'disk-application', 'style': 'flex-basis: 100%; min-width: 250px; padding: .25em;'}, [
E('label', _('Disk space') + ' : '),
E('div', {'class': 'cbi-progressbar', 'title': _('%s used (%s used of %s, %s free)').format(data.storage.percentage, data.storage.use, data.storage.size, data.storage.free)}, [
E('div', {'style': `width: ${data.storage.percentage};`}, '&nbsp;')
])
]),
E('div', {'class': 'filter-application', 'style': 'padding: .25em;'}, [
E('label', _('Filter') + ' : '),
E('span', {'class': 'control-group', 'style': 'display: flex;'}, [
E('input', {
'type': 'text',
'class': 'filter-input',
'placeholder': 'Type to filter…',
'keyup': function(event) {
filter = event.target.value;
page = 1;
updateTable(data, display);
}
}),
E('button', {
'class': 'btn cbi-button',
'click': function() {
filter = '';
page = 1;
updateTable(data, display);
document.querySelector('.filter-input').value = '';
}
}, _('Clear'))
])
]),
E('div', {'class': 'action-application', 'style': 'padding: .25em;'}, [
E('label', _('Actions') + ' : '),
E('span', {'class': 'control-group', 'style': 'display: flex;'}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'style': 'margin-right: 10px',
'click': function() {
ui.showModal(_('Updating application list'), [
E('p', {'class': 'spinning'}, _('Waiting for update application list command to complete…'))
]);
setTimeout(function() {
ui.showModal(_('Update application list completed'), [
E('p', _('Application list has been successfully updated.')),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
}, 10000);
}
}, _('Update list')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.uploadFile(apkFile).then(function(result) {
var apkName = result.name;
ui.showModal(_('Install application'), [
E('p', _('Installing application from untrusted sources is a potential security risk, really attempt to install <em>%s</em>?').format(apkName)),
E('ul', [
result.size ? E('li', '%s: %1024.2mB'.format(_('Size'), result.size)) : '',
result.checksum ? E('li', '%s: %s'.format(_('MD5'), result.checksum)) : '',
result.sha256sum ? E('li', '%s: %s'.format(_('SHA256'), result.sha256sum)) : ''
]),
E('div', {'class': 'right'}, [
E('button', {
'class': 'btn cbi-button cbi-button-remove',
'style': 'margin-right: 10px',
'click': function() {
fs.remove(apkFile);
ui.hideModal();
}
}, _('Cancel')),
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': function() {
ui.showModal(_('Installing application'), [
E('p', {'class': 'spinning'}, _('Waiting for application installation to complete…'))
]);
fs.exec_direct('adb', ['-s', data.device, 'install', apkFile]).then(function(result) {
var apk = result.trim();
if (apk === 'Success') {
writeLog(_('Application %s has been successfully installed.').format(apkName), 'application', false);
ui.showModal(_('Application installation completed'), [
E('p', _('Application <em>%s</em> has been successfully installed.').format(apkName)),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
} else {
writeLog(_('Failed to install %s application').format(apkName), 'application', apk);
ui.showModal(_('Application installation failed'), [
E('p', _('Failed to install <em>%s</em> application.').format(apkName)),
E('em', {'style': 'color: red;'}, apk),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
};
return fs.remove(apkFile);
});
}
}, _('Install'))
])
])
}).catch(function(error) {
if (error.message === 'Upload has been cancelled') {
ui.hideModal();
} else {
writeLog(_('Failed to upload %s application.').format(apkName), 'application', error);
ui.showModal(_('Error uploading application'), [
E('p', _('Failed to upload <em>%s</em> application.').format(apkName)),
E('em', {'style': 'color: red;'}, error),
E('div', {'class': 'right'}, [
E('div', {'class': 'right'}, [E(closeUi('OK'))])
])
]);
};
});
}
}, _('Upload application'))
])
])
]),
E('div', {'class': 'controls', 'style': 'display: flex; flex-wrap: wrap; justify-content: space-around; padding: 1em 0;'}, [
E('button', {
'class': 'btn cbi-button-neutral prev',
'style': 'flex-basis: 20%; text-align: center;',
'disabled': true,
'click': function() {
page--;
updateTable(data, display);
}
}, '«'),
E('div', {'class': 'text', 'id': 'page-info', 'style': 'flex-grow: 1; align-self: center; text-align: center;'}, _('Displaying 1 - %s of %s').format(display, application.length)),
E('button', {
'class': 'btn cbi-button-neutral next',
'style': 'flex-basis: 20%; text-align: center;',
'click': function() {
page++;
updateTable(data, display);
}
}, '»')
]),
E('div', {'class': 'table-container'}, renderTable(data, display))
];
return E('div', {'class': 'cbi-map'}, [
E(header),
E('div', {'class': 'cbi-section'}, powerMenu),
E('div', {'class': 'cbi-section'}, applicationMenu)
]);
};
}
});
@@ -0,0 +1,180 @@
/* This is free software, licensed under the Apache License, Version 2.0
*
* Copyright (C) 2024 Hilman Maulana <hilman0.0maulana@gmail.com>
*/
'use strict';
'require view';
'require form';
'require uci';
'require fs';
'require tools.widgets as widgets';
return view.extend({
load: function() {
return uci.load('droidnet').then(function() {
return Promise.all([
fs.exec('adb', ['devices', '-l']).then(function(result) {
var devices = {};
var stdout = result.stdout;
var stderr = result.stderr;
if (stderr || stdout === 'List of devices attached \n\n') {
devices = false;
} else {
var lines = stdout.trim().split('\n');
lines.shift();
lines.forEach(function(line) {
var model = '';
var parts = line.split(/\s+/);
var device = parts[0].trim();
parts.forEach(function(part) {
if (part.startsWith('model:')) {
model = part.substring(6);
};
});
devices[device] = model;
});
};
return {devices};
}).catch(function(error) {
throw new Error(error);
}),
fs.list('/etc/init.d').then(function(result) {
var tunnelService = {};
var fileNames = {
'neko': 'Neko',
'openclash': 'OpenClash',
'passwall': 'PassWall',
'v2ray': 'V2Ray'
};
result.forEach(function(file) {
if (fileNames[file.name]) {
tunnelService[file.name] = fileNames[file.name];
};
});
if (Object.keys(tunnelService).length === 0) {
tunnelService = false;
}
return {tunnelService};
}).catch(function(error) {
throw new Error(error);
}),
fs.exec('pgrep', ['-f', '/usr/share/droidnet/monitoring']).then(function(result) {
var status = {};
if (result && result.code === 1) {
status = false;
} else {
status = true;
};
return {status};
}).catch(function(error) {
throw new Error(error);
})
]).then(function(results) {
var devices = results[0];
var tunnelService = results[1];
var status = results[2];
return Object.assign(devices, tunnelService, status)
}).catch(function(error) {
throw new Error(error);
});
});
},
render: function(data) {
var m, s, o;
m = new form.Map('droidnet', _('DroidNet'),
_('Manage Android modem and optimize network settings.'));
s = m.section(form.NamedSection, 'device', 'droidnet', _('Base Setting'));
if (data.devices == false) {
o = s.option(form.DummyValue, 'dummy', _('Device'));
o.default = _('No device detected.');
} else {
o = s.option(form.ListValue, 'id', _('Device'));
Object.keys(data.devices).forEach(function(deviceID) {
o.value(deviceID, data.devices[deviceID]);
});
o.rmempty = false;
};
o = s.option(form.Value, 'display_app', _('Application limit'),
_('Set display limit per page for application manager.'));
o.placeholder = _('1 - 100');
o.datatype = 'range(1,100)';
o.rmempty = false;
o = s.option(form.Value, 'display_msg', _('Messages limit'),
_('Set display limit per page for messages information.'));
o.placeholder = _('1 - 100');
o.datatype = 'range(1,100)';
o.rmempty = false;
s = m.section(form.NamedSection, 'monitoring', 'droidnet', _('Monitoring Service'),
_('Monitor network performance on android modem to ensure optimal connectivity stability.'));
o = s.option(form.DummyValue, 'dummy', _('Status'));
o.rawhtml = true;
o.cfgvalue = function(section_id) {
var span = '<b><span style="color:%s">%s</span></b>';
var renderHTML = data.status ?
String.format(span, 'green', _('Running')) :
String.format(span, 'red', _('Not Running'));
return renderHTML;
};
o = s.option(form.Flag, 'enable', _('Enable'));
o.rmempty = false;
o = s.option(form.ListValue, 'ping', _('Ping method'),
_('Set method for pinging host address.'));
o.value('http', _('HTTP'));
o.value('https', _('HTTPS'));
o.value('icmp', _('ICMP'));
o.value('tcp', _('TCP'));
o.rmempty = false;
o = s.option(form.Value, 'host', _('Host'),
_('Host address you want to ping. Recommended to use bug on Tun.'));
o.placeholder = _('Host address');
o.rmempty = false;
o = s.option(form.ListValue, 'success_limit', _('Max ping successes'),
_('Maximum number of successful ping attempts to log "Host reachable" message.'));
o.value('1', _('1 successes'));
o.value('2', _('2 successes'));
o.value('3', _('3 successes'));
o.value('4', _('4 successes'));
o.value('5', _('5 successes'));
o.value('unlimited', _('Unlimited'));
o.rmempty = false;
o = s.option(form.ListValue, 'failure_limit', _('Max ping attempts'),
_('Maximum number of unsuccessful ping attempts to trigger service.'));
o.value('1', _('1 attempts'));
o.value('2', _('2 attempts'));
o.value('3', _('3 attempts'));
o.value('4', _('4 attempts'));
o.value('5', _('5 attempts'));
o.rmempty = false;
o = s.option(form.ListValue, 'wait_time', _('Waiting time'),
_('Time to wait (in seconds) before activating airplane mode after ping failure.'));
o.value('1', _('1 seconds'));
o.value('2', _('2 seconds'));
o.value('3', _('3 seconds'));
o.value('4', _('4 seconds'));
o.value('5', _('5 seconds'));
o.rmempty = false;
o = s.option(widgets.NetworkSelect, 'interface', _('Interface'),
_('Name of interface to be restarted.'));
o.nocreate = true;
o.rmempty = false;
o = s.option(form.Flag, 'restart', _('Restart the tunnel'),
_('Enable to automatically restart tunneling tool.'));
o.rmempty = false;
if (data.tunnelService === false) {
o = s.option(form.DummyValue, 'dummy', _('Tunneling tool'),
_('Set tunneling tool to be restarted.'));
o.default = _('No tunneling tools found.');
} else {
o = s.option(form.ListValue, 'tunnel_service', _('Tunneling tool'),
_('Set tunneling tool to be restarted.'));
Object.keys(data.tunnelService).forEach(function(pathID) {
o.value(pathID, data.tunnelService[pathID]);
});
};
return m.render();
}
});