├── .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 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
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 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | 1665348827311
102 |
103 |
104 | 1665348827311
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | extension-pkg-whitelist=PyQt5
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ADB File Explorer
2 |
3 | 
4 | [](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 |
20 |
21 | Files
22 |
23 |
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 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/close.svg:
--------------------------------------------------------------------------------
1 |
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 |
7 |
--------------------------------------------------------------------------------
/src/resources/icons/files/actions/folder_create.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/resources/icons/files/actions/folder_upload.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/resources/icons/files/file.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/resources/icons/files/file_unknown.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/resources/icons/files/folder.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/resources/icons/files/link_file.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/resources/icons/files/link_file_unknown.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/resources/icons/files/link_folder.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/resources/icons/link.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/resources/icons/logo.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/src/resources/icons/no_link.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/resources/icons/phone.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/resources/icons/phone_unknown.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/resources/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/up.svg:
--------------------------------------------------------------------------------
1 |
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 | }
--------------------------------------------------------------------------------