├── .gitignore ├── .idea ├── ADBFileExplorer.iml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── .pylintrc ├── LICENSE ├── README.md ├── requirements.txt ├── run.bat ├── run.sh └── src ├── __init__.py ├── app ├── __init__.py ├── __main__.py ├── core │ ├── __init__.py │ ├── configurations.py │ ├── main.py │ └── managers.py ├── data │ ├── __init__.py │ ├── models.py │ └── repositories │ │ ├── __init__.py │ │ ├── android_adb.py │ │ └── python_adb.py ├── gui │ ├── __init__.py │ ├── explorer │ │ ├── __init__.py │ │ ├── devices.py │ │ ├── files.py │ │ └── toolbar.py │ ├── help.py │ ├── notification.py │ └── window.py ├── helpers │ ├── __init__.py │ ├── converters.py │ └── tools.py ├── services │ ├── __init__.py │ └── adb.py ├── settings.json └── test.py └── resources ├── __init__.py ├── anim ├── __init__.py └── loading.gif ├── icons ├── __init__.py ├── arrow.svg ├── close.svg ├── files │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── files_upload.svg │ │ ├── folder_create.svg │ │ └── folder_upload.svg │ ├── file.svg │ ├── file_unknown.svg │ ├── folder.svg │ ├── link_file.svg │ ├── link_file_unknown.svg │ └── link_folder.svg ├── link.svg ├── logo.svg ├── no_link.svg ├── phone.svg ├── phone_unknown.svg ├── plus.svg └── up.svg └── styles ├── __init__.py ├── device-list.qss ├── file-list.qss ├── notification-button.qss └── window.qss /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.idea/ADBFileExplorer.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 27 | 37 | 38 | 44 | 45 | 46 | 48 | 49 | 50 | 53 | { 54 | "keyToString": { 55 | "RunOnceActivity.OpenProjectViewOnStart": "true", 56 | "RunOnceActivity.ShowReadmeOnStart": "true", 57 | "WebServerToolWindowFactoryState": "false", 58 | "last_opened_file_path": "C:/Users/Azat/ADBFileExplorer/run.bat", 59 | "node.js.detected.package.eslint": "true", 60 | "node.js.detected.package.tslint": "true", 61 | "node.js.selected.package.eslint": "(autodetect)", 62 | "node.js.selected.package.tslint": "(autodetect)", 63 | "nodejs_package_manager_path": "npm", 64 | "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PythonContentEntriesConfigurable", 65 | "vue.rearranger.settings.migration": "true" 66 | } 67 | } 68 | 69 | 70 | 71 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 1665348827311 102 | 110 | 111 | 112 | 113 | 115 | 116 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | extension-pkg-whitelist=PyQt5 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | 3 | ![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) 4 | [![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/PyCQA/pylint) 5 | 6 | Simple File Explorer for adb devices. Uses python library [`adb-shell`](https://github.com/JeffLIrion/adb_shell) or command-line tool [`adb`](https://developer.android.com/studio/command-line/adb). 7 | 8 | Features: 9 | 10 | * List of adb devices 11 | * Connect via IP (TCP) 12 | * Listing / Pulling / Pushing files 13 | * Renaming and Deleting files 14 | 15 | ## Screenshots 16 | 17 | Devices & Notifications 18 | 19 | Devices & Notifications 20 | 21 | Files 22 | 23 | Files 24 | 25 | ## Requirements 26 | 27 | * `Python3` (below version 3.8 not tested) 28 | ```shell 29 | sudo apt-get install python3-pip python3-pyqt5 # For Linux Ubuntu 30 | pip install PyQt5 libusb1 adb-shell 31 | ``` 32 | * `adb` (binary) should exist in project root folder or in `$PATH` 33 | 34 | ## Launch 35 | 36 | 1. Clone the repo 37 | 2. cd ADBFileExplorer 38 | 3. Edit [settings.json](src%2Fapp%2Fsettings.json) from the project root if necessary. `src/app/settings.json` 39 | 40 | ```json5 41 | { 42 | "adb_path": "adb", 43 | // "adb_core": "external", 44 | "adb_kill_server_at_exit": false, 45 | "preserve_timestamp": true, 46 | "adb_run_as_root": false 47 | } 48 | ``` 49 | 50 | + `adb_path` - Full adb path or just 'adb' if the executable is in `$PATH` 51 | + `adb_core` - Set to 'external' to use external `adb` executable, otherwise the app will use `adb-shell` 52 | 53 | 54 | ```shell 55 | # First install python-venv in root folder. It should be like ADBFileExplorer/venv 56 | pip install -r requirements.txt 57 | run.bat # To start application on Windows 58 | bash run.sh # To start application on Linux... 59 | ``` 60 | 61 | ## Attention 62 | 63 | Application uses by default `adb-shell`. There may be problems with listing, pushing, or pulling files using `adb-shell`. 64 | For a better experience, try adding `"adb_core": "external"` to `settings.json`. 65 | 66 | ## License 67 | 68 | ```text 69 | ADB File Explorer [python-app] 70 | Copyright (C) 2022 Azat Aldeshov 71 | 72 | This program is free software: you can redistribute it and/or modify 73 | it under the terms of the GNU General Public License as published by 74 | the Free Software Foundation, either version 3 of the License, or 75 | (at your option) any later version. 76 | 77 | This program is distributed in the hope that it will be useful, 78 | but WITHOUT ANY WARRANTY; without even the implied warranty of 79 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 80 | GNU General Public License for more details. 81 | 82 | You should have received a copy of the GNU General Public License 83 | along with this program. If not, see . 84 | ``` 85 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/requirements.txt -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | call .\venv\Scripts\activate.bat 2 | set PYTHONUNBUFFERED=1 3 | set PYTHONPATH=%cd%\src\app;%cd%\src;%cd% 4 | python.exe .\src\app 5 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | source ./venv/bin/activate 2 | app=$(pwd)/src/app 3 | src=$(pwd)/src 4 | root=$(pwd) 5 | path=$app:$src:$root 6 | export PYTHONUNBUFFERED=1 7 | export PYTHONPATH=$path:$PYTHONPATH 8 | python ./src/app 9 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/__init__.py -------------------------------------------------------------------------------- /src/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/app/__init__.py -------------------------------------------------------------------------------- /src/app/__main__.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer (python) 2 | # Copyright (C) 2022 Azat Aldeshov 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import sys 17 | 18 | from PyQt5.QtWidgets import QApplication 19 | 20 | from core.configurations import Resources, Application 21 | from core.main import Adb 22 | from gui.window import MainWindow 23 | from helpers.tools import read_string_from_file 24 | 25 | if __name__ == '__main__': 26 | Application() 27 | Adb.start() 28 | app = QApplication(sys.argv) 29 | 30 | window = MainWindow() 31 | window.setStyleSheet(read_string_from_file(Resources.style_window)) 32 | window.show() 33 | 34 | sys.exit(app.exec_()) 35 | -------------------------------------------------------------------------------- /src/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/app/core/__init__.py -------------------------------------------------------------------------------- /src/app/core/configurations.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import os 4 | import platform 5 | 6 | from PyQt5.QtCore import QFile, QIODevice 7 | from pkg_resources import resource_filename 8 | 9 | from app.data.models import Device 10 | from app.helpers.tools import Singleton, json_to_dict 11 | 12 | 13 | class Application(metaclass=Singleton): 14 | __version__ = '1.3.0' 15 | __author__ = 'Azat Aldeshov' 16 | 17 | def __init__(self): 18 | print('─────────────────────────────────') 19 | print('ADB File Explorer v%s' % self.__version__) 20 | print('Copyright (C) 2022 %s' % self.__author__) 21 | print('─────────────────────────────────') 22 | print('Platform %s' % platform.platform()) 23 | 24 | 25 | class Settings(metaclass=Singleton): 26 | downloads_path = os.path.join(os.path.expanduser('~'), 'Downloads') 27 | filename = resource_filename('app', 'settings.json') 28 | data = None 29 | 30 | @classmethod 31 | def initialize(cls): 32 | if cls.data is not None: 33 | return True 34 | 35 | if not os.path.exists(cls.filename): 36 | print('Settings file not found! Creating one: %s' % cls.filename) 37 | file = QFile(cls.filename) 38 | file.open(QIODevice.WriteOnly) 39 | file.write(b'{}') 40 | file.close() 41 | 42 | cls.data = json_to_dict(cls.filename) 43 | 44 | @classmethod 45 | def adb_kill_server_at_exit(cls): 46 | cls.initialize() 47 | if 'adb_kill_server_at_exit' in cls.data: 48 | return bool(cls.data['adb_kill_server_at_exit']) 49 | return None 50 | 51 | @classmethod 52 | def adb_path(cls): 53 | cls.initialize() 54 | if 'adb_path' in cls.data: 55 | return str(cls.data['adb_path']) 56 | return 'adb' 57 | 58 | @classmethod 59 | def adb_core(cls): 60 | cls.initialize() 61 | if 'adb_core' in cls.data and cls.data['adb_core'] == 'external': 62 | return 'external' 63 | return 'python' 64 | 65 | @classmethod 66 | def adb_run_as_root(cls): 67 | cls.initialize() 68 | return 'adb_run_as_root' in cls.data and cls.data['adb_run_as_root'] is True 69 | 70 | @classmethod 71 | def preserve_timestamp(cls): 72 | cls.initialize() 73 | return 'preserve_timestamp' in cls.data and cls.data['preserve_timestamp'] is True 74 | 75 | @classmethod 76 | def device_downloads_path(cls, device: Device) -> str: 77 | if not os.path.isdir(Settings.downloads_path): 78 | os.mkdir(Settings.downloads_path) 79 | if device: 80 | downloads_path = os.path.join(Settings.downloads_path, device.name) 81 | if not os.path.isdir(downloads_path): 82 | os.mkdir(downloads_path) 83 | return downloads_path 84 | return Settings.downloads_path 85 | 86 | 87 | class Resources: 88 | __metaclass__ = Singleton 89 | 90 | style_window = resource_filename('resources.styles', 'window.qss') 91 | style_file_list = resource_filename('resources.styles', 'file-list.qss') 92 | style_device_list = resource_filename('resources.styles', 'device-list.qss') 93 | style_notification_button = resource_filename('resources.styles', 'notification-button.qss') 94 | 95 | icon_logo = resource_filename('resources.icons', 'logo.svg') 96 | icon_link = resource_filename('resources.icons', 'link.svg') 97 | icon_no_link = resource_filename('resources.icons', 'no_link.svg') 98 | icon_close = resource_filename('resources.icons', 'close.svg') 99 | icon_phone = resource_filename('resources.icons', 'phone.svg') 100 | icon_phone_unknown = resource_filename('resources.icons', 'phone_unknown.svg') 101 | icon_plus = resource_filename('resources.icons', 'plus.svg') 102 | icon_up = resource_filename('resources.icons', 'up.svg') 103 | icon_arrow = resource_filename('resources.icons', 'arrow.svg') 104 | icon_file = resource_filename('resources.icons.files', 'file.svg') 105 | icon_folder = resource_filename('resources.icons.files', 'folder.svg') 106 | icon_file_unknown = resource_filename('resources.icons.files', 'file_unknown.svg') 107 | icon_link_file = resource_filename('resources.icons.files', 'link_file.svg') 108 | icon_link_folder = resource_filename('resources.icons.files', 'link_folder.svg') 109 | icon_link_file_unknown = resource_filename('resources.icons.files', 'link_file_unknown.svg') 110 | icon_files_upload = resource_filename('resources.icons.files.actions', 'files_upload.svg') 111 | icon_folder_upload = resource_filename('resources.icons.files.actions', 'folder_upload.svg') 112 | icon_folder_create = resource_filename('resources.icons.files.actions', 'folder_create.svg') 113 | 114 | anim_loading = resource_filename('resources.anim', 'loading.gif') 115 | -------------------------------------------------------------------------------- /src/app/core/main.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import sys 4 | from typing import Union 5 | 6 | import adb_shell 7 | 8 | from app.core.configurations import Settings 9 | from app.core.managers import PythonADBManager, ADBManager, WorkersManager 10 | from app.helpers.tools import Singleton 11 | from app.services import adb 12 | 13 | 14 | class Adb(metaclass=Singleton): 15 | PYTHON_ADB_SHELL = 'python' # Python library `adb-shell` 16 | EXTERNAL_TOOL_ADB = 'external' # Command-line tool `adb` 17 | 18 | core = Settings.adb_core() 19 | 20 | @classmethod 21 | def start(cls): 22 | if cls.core == cls.PYTHON_ADB_SHELL: 23 | if adb.kill_server().IsSuccessful: 24 | print("adb server stopped.") 25 | 26 | print('Using Python "adb-shell" version %s' % adb_shell.__version__) 27 | 28 | elif cls.core == cls.EXTERNAL_TOOL_ADB and adb.validate(): 29 | print(adb.version().OutputData) 30 | 31 | adb_server = adb.start_server() 32 | if adb_server.ErrorData: 33 | print(adb_server.ErrorData, file=sys.stderr) 34 | 35 | print(adb_server.OutputData or 'ADB server running...') 36 | 37 | @classmethod 38 | def stop(cls): 39 | if cls.core == cls.PYTHON_ADB_SHELL: 40 | # Closing device connection 41 | if PythonADBManager.device and PythonADBManager.device.available: 42 | name = PythonADBManager.get_device().name if PythonADBManager.get_device() else "Unknown" 43 | print('Connection to device %s closed' % name) 44 | PythonADBManager.device.close() 45 | return True 46 | 47 | elif cls.core == cls.EXTERNAL_TOOL_ADB: 48 | if adb.kill_server().IsSuccessful: 49 | print("ADB Server stopped") 50 | return True 51 | 52 | @classmethod 53 | def manager(cls) -> Union[ADBManager, PythonADBManager]: 54 | if cls.core == cls.PYTHON_ADB_SHELL: 55 | return PythonADBManager() 56 | elif cls.core == cls.EXTERNAL_TOOL_ADB: 57 | return ADBManager() 58 | 59 | @classmethod 60 | def worker(cls) -> WorkersManager: 61 | return WorkersManager() 62 | -------------------------------------------------------------------------------- /src/app/core/managers.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import logging 4 | import posixpath 5 | 6 | from PyQt5.QtCore import QObject 7 | from adb_shell.adb_device import AdbDeviceTcp, AdbDeviceUsb 8 | 9 | from app.data.models import File, Device 10 | from app.helpers.tools import Communicate, Singleton, get_python_rsa_keys_signer, AsyncRepositoryWorker 11 | 12 | 13 | class ADBManager: 14 | __metaclass__ = Singleton 15 | 16 | __path = [] 17 | __device = None 18 | 19 | @classmethod 20 | def path(cls) -> str: 21 | return posixpath.join('/', *cls.__path) + '/' if cls.__path else '/' 22 | 23 | @classmethod 24 | def open(cls, file: File) -> bool: 25 | if not cls.__device: 26 | return False 27 | 28 | if file.isdir and file.name: 29 | cls.__path.append(file.name) 30 | return True 31 | return False 32 | 33 | @classmethod 34 | def go(cls, file: File) -> bool: 35 | if file.isdir and file.location: 36 | cls.__path.clear() 37 | for name in file.path.split('/'): 38 | cls.__path.append(name) if name else '' 39 | return True 40 | return False 41 | 42 | @classmethod 43 | def up(cls) -> bool: 44 | if cls.__path: 45 | cls.__path.pop() 46 | return True 47 | return False 48 | 49 | @classmethod 50 | def get_device(cls) -> Device: 51 | return cls.__device 52 | 53 | @classmethod 54 | def set_device(cls, device: Device) -> bool: 55 | if device: 56 | cls.clear() 57 | cls.__device = device 58 | return True 59 | 60 | @classmethod 61 | def clear(cls): 62 | cls.__device = None 63 | cls.__path.clear() 64 | 65 | @staticmethod 66 | def clear_path(path: str) -> str: 67 | return posixpath.normpath(path) 68 | 69 | 70 | class PythonADBManager(ADBManager): 71 | signer = get_python_rsa_keys_signer() 72 | device = None 73 | 74 | @classmethod 75 | def connect(cls, device_id: str) -> str: 76 | if device_id.__contains__('.'): 77 | port = 5555 78 | host = device_id 79 | if device_id.__contains__(':'): 80 | host = device_id.split(':')[0] 81 | port = device_id.split(':')[1] 82 | cls.device = AdbDeviceTcp(host=host, port=port, default_transport_timeout_s=10.) 83 | cls.device.connect(rsa_keys=[cls.signer], auth_timeout_s=1.) 84 | return '%s:%s' % (host, port) 85 | 86 | cls.device = AdbDeviceUsb(serial=device_id, default_transport_timeout_s=3.) 87 | cls.device.connect(rsa_keys=[cls.signer], auth_timeout_s=30.) 88 | return device_id 89 | 90 | @classmethod 91 | def set_device(cls, device: Device) -> bool: 92 | super(PythonADBManager, cls).set_device(device) 93 | if not cls.device or not cls.device.available: 94 | try: 95 | cls.connect(device.id) 96 | return True 97 | except BaseException as error: 98 | logging.error(error) 99 | return False 100 | 101 | 102 | class WorkersManager: 103 | """ 104 | Async Workers Manager 105 | Contains a list of workers 106 | """ 107 | __metaclass__ = Singleton 108 | instance = QObject() 109 | workers = [] 110 | 111 | @classmethod 112 | def work(cls, worker: AsyncRepositoryWorker) -> bool: 113 | for _worker in cls.workers: 114 | if _worker == worker or _worker.id == worker.id: 115 | cls.workers.remove(_worker) 116 | del _worker 117 | break 118 | worker.setParent(cls.instance) 119 | cls.workers.append(worker) 120 | return True 121 | 122 | @classmethod 123 | def check(cls, worker_id: int) -> bool: 124 | for worker in cls.workers: 125 | if worker.id == worker_id: 126 | if worker.closed: 127 | return True 128 | return False 129 | return False 130 | 131 | 132 | class Global: 133 | __metaclass__ = Singleton 134 | communicate = Communicate() 135 | -------------------------------------------------------------------------------- /src/app/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/app/data/__init__.py -------------------------------------------------------------------------------- /src/app/data/models.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import datetime 4 | import posixpath 5 | 6 | size_types = ( 7 | ('BYTE', 'B'), 8 | ('KILOBYTE', 'KB'), 9 | ('MEGABYTE', 'MB'), 10 | ('GIGABYTE', 'GB'), 11 | ('TERABYTE', 'TB') 12 | ) 13 | 14 | file_types = ( 15 | ('-', 'File'), 16 | ('d', 'Directory'), 17 | ('l', 'Link'), 18 | ('c', 'Character'), 19 | ('b', 'Block'), 20 | ('s', 'Socket'), 21 | ('p', 'FIFO') 22 | ) 23 | 24 | months = ( 25 | ('NONE', 'None', 'None'), 26 | ('JANUARY', 'Jan.', 'January'), 27 | ('FEBRUARY', 'Feb.', 'February'), 28 | ('MARCH', 'Mar.', 'March'), 29 | ('APRIL', 'Apr.', 'April'), 30 | ('MAY', 'May', 'May'), 31 | ('JUNE', 'Jun.', 'June'), 32 | ('JULY', 'Jul.', 'July'), 33 | ('AUGUST', 'Aug.', 'August'), 34 | ('SEPTEMBER', 'Sep.', 'September'), 35 | ('OCTOBER', 'Oct.', 'October'), 36 | ('NOVEMBER', 'Nov.', 'November'), 37 | ('DECEMBER', 'Dec.', 'December'), 38 | ) 39 | 40 | days = ( 41 | ('MONDAY', 'Monday'), 42 | ('TUESDAY', 'Tuesday'), 43 | ('WEDNESDAY', 'Wednesday'), 44 | ('THURSDAY', 'Thursday'), 45 | ('FRIDAY', 'Friday'), 46 | ('SATURDAY', 'Saturday'), 47 | ('SUNDAY', 'Sunday'), 48 | ) 49 | 50 | 51 | class File: 52 | def __init__(self, **kwargs): 53 | self.name = str(kwargs.get("name")) 54 | self.owner = str(kwargs.get("owner")) 55 | self.group = str(kwargs.get("group")) 56 | self.other = str(kwargs.get("other")) 57 | self.path = str(kwargs.get("path")) 58 | self.link = str(kwargs.get("link")) 59 | self.link_type = str(kwargs.get("link_type")) 60 | self.file_type = str(kwargs.get("file_type")) 61 | self.permissions = str(kwargs.get("permissions")) 62 | 63 | self.raw_size = kwargs.get("size") or 0 64 | self.raw_date = kwargs.get("date_time") 65 | 66 | def __str__(self): 67 | return "%s '%s' (at '%s')" % (self.type, self.name, self.location) 68 | 69 | @property 70 | def size(self): 71 | if not self.raw_size: 72 | return '' 73 | count = 0 74 | result = self.raw_size 75 | while result >= 1024 and count < len(size_types): 76 | result /= 1024 77 | count += 1 78 | 79 | return '%s %s' % (round(result, 2), size_types[count][1]) 80 | 81 | @property 82 | def date(self): 83 | if not self.raw_date: 84 | return None 85 | 86 | created = self.raw_date 87 | now = datetime.datetime.now() 88 | if created.year < now.year: 89 | return '%s %s %s' % (created.day, months[created.month][1], created.year) 90 | elif created.month < now.month: 91 | return '%s %s' % (created.day, months[created.month][1]) 92 | elif created.day + 7 < now.day: 93 | return '%s %s' % (created.day, months[created.month][2]) 94 | elif created.day + 1 < now.day: 95 | return '%s at %s' % (days[created.weekday()][1], str(created.time())[:-3]) 96 | elif created.day < now.day: 97 | return "Yesterday at %s" % str(created.time())[:-3] 98 | else: 99 | return str(created.time())[:-3] 100 | 101 | @property 102 | def location(self): 103 | return posixpath.dirname(self.path or '') + '/' 104 | 105 | @property 106 | def type(self): 107 | for ft in file_types: 108 | if self.permissions and self.permissions[0] == ft[0]: 109 | return ft[1] 110 | return 'Unknown' 111 | 112 | @property 113 | def isdir(self): 114 | return self.type == FileType.DIRECTORY or self.link_type == FileType.DIRECTORY 115 | 116 | 117 | class FileType: 118 | FILE = 'File' 119 | DIRECTORY = 'Directory' 120 | LINK = 'Link' 121 | CHARACTER = 'Character' 122 | BLOCK = 'Block' 123 | SOCKET = 'Socket' 124 | FIFO = 'FIFO' 125 | UNKNOWN = 'Unknown' 126 | 127 | 128 | class Device: 129 | def __init__(self, **kwargs): 130 | self.id = kwargs.get("id") 131 | self.name = kwargs.get("name") 132 | self.type = kwargs.get("type") 133 | 134 | 135 | class DeviceType: 136 | DEVICE = 'device' 137 | UNKNOWN = 'Unknown' 138 | 139 | 140 | class MessageData: 141 | def __init__(self, **kwargs): 142 | self.timeout = kwargs.get("timeout") or 0 143 | self.title = kwargs.get("title") or "Message" 144 | self.body = kwargs.get("body") 145 | self.message_type = kwargs.get("message_type") or MessageType.MESSAGE 146 | self.message_catcher = kwargs.get("message_catcher") or None 147 | 148 | 149 | class MessageType: 150 | MESSAGE = 1 151 | LOADING_MESSAGE = 2 152 | -------------------------------------------------------------------------------- /src/app/data/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from typing import List 4 | 5 | from app.core.main import Adb 6 | from app.data.models import Device, File 7 | from app.data.repositories import android_adb, python_adb 8 | 9 | 10 | class FileRepository: 11 | @classmethod 12 | def file(cls, path: str) -> (File, str): 13 | if Adb.core == Adb.PYTHON_ADB_SHELL: 14 | return python_adb.FileRepository.file(path=path) 15 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 16 | return android_adb.FileRepository.file(path=path) 17 | 18 | @classmethod 19 | def files(cls) -> (List[File], str): 20 | if Adb.core == Adb.PYTHON_ADB_SHELL: 21 | return python_adb.FileRepository.files() 22 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 23 | return android_adb.FileRepository.files() 24 | 25 | @classmethod 26 | def rename(cls, file: File, name: str) -> (str, str): 27 | if Adb.core == Adb.PYTHON_ADB_SHELL: 28 | return python_adb.FileRepository.rename(file, name) 29 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 30 | return android_adb.FileRepository.rename(file, name) 31 | 32 | @classmethod 33 | def open_file(cls, file: File) -> (str, str): 34 | if Adb.core == Adb.PYTHON_ADB_SHELL: 35 | return python_adb.FileRepository.open_file(file) 36 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 37 | return android_adb.FileRepository.open_file(file) 38 | 39 | @classmethod 40 | def delete(cls, file: File) -> (str, str): 41 | if Adb.core == Adb.PYTHON_ADB_SHELL: 42 | return python_adb.FileRepository.delete(file) 43 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 44 | return android_adb.FileRepository.delete(file) 45 | 46 | @classmethod 47 | def download(cls, progress_callback: callable, source: str, destination: str) -> (str, str): 48 | if Adb.core == Adb.PYTHON_ADB_SHELL: 49 | return python_adb.FileRepository.download( 50 | progress_callback=progress_callback, 51 | source=source, 52 | destination=destination 53 | ) 54 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 55 | return android_adb.FileRepository.download( 56 | progress_callback=progress_callback, 57 | source=source, 58 | destination=destination 59 | ) 60 | 61 | @classmethod 62 | def new_folder(cls, name) -> (str, str): 63 | if Adb.core == Adb.PYTHON_ADB_SHELL: 64 | return python_adb.FileRepository.new_folder(name=name) 65 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 66 | return android_adb.FileRepository.new_folder(name=name) 67 | 68 | @classmethod 69 | def upload(cls, progress_callback: callable, source: str) -> (str, str): 70 | if Adb.core == Adb.PYTHON_ADB_SHELL: 71 | return python_adb.FileRepository.upload( 72 | progress_callback=progress_callback, 73 | source=source 74 | ) 75 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 76 | return android_adb.FileRepository.upload( 77 | progress_callback=progress_callback, 78 | source=source 79 | ) 80 | 81 | 82 | class DeviceRepository: 83 | @classmethod 84 | def devices(cls) -> (List[Device], str): 85 | if Adb.core == Adb.PYTHON_ADB_SHELL: 86 | return python_adb.DeviceRepository.devices() 87 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 88 | return android_adb.DeviceRepository.devices() 89 | 90 | @classmethod 91 | def connect(cls, device_id) -> (str, str): 92 | if Adb.core == Adb.PYTHON_ADB_SHELL: 93 | return python_adb.DeviceRepository.connect(device_id=device_id) 94 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 95 | return android_adb.DeviceRepository.connect(device_id=device_id) 96 | 97 | @classmethod 98 | def disconnect(cls) -> (str, str): 99 | if Adb.core == Adb.PYTHON_ADB_SHELL: 100 | return python_adb.DeviceRepository.disconnect() 101 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 102 | return android_adb.DeviceRepository.disconnect() 103 | -------------------------------------------------------------------------------- /src/app/data/repositories/android_adb.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from typing import List 4 | 5 | from app.core.configurations import Settings 6 | from app.core.managers import ADBManager 7 | from app.data.models import FileType, Device, File 8 | from app.helpers.converters import convert_to_devices, convert_to_file, convert_to_file_list_a 9 | from app.services import adb 10 | 11 | 12 | class FileRepository: 13 | @classmethod 14 | def file(cls, path: str) -> (File, str): 15 | if not ADBManager.get_device(): 16 | return None, "No device selected!" 17 | 18 | path = ADBManager.clear_path(path) 19 | args = adb.ShellCommand.LS_LIST_DIRS + [path.replace(' ', r'\ ')] 20 | response = adb.shell(ADBManager.get_device().id, args) 21 | if not response.IsSuccessful: 22 | return None, response.ErrorData or response.OutputData 23 | 24 | file = convert_to_file(response.OutputData.strip()) 25 | if not file: 26 | return None, "Unexpected string:\n%s" % response.OutputData 27 | 28 | if file.type == FileType.LINK: 29 | args = adb.ShellCommand.LS_LIST_DIRS + [path.replace(' ', r'\ ') + '/'] 30 | response = adb.shell(ADBManager.get_device().id, args) 31 | file.link_type = FileType.UNKNOWN 32 | if response.OutputData and response.OutputData.startswith('d'): 33 | file.link_type = FileType.DIRECTORY 34 | elif response.OutputData and response.OutputData.__contains__('Not a'): 35 | file.link_type = FileType.FILE 36 | file.path = path 37 | return file, response.ErrorData 38 | 39 | @classmethod 40 | def files(cls) -> (List[File], str): 41 | if not ADBManager.get_device(): 42 | return None, "No device selected!" 43 | 44 | path = ADBManager.path() 45 | args = adb.ShellCommand.LS_ALL_LIST + [path.replace(' ', r'\ ')] 46 | response = adb.shell(ADBManager.get_device().id, args) 47 | if not response.IsSuccessful and response.ExitCode != 1: 48 | return [], response.ErrorData or response.OutputData 49 | 50 | if not response.OutputData: 51 | return [], response.ErrorData 52 | 53 | args = adb.ShellCommand.LS_ALL_DIRS + [path.replace(' ', r'\ ') + "*/"] 54 | response_dirs = adb.shell(ADBManager.get_device().id, args) 55 | if not response_dirs.IsSuccessful and response_dirs.ExitCode != 1: 56 | return [], response_dirs.ErrorData or response_dirs.OutputData 57 | 58 | dirs = response_dirs.OutputData.split() if response_dirs.OutputData else [] 59 | files = convert_to_file_list_a(response.OutputData, dirs=dirs, path=path) 60 | return files, response.ErrorData 61 | 62 | @classmethod 63 | def rename(cls, file: File, name) -> (str, str): 64 | if name.__contains__('/') or name.__contains__('\\'): 65 | return None, "Invalid name" 66 | args = [adb.ShellCommand.MV, file.path.replace(' ', r'\ '), (file.location + name).replace(' ', r'\ ')] 67 | response = adb.shell(ADBManager.get_device().id, args) 68 | return None, response.ErrorData or response.OutputData 69 | 70 | @classmethod 71 | def open_file(cls, file: File) -> (str, str): 72 | args = [adb.ShellCommand.CAT, file.path.replace(' ', r'\ ')] 73 | if file.isdir: 74 | return None, "Can't open. %s is a directory" % file.path 75 | response = adb.shell(ADBManager.get_device().id, args) 76 | if not response.IsSuccessful: 77 | return None, response.ErrorData or response.OutputData 78 | return response.OutputData, response.ErrorData 79 | 80 | @classmethod 81 | def delete(cls, file: File) -> (str, str): 82 | args = [adb.ShellCommand.RM, file.path.replace(' ', r'\ ')] 83 | if file.isdir: 84 | args = adb.ShellCommand.RM_DIR_FORCE + [file.path.replace(' ', r'\ ')] 85 | response = adb.shell(ADBManager.get_device().id, args) 86 | if not response.IsSuccessful or response.OutputData: 87 | return None, response.ErrorData or response.OutputData 88 | return "%s '%s' has been deleted" % ('Folder' if file.isdir else 'File', file.path), None 89 | 90 | class UpDownHelper: 91 | def __init__(self, callback: callable): 92 | self.messages = [] 93 | self.callback = callback 94 | 95 | def call(self, data: str): 96 | if data.startswith('['): 97 | progress = data[1:4].strip() 98 | if progress.isdigit(): 99 | self.callback(data[7:], int(progress)) 100 | elif data: 101 | self.messages.append(data) 102 | 103 | @classmethod 104 | def download(cls, progress_callback: callable, source: str, destination: str) -> (str, str): 105 | if not destination: 106 | destination = Settings.device_downloads_path(ADBManager.get_device()) 107 | if ADBManager.get_device() and source and destination: 108 | helper = cls.UpDownHelper(progress_callback) 109 | response = adb.pull(ADBManager.get_device().id, source, destination, helper.call) 110 | if not response.IsSuccessful: 111 | return None, response.ErrorData or "\n".join(helper.messages) 112 | 113 | return "\n".join(helper.messages), response.ErrorData 114 | return None, None 115 | 116 | @classmethod 117 | def new_folder(cls, name) -> (str, str): 118 | if not ADBManager.get_device(): 119 | return None, "No device selected!" 120 | 121 | args = [adb.ShellCommand.MKDIR, (ADBManager.path() + name).replace(' ', r"\ ")] 122 | response = adb.shell(ADBManager.get_device().id, args) 123 | if not response.IsSuccessful: 124 | return None, response.ErrorData or response.OutputData 125 | return response.OutputData, response.ErrorData 126 | 127 | @classmethod 128 | def upload(cls, progress_callback: callable, source: str) -> (str, str): 129 | if ADBManager.get_device() and ADBManager.path() and source: 130 | helper = cls.UpDownHelper(progress_callback) 131 | response = adb.push(ADBManager.get_device().id, source, ADBManager.path(), helper.call) 132 | if not response.IsSuccessful: 133 | return None, response.ErrorData or "\n".join(helper.messages) 134 | 135 | return "\n".join(helper.messages), response.ErrorData 136 | return None, None 137 | 138 | 139 | class DeviceRepository: 140 | @classmethod 141 | def devices(cls) -> (List[Device], str): 142 | response = adb.devices() 143 | if not response.IsSuccessful: 144 | return [], response.ErrorData or response.OutputData 145 | 146 | devices = convert_to_devices(response.OutputData) 147 | return devices, response.ErrorData 148 | 149 | @classmethod 150 | def connect(cls, device_id) -> (str, str): 151 | if not device_id: 152 | return None, None 153 | 154 | response = adb.connect(device_id) 155 | if not response.IsSuccessful: 156 | return None, response.ErrorData or response.OutputData 157 | return response.OutputData, response.ErrorData 158 | 159 | @classmethod 160 | def disconnect(cls) -> (str, str): 161 | response = adb.disconnect() 162 | if not response.IsSuccessful: 163 | return None, response.ErrorData or response.OutputData 164 | 165 | return response.OutputData, response.ErrorData 166 | -------------------------------------------------------------------------------- /src/app/data/repositories/python_adb.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import datetime 4 | import logging 5 | import os 6 | import shlex 7 | from typing import List 8 | 9 | from usb1 import USBContext 10 | 11 | from app.core.configurations import Settings 12 | from app.core.managers import PythonADBManager 13 | from app.data.models import Device, File, FileType 14 | from app.helpers.converters import __converter_to_permissions_default__ 15 | from app.services.adb import ShellCommand 16 | 17 | 18 | class FileRepository: 19 | @classmethod 20 | def file(cls, path: str) -> (File, str): 21 | if not PythonADBManager.device: 22 | return None, "No device selected!" 23 | if not PythonADBManager.device.available: 24 | return None, "Device not available!" 25 | try: 26 | path = PythonADBManager.clear_path(path) 27 | mode, size, mtime = PythonADBManager.device.stat(path) 28 | file = File( 29 | name=os.path.basename(os.path.normpath(path)), 30 | size=size, 31 | date_time=datetime.datetime.utcfromtimestamp(mtime), 32 | permissions=__converter_to_permissions_default__(list(oct(mode)[2:])) 33 | ) 34 | 35 | if file.type == FileType.LINK: 36 | args = ShellCommand.LS_LIST_DIRS + [path.replace(' ', r'\ ') + '/'] 37 | response = PythonADBManager.device.shell(shlex.join(args)) 38 | file.link_type = FileType.UNKNOWN 39 | if response and response.startswith('d'): 40 | file.link_type = FileType.DIRECTORY 41 | elif response and response.__contains__('Not a'): 42 | file.link_type = FileType.FILE 43 | file.path = path 44 | return file, None 45 | 46 | except BaseException as error: 47 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 48 | return None, error 49 | 50 | @classmethod 51 | def files(cls) -> (List[File], str): 52 | if not PythonADBManager.device: 53 | return None, "No device selected!" 54 | if not PythonADBManager.device.available: 55 | return None, "Device not available!" 56 | 57 | files = [] 58 | try: 59 | path = PythonADBManager.path() 60 | response = PythonADBManager.device.list(path) 61 | 62 | args = ShellCommand.LS_ALL_DIRS + [path.replace(' ', r'\ ') + "*/"] 63 | dirs = PythonADBManager.device.shell(" ".join(args)).split() 64 | 65 | for file in response: 66 | if file.filename.decode() == '.' or file.filename.decode() == '..': 67 | continue 68 | 69 | permissions = __converter_to_permissions_default__(list(oct(file.mode)[2:])) 70 | link_type = None 71 | if permissions[0] == 'l': 72 | link_type = FileType.FILE 73 | if dirs.__contains__(path + file.filename.decode() + "/"): 74 | link_type = FileType.DIRECTORY 75 | 76 | files.append( 77 | File( 78 | name=file.filename.decode(), 79 | size=file.size, 80 | path=(path + file.filename.decode()), 81 | link_type=link_type, 82 | date_time=datetime.datetime.utcfromtimestamp(file.mtime), 83 | permissions=permissions, 84 | ) 85 | ) 86 | 87 | return files, None 88 | 89 | except BaseException as error: 90 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 91 | return files, error 92 | 93 | @classmethod 94 | def rename(cls, file: File, name: str) -> (str, str): 95 | if not PythonADBManager.device: 96 | return None, "No device selected!" 97 | if not PythonADBManager.device.available: 98 | return None, "Device not available!" 99 | if name.__contains__('/') or name.__contains__('\\'): 100 | return None, "Invalid name" 101 | 102 | try: 103 | args = [ShellCommand.MV, file.path, file.location + name] 104 | response = PythonADBManager.device.shell(shlex.join(args)) 105 | if response: 106 | return None, response 107 | return None, None 108 | except BaseException as error: 109 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 110 | return None, error 111 | 112 | @classmethod 113 | def open_file(cls, file: File) -> (str, str): 114 | if not PythonADBManager.device: 115 | return None, "No device selected!" 116 | if not PythonADBManager.device.available: 117 | return None, "Device not available!" 118 | try: 119 | args = [ShellCommand.CAT, file.path.replace(' ', r'\ ')] 120 | if file.isdir: 121 | return None, "Can't open. %s is a directory" % file.path 122 | response = PythonADBManager.device.shell(shlex.join(args)) 123 | return response, None 124 | except BaseException as error: 125 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 126 | return None, error 127 | 128 | @classmethod 129 | def delete(cls, file: File) -> (str, str): 130 | if not PythonADBManager.device: 131 | return None, "No device selected!" 132 | if not PythonADBManager.device.available: 133 | return None, "Device not available!" 134 | try: 135 | args = [ShellCommand.RM, file.path] 136 | if file.isdir: 137 | args = ShellCommand.RM_DIR_FORCE + [file.path] 138 | response = PythonADBManager.device.shell(shlex.join(args)) 139 | if response: 140 | return None, response 141 | return "%s '%s' has been deleted" % ('Folder' if file.isdir else 'File', file.path), None 142 | except BaseException as error: 143 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 144 | return None, error 145 | 146 | class UpDownHelper: 147 | def __init__(self, callback: callable): 148 | self.callback = callback 149 | self.written = 0 150 | self.total = 0 151 | 152 | def call(self, path: str, written: int, total: int): 153 | if self.total != total: 154 | self.total = total 155 | self.written = 0 156 | 157 | self.written += written 158 | self.callback(path, int(self.written / self.total * 100)) 159 | 160 | @classmethod 161 | def download(cls, progress_callback: callable, source: str, destination: str = None) -> (str, str): 162 | if not destination: 163 | destination = Settings.device_downloads_path(PythonADBManager.get_device()) 164 | 165 | helper = cls.UpDownHelper(progress_callback) 166 | destination = os.path.join(destination, os.path.basename(os.path.normpath(source))) 167 | if PythonADBManager.device and PythonADBManager.device.available and source: 168 | try: 169 | PythonADBManager.device.pull( 170 | device_path=source, 171 | local_path=destination, 172 | progress_callback=helper.call 173 | ) 174 | return "Download successful!\nDest: %s" % destination, None 175 | except BaseException as error: 176 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 177 | return None, error 178 | return None, None 179 | 180 | @classmethod 181 | def new_folder(cls, name) -> (str, str): 182 | if not PythonADBManager.device: 183 | return None, "No device selected!" 184 | if not PythonADBManager.device.available: 185 | return None, "Device not available!" 186 | 187 | try: 188 | args = [ShellCommand.MKDIR, (PythonADBManager.path() + name)] 189 | response = PythonADBManager.device.shell(shlex.join(args)) 190 | return None, response 191 | 192 | except BaseException as error: 193 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 194 | return None, error 195 | 196 | @classmethod 197 | def upload(cls, progress_callback: callable, source: str) -> (str, str): 198 | helper = cls.UpDownHelper(progress_callback) 199 | destination = PythonADBManager.path() + os.path.basename(os.path.normpath(source)) 200 | if PythonADBManager.device and PythonADBManager.device.available and PythonADBManager.path() and source: 201 | try: 202 | PythonADBManager.device.push( 203 | local_path=source, 204 | device_path=destination, 205 | progress_callback=helper.call 206 | ) 207 | return "Upload successful!\nDest: %s" % destination, None 208 | except BaseException as error: 209 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 210 | return None, error 211 | return None, None 212 | 213 | 214 | class DeviceRepository: 215 | @classmethod 216 | def devices(cls) -> (List[Device], str): 217 | if PythonADBManager.device: 218 | PythonADBManager.device.close() 219 | 220 | errors = [] 221 | devices = [] 222 | for device in USBContext().getDeviceList(skip_on_error=True): 223 | for setting in device.iterSettings(): 224 | if (setting.getClass(), setting.getSubClass(), setting.getProtocol()) == (0xFF, 0x42, 0x01): 225 | try: 226 | device_id = device.getSerialNumber() 227 | PythonADBManager.connect(device_id) 228 | device_name = " ".join( 229 | PythonADBManager.device.shell(" ".join(ShellCommand.GETPROP_PRODUCT_MODEL)).split() 230 | ) 231 | device_type = "device" if PythonADBManager.device.available else "unknown" 232 | devices.append(Device(id=device_id, name=device_name, type=device_type)) 233 | PythonADBManager.device.close() 234 | except BaseException as error: 235 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 236 | errors.append(str(error)) 237 | 238 | return devices, str("\n".join(errors)) 239 | 240 | @classmethod 241 | def connect(cls, device_id: str) -> (str, str): 242 | try: 243 | if PythonADBManager.device: 244 | PythonADBManager.device.close() 245 | serial = PythonADBManager.connect(device_id) 246 | if PythonADBManager.device.available: 247 | device_name = " ".join( 248 | PythonADBManager.device.shell(" ".join(ShellCommand.GETPROP_PRODUCT_MODEL)).split() 249 | ) 250 | PythonADBManager.set_device(Device(id=serial, name=device_name, type="device")) 251 | return "Connection established", None 252 | return None, "Device not available" 253 | 254 | except BaseException as error: 255 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 256 | return None, error 257 | 258 | @classmethod 259 | def disconnect(cls) -> (str, str): 260 | try: 261 | if PythonADBManager.device: 262 | PythonADBManager.device.close() 263 | return "Disconnected", None 264 | return None, None 265 | except BaseException as error: 266 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 267 | return None, error 268 | -------------------------------------------------------------------------------- /src/app/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/app/gui/__init__.py -------------------------------------------------------------------------------- /src/app/gui/explorer/__init__.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from PyQt5.QtWidgets import QWidget, QVBoxLayout 4 | 5 | from app.core.main import Adb 6 | from app.core.managers import Global 7 | from app.gui.explorer.devices import DeviceExplorerWidget 8 | from app.gui.explorer.files import FileExplorerWidget 9 | 10 | 11 | class MainExplorer(QWidget): 12 | def __init__(self, parent=None): 13 | super(MainExplorer, self).__init__(parent) 14 | self.setLayout(QVBoxLayout(self)) 15 | self.body = QWidget(self) 16 | 17 | Global().communicate.files.connect(self.files) 18 | Global().communicate.devices.connect(self.devices) 19 | 20 | def files(self): 21 | self.clear() 22 | 23 | self.body = FileExplorerWidget(self) 24 | self.layout().addWidget(self.body) 25 | self.body.update() 26 | 27 | def devices(self): 28 | self.clear() 29 | Adb.manager().clear() 30 | 31 | self.body = DeviceExplorerWidget(self) 32 | self.layout().addWidget(self.body) 33 | self.body.update() 34 | 35 | def clear(self): 36 | self.layout().removeWidget(self.body) 37 | self.body.close() 38 | self.body.deleteLater() 39 | -------------------------------------------------------------------------------- /src/app/gui/explorer/devices.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from typing import Any 4 | 5 | from PyQt5 import QtGui, QtCore 6 | from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex, QRect, QVariant, QSize 7 | from PyQt5.QtGui import QPalette, QPixmap, QMovie 8 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QStyledItemDelegate, QStyleOptionViewItem, QApplication, \ 9 | QStyle, QListView 10 | 11 | from app.core.configurations import Resources 12 | from app.core.main import Adb 13 | from app.core.managers import Global 14 | from app.data.models import DeviceType, MessageData 15 | from app.data.repositories import DeviceRepository 16 | from app.helpers.tools import AsyncRepositoryWorker, read_string_from_file 17 | 18 | 19 | class DeviceItemDelegate(QStyledItemDelegate): 20 | def sizeHint(self, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtCore.QSize: 21 | result = super(DeviceItemDelegate, self).sizeHint(option, index) 22 | result.setHeight(40) 23 | return result 24 | 25 | def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): 26 | if not index.data(): 27 | return super(DeviceItemDelegate, self).paint(painter, option, index) 28 | 29 | top = option.rect.top() 30 | bottom = option.rect.height() 31 | first_start = option.rect.left() + 50 32 | second_start = option.rect.left() + int(option.rect.width() / 2) 33 | end = option.rect.width() + option.rect.left() 34 | 35 | self.initStyleOption(option, index) 36 | style = option.widget.style() if option.widget else QApplication.style() 37 | style.drawControl(QStyle.CE_ItemViewItem, option, painter, option.widget) 38 | painter.setPen(option.palette.color(QPalette.Normal, QPalette.Text)) 39 | 40 | painter.drawText( 41 | QRect(first_start, top, second_start - first_start - 4, bottom), 42 | option.displayAlignment, index.data().name 43 | ) 44 | 45 | painter.drawText( 46 | QRect(second_start, top, end - second_start, bottom), 47 | Qt.AlignCenter | option.displayAlignment, index.data().id 48 | ) 49 | 50 | 51 | class DeviceListModel(QAbstractListModel): 52 | def __init__(self, parent=None): 53 | super().__init__(parent) 54 | self.items = [] 55 | 56 | def clear(self): 57 | self.beginResetModel() 58 | self.items.clear() 59 | self.endResetModel() 60 | 61 | def populate(self, devices: list): 62 | self.beginResetModel() 63 | self.items.clear() 64 | self.items = devices 65 | self.endResetModel() 66 | 67 | def rowCount(self, parent: QModelIndex = ...) -> int: 68 | return len(self.items) 69 | 70 | def icon_path(self, index: QModelIndex = ...): 71 | return Resources.icon_phone if self.items[index.row()].type == DeviceType.DEVICE \ 72 | else Resources.icon_phone_unknown 73 | 74 | def data(self, index: QModelIndex, role: int = ...) -> Any: 75 | if not index.isValid(): 76 | return QVariant() 77 | 78 | if role == Qt.DisplayRole: 79 | return self.items[index.row()] 80 | elif role == Qt.DecorationRole: 81 | return QPixmap(self.icon_path(index)).scaled(32, 32, Qt.KeepAspectRatio) 82 | return QVariant() 83 | 84 | 85 | class DeviceExplorerWidget(QWidget): 86 | DEVICES_WORKER_ID = 200 87 | 88 | def __init__(self, parent=None): 89 | super(DeviceExplorerWidget, self).__init__(parent) 90 | self.main_layout = QVBoxLayout(self) 91 | 92 | self.header = QLabel('Connected devices', self) 93 | self.header.setAlignment(Qt.AlignCenter) 94 | self.main_layout.addWidget(self.header) 95 | 96 | self.list = QListView(self) 97 | self.model = DeviceListModel(self.list) 98 | 99 | self.list.setSpacing(1) 100 | self.list.setModel(self.model) 101 | self.list.clicked.connect(self.open) 102 | self.list.setItemDelegate(DeviceItemDelegate(self.list)) 103 | self.list.setStyleSheet(read_string_from_file(Resources.style_device_list)) 104 | self.main_layout.addWidget(self.list) 105 | 106 | self.loading = QLabel(self) 107 | self.loading.setAlignment(Qt.AlignCenter) 108 | self.loading_movie = QMovie(Resources.anim_loading, parent=self.loading) 109 | self.loading_movie.setScaledSize(QSize(48, 48)) 110 | self.loading.setMovie(self.loading_movie) 111 | self.main_layout.addWidget(self.loading) 112 | 113 | self.empty_label = QLabel("No connected devices", self) 114 | self.empty_label.setAlignment(Qt.AlignTop) 115 | self.empty_label.setContentsMargins(15, 10, 0, 0) 116 | self.empty_label.setStyleSheet("color: #969696; border: 1px solid #969696") 117 | self.main_layout.addWidget(self.empty_label) 118 | 119 | self.main_layout.setStretch(self.layout().count() - 1, 1) 120 | self.main_layout.setStretch(self.layout().count() - 2, 1) 121 | self.setLayout(self.main_layout) 122 | 123 | def update(self): 124 | super(DeviceExplorerWidget, self).update() 125 | worker = AsyncRepositoryWorker( 126 | name="Devices", 127 | worker_id=self.DEVICES_WORKER_ID, 128 | repository_method=DeviceRepository.devices, 129 | arguments=(), 130 | response_callback=self._async_response 131 | ) 132 | if Adb.worker().work(worker): 133 | # First Setup loading view 134 | self.model.clear() 135 | self.list.setHidden(True) 136 | self.loading.setHidden(False) 137 | self.empty_label.setHidden(True) 138 | self.loading_movie.start() 139 | 140 | # Then start async worker 141 | worker.start() 142 | 143 | @property 144 | def device(self): 145 | if self.list and self.list.currentIndex() is not None: 146 | return self.model.items[self.list.currentIndex().row()] 147 | 148 | def _async_response(self, devices, error): 149 | self.loading_movie.stop() 150 | self.loading.setHidden(True) 151 | 152 | if error: 153 | Global().communicate.notification.emit( 154 | MessageData( 155 | title='Devices', 156 | timeout=15000, 157 | body=" %s " % error 158 | ) 159 | ) 160 | if not devices: 161 | self.empty_label.setHidden(False) 162 | else: 163 | self.list.setHidden(False) 164 | self.model.populate(devices) 165 | 166 | def open(self): 167 | if self.device.id: 168 | if Adb.manager().set_device(self.device): 169 | Global().communicate.files.emit() 170 | else: 171 | Global().communicate.notification.emit( 172 | MessageData( 173 | title='Device', 174 | timeout=10000, 175 | body="Could not open the device %s" % Adb.manager().get_device().name 176 | ) 177 | ) 178 | -------------------------------------------------------------------------------- /src/app/gui/explorer/files.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import sys 4 | from typing import Any 5 | 6 | from PyQt5 import QtCore, QtGui 7 | from PyQt5.QtCore import Qt, QPoint, QModelIndex, QAbstractListModel, QVariant, QRect, QSize, QEvent, QObject 8 | from PyQt5.QtGui import QPixmap, QColor, QPalette, QMovie, QKeySequence 9 | from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QFileDialog, QStyle, QWidget, QStyledItemDelegate, \ 10 | QStyleOptionViewItem, QApplication, QListView, QVBoxLayout, QLabel, QSizePolicy, QHBoxLayout, QTextEdit, \ 11 | QMainWindow 12 | 13 | from app.core.configurations import Resources 14 | from app.core.main import Adb 15 | from app.core.managers import Global 16 | from app.data.models import FileType, MessageData, MessageType 17 | from app.data.repositories import FileRepository 18 | from app.gui.explorer.toolbar import ParentButton, UploadTools, PathBar 19 | from app.helpers.tools import AsyncRepositoryWorker, ProgressCallbackHelper, read_string_from_file 20 | 21 | 22 | class FileHeaderWidget(QWidget): 23 | def __init__(self, parent=None): 24 | super(FileHeaderWidget, self).__init__(parent) 25 | self.setLayout(QHBoxLayout(self)) 26 | policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) 27 | 28 | self.file = QLabel('File', self) 29 | self.file.setContentsMargins(45, 0, 0, 0) 30 | policy.setHorizontalStretch(39) 31 | self.file.setSizePolicy(policy) 32 | self.layout().addWidget(self.file) 33 | 34 | self.permissions = QLabel('Permissions', self) 35 | self.permissions.setAlignment(Qt.AlignCenter) 36 | policy.setHorizontalStretch(18) 37 | self.permissions.setSizePolicy(policy) 38 | self.layout().addWidget(self.permissions) 39 | 40 | self.size = QLabel('Size', self) 41 | self.size.setAlignment(Qt.AlignCenter) 42 | policy.setHorizontalStretch(21) 43 | self.size.setSizePolicy(policy) 44 | self.layout().addWidget(self.size) 45 | 46 | self.date = QLabel('Date', self) 47 | self.date.setAlignment(Qt.AlignCenter) 48 | policy.setHorizontalStretch(22) 49 | self.date.setSizePolicy(policy) 50 | self.layout().addWidget(self.date) 51 | 52 | self.setStyleSheet("QWidget { background-color: #E5E5E5; font-weight: 500; border: 1px solid #C0C0C0 }") 53 | 54 | 55 | class FileExplorerToolbar(QWidget): 56 | def __init__(self, parent=None): 57 | super(FileExplorerToolbar, self).__init__(parent) 58 | self.setLayout(QHBoxLayout(self)) 59 | policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) 60 | policy.setHorizontalStretch(1) 61 | 62 | self.upload_tools = UploadTools(self) 63 | self.upload_tools.setSizePolicy(policy) 64 | self.layout().addWidget(self.upload_tools) 65 | 66 | self.parent_button = ParentButton(self) 67 | self.parent_button.setSizePolicy(policy) 68 | self.layout().addWidget(self.parent_button) 69 | 70 | self.path_bar = PathBar(self) 71 | policy.setHorizontalStretch(8) 72 | self.path_bar.setSizePolicy(policy) 73 | self.layout().addWidget(self.path_bar) 74 | 75 | 76 | class FileItemDelegate(QStyledItemDelegate): 77 | def sizeHint(self, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtCore.QSize: 78 | result = super(FileItemDelegate, self).sizeHint(option, index) 79 | result.setHeight(40) 80 | return result 81 | 82 | def setEditorData(self, editor: QWidget, index: QtCore.QModelIndex): 83 | editor.setText(index.model().data(index, Qt.EditRole)) 84 | 85 | def updateEditorGeometry(self, editor: QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): 86 | editor.setGeometry( 87 | option.rect.left() + 48, option.rect.top(), int(option.rect.width() / 2.5) - 55, option.rect.height() 88 | ) 89 | 90 | def setModelData(self, editor: QWidget, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex): 91 | model.setData(index, editor.text(), Qt.EditRole) 92 | 93 | @staticmethod 94 | def paint_line(painter: QtGui.QPainter, color: QColor, x, y, w, h): 95 | painter.setPen(color) 96 | painter.drawLine(x, y, w, h) 97 | 98 | @staticmethod 99 | def paint_text(painter: QtGui.QPainter, text: str, color: QColor, options, x, y, w, h): 100 | painter.setPen(color) 101 | painter.drawText(QRect(x, y, w, h), options, text) 102 | 103 | def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): 104 | if not index.data(): 105 | return super(FileItemDelegate, self).paint(painter, option, index) 106 | 107 | self.initStyleOption(option, index) 108 | style = option.widget.style() if option.widget else QApplication.style() 109 | style.drawControl(QStyle.CE_ItemViewItem, option, painter, option.widget) 110 | 111 | line_color = QColor("#CCCCCC") 112 | text_color = option.palette.color(QPalette.Normal, QPalette.Text) 113 | 114 | top = option.rect.top() 115 | bottom = option.rect.height() 116 | 117 | first_start = option.rect.left() + 50 118 | second_start = option.rect.left() + int(option.rect.width() / 2.5) 119 | third_start = option.rect.left() + int(option.rect.width() / 1.75) 120 | fourth_start = option.rect.left() + int(option.rect.width() / 1.25) 121 | end = option.rect.width() + option.rect.left() 122 | 123 | self.paint_text( 124 | painter, index.data().name, text_color, option.displayAlignment, 125 | first_start, top, second_start - first_start - 4, bottom 126 | ) 127 | 128 | self.paint_line(painter, line_color, second_start - 2, top, second_start - 1, bottom) 129 | 130 | self.paint_text( 131 | painter, index.data().permissions, text_color, Qt.AlignCenter | option.displayAlignment, 132 | second_start, top, third_start - second_start - 4, bottom 133 | ) 134 | 135 | self.paint_line(painter, line_color, third_start - 2, top, third_start - 1, bottom) 136 | 137 | self.paint_text( 138 | painter, index.data().size, text_color, Qt.AlignCenter | option.displayAlignment, 139 | third_start, top, fourth_start - third_start - 4, bottom 140 | ) 141 | 142 | self.paint_line(painter, line_color, fourth_start - 2, top, fourth_start - 1, bottom) 143 | 144 | self.paint_text( 145 | painter, index.data().date, text_color, Qt.AlignCenter | option.displayAlignment, 146 | fourth_start, top, end - fourth_start, bottom 147 | ) 148 | 149 | 150 | class FileListModel(QAbstractListModel): 151 | def __init__(self, parent=None): 152 | super().__init__(parent) 153 | self.items = [] 154 | 155 | def clear(self): 156 | self.beginResetModel() 157 | self.items.clear() 158 | self.endResetModel() 159 | 160 | def populate(self, files: list): 161 | self.beginResetModel() 162 | self.items.clear() 163 | self.items = files 164 | self.endResetModel() 165 | 166 | def rowCount(self, parent: QModelIndex = ...) -> int: 167 | return len(self.items) 168 | 169 | def icon_path(self, index: QModelIndex = ...): 170 | file_type = self.items[index.row()].type 171 | if file_type == FileType.DIRECTORY: 172 | return Resources.icon_folder 173 | elif file_type == FileType.FILE: 174 | return Resources.icon_file 175 | elif file_type == FileType.LINK: 176 | link_type = self.items[index.row()].link_type 177 | if link_type == FileType.DIRECTORY: 178 | return Resources.icon_link_folder 179 | elif link_type == FileType.FILE: 180 | return Resources.icon_link_file 181 | return Resources.icon_link_file_unknown 182 | return Resources.icon_file_unknown 183 | 184 | def flags(self, index: QModelIndex) -> Qt.ItemFlags: 185 | if not index.isValid(): 186 | return Qt.NoItemFlags 187 | 188 | return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable 189 | 190 | def setData(self, index: QModelIndex, value: Any, role: int = ...) -> bool: 191 | if role == Qt.EditRole and value: 192 | data, error = FileRepository.rename(self.items[index.row()], value) 193 | if error: 194 | Global().communicate.notification.emit( 195 | MessageData( 196 | timeout=10000, 197 | title="Rename", 198 | body=" %s " % error, 199 | ) 200 | ) 201 | Global.communicate.files__refresh.emit() 202 | return super(FileListModel, self).setData(index, value, role) 203 | 204 | def data(self, index: QModelIndex, role: int = ...) -> Any: 205 | if not index.isValid(): 206 | return QVariant() 207 | 208 | if role == Qt.DisplayRole: 209 | return self.items[index.row()] 210 | elif role == Qt.EditRole: 211 | return self.items[index.row()].name 212 | elif role == Qt.DecorationRole: 213 | return QPixmap(self.icon_path(index)).scaled(32, 32, Qt.KeepAspectRatio) 214 | return QVariant() 215 | 216 | 217 | class FileExplorerWidget(QWidget): 218 | FILES_WORKER_ID = 300 219 | DOWNLOAD_WORKER_ID = 399 220 | 221 | def __init__(self, parent=None): 222 | super(FileExplorerWidget, self).__init__(parent) 223 | self.main_layout = QVBoxLayout(self) 224 | 225 | self.toolbar = FileExplorerToolbar(self) 226 | self.main_layout.addWidget(self.toolbar) 227 | 228 | self.header = FileHeaderWidget(self) 229 | self.main_layout.addWidget(self.header) 230 | 231 | self.list = QListView(self) 232 | self.model = FileListModel(self.list) 233 | 234 | self.list.setSpacing(1) 235 | self.list.setModel(self.model) 236 | self.list.installEventFilter(self) 237 | self.list.doubleClicked.connect(self.open) 238 | self.list.setItemDelegate(FileItemDelegate(self.list)) 239 | self.list.setContextMenuPolicy(Qt.CustomContextMenu) 240 | self.list.customContextMenuRequested.connect(self.context_menu) 241 | self.list.setStyleSheet(read_string_from_file(Resources.style_file_list)) 242 | self.list.setSelectionMode(QListView.SelectionMode.ExtendedSelection) 243 | self.layout().addWidget(self.list) 244 | 245 | self.loading = QLabel(self) 246 | self.loading.setAlignment(Qt.AlignCenter) 247 | self.loading_movie = QMovie(Resources.anim_loading, parent=self.loading) 248 | self.loading_movie.setScaledSize(QSize(48, 48)) 249 | self.loading.setMovie(self.loading_movie) 250 | self.main_layout.addWidget(self.loading) 251 | 252 | self.empty_label = QLabel("Folder is empty", self) 253 | self.empty_label.setAlignment(Qt.AlignCenter) 254 | self.empty_label.setStyleSheet("color: #969696; border: 1px solid #969696") 255 | self.layout().addWidget(self.empty_label) 256 | 257 | self.main_layout.setStretch(self.layout().count() - 1, 1) 258 | self.main_layout.setStretch(self.layout().count() - 2, 1) 259 | 260 | self.text_view_window = None 261 | self.setLayout(self.main_layout) 262 | 263 | Global().communicate.files__refresh.connect(self.update) 264 | 265 | @property 266 | def file(self): 267 | if self.list and self.list.currentIndex(): 268 | return self.model.items[self.list.currentIndex().row()] 269 | 270 | @property 271 | def files(self): 272 | if self.list and len(self.list.selectedIndexes()) > 0: 273 | return map(lambda index: self.model.items[index.row()], self.list.selectedIndexes()) 274 | 275 | def update(self): 276 | super(FileExplorerWidget, self).update() 277 | worker = AsyncRepositoryWorker( 278 | name="Files", 279 | worker_id=self.FILES_WORKER_ID, 280 | repository_method=FileRepository.files, 281 | response_callback=self._async_response, 282 | arguments=() 283 | ) 284 | if Adb.worker().work(worker): 285 | # First Setup loading view 286 | self.model.clear() 287 | self.list.setHidden(True) 288 | self.loading.setHidden(False) 289 | self.empty_label.setHidden(True) 290 | self.loading_movie.start() 291 | 292 | # Then start async worker 293 | worker.start() 294 | Global().communicate.path_toolbar__refresh.emit() 295 | 296 | def close(self) -> bool: 297 | Global().communicate.files__refresh.disconnect() 298 | return super(FileExplorerWidget, self).close() 299 | 300 | def _async_response(self, files: list, error: str): 301 | self.loading_movie.stop() 302 | self.loading.setHidden(True) 303 | 304 | if error: 305 | print(error, file=sys.stderr) 306 | if not files: 307 | Global().communicate.notification.emit( 308 | MessageData( 309 | title='Files', 310 | timeout=15000, 311 | body=" %s " % error 312 | ) 313 | ) 314 | if not files: 315 | self.empty_label.setHidden(False) 316 | else: 317 | self.list.setHidden(False) 318 | self.model.populate(files) 319 | self.list.setFocus() 320 | 321 | def eventFilter(self, obj: 'QObject', event: 'QEvent') -> bool: 322 | if obj == self.list and \ 323 | event.type() == QEvent.KeyPress and \ 324 | event.matches(QKeySequence.InsertParagraphSeparator) and \ 325 | not self.list.isPersistentEditorOpen(self.list.currentIndex()): 326 | self.open(self.list.currentIndex()) 327 | return super(FileExplorerWidget, self).eventFilter(obj, event) 328 | 329 | def open(self, index: QModelIndex = ...): 330 | if Adb.manager().open(self.model.items[index.row()]): 331 | Global().communicate.files__refresh.emit() 332 | 333 | def context_menu(self, pos: QPoint): 334 | menu = QMenu() 335 | menu.addSection("Actions") 336 | 337 | action_copy = QAction('Copy to...', self) 338 | action_copy.setDisabled(True) 339 | menu.addAction(action_copy) 340 | 341 | action_move = QAction('Move to...', self) 342 | action_move.setDisabled(True) 343 | menu.addAction(action_move) 344 | 345 | action_rename = QAction('Rename', self) 346 | action_rename.triggered.connect(self.rename) 347 | menu.addAction(action_rename) 348 | 349 | action_open_file = QAction('Open', self) 350 | action_open_file.triggered.connect(self.open_file) 351 | menu.addAction(action_open_file) 352 | 353 | action_delete = QAction('Delete', self) 354 | action_delete.triggered.connect(self.delete) 355 | menu.addAction(action_delete) 356 | 357 | action_download = QAction('Download', self) 358 | action_download.triggered.connect(self.download_files) 359 | menu.addAction(action_download) 360 | 361 | action_download_to = QAction('Download to...', self) 362 | action_download_to.triggered.connect(self.download_to) 363 | menu.addAction(action_download_to) 364 | 365 | menu.addSeparator() 366 | 367 | action_properties = QAction('Properties', self) 368 | action_properties.triggered.connect(self.file_properties) 369 | menu.addAction(action_properties) 370 | 371 | menu.exec(self.mapToGlobal(pos)) 372 | 373 | @staticmethod 374 | def default_response(data, error): 375 | if error: 376 | Global().communicate.notification.emit( 377 | MessageData( 378 | title='Download error', 379 | timeout=15000, 380 | body=" %s " % error 381 | ) 382 | ) 383 | if data: 384 | Global().communicate.notification.emit( 385 | MessageData( 386 | title='Downloaded', 387 | timeout=15000, 388 | body=data 389 | ) 390 | ) 391 | 392 | def rename(self): 393 | self.list.edit(self.list.currentIndex()) 394 | 395 | def open_file(self): 396 | # QDesktopServices.openUrl(QUrl.fromLocalFile("downloaded_path")) open via external app 397 | if not self.file.isdir: 398 | data, error = FileRepository.open_file(self.file) 399 | if error: 400 | Global().communicate.notification.emit( 401 | MessageData( 402 | title='File', 403 | timeout=15000, 404 | body=" %s " % error 405 | ) 406 | ) 407 | else: 408 | self.text_view_window = TextView(self.file.name, data) 409 | self.text_view_window.show() 410 | 411 | def delete(self): 412 | file_names = ', '.join(map(lambda f: f.name, self.files)) 413 | reply = QMessageBox.critical( 414 | self, 415 | 'Delete', 416 | "Do you want to delete '%s'? It cannot be undone!" % file_names, 417 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No 418 | ) 419 | 420 | if reply == QMessageBox.Yes: 421 | for file in self.files: 422 | data, error = FileRepository.delete(file) 423 | if data: 424 | Global().communicate.notification.emit( 425 | MessageData( 426 | timeout=10000, 427 | title="Delete", 428 | body=data, 429 | ) 430 | ) 431 | if error: 432 | Global().communicate.notification.emit( 433 | MessageData( 434 | timeout=10000, 435 | title="Delete", 436 | body=" %s " % error, 437 | ) 438 | ) 439 | Global.communicate.files__refresh.emit() 440 | 441 | def download_to(self): 442 | dir_name = QFileDialog.getExistingDirectory(self, 'Download to', '~') 443 | if dir_name: 444 | self.download_files(dir_name) 445 | 446 | def download_files(self, destination: str = None): 447 | for file in self.files: 448 | helper = ProgressCallbackHelper() 449 | worker = AsyncRepositoryWorker( 450 | worker_id=self.DOWNLOAD_WORKER_ID, 451 | name="Download", 452 | repository_method=FileRepository.download, 453 | response_callback=self.default_response, 454 | arguments=( 455 | helper.progress_callback.emit, file.path, destination 456 | ) 457 | ) 458 | if Adb.worker().work(worker): 459 | Global().communicate.notification.emit( 460 | MessageData( 461 | title="Downloading to", 462 | message_type=MessageType.LOADING_MESSAGE, 463 | message_catcher=worker.set_loading_widget 464 | ) 465 | ) 466 | helper.setup(worker, worker.update_loading_widget) 467 | worker.start() 468 | 469 | def file_properties(self): 470 | file, error = FileRepository.file(self.file.path) 471 | file = file if file else self.file 472 | 473 | if error: 474 | Global().communicate.notification.emit( 475 | MessageData( 476 | timeout=10000, 477 | title="Opening folder", 478 | body=" %s " % error, 479 | ) 480 | ) 481 | 482 | info = "
%s
" % str(file) 483 | info += "
Name:        %s
" % file.name or '-' 484 | info += "
Owner:       %s
" % file.owner or '-' 485 | info += "
Group:       %s
" % file.group or '-' 486 | info += "
Size:        %s
" % file.raw_size or '-' 487 | info += "
Permissions: %s
" % file.permissions or '-' 488 | info += "
Date:        %s
" % file.raw_date or '-' 489 | info += "
Type:        %s
" % file.type or '-' 490 | 491 | if file.type == FileType.LINK: 492 | info += "
Links to:    %s
" % file.link or '-' 493 | 494 | properties = QMessageBox(self) 495 | properties.setStyleSheet("background-color: #DDDDDD") 496 | properties.setIconPixmap( 497 | QPixmap(self.model.icon_path(self.list.currentIndex())).scaled(128, 128, Qt.KeepAspectRatio) 498 | ) 499 | properties.setWindowTitle('Properties') 500 | properties.setInformativeText(info) 501 | properties.exec_() 502 | 503 | 504 | class TextView(QMainWindow): 505 | def __init__(self, filename, data): 506 | QMainWindow.__init__(self) 507 | 508 | self.setMinimumSize(QSize(500, 300)) 509 | self.setWindowTitle(filename) 510 | 511 | self.text_edit = QTextEdit(self) 512 | self.setCentralWidget(self.text_edit) 513 | self.text_edit.insertPlainText(data) 514 | self.text_edit.move(10, 10) 515 | -------------------------------------------------------------------------------- /src/app/gui/explorer/toolbar.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from PyQt5.QtCore import QObject, QEvent 4 | from PyQt5.QtGui import QIcon 5 | from PyQt5.QtWidgets import QToolButton, QMenu, QWidget, QAction, QFileDialog, QInputDialog, QLineEdit, QHBoxLayout 6 | 7 | from app.core.configurations import Resources 8 | from app.core.main import Adb 9 | from app.core.managers import Global 10 | from app.data.models import MessageData, MessageType 11 | from app.data.repositories import FileRepository 12 | from app.helpers.tools import AsyncRepositoryWorker, ProgressCallbackHelper 13 | 14 | 15 | class UploadTools(QToolButton): 16 | def __init__(self, parent): 17 | super(UploadTools, self).__init__(parent) 18 | self.menu = QMenu(self) 19 | self.uploader = self.FilesUploader() 20 | 21 | self.show_action = QAction(QIcon(Resources.icon_plus), 'Upload', self) 22 | self.show_action.triggered.connect(self.showMenu) 23 | self.setDefaultAction(self.show_action) 24 | 25 | upload_files = QAction(QIcon(Resources.icon_files_upload), '&Upload files', self) 26 | upload_files.triggered.connect(self.__action_upload_files__) 27 | self.menu.addAction(upload_files) 28 | 29 | upload_directory = QAction(QIcon(Resources.icon_folder_upload), '&Upload directory', self) 30 | upload_directory.triggered.connect(self.__action_upload_directory__) 31 | self.menu.addAction(upload_directory) 32 | 33 | upload_files = QAction(QIcon(Resources.icon_folder_create), '&Create folder', self) 34 | upload_files.triggered.connect(self.__action_create_folder__) 35 | self.menu.addAction(upload_files) 36 | self.setMenu(self.menu) 37 | 38 | def __action_upload_files__(self): 39 | file_names = QFileDialog.getOpenFileNames(self, 'Select files', '~')[0] 40 | 41 | if file_names: 42 | self.uploader.setup(file_names) 43 | self.uploader.upload() 44 | 45 | def __action_upload_directory__(self): 46 | dir_name = QFileDialog.getExistingDirectory(self, 'Select directory', '~') 47 | 48 | if dir_name: 49 | self.uploader.setup([dir_name]) 50 | self.uploader.upload() 51 | 52 | def __action_create_folder__(self): 53 | text, ok = QInputDialog.getText(self, 'New folder', 'Enter new folder name:') 54 | 55 | if ok and text: 56 | data, error = FileRepository.new_folder(text) 57 | if error: 58 | Global().communicate.notification.emit( 59 | MessageData( 60 | timeout=15000, 61 | title="Creating folder", 62 | body=" %s " % error, 63 | ) 64 | ) 65 | if data: 66 | Global().communicate.notification.emit( 67 | MessageData( 68 | title="Creating folder", 69 | timeout=15000, 70 | body=data, 71 | ) 72 | ) 73 | Global().communicate.files__refresh.emit() 74 | 75 | class FilesUploader: 76 | UPLOAD_WORKER_ID = 398 77 | 78 | def __init__(self): 79 | self.files = [] 80 | 81 | def setup(self, files: list): 82 | self.files = files 83 | 84 | def upload(self, data=None, error=None): 85 | if self.files: 86 | helper = ProgressCallbackHelper() 87 | worker = AsyncRepositoryWorker( 88 | worker_id=self.UPLOAD_WORKER_ID, 89 | name="Upload", 90 | repository_method=FileRepository.upload, 91 | response_callback=self.upload, 92 | arguments=(helper.progress_callback.emit, self.files.pop()) 93 | ) 94 | if Adb.worker().work(worker): 95 | Global().communicate.notification.emit( 96 | MessageData( 97 | title="Uploading", 98 | message_type=MessageType.LOADING_MESSAGE, 99 | message_catcher=worker.set_loading_widget 100 | ) 101 | ) 102 | helper.setup(worker, worker.update_loading_widget) 103 | worker.start() 104 | else: 105 | Global().communicate.files__refresh.emit() 106 | 107 | if error: 108 | Global().communicate.notification.emit( 109 | MessageData( 110 | timeout=15000, 111 | title='Upload error', 112 | body=" %s " % error, 113 | ) 114 | ) 115 | if data: 116 | Global().communicate.notification.emit( 117 | MessageData( 118 | title='Uploaded', 119 | timeout=15000, 120 | body=data, 121 | ) 122 | ) 123 | 124 | 125 | class ParentButton(QToolButton): 126 | def __init__(self, parent): 127 | super(ParentButton, self).__init__(parent) 128 | self.action = QAction(QIcon(Resources.icon_up), 'Parent', self) 129 | self.action.setShortcut('Escape') 130 | self.action.triggered.connect( 131 | lambda: Global().communicate.files__refresh.emit() if Adb.worker().check(300) and Adb.manager().up() else '' 132 | ) 133 | self.setDefaultAction(self.action) 134 | 135 | 136 | class PathBar(QWidget): 137 | def __init__(self, parent: QWidget): 138 | super(PathBar, self).__init__(parent) 139 | self.setLayout(QHBoxLayout(self)) 140 | 141 | self.prefix = Adb.manager().get_device().name + ":" 142 | self.value = Adb.manager().path() 143 | 144 | self.text = QLineEdit(self) 145 | self.text.installEventFilter(self) 146 | self.text.setStyleSheet("padding: 5;") 147 | self.text.setText(self.prefix + self.value) 148 | self.text.textEdited.connect(self._update) 149 | self.text.returnPressed.connect(self._action) 150 | self.layout().addWidget(self.text) 151 | 152 | self.go = QToolButton(self) 153 | self.go.setStyleSheet("padding: 4;") 154 | self.action = QAction(QIcon(Resources.icon_arrow), 'Go', self) 155 | self.action.triggered.connect(self._action) 156 | self.go.setDefaultAction(self.action) 157 | self.layout().addWidget(self.go) 158 | 159 | self.layout().setContentsMargins(0, 0, 0, 0) 160 | Global().communicate.path_toolbar__refresh.connect(self._clear) 161 | 162 | def eventFilter(self, obj: 'QObject', event: 'QEvent') -> bool: 163 | if obj == self.text and event.type() == QEvent.FocusIn: 164 | self.text.setText(self.value) 165 | elif obj == self.text and event.type() == QEvent.FocusOut: 166 | self.text.setText(self.prefix + self.value) 167 | return super(PathBar, self).eventFilter(obj, event) 168 | 169 | def _clear(self): 170 | self.value = Adb.manager().path() 171 | self.text.setText(self.prefix + self.value) 172 | 173 | def _update(self, text: str): 174 | self.value = text 175 | 176 | def _action(self): 177 | self.text.clearFocus() 178 | file, error = FileRepository.file(self.value) 179 | if error: 180 | Global().communicate.path_toolbar__refresh.emit() 181 | Global().communicate.notification.emit( 182 | MessageData( 183 | timeout=10000, 184 | title="Opening folder", 185 | body=" %s " % error, 186 | ) 187 | ) 188 | elif file and Adb.manager().go(file): 189 | Global().communicate.files__refresh.emit() 190 | else: 191 | Global().communicate.path_toolbar__refresh.emit() 192 | Global().communicate.notification.emit( 193 | MessageData( 194 | timeout=10000, 195 | title="Opening folder", 196 | body=" Cannot open location ", 197 | ) 198 | ) 199 | -------------------------------------------------------------------------------- /src/app/gui/help.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from PyQt5.QtCore import Qt 4 | from PyQt5.QtGui import QPixmap, QIcon 5 | from PyQt5.QtWidgets import QWidget, QLabel, QApplication 6 | 7 | from app.core.configurations import Resources, Application 8 | 9 | 10 | class About(QWidget): 11 | def __init__(self): 12 | super(QWidget, self).__init__() 13 | icon = QLabel(self) 14 | icon.setPixmap(QPixmap(Resources.icon_logo).scaled(64, 64, Qt.KeepAspectRatio)) 15 | icon.move(168, 40) 16 | about_text = "

" 17 | about_text += "ADB File Explorer
" 18 | about_text += 'Version: %s
' % Application.__version__ 19 | about_text += '
' 20 | about_text += "Open source application written in Python
" 21 | about_text += "UI Library: PyQt5
" 22 | about_text += "Developer: Azat Aldeshov
" 23 | link = 'https://github.com/Aldeshov/ADBFileExplorer' 24 | about_text += "Github: %s" % (link, link) 25 | about_label = QLabel(about_text, self) 26 | about_label.setOpenExternalLinks(True) 27 | about_label.move(10, 100) 28 | 29 | self.setAttribute(Qt.WA_QuitOnClose, False) 30 | self.setWindowIcon(QIcon(Resources.icon_logo)) 31 | self.setWindowTitle('About') 32 | self.setFixedHeight(320) 33 | self.setFixedWidth(400) 34 | 35 | center = QApplication.desktop().availableGeometry(self).center() 36 | self.move(int(center.x() - self.width() * 0.5), int(center.y() - self.height() * 0.5)) 37 | -------------------------------------------------------------------------------- /src/app/gui/notification.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from typing import Union 4 | 5 | from PyQt5 import QtGui, QtCore 6 | from PyQt5.QtCore import Qt, QTimer, QPoint, QSize, QPropertyAnimation, QAbstractAnimation, QObject 7 | from PyQt5.QtGui import QIcon, QPaintEvent, QPainter, QMovie 8 | from PyQt5.QtWidgets import QLabel, QWidget, QHBoxLayout, QPushButton, QStyleOption, QStyle, \ 9 | QGraphicsDropShadowEffect, QVBoxLayout, QScrollArea, QSizePolicy, QFrame, QGraphicsOpacityEffect, QProgressBar 10 | 11 | from app.core.configurations import Resources 12 | from app.data.models import MessageType 13 | from app.helpers.tools import read_string_from_file 14 | 15 | 16 | class BaseMessage(QWidget): 17 | def __init__(self, parent: QObject): 18 | super(BaseMessage, self).__init__(parent) 19 | self.notification_center = parent 20 | self.header = QHBoxLayout() 21 | self.body = QVBoxLayout(self) 22 | self.body.setContentsMargins(0, 0, 0, 0) 23 | self.body.setSpacing(0) 24 | self.body.addLayout(self.header) 25 | self.setLayout(self.body) 26 | 27 | self.opacity_effect = QGraphicsOpacityEffect(self) 28 | self.opacity_effect.setOpacity(0.) 29 | self.setGraphicsEffect(self.opacity_effect) 30 | 31 | self.animation = QPropertyAnimation(self.opacity_effect, b"opacity") 32 | self.animation.setDuration(200) 33 | self.animation.setStartValue(0) 34 | self.animation.setEndValue(1) 35 | 36 | self.setStyleSheet('QWidget { background: #d3d7cf; }') 37 | self.setAttribute(Qt.WA_DeleteOnClose) 38 | self.setMinimumSize(self.sizeHint()) 39 | self.setMinimumHeight(80) 40 | self.setFixedWidth(320) 41 | self.show() 42 | 43 | def paintEvent(self, event: QPaintEvent): 44 | option = QStyleOption() 45 | option.initFrom(self) 46 | painter = QPainter(self) 47 | self.style().drawPrimitive(QStyle.PrimitiveElement(), option, painter, self) 48 | super().paintEvent(event) 49 | 50 | def set_opacity(self, opacity): 51 | self.opacity_effect.setOpacity(opacity) 52 | if opacity == 1: 53 | shadow_effect = QGraphicsDropShadowEffect(self) 54 | shadow_effect.setBlurRadius(10) 55 | shadow_effect.setOffset(1, 1) 56 | self.setGraphicsEffect(shadow_effect) 57 | 58 | def show(self): 59 | self.animation.valueChanged.connect(self.set_opacity) 60 | self.animation.setDirection(QAbstractAnimation.Forward) 61 | self.animation.start() 62 | return super().show() 63 | 64 | def closeEvent(self, event: QtGui.QCloseEvent): 65 | self.notification_center.remove(self) 66 | self.deleteLater() 67 | return event.accept() 68 | 69 | def create_loading(self): 70 | gif = QLabel(self) 71 | movie = QMovie(Resources.anim_loading) 72 | movie.setScaledSize(QSize(24, 24)) 73 | gif.setContentsMargins(5, 0, 5, 0) 74 | gif.setAlignment(Qt.AlignCenter) 75 | gif.setMovie(movie) 76 | 77 | self.header.addWidget(gif) 78 | movie.start() 79 | 80 | def create_title(self, text): 81 | title = QLabel(text, self) 82 | title.setAlignment(Qt.AlignVCenter) 83 | title.setStyleSheet("QLabel { font-size: 16px; font-weight: bold; }") 84 | title.setContentsMargins(5, 0, 0, 0) 85 | self.header.addWidget(title, 1) 86 | 87 | def create_close(self): 88 | button = QPushButton(self) 89 | button.setObjectName("close") 90 | button.setIcon(QIcon(Resources.icon_close)) 91 | button.setFixedSize(32, 32) 92 | button.setIconSize(QSize(10, 10)) 93 | button.setStyleSheet(read_string_from_file(Resources.style_notification_button)) 94 | button.clicked.connect(lambda: self.close() or None) 95 | self.header.addWidget(button) 96 | 97 | def resizeEvent(self, event: QtGui.QResizeEvent): 98 | self.setMinimumHeight(self.height()) 99 | return event.accept() 100 | 101 | def default_body_message(self, message): 102 | body = QLabel(message, self) 103 | body.setWordWrap(True) 104 | body.setContentsMargins(15, 5, 20, 10) 105 | body.setStyleSheet("font-size: 14px; font-weight: normal;") 106 | return body 107 | 108 | 109 | class LoadingMessage(BaseMessage): 110 | def __init__(self, parent: QWidget, title: str, body: Union[QWidget, str] = None): 111 | super(LoadingMessage, self).__init__(parent) 112 | 113 | self.label = None 114 | self.progress = None 115 | self.create_loading() 116 | self.create_title(title) 117 | if not body: 118 | self.label = QLabel("Waiting...", self) 119 | self.label.setWordWrap(True) 120 | self.label.setContentsMargins(10, 5, 10, 10) 121 | 122 | self.progress = QProgressBar(self) 123 | self.progress.setValue(0) 124 | self.progress.setMaximumHeight(16) 125 | self.progress.setAlignment(Qt.AlignCenter) 126 | 127 | self.layout().addWidget(self.label) 128 | self.layout().addWidget(self.progress) 129 | elif isinstance(body, QWidget): 130 | self.layout().addWidget(body) 131 | elif isinstance(body, str): 132 | self.layout().addWidget(self.default_body_message(body)) 133 | 134 | def update_progress(self, title: str, progress: int): 135 | if self.label: 136 | self.label.setText(title) 137 | if self.progress: 138 | self.progress.setValue(progress) 139 | 140 | 141 | class Message(BaseMessage): 142 | def __init__(self, parent: QWidget, title: str, body: Union[QWidget, str], timeout=5000): 143 | super(Message, self).__init__(parent) 144 | 145 | self.create_title(title) 146 | self.create_close() 147 | if isinstance(body, QWidget): 148 | self.layout().addWidget(body) 149 | elif isinstance(body, str): 150 | self.layout().addWidget(self.default_body_message(body)) 151 | 152 | if timeout >= 1000: 153 | QTimer.singleShot(timeout, self.on_close) 154 | 155 | def on_close(self): 156 | self.opacity_effect = QGraphicsOpacityEffect(self) 157 | self.setGraphicsEffect(self.opacity_effect) 158 | self.animation = QPropertyAnimation(self.opacity_effect, b"opacity") 159 | self.animation.setDuration(100) 160 | self.animation.setStartValue(1) 161 | self.animation.setEndValue(0) 162 | self.animation.valueChanged.connect(self.closing) 163 | self.animation.setDirection(QAbstractAnimation.Forward) 164 | self.animation.start() 165 | 166 | def closing(self, opacity): 167 | self.opacity_effect.setOpacity(opacity) 168 | if opacity == 0: 169 | self.close() 170 | 171 | 172 | class NotificationCenter(QScrollArea): 173 | def __init__(self, parent=None): 174 | super().__init__(parent) 175 | self.notifications = QFrame(self) 176 | self.notifications.setLayout(QVBoxLayout(self.notifications)) 177 | self.notifications.installEventFilter(self) 178 | self.notifications.layout().setSpacing(5) 179 | self.notifications.layout().addStretch() 180 | 181 | self.setWidgetResizable(True) 182 | self.setWidget(self.notifications) 183 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 184 | self.setWindowFlags(Qt.FramelessWindowHint | Qt.SubWindow) 185 | self.setStyleSheet("QWidget { background: transparent; border: 0; }") 186 | self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 187 | self.verticalScrollBar().rangeChanged.connect(lambda x, y: self.verticalScrollBar().setValue(y)) 188 | 189 | self.setMinimumSize(355, 20) 190 | self.update_position() 191 | self.adjustSize() 192 | self.show() 193 | 194 | def eventFilter(self, obj: QtCore.QObject, event: Union[QtCore.QEvent, QtGui.QResizeEvent]) -> bool: 195 | if obj == self.notifications and event.type() == event.Resize: 196 | if self.maximumHeight() > self.rect().height() < event.size().height(): 197 | self.resize(self.rect().width(), event.size().height() + self.notifications.layout().spacing()) 198 | 199 | return super(NotificationCenter, self).eventFilter(obj, event) 200 | 201 | def resizeEvent(self, event: QtGui.QResizeEvent): 202 | super(NotificationCenter, self).resizeEvent(event) 203 | self.update_position() 204 | 205 | def update_position(self): 206 | geometry = self.geometry() 207 | geometry.moveTopLeft( 208 | QPoint( 209 | self.parent().rect().width() - self.rect().width(), 210 | self.parent().rect().height() - self.rect().height() 211 | ) 212 | ) 213 | self.setGeometry(geometry) 214 | self.setMaximumHeight(self.parent().rect().height()) 215 | 216 | def append_notification(self, title: str, body: Union[QWidget, str], timeout=0, message_type=MessageType.MESSAGE): 217 | if message_type == MessageType.MESSAGE: 218 | message = Message(self, title, body, timeout) 219 | self.append(message) 220 | return message 221 | elif message_type == MessageType.LOADING_MESSAGE: 222 | message = LoadingMessage(self, title, body) 223 | self.append(message) 224 | return message 225 | 226 | def append(self, message: BaseMessage): 227 | self.notifications.layout().addWidget(message) 228 | self.notifications.adjustSize() 229 | self.update_position() 230 | 231 | def remove(self, message: BaseMessage): 232 | self.notifications.layout().removeWidget(message) 233 | self.adjustSize() 234 | self.update_position() 235 | -------------------------------------------------------------------------------- /src/app/gui/window.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from PyQt5.QtGui import QIcon 4 | from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QInputDialog, QMenuBar, QMessageBox 5 | 6 | from app.core.configurations import Resources, Settings 7 | from app.core.main import Adb 8 | from app.core.managers import Global 9 | from app.data.models import MessageData, MessageType 10 | from app.data.repositories import DeviceRepository 11 | from app.gui.explorer import MainExplorer 12 | from app.gui.help import About 13 | from app.gui.notification import NotificationCenter 14 | from app.helpers.tools import AsyncRepositoryWorker 15 | 16 | 17 | class MenuBar(QMenuBar): 18 | CONNECT_WORKER_ID = 100 19 | DISCONNECT_WORKER_ID = 101 20 | 21 | def __init__(self, parent): 22 | super(MenuBar, self).__init__(parent) 23 | 24 | self.about = About() 25 | self.file_menu = self.addMenu('&File') 26 | self.help_menu = self.addMenu('&Help') 27 | 28 | self.connect_action = QAction(QIcon(Resources.icon_link), '&Connect', self) 29 | self.connect_action.setShortcut('Alt+C') 30 | self.connect_action.triggered.connect(self.connect_device) 31 | self.file_menu.addAction(self.connect_action) 32 | 33 | disconnect_action = QAction(QIcon(Resources.icon_no_link), '&Disconnect', self) 34 | disconnect_action.setShortcut('Alt+X') 35 | disconnect_action.triggered.connect(self.disconnect) 36 | self.file_menu.addAction(disconnect_action) 37 | 38 | devices_action = QAction(QIcon(Resources.icon_phone), '&Show devices', self) 39 | devices_action.setShortcut('Alt+D') 40 | devices_action.triggered.connect(Global().communicate.devices.emit) 41 | self.file_menu.addAction(devices_action) 42 | 43 | exit_action = QAction('&Exit', self) 44 | exit_action.setShortcut('Alt+Q') 45 | exit_action.triggered.connect(qApp.quit) 46 | self.file_menu.addAction(exit_action) 47 | 48 | about_action = QAction('About', self) 49 | about_action.triggered.connect(self.about.show) 50 | self.help_menu.addAction(about_action) 51 | 52 | def disconnect(self): 53 | worker = AsyncRepositoryWorker( 54 | worker_id=self.DISCONNECT_WORKER_ID, 55 | name="Disconnecting", 56 | repository_method=DeviceRepository.disconnect, 57 | response_callback=self.__async_response_disconnect, 58 | arguments=() 59 | ) 60 | if Adb.worker().work(worker): 61 | Global().communicate.notification.emit( 62 | MessageData( 63 | title='Disconnect', 64 | body="Disconnecting from devices, please wait", 65 | message_type=MessageType.LOADING_MESSAGE, 66 | message_catcher=worker.set_loading_widget 67 | ) 68 | ) 69 | Global().communicate.status_bar.emit('Operation: %s... Please wait.' % worker.name, 3000) 70 | worker.start() 71 | 72 | def connect_device(self): 73 | text, ok = QInputDialog.getText(self, 'Connect Device', 'Enter device IP:') 74 | Global().communicate.status_bar.emit('Operation: Connecting canceled.', 3000) 75 | 76 | if ok and text: 77 | worker = AsyncRepositoryWorker( 78 | worker_id=self.CONNECT_WORKER_ID, 79 | name="Connecting to device", 80 | repository_method=DeviceRepository.connect, 81 | arguments=(str(text),), 82 | response_callback=self.__async_response_connect 83 | ) 84 | if Adb.worker().work(worker): 85 | Global().communicate.notification.emit( 86 | MessageData( 87 | title='Connect', 88 | body="Connecting to device via IP, please wait", 89 | message_type=MessageType.LOADING_MESSAGE, 90 | message_catcher=worker.set_loading_widget 91 | ) 92 | ) 93 | Global().communicate.status_bar.emit('Operation: %s... Please wait.' % worker.name, 3000) 94 | worker.start() 95 | 96 | @staticmethod 97 | def __async_response_disconnect(data, error): 98 | if data: 99 | Global().communicate.devices.emit() 100 | Global().communicate.notification.emit( 101 | MessageData( 102 | title="Disconnect", 103 | timeout=15000, 104 | body=data 105 | ) 106 | ) 107 | if error: 108 | Global().communicate.devices.emit() 109 | Global().communicate.notification.emit( 110 | MessageData( 111 | timeout=15000, 112 | title="Disconnect", 113 | body="" % error 114 | ) 115 | ) 116 | Global().communicate.status_bar.emit('Operation: Disconnecting finished.', 3000) 117 | 118 | @staticmethod 119 | def __async_response_connect(data, error): 120 | if data: 121 | if Adb.core == Adb.PYTHON_ADB_SHELL: 122 | Global().communicate.files.emit() 123 | elif Adb.core == Adb.EXTERNAL_TOOL_ADB: 124 | Global().communicate.devices.emit() 125 | Global().communicate.notification.emit(MessageData(title="Connecting to device", timeout=15000, body=data)) 126 | if error: 127 | Global().communicate.devices.emit() 128 | Global().communicate.notification.emit( 129 | MessageData( 130 | timeout=15000, 131 | title="Connect to device", 132 | body="%s" % error 133 | ) 134 | ) 135 | Global().communicate.status_bar.emit('Operation: Connecting to device finished.', 3000) 136 | 137 | 138 | class MainWindow(QMainWindow): 139 | def __init__(self): 140 | super(MainWindow, self).__init__() 141 | 142 | self.setMenuBar(MenuBar(self)) 143 | self.setCentralWidget(MainExplorer(self)) 144 | 145 | self.resize(640, 480) 146 | self.setMinimumSize(480, 360) 147 | self.setWindowTitle('ADB File Explorer') 148 | self.setWindowIcon(QIcon(Resources.icon_logo)) 149 | 150 | # Show Devices Widget 151 | Global().communicate.devices.emit() 152 | 153 | # Connect to Global class to use it anywhere 154 | Global().communicate.status_bar.connect(self.statusBar().showMessage) 155 | 156 | # Important to add last to stay on top! 157 | self.notification_center = NotificationCenter(self) 158 | Global().communicate.notification.connect(self.notify) 159 | 160 | # Welcome notification texts 161 | welcome_title = "Welcome to ADBFileExplorer!" 162 | welcome_body = "Here you can see the list of your connected adb devices. Click one of them to see files.
"\ 163 | "Current selected core: %s
" \ 164 | "To change it - settings.json file" % Settings.adb_core() 165 | 166 | Global().communicate.status_bar.emit('Ready', 5000) 167 | Global().communicate.notification.emit(MessageData(title=welcome_title, body=welcome_body, timeout=30000)) 168 | 169 | def notify(self, data: MessageData): 170 | message = self.notification_center.append_notification( 171 | title=data.title, 172 | body=data.body, 173 | timeout=data.timeout, 174 | message_type=data.message_type 175 | ) 176 | if data.message_catcher: 177 | data.message_catcher(message) 178 | 179 | def closeEvent(self, event): 180 | if Adb.core == Adb.EXTERNAL_TOOL_ADB: 181 | if Settings.adb_kill_server_at_exit() is None: 182 | reply = QMessageBox.question(self, 'ADB Server', "Do you want to kill adb server?", 183 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 184 | 185 | if reply == QMessageBox.Yes: 186 | Adb.stop() 187 | elif Settings.adb_kill_server_at_exit(): 188 | Adb.stop() 189 | elif Adb.core == Adb.PYTHON_ADB_SHELL: 190 | Adb.stop() 191 | 192 | event.accept() 193 | 194 | # This helps the "notification_center" maintain the place after window get resized 195 | def resizeEvent(self, e): 196 | if self.notification_center: 197 | self.notification_center.update_position() 198 | return super(MainWindow, self).resizeEvent(e) 199 | -------------------------------------------------------------------------------- /src/app/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/app/helpers/__init__.py -------------------------------------------------------------------------------- /src/app/helpers/converters.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import datetime 4 | import re 5 | from typing import List 6 | 7 | from app.data.models import Device, File, FileType 8 | 9 | 10 | # Converter to Device list 11 | # command: adb devices -l 12 | # List of attached devices 13 | # 14 | def convert_to_devices(data: str) -> List[Device]: 15 | lines = convert_to_lines(data)[1:] # Removing 'List of attached devices' 16 | 17 | devices = [] 18 | for line in lines: 19 | data = line.split() 20 | name = "Unknown Device" 21 | for i in range(2, len(data)): 22 | if data[i].startswith("model:"): 23 | name = data[i][6:] 24 | break 25 | 26 | devices.append( 27 | Device( 28 | id=data[0], 29 | name=name.replace('_', ' '), 30 | type=data[1] 31 | ) 32 | ) 33 | return devices 34 | 35 | 36 | # Converter to File object 37 | # command: adb -s shell ls -l -d 38 | # 39 | def convert_to_file(data: str) -> File: 40 | date_pattern = '%Y-%m-%d %H:%M' 41 | if re.fullmatch(r'[-dlcbsp][-rwxst]{9}\s+\d+\s+\S+\s+\S+\s*\d*,?\s+\d+\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2} .+', data): 42 | fields = data.split() 43 | 44 | size = 0 45 | date = None 46 | link = None 47 | other = None 48 | name = '' 49 | 50 | owner = fields[2] 51 | group = fields[3] 52 | permission = fields[0] 53 | file_type = int(fields[1]) 54 | 55 | code = permission[0] 56 | if ['s', 'd', '-', 'l'].__contains__(code): 57 | size = int(fields[4]) 58 | date = datetime.datetime.strptime("%s %s" % (fields[5], fields[6]), date_pattern) 59 | name = " ".join(fields[7:]) 60 | if code == 'l': 61 | name = " ".join(fields[7:fields.index('->')]) 62 | link = " ".join(fields[fields.index('->') + 1:]) 63 | elif ['c', 'b'].__contains__(code): 64 | other = fields[4] 65 | size = int(fields[5]) 66 | date = datetime.datetime.strptime("%s %s" % (fields[6], fields[7]), date_pattern) 67 | name = fields[8] 68 | 69 | if name.startswith('/'): 70 | name = name[name.rindex('/') + 1:] 71 | 72 | return File( 73 | name=name, 74 | size=size, 75 | link=link, 76 | owner=owner, 77 | group=group, 78 | other=other, 79 | date_time=date, 80 | file_type=file_type, 81 | permissions=permission, 82 | ) 83 | elif re.fullmatch(r'[-dlcbsp][-rwxst]{9}\s+\S+\s+\S+\s*\d*,?\s*\d*\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2} .*', data): 84 | fields = data.split() 85 | 86 | size = 0 87 | link = None 88 | date = None 89 | other = None 90 | name = '' 91 | 92 | permission = fields[0] 93 | owner = fields[1] 94 | group = fields[2] 95 | 96 | code = permission[0] 97 | if code == 'd' or code == 's': 98 | name = " ".join(fields[5:]) 99 | date = datetime.datetime.strptime("%s %s" % (fields[3], fields[4]), date_pattern) 100 | elif code == '-': 101 | size = int(fields[3]) 102 | name = " ".join(fields[6:]) 103 | date = datetime.datetime.strptime("%s %s" % (fields[4], fields[5]), date_pattern) 104 | elif code == 'l': 105 | name = " ".join(fields[5:fields.index('->')]) 106 | link = " ".join(fields[fields.index('->') + 1:]) 107 | date = datetime.datetime.strptime("%s %s" % (fields[3], fields[4]), date_pattern) 108 | elif code == 'c' or code == 'b': 109 | size = int(fields[4]) 110 | other = fields[3] 111 | name = " ".join(fields[7:]) 112 | date = datetime.datetime.strptime("%s %s" % (fields[5], fields[6]), date_pattern) 113 | 114 | if name.startswith('/'): 115 | name = name[name.rindex('/') + 1:] 116 | 117 | return File( 118 | name=name, 119 | link=link, 120 | size=size, 121 | owner=owner, 122 | group=group, 123 | other=other, 124 | date_time=date, 125 | permissions=permission, 126 | ) 127 | 128 | 129 | # Converter to File list (a) 130 | # command: adb -s shell ls -a -l 131 | # 132 | def convert_to_file_list_a(data: str, **kwargs) -> List[File]: 133 | lines = convert_to_lines(data) 134 | dirs = kwargs.get('dirs') 135 | path = kwargs.get('path') 136 | 137 | if lines[0].startswith('total'): 138 | lines = lines[1:] # Skip first line: total x 139 | 140 | files = [] 141 | for line in lines: 142 | re__permission = re.search(r'[-dlcbsp][-rwxst]{9}', line) 143 | re__size_datetime_name = re.search(r'\d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} .+', line) 144 | if re__permission and re__size_datetime_name: 145 | size_date_name = re__size_datetime_name[0].split(' ') 146 | if len(size_date_name) == 4 and size_date_name[3] == '.': 147 | continue 148 | elif len(size_date_name) == 4 and size_date_name[3] == '..': 149 | continue 150 | 151 | permission = re__permission[0] 152 | size = int(size_date_name[0] or 0) 153 | date_time = datetime.datetime.strptime( 154 | "%s %s" % (size_date_name[1], size_date_name[2]), '%Y-%m-%d %H:%M' 155 | ) 156 | names = size_date_name[3:] 157 | 158 | link = None 159 | link_type = None 160 | name = " ".join(names) 161 | if permission[0] == 'l': 162 | name = " ".join(names[:names.index('->')]) 163 | link = " ".join(names[names.index('->') + 1:]) 164 | link_type = FileType.FILE 165 | if dirs.__contains__(path + name + '/'): 166 | link_type = FileType.DIRECTORY 167 | files.append( 168 | File( 169 | name=name, 170 | size=size, 171 | link=link, 172 | path=(path + name), 173 | link_type=link_type, 174 | date_time=date_time, 175 | permissions=permission, 176 | ) 177 | ) 178 | return files 179 | 180 | 181 | # Converter to File list 182 | # command: adb -s ls 183 | # 184 | def convert_to_file_list_b(data: str) -> List[File]: 185 | lines = convert_to_lines(data)[2:] # Skip first two lines 186 | 187 | files = [] 188 | for line in lines: 189 | fields = line.split() 190 | octal = oct(int(fields[0], 16))[2:] 191 | 192 | size = int(fields[1], 16) 193 | name = " ".join(fields[3:]) 194 | permission = __converter_to_permissions_default__(list(octal)) 195 | date_time = datetime.datetime.utcfromtimestamp(int(fields[2], 16)) 196 | 197 | files.append( 198 | File( 199 | name=name, 200 | size=size, 201 | date_time=date_time, 202 | permissions=permission 203 | ) 204 | ) 205 | return files 206 | 207 | 208 | # Get lines from raw data 209 | def convert_to_lines(data: str) -> List[str]: 210 | if not data: 211 | return list() 212 | 213 | lines = re.split(r'\n', data) 214 | for index, line in enumerate(lines): 215 | regex = re.compile(r'[\r\t]') 216 | lines[index] = regex.sub('', lines[index]) 217 | filtered = filter(bool, lines) 218 | return list(filtered) 219 | 220 | 221 | # Converting octal data to normal permissions' field 222 | # Created for: convert_to_file_list_b() 223 | # 100777 (.8) ---> '- rwx rwx rwx' (str) 224 | def __converter_to_permissions_default__(octal_data: list) -> str: 225 | permission = ( 226 | ['-', '-', '-'], 227 | ['-', '-', 'x'], 228 | ['-', 'w', '-'], 229 | ['-', 'w', 'x'], 230 | ['r', '-', '-'], 231 | ['r', '-', 'x'], 232 | ['r', 'w', '-'], 233 | ['r', 'w', 'x'] 234 | ) 235 | 236 | dir_mode = { 237 | 0: '', 238 | 1: 'p', 239 | 2: 'c', 240 | 4: 'd', 241 | 6: 'b', 242 | } 243 | 244 | file_mode = { 245 | 0: '-', 246 | 2: 'l', 247 | 4: 's', 248 | } 249 | 250 | octal_data.reverse() 251 | for i in range(0, len(octal_data)): 252 | octal_data[i] = int(octal_data[i]) 253 | octal_data.extend([0] * (8 - len(octal_data))) 254 | 255 | others = permission[octal_data[0]] 256 | group = permission[octal_data[1]] 257 | owner = permission[octal_data[2]] 258 | if octal_data[3] == 1: 259 | others[2] = 't' 260 | elif octal_data[3] == 2: 261 | others[1] = 's' 262 | elif octal_data[3] == 4: 263 | others[0] = 's' 264 | 265 | if octal_data[5] == 0: 266 | file_type = dir_mode.get(octal_data[4]) 267 | else: 268 | file_type = file_mode.get(octal_data[4]) 269 | 270 | permissions = [file_type] + owner + group + others 271 | return "".join(permissions) 272 | -------------------------------------------------------------------------------- /src/app/helpers/tools.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | import json 4 | import logging 5 | import os 6 | import shutil 7 | import subprocess 8 | 9 | from PyQt5 import QtCore 10 | from PyQt5.QtCore import QThread, QObject, QFile, QIODevice, QTextStream 11 | from PyQt5.QtWidgets import QWidget 12 | from adb_shell.auth.keygen import keygen 13 | from adb_shell.auth.sign_pythonrsa import PythonRSASigner 14 | 15 | from app.data.models import MessageData 16 | 17 | 18 | class CommonProcess: 19 | """ 20 | CommonProcess - executes subprocess then saves output data and exit code. 21 | If 'stdout_callback' is defined then every output data line will call this function 22 | 23 | Keyword arguments: 24 | arguments -- array list of arguments 25 | stdout -- define stdout (default subprocess.PIPE) 26 | stdout_callback -- callable function, params: (data: str) -> None (default None) 27 | """ 28 | 29 | def __init__(self, arguments: list, stdout=subprocess.PIPE, stdout_callback: callable = None): 30 | self.ErrorData = None 31 | self.OutputData = None 32 | self.IsSuccessful = False 33 | if arguments: 34 | try: 35 | process = subprocess.Popen(arguments, stdout=stdout, stderr=subprocess.PIPE) 36 | if stdout == subprocess.PIPE and stdout_callback: 37 | for line in iter(process.stdout.readline, b''): 38 | stdout_callback(line.decode(encoding='utf-8')) 39 | data, error = process.communicate() 40 | self.ExitCode = process.poll() 41 | self.IsSuccessful = self.ExitCode == 0 42 | self.ErrorData = error.decode(encoding='utf-8') if error else None 43 | self.OutputData = data.decode(encoding='utf-8') if data else None 44 | except FileNotFoundError: 45 | self.ErrorData = "Command '%s' failed! File (command) '%s' not found!" % \ 46 | (' '.join(arguments), arguments[0]) 47 | except BaseException as error: 48 | logging.exception("Unexpected error=%s, type(error)=%s" % (error, type(error))) 49 | self.ErrorData = str(error) 50 | 51 | 52 | class AsyncRepositoryWorker(QThread): 53 | on_response = QtCore.pyqtSignal(object, object) # Response : data, error 54 | 55 | def __init__( 56 | self, worker_id: int, name: str, 57 | repository_method: callable, 58 | arguments: tuple, response_callback: callable 59 | ): 60 | super(AsyncRepositoryWorker, self).__init__() 61 | self.on_response.connect(response_callback) 62 | self.finished.connect(self.close) 63 | 64 | self.__repository_method = repository_method 65 | self.__arguments = arguments 66 | self.loading_widget = None 67 | self.closed = False 68 | self.id = worker_id 69 | self.name = name 70 | 71 | def run(self): 72 | data, error = self.__repository_method(*self.__arguments) 73 | self.on_response.emit(data, error) 74 | 75 | def close(self): 76 | if self.loading_widget: 77 | self.loading_widget.close() 78 | self.deleteLater() 79 | self.closed = True 80 | 81 | def set_loading_widget(self, widget: QWidget): 82 | self.loading_widget = widget 83 | 84 | def update_loading_widget(self, path, progress): 85 | if self.loading_widget and not self.closed: 86 | self.loading_widget.update_progress('SOURCE: %s' % path, progress) 87 | 88 | 89 | class ProgressCallbackHelper(QObject): 90 | progress_callback = QtCore.pyqtSignal(str, int) 91 | 92 | def setup(self, parent: QObject, callback: callable): 93 | self.setParent(parent) 94 | self.progress_callback.connect(callback) 95 | 96 | 97 | class Communicate(QObject): 98 | files = QtCore.pyqtSignal() 99 | devices = QtCore.pyqtSignal() 100 | 101 | up = QtCore.pyqtSignal() 102 | files__refresh = QtCore.pyqtSignal() 103 | path_toolbar__refresh = QtCore.pyqtSignal() 104 | 105 | status_bar = QtCore.pyqtSignal(str, int) # Message, Duration 106 | notification = QtCore.pyqtSignal(MessageData) 107 | 108 | 109 | class Singleton(type): 110 | _instances = {} 111 | 112 | def __call__(cls, *args, **kwargs): 113 | if cls not in cls._instances: 114 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 115 | return cls._instances[cls] 116 | 117 | 118 | def get_python_rsa_keys_signer(rerun=True) -> PythonRSASigner: 119 | privkey = os.path.expanduser('~/.android/adbkey') 120 | if os.path.isfile(privkey): 121 | with open(privkey) as f: 122 | private = f.read() 123 | pubkey = privkey + '.pub' 124 | if not os.path.isfile(pubkey): 125 | if shutil.which('ssh-keygen'): 126 | os.system(f'ssh-keygen -y -f {privkey} > {pubkey}') 127 | else: 128 | raise OSError('Could not call ssh-keygen!') 129 | with open(pubkey) as f: 130 | public = f.read() 131 | return PythonRSASigner(public, private) 132 | elif rerun: 133 | path = os.path.expanduser('~/.android') 134 | if not os.path.isfile(path): 135 | if not os.path.isdir(path): 136 | os.mkdir(path) 137 | keygen(key) 138 | return get_python_rsa_keys_signer(False) 139 | 140 | 141 | def read_string_from_file(path: str): 142 | file = QFile(path) 143 | if file.open(QIODevice.ReadOnly | QIODevice.Text): 144 | text = QTextStream(file).readAll() 145 | file.close() 146 | return text 147 | return str() 148 | 149 | 150 | def quote_file_name(path: str): 151 | return '\'' + path + '\'' 152 | 153 | 154 | def json_to_dict(path: str): 155 | try: 156 | return dict(json.loads(read_string_from_file(path))) 157 | except BaseException as exception: 158 | logging.error('File %s. %s' % (path, exception)) 159 | return dict() 160 | -------------------------------------------------------------------------------- /src/app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/app/services/__init__.py -------------------------------------------------------------------------------- /src/app/services/adb.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer 2 | # Copyright (C) 2022 Azat Aldeshov 3 | from app.core.configurations import Settings 4 | from app.helpers.tools import CommonProcess 5 | 6 | ADB_PATH = Settings.adb_path() 7 | RUN_AS_ROOT = Settings.adb_run_as_root() 8 | PRESERVE_TIMESTAMP = Settings.preserve_timestamp() 9 | 10 | 11 | class Parameter: 12 | ROOT = 'root' 13 | DEVICE = '-s' 14 | PULL = 'pull' 15 | PUSH = 'push' 16 | SHELL = 'shell' 17 | CONNECT = 'connect' 18 | HELP = '--help' 19 | VERSION = '--version' 20 | DEVICES = 'devices' 21 | DEVICES_LONG = '-l' 22 | PRESERVE_TIMESTAMP = '-a' 23 | DISCONNECT = 'disconnect' 24 | START_SERVER = 'start-server' 25 | KILL_SERVER = 'kill-server' 26 | 27 | 28 | class ShellCommand: 29 | LS = 'ls' 30 | LS_ALL = [LS, '-a'] 31 | LS_DIRS = [LS, '-d'] 32 | LS_LIST = [LS, '-l'] 33 | LS_LIST_DIRS = [LS, '-l', '-d'] 34 | LS_ALL_DIRS = [LS, '-a', '-d'] 35 | LS_ALL_LIST = [LS, '-a', '-l'] 36 | LS_ALL_LIST_DIRS = [LS, '-a', '-l', '-d'] 37 | LS_VERSION = [LS, '--version'] 38 | 39 | CP = 'cp' 40 | MV = 'mv' 41 | RM = 'rm' 42 | RM_DIR = [RM, '-r'] 43 | RM_DIR_FORCE = [RM, '-r', '-f'] 44 | 45 | GETPROP = 'getprop' 46 | GETPROP_PRODUCT_MODEL = [GETPROP, 'ro.product.model'] 47 | 48 | MKDIR = 'mkdir' 49 | 50 | CAT = 'cat' 51 | 52 | 53 | def validate(): 54 | return version().IsSuccessful 55 | 56 | 57 | def version(): 58 | return CommonProcess([ADB_PATH, Parameter.VERSION]) 59 | 60 | 61 | def devices(): 62 | return CommonProcess([ADB_PATH, Parameter.DEVICES, Parameter.DEVICES_LONG]) 63 | 64 | 65 | def start_server(): 66 | return CommonProcess([ADB_PATH, Parameter.START_SERVER]) 67 | 68 | 69 | def kill_server(): 70 | return CommonProcess([ADB_PATH, Parameter.KILL_SERVER]) 71 | 72 | 73 | def connect(device_id: str): 74 | return CommonProcess([ADB_PATH, Parameter.CONNECT, device_id]) 75 | 76 | 77 | def disconnect(): 78 | return CommonProcess([ADB_PATH, Parameter.DISCONNECT]) 79 | 80 | 81 | def pull(device_id: str, source_path: str, destination_path: str, stdout_callback: callable): 82 | pull_options = [Parameter.PULL, Parameter.PRESERVE_TIMESTAMP] if PRESERVE_TIMESTAMP else [Parameter.PULL] 83 | args = [ADB_PATH, Parameter.DEVICE, device_id, *pull_options, source_path, destination_path] 84 | return CommonProcess(arguments=args, stdout_callback=stdout_callback) 85 | 86 | 87 | def push(device_id: str, source_path: str, destination_path: str, stdout_callback: callable): 88 | args = [ADB_PATH, Parameter.DEVICE, device_id, Parameter.PUSH, source_path, destination_path] 89 | return CommonProcess(arguments=args, stdout_callback=stdout_callback) 90 | 91 | 92 | def shell(device_id: str, args: list): 93 | if RUN_AS_ROOT: 94 | return CommonProcess([ADB_PATH, Parameter.DEVICE, device_id, Parameter.ROOT] + args) 95 | return CommonProcess([ADB_PATH, Parameter.DEVICE, device_id, Parameter.SHELL] + args) 96 | 97 | 98 | def file_list(device_id: str, path: str): 99 | return CommonProcess([ADB_PATH, Parameter.DEVICE, device_id, ShellCommand.LS, path]) 100 | 101 | 102 | def read_file(device_id: str, path: str): 103 | return CommonProcess([ADB_PATH, Parameter.DEVICE, device_id, ShellCommand.CAT, path]) 104 | -------------------------------------------------------------------------------- /src/app/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "adb_path": "adb", 3 | "adb_core": "external", 4 | "adb_kill_server_at_exit": false, 5 | "preserve_timestamp": true, 6 | "adb_run_as_root": false 7 | } 8 | -------------------------------------------------------------------------------- /src/app/test.py: -------------------------------------------------------------------------------- 1 | # ADB File Explorer (python) 2 | # Copyright (C) 2022 Azat Aldeshov 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # from adb_shell.adb_device import AdbDeviceUsb 18 | # 19 | # from helpers.tools import get_python_rsa_keys_signer 20 | # 21 | # device = AdbDeviceUsb() 22 | # device.connect(rsa_keys=[get_python_rsa_keys_signer()], auth_timeout_s=0.1) 23 | # 24 | # 25 | # if __name__ == "__main__": 26 | # files = device.list('/sdcard/Download/') 27 | # for file in files: 28 | # print(file) 29 | import sys 30 | 31 | from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout 32 | 33 | from data.models import MessageType 34 | from app.gui.notification import NotificationCenter 35 | 36 | 37 | class NotifyExample(QWidget): 38 | def __init__(self): 39 | super().__init__() 40 | self.counter = 0 41 | self.setLayout(QGridLayout(self)) 42 | 43 | button_notify = QPushButton('Notify', self) 44 | button_notify.clicked.connect(self.notify) 45 | self.layout().addWidget(button_notify) 46 | 47 | self.setMinimumSize(640, 480) 48 | self.notification_center = NotificationCenter(self) 49 | self.notification_center.setStyleSheet("background: #00FF00") # Scroll Area Test 50 | 51 | def notify(self): 52 | if self.counter % 2 == 0: 53 | text = "Lorem ipsum dolor sit amet, consecrate disciplining elit," \ 54 | "sed do usermod tempor incident ut labor et color magna aliquot." \ 55 | "Ut enum ad minim venial, quits nostrum excitation McCull-och labors" \ 56 | "nisei ut aliquot ex ea common consequent. Dis auto inure dolor in" \ 57 | "reprehend in voluptuary valid esse cilium color eu fugit null" \ 58 | "paginator. Except saint toccata cupidity non president, sunt in gulp" \ 59 | "qui official underused moll-it anim id est labor." 60 | self.notification_center.append_notification(title="Message TEST", body=text, timeout=10000) 61 | elif self.counter % 2 == 1: 62 | self.notification_center.append_notification( 63 | title="Message", 64 | body="Lorem ipsum dolor sit amet", 65 | message_type=MessageType.LOADING_MESSAGE 66 | ) 67 | self.counter = self.counter + 1 68 | 69 | def resizeEvent(self, e): 70 | if self.notification_center: 71 | self.notification_center.update_position() 72 | return super().resizeEvent(e) 73 | 74 | 75 | if __name__ == "__main__": 76 | app = QApplication(sys.argv) 77 | example = NotifyExample() 78 | example.show() 79 | app.exec_() 80 | -------------------------------------------------------------------------------- /src/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/__init__.py -------------------------------------------------------------------------------- /src/resources/anim/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/anim/__init__.py -------------------------------------------------------------------------------- /src/resources/anim/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/anim/loading.gif -------------------------------------------------------------------------------- /src/resources/icons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/icons/__init__.py -------------------------------------------------------------------------------- /src/resources/icons/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/resources/icons/files/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/icons/files/__init__.py -------------------------------------------------------------------------------- /src/resources/icons/files/actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/icons/files/actions/__init__.py -------------------------------------------------------------------------------- /src/resources/icons/files/actions/files_upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/resources/icons/files/actions/folder_create.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/resources/icons/files/actions/folder_upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/resources/icons/files/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/resources/icons/files/file_unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/resources/icons/files/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/resources/icons/files/link_file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/resources/icons/files/link_file_unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/resources/icons/files/link_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/resources/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/resources/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 29 | -------------------------------------------------------------------------------- /src/resources/icons/no_link.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/resources/icons/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/resources/icons/phone_unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /src/resources/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/up.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/styles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aldeshov/ADBFileExplorer/baba3771115046b136fb8e675b923e63b8e920eb/src/resources/styles/__init__.py -------------------------------------------------------------------------------- /src/resources/styles/device-list.qss: -------------------------------------------------------------------------------- 1 | QListView { 2 | outline: 0; 3 | background-color: #F0F0F0; 4 | } 5 | 6 | QListView::item{ 7 | background-color: #E6E6E6; 8 | } 9 | 10 | QListView::item:hover{ 11 | background-color: #D6D6D6; 12 | } 13 | 14 | QListView::item:active:selected{ 15 | color: black; 16 | background-color: #C1C1C1; 17 | } -------------------------------------------------------------------------------- /src/resources/styles/file-list.qss: -------------------------------------------------------------------------------- 1 | QListView { 2 | outline: 0; 3 | background-color: #F0F0F0; 4 | } 5 | 6 | QListView::item{ 7 | background-color: #E6E6E6; 8 | } 9 | 10 | QListView::item:hover{ 11 | background-color: #D6D6D6; 12 | } 13 | 14 | QListView::item:active:selected{ 15 | color: black; 16 | background-color: #C1C1C1; 17 | border: 1px solid #666666; 18 | } 19 | 20 | QListView::item:active:!selected{ 21 | border: 1px solid #AAAAAA; 22 | } -------------------------------------------------------------------------------- /src/resources/styles/notification-button.qss: -------------------------------------------------------------------------------- 1 | QPushButton#close { 2 | background-color: #c4c8c0; 3 | border: 0; 4 | } 5 | 6 | QPushButton#close:hover { 7 | background-color: #AFA8FA; 8 | border: 0; 9 | } 10 | 11 | QPushButton#close:hover:!pressed { 12 | border: 1px solid #584FBA; 13 | } 14 | 15 | QPushButton#close:pressed { 16 | background-color: #7C74DA; 17 | } -------------------------------------------------------------------------------- /src/resources/styles/window.qss: -------------------------------------------------------------------------------- 1 | QWidget { 2 | font-family: url("-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"); 3 | } --------------------------------------------------------------------------------