I'm Azat
- ๐ญ Iโm currently working on d66b76b4e1433e63c7d0dbc2d188fea900df98cb
File Explorer for Android devices
License: GNU General Public License v3.0
I got the following error:
from PyQt5.QtWidgets import QApplication
ImportError: No module named PyQt5.QtWidgets
Hi!
I love ADBFileExplorer! There is no progress bar when pushing to or pulling from the device, however. Also, I've noticed that you are using a custom wrapper around adb instead of a library like python-adb
, which has this functionality implemented: you can get transfer progress when pushing and pulling.
So, you could use python-adb
to add a progress bar.
I hope it will help you!
for ages i searched for an application to also rename and delete files on phone using remote pc. thank you!
still some very important features are missing which hurt the use of application like sorting on files, better gui, different views like thumbnails or lists and viewing folders which have special characters like underline( _ ). i have such a folder and have plenty of content in it but applicatons said that the folder is empty.
We all know that transferring files to Windows subsystem for android is a hassle.
There is already a script which pushes files to WSA using adb: EasySideload-WSA
It works but it isn't as near as ADBFileExplorer, you can't browse WSA files or even pull them and it doesn't have a GUI, I think it may be useful to get an idea for how to add WSA support in ADBFileExplorer.
So adding support for WSA in ADBFileExplorer would be very useful.
Thanks.
`# ADB File Explorer
file_selection_action = QAction('Select Multiple Files', self)
file_selection_action.triggered.connect(self.select_multiple_files)
self.file_menu.addAction(file_selection_action)
022 Azat Aldeshov
import sys
from typing import Any
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import Qt, QPoint, QModelIndex, QAbstractListModel, QVariant, QRect, QSize, QEvent, QObject
from PyQt5.QtGui import QPixmap, QColor, QPalette, QMovie, QKeySequence
from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QFileDialog, QStyle, QWidget, QStyledItemDelegate,
QStyleOptionViewItem, QApplication, QListView, QVBoxLayout, QLabel, QSizePolicy, QHBoxLayout, QTextEdit,
QMainWindow
from app.core.configurations import Resources
from app.core.main import Adb
from app.core.managers import Global
from app.data.models import FileType, MessageData, MessageType
from app.data.repositories import FileRepository
from app.gui.explorer.toolbar import ParentButton, UploadTools, PathBar
from app.helpers.tools import AsyncRepositoryWorker, ProgressCallbackHelper, read_string_from_file
class FileHeaderWidget(QWidget):
def init(self, parent=None):
super(FileHeaderWidget, self).init(parent)
self.setLayout(QHBoxLayout(self))
policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
self.file = QLabel('File', self)
self.file.setContentsMargins(45, 0, 0, 0)
policy.setHorizontalStretch(39)
self.file.setSizePolicy(policy)
self.layout().addWidget(self.file)
self.permissions = QLabel('Permissions', self)
self.permissions.setAlignment(Qt.AlignCenter)
policy.setHorizontalStretch(18)
self.permissions.setSizePolicy(policy)
self.layout().addWidget(self.permissions)
self.size = QLabel('Size', self)
self.size.setAlignment(Qt.AlignCenter)
policy.setHorizontalStretch(21)
self.size.setSizePolicy(policy)
self.layout().addWidget(self.size)
self.date = QLabel('Date', self)
self.date.setAlignment(Qt.AlignCenter)
policy.setHorizontalStretch(22)
self.date.setSizePolicy(policy)
self.layout().addWidget(self.date)
self.setStyleSheet("QWidget { background-color: #E5E5E5; font-weight: 500; border: 1px solid #C0C0C0 }")
class FileExplorerToolbar(QWidget):
def init(self, parent=None):
super(FileExplorerToolbar, self).init(parent)
self.setLayout(QHBoxLayout(self))
policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
policy.setHorizontalStretch(1)
self.upload_tools = UploadTools(self)
self.upload_tools.setSizePolicy(policy)
self.layout().addWidget(self.upload_tools)
self.parent_button = ParentButton(self)
self.parent_button.setSizePolicy(policy)
self.layout().addWidget(self.parent_button)
self.path_bar = PathBar(self)
policy.setHorizontalStretch(8)
self.path_bar.setSizePolicy(policy)
self.layout().addWidget(self.path_bar)
class FileItemDelegate(QStyledItemDelegate):
def sizeHint(self, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtCore.QSize:
result = super(FileItemDelegate, self).sizeHint(option, index)
result.setHeight(40)
return result
def setEditorData(self, editor: QWidget, index: QtCore.QModelIndex):
editor.setText(index.model().data(index, Qt.EditRole))
def updateEditorGeometry(self, editor: QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex):
editor.setGeometry(
option.rect.left() + 48, option.rect.top(), int(option.rect.width() / 2.5) - 55, option.rect.height()
)
def setModelData(self, editor: QWidget, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex):
model.setData(index, editor.text(), Qt.EditRole)
@staticmethod
def paint_line(painter: QtGui.QPainter, color: QColor, x, y, w, h):
painter.setPen(color)
painter.drawLine(x, y, w, h)
@staticmethod
def paint_text(painter: QtGui.QPainter, text: str, color: QColor, options, x, y, w, h):
painter.setPen(color)
painter.drawText(QRect(x, y, w, h), options, text)
def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex):
if not index.data():
return super(FileItemDelegate, self).paint(painter, option, index)
self.initStyleOption(option, index)
style = option.widget.style() if option.widget else QApplication.style()
style.drawControl(QStyle.CE_ItemViewItem, option, painter, option.widget)
line_color = QColor("#CCCCCC")
text_color = option.palette.color(QPalette.Normal, QPalette.Text)
top = option.rect.top()
bottom = option.rect.height()
first_start = option.rect.left() + 50
second_start = option.rect.left() + int(option.rect.width() / 2.5)
third_start = option.rect.left() + int(option.rect.width() / 1.75)
fourth_start = option.rect.left() + int(option.rect.width() / 1.25)
end = option.rect.width() + option.rect.left()
self.paint_text(
painter, index.data().name, text_color, option.displayAlignment,
first_start, top, second_start - first_start - 4, bottom
)
self.paint_line(painter, line_color, second_start - 2, top, second_start - 1, bottom)
self.paint_text(
painter, index.data().permissions, text_color, Qt.AlignCenter | option.displayAlignment,
second_start, top, third_start - second_start - 4, bottom
)
self.paint_line(painter, line_color, third_start - 2, top, third_start - 1, bottom)
self.paint_text(
painter, index.data().size, text_color, Qt.AlignCenter | option.displayAlignment,
third_start, top, fourth_start - third_start - 4, bottom
)
self.paint_line(painter, line_color, fourth_start - 2, top, fourth_start - 1, bottom)
self.paint_text(
painter, index.data().date, text_color, Qt.AlignCenter | option.displayAlignment,
fourth_start, top, end - fourth_start, bottom
)
class FileListModel(QAbstractListModel):
def init(self, parent=None):
super().init(parent)
self.items = []
def clear(self):
self.beginResetModel()
self.items.clear()
self.endResetModel()
def populate(self, files: list):
self.beginResetModel()
self.items.clear()
self.items = files
self.endResetModel()
def rowCount(self, parent: QModelIndex = ...) -> int:
return len(self.items)
def icon_path(self, index: QModelIndex = ...):
file_type = self.items[index.row()].type
if file_type == FileType.DIRECTORY:
return Resources.icon_folder
elif file_type == FileType.FILE:
return Resources.icon_file
elif file_type == FileType.LINK:
link_type = self.items[index.row()].link_type
if link_type == FileType.DIRECTORY:
return Resources.icon_link_folder
elif link_type == FileType.FILE:
return Resources.icon_link_file
return Resources.icon_link_file_unknown
return Resources.icon_file_unknown
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def setData(self, index: QModelIndex, value: Any, role: int = ...) -> bool:
if role == Qt.EditRole and value:
data, error = FileRepository.rename(self.items[index.row()], value)
if error:
Global().communicate.notification.emit(
MessageData(
timeout=10000,
title="Rename",
body="<span style='color: red; font-weight: 600'> %s </span>" % error,
)
)
Global.communicate.files__refresh.emit()
return super(FileListModel, self).setData(index, value, role)
def data(self, index: QModelIndex, role: int = ...) -> Any:
if not index.isValid():
return QVariant()
if role == Qt.DisplayRole:
return self.items[index.row()]
elif role == Qt.EditRole:
return self.items[index.row()].name
elif role == Qt.DecorationRole:
return QPixmap(self.icon_path(index)).scaled(32, 32, Qt.KeepAspectRatio)
return QVariant()
class FileExplorerWidget(QWidget):
FILES_WORKER_ID = 300
DOWNLOAD_WORKER_ID = 399
def __init__(self, parent=None):
super(FileExplorerWidget, self).__init__(parent)
self.main_layout = QVBoxLayout(self)
self.toolbar = FileExplorerToolbar(self)
self.main_layout.addWidget(self.toolbar)
self.header = FileHeaderWidget(self)
self.main_layout.addWidget(self.header)
self.list = QListView(self)
self.model = FileListModel(self.list)
self.list.setSpacing(1)
self.list.setModel(self.model)
self.list.installEventFilter(self)
self.list.doubleClicked.connect(self.open)
self.list.setItemDelegate(FileItemDelegate(self.list))
self.list.setContextMenuPolicy(Qt.CustomContextMenu)
self.list.customContextMenuRequested.connect(self.context_menu)
self.list.setStyleSheet(read_string_from_file(Resources.style_file_list))
self.list.setSelectionMode(QListView.SelectionMode.ExtendedSelection)
self.layout().addWidget(self.list)
self.loading = QLabel(self)
self.loading.setAlignment(Qt.AlignCenter)
self.loading_movie = QMovie(Resources.anim_loading, parent=self.loading)
self.loading_movie.setScaledSize(QSize(48, 48))
self.loading.setMovie(self.loading_movie)
self.main_layout.addWidget(self.loading)
self.empty_label = QLabel("Folder is empty", self)
self.empty_label.setAlignment(Qt.AlignCenter)
self.empty_label.setStyleSheet("color: #969696; border: 1px solid #969696")
self.layout().addWidget(self.empty_label)
self.main_layout.setStretch(self.layout().count() - 1, 1)
self.main_layout.setStretch(self.layout().count() - 2, 1)
self.text_view_window = None
self.setLayout(self.main_layout)
Global().communicate.files__refresh.connect(self.update)
@property
def file(self):
if self.list and self.list.currentIndex():
return self.model.items[self.list.currentIndex().row()]
@property
def files(self):
if self.list and len(self.list.selectedIndexes()) > 0:
return map(lambda index: self.model.items[index.row()], self.list.selectedIndexes())
def update(self):
super(FileExplorerWidget, self).update()
worker = AsyncRepositoryWorker(
name="Files",
worker_id=self.FILES_WORKER_ID,
repository_method=FileRepository.files,
response_callback=self._async_response,
arguments=()
)
if Adb.worker().work(worker):
# First Setup loading view
self.model.clear()
self.list.setHidden(True)
self.loading.setHidden(False)
self.empty_label.setHidden(True)
self.loading_movie.start()
# Then start async worker
worker.start()
Global().communicate.path_toolbar__refresh.emit()
def close(self) -> bool:
Global().communicate.files__refresh.disconnect()
return super(FileExplorerWidget, self).close()
def _async_response(self, files: list, error: str):
self.loading_movie.stop()
self.loading.setHidden(True)
if error:
print(error, file=sys.stderr)
if not files:
Global().communicate.notification.emit(
MessageData(
title='Files',
timeout=15000,
body="<span style='color: red; font-weight: 600'> %s </span>" % error
)
)
if not files:
self.empty_label.setHidden(False)
else:
self.list.setHidden(False)
self.model.populate(files)
self.list.setFocus()
def eventFilter(self, obj: 'QObject', event: 'QEvent') -> bool:
if obj == self.list and \
event.type() == QEvent.KeyPress and \
event.matches(QKeySequence.InsertParagraphSeparator) and \
not self.list.isPersistentEditorOpen(self.list.currentIndex()):
self.open(self.list.currentIndex())
return super(FileExplorerWidget, self).eventFilter(obj, event)
def open(self, index: QModelIndex = ...):
if Adb.manager().open(self.model.items[index.row()]):
Global().communicate.files__refresh.emit()
def context_menu(self, pos: QPoint):
menu = QMenu()
menu.addSection("Actions")
action_copy = QAction('Copy to...', self)
action_copy.setDisabled(True)
menu.addAction(action_copy)
action_move = QAction('Move to...', self)
action_move.setDisabled(True)
menu.addAction(action_move)
action_rename = QAction('Rename', self)
action_rename.triggered.connect(self.rename)
menu.addAction(action_rename)
action_open_file = QAction('Open', self)
action_open_file.triggered.connect(self.open_file)
menu.addAction(action_open_file)
action_delete = QAction('Delete', self)
action_delete.triggered.connect(self.delete)
menu.addAction(action_delete)
action_download = QAction('Download', self)
action_download.triggered.connect(self.download_files)
menu.addAction(action_download)
action_download_to = QAction('Download to...', self)
action_download_to.triggered.connect(self.download_to)
menu.addAction(action_download_to)
menu.addSeparator()
action_properties = QAction('Properties', self)
action_properties.triggered.connect(self.file_properties)
menu.addAction(action_properties)
menu.exec(self.mapToGlobal(pos))
@staticmethod
def default_response(data, error):
if error:
Global().communicate.notification.emit(
MessageData(
title='Download error',
timeout=15000,
body="<span style='color: red; font-weight: 600'> %s </span>" % error
)
)
if data:
Global().communicate.notification.emit(
MessageData(
title='Downloaded',
timeout=15000,
body=data
)
)
def rename(self):
self.list.edit(self.list.currentIndex())
def open_file(self):
# QDesktopServices.openUrl(QUrl.fromLocalFile("downloaded_path")) open via external app
if not self.file.isdir:
data, error = FileRepository.open_file(self.file)
if error:
Global().communicate.notification.emit(
MessageData(
title='File',
timeout=15000,
body="<span style='color: red; font-weight: 600'> %s </span>" % error
)
)
else:
self.text_view_window = TextView(self.file.name, data)
self.text_view_window.show()
def delete(self):
file_names = ', '.join(map(lambda f: f.name, self.files))
reply = QMessageBox.critical(
self,
'Delete',
"Do you want to delete '%s'? It cannot be undone!" % file_names,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
for file in self.files:
data, error = FileRepository.delete(file)
if data:
Global().communicate.notification.emit(
MessageData(
timeout=10000,
title="Delete",
body=data,
)
)
if error:
Global().communicate.notification.emit(
MessageData(
timeout=10000,
title="Delete",
body="<span style='color: red; font-weight: 600'> %s </span>" % error,
)
)
Global.communicate.files__refresh.emit()
def download_to(self):
dir_name = QFileDialog.getExistingDirectory(self, 'Download to', '~')
if dir_name:
self.download_files(dir_name)
def download_files(self, destination: str = None):
for file in self.files:
helper = ProgressCallbackHelper()
worker = AsyncRepositoryWorker(
worker_id=self.DOWNLOAD_WORKER_ID,
name="Download",
repository_method=FileRepository.download,
response_callback=self.default_response,
arguments=(
helper.progress_callback.emit, file.path, destination
)
)
if Adb.worker().work(worker):
Global().communicate.notification.emit(
MessageData(
title="Downloading to",
message_type=MessageType.LOADING_MESSAGE,
message_catcher=worker.set_loading_widget
)
)
helper.setup(worker, worker.update_loading_widget)
worker.start()
def file_properties(self):
file, error = FileRepository.file(self.file.path)
file = file if file else self.file
if error:
Global().communicate.notification.emit(
MessageData(
timeout=10000,
title="Opening folder",
body="<span style='color: red; font-weight: 600'> %s </span>" % error,
)
)
info = "<br/><u><b>%s</b></u><br/>" % str(file)
info += "<pre>Name: %s</pre>" % file.name or '-'
info += "<pre>Owner: %s</pre>" % file.owner or '-'
info += "<pre>Group: %s</pre>" % file.group or '-'
info += "<pre>Size: %s</pre>" % file.raw_size or '-'
info += "<pre>Permissions: %s</pre>" % file.permissions or '-'
info += "<pre>Date: %s</pre>" % file.raw_date or '-'
info += "<pre>Type: %s</pre>" % file.type or '-'
if file.type == FileType.LINK:
info += "<pre>Links to: %s</pre>" % file.link or '-'
properties = QMessageBox(self)
properties.setStyleSheet("background-color: #DDDDDD")
properties.setIconPixmap(
QPixmap(self.model.icon_path(self.list.currentIndex())).scaled(128, 128, Qt.KeepAspectRatio)
)
properties.setWindowTitle('Properties')
properties.setInformativeText(info)
properties.exec_()
class TextView(QMainWindow):
def init(self, filename, data):
QMainWindow.init(self)
self.setMinimumSize(QSize(500, 300))
self.setWindowTitle(filename)
self.text_edit = QTextEdit(self)
self.setCentralWidget(self.text_edit)
self.text_edit.insertPlainText(data)
self.text_edit.move(10, 10)
def select_multiple_files(self):
file_names, _ = QFileDialog.getOpenFileNames(self, 'Select Files')
if file_names:
self.handle_selected_files(file_names)
def handle_selected_files(self, file_names: list):
# Implement your logic here
# For now, let's just print the selected files
print("Selected files:", file_names)
`
modified_files.zip
When accessing some root folders with the file manger (e.g. /efs) there are no files shown inside...!? it seems the app has adb without root - only adb (root) shell works...!?
Downloaded and installed Windows version, double-clicked on the .exe
It shows my phone as connected, but with a red icon overlaying it and I cannot explore.
for dev tools
Hi, I just installed your tool and I think the readme should be improved.
src
folder.adb.set_core(Adb.EXTERNAL_TOOL_ADB)
to do that. Also I was under the impression I would not have to install adb-shell
when using adb binary, yet I have to do that. Please write in the readme, that you have to uncomment that line, or build something that autodetects adb binary or let one choose from within the program.Thank you
I would like to suggest that it would be better to transfer to PC (pull) with the option adb pull -a
which preserves file timestamps.
Hello,
thanks for ADB File Explorer - so much faster than the bloddy MTP...
One question: What does ADB_FE do if I "download to..." a folder and the Folder already exists with some files already in it (e.g. from a previous transfer). Will it overwrite the existing files or skip them?
Regards,
Hendrik
Whenever I try deleting or renaming a file or folder with parentheses in its name while it's on my phone, the operation fails with the error "/system/bin/sh: syntax error: unexpected '('". According to a couple of posts I found on Stack Exchange, this is apparently because dash is being used instead of bash. Idk how to fix this, assuming this is truly an issue on my end.
I'm running this on Windows 10 x64, and "python -VV" reports "Python 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)]"
My src/app/settings.json is as follows:
{
"adb_path": "adb",
"adb_core": "external",
"adb_kill_server_at_exit": false,
"preserve_timestamp": true,
"adb_run_as_root": false
}
I've tried running it with "adb_core": "external",
commented out or removed, but it gave me the following error in the console window:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ADB File Explorer v1.3.0
Copyright (C) 2022 Azat Aldeshov
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Platform Windows-10-10.0.19041-SP0
adb server stopped.
Using Python "adb-shell" version 0.4.3
ERROR:root:Unexpected error=LIBUSB_ERROR_NOT_SUPPORTED [-12], type(error)=<class 'usb1.USBErrorNotSupported'>
Traceback (most recent call last):
File "C:\Other Programs\ADB File Explorer\src\app\data\repositories\python_adb.py", line 226, in devices
device_id = device.getSerialNumber()
File "C:\Other Programs\ADB File Explorer\venv\lib\site-packages\usb1\__init__.py", line 2019, in getSerialNumber
return self.open().getSerialNumber()
File "C:\Other Programs\ADB File Explorer\venv\lib\site-packages\usb1\__init__.py", line 2055, in open
mayRaiseUSBError(libusb1.libusb_open(self.device_p, byref(handle)))
File "C:\Other Programs\ADB File Explorer\venv\lib\site-packages\usb1\__init__.py", line 127, in mayRaiseUSBError
__raiseUSBError(value)
File "C:\Other Programs\ADB File Explorer\venv\lib\site-packages\usb1\__init__.py", line 119, in raiseUSBError
raise __STATUS_TO_EXCEPTION_DICT.get(value, __USBError)(value)
usb1.USBErrorNotSupported: LIBUSB_ERROR_NOT_SUPPORTED [-12]
When attempting to download a lot of files (e.g. from Camera
), the app crashes with:
(process:64670): GLib-ERROR **: 01:46:40.739: Creating pipes for GWakeup: Too many open files
./run.sh: line 8: 64670 Trace/breakpoint trap (core dumped) python ./src/app
I was using adb-shell
because external adb
was unable to read the device at all:
There were no other adb processes running and the app had started fine (except for lacking venv):
$ ./run.sh
./run.sh: line 1: ./venv/bin/activate: No such file or directory
Android Debug Bridge version 1.0.41
Version 28.0.2-debian
Installed as /usr/lib/android-sdk/platform-tools/adb
* daemon not running; starting now at tcp:5037
* daemon started successfully
ADB server running...
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.