├── .gitignore
├── AndroidDrive-install.exe
├── AndroidDrive-portable.zip
├── LICENSE
├── README.md
├── dependencies
├── AdbWinApi.dll
├── AdbWinUsbApi.dll
├── D3Dcompiler_47.dll
├── Qt6Core.dll
├── Qt6Gui.dll
├── Qt6Network.dll
├── Qt6Pdf.dll
├── Qt6Svg.dll
├── Qt6Widgets.dll
├── adb.exe
├── dokan2.dll
├── generic
│ └── qtuiotouchplugin.dll
├── iconengines
│ └── qsvgicon.dll
├── imageformats
│ ├── qgif.dll
│ ├── qicns.dll
│ ├── qico.dll
│ ├── qjpeg.dll
│ ├── qpdf.dll
│ ├── qsvg.dll
│ ├── qtga.dll
│ ├── qtiff.dll
│ ├── qwbmp.dll
│ └── qwebp.dll
├── networkinformation
│ └── qnetworklistmanager.dll
├── opengl32sw.dll
├── platforms
│ └── qwindows.dll
├── styles
│ └── qwindowsvistastyle.dll
└── tls
│ ├── qcertonlybackend.dll
│ ├── qopensslbackend.dll
│ └── qschannelbackend.dll
└── sources
├── AndroidDrive.pro
├── androiddevice.cpp
├── androiddevice.hpp
├── androiddrive-install-languages.iss
├── androiddrive-install.iss
├── androiddrive.cpp
├── androiddrive.hpp
├── debuglogger.cpp
├── debuglogger.hpp
├── devicelistmodel.cpp
├── devicelistmodel.hpp
├── devicelistwindow.cpp
├── devicelistwindow.hpp
├── dokanoperations.cpp
├── dokanoperations.hpp
├── drive.svg
├── helperfunctions.cpp
├── helperfunctions.hpp
├── icon.ico
├── icon.svg
├── license.rtf
├── main.cpp
├── phone.svg
├── resource.qrc
├── resource.rc
├── settings.cpp
├── settings.hpp
├── settingswindow.cpp
├── settingswindow.hpp
├── systemdrive.svg
├── temporaryfile.cpp
├── temporaryfile.hpp
├── translations
├── androiddrive_de.qm
├── androiddrive_de.ts
├── androiddrive_empty.ts
├── androiddrive_fr.qm
├── androiddrive_fr.ts
├── androiddrive_hu.qm
├── androiddrive_hu.ts
├── androiddrive_it.qm
├── androiddrive_it.ts
├── androiddrive_sv.qm
├── androiddrive_sv.ts
├── contribute.md
├── qtbase_de.qm
├── qtbase_fr.qm
├── qtbase_hu.qm
├── qtbase_it.qm
└── qtbase_sv.qm
├── updates.cpp
├── updates.hpp
└── version.h
/.gitignore:
--------------------------------------------------------------------------------
1 | /build/
2 | *.pro.user
3 | *.pro.user.*
4 |
--------------------------------------------------------------------------------
/AndroidDrive-install.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/AndroidDrive-install.exe
--------------------------------------------------------------------------------
/AndroidDrive-portable.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/AndroidDrive-portable.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AndroidDrive
2 | AndroidDrive is a program that allows mounting Android smartphones as drives on Windows. It does not require rooting.
3 |
4 |
5 |
6 |
7 |
8 |
9 | # Setup
10 | To be able to use AndroidDrive, you need to do three things (the order in which you do them doesn't matter):
11 | - **Install AndroidDrive on your Windows computer**: Either use the [installation program](https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/main/AndroidDrive-install.exe), or download and extract the [zip file](https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/main/AndroidDrive-portable.zip). If you want to start AndroidDrive when Windows starts, create a shortcut to AndroidDrive in `%appdata%\Microsoft\Windows\Start Menu\Programs\Startup`.
12 |
13 | - **Install Dokan on your Windows computer**: The easiest way is to use their [installation program](https://github.com/dokan-dev/dokany/releases/download/v2.0.6.1000/DokanSetup.exe) (this is the installation program for Dokan 2.0.6, which is the version AndroidDrive was tested on). If you prefer, you can find other ways to install Dokan in [their documentation](https://github.com/dokan-dev/dokany/wiki/Installation).
14 |
15 | - **Enable USB debugging on your Android device**: For AndroidDrive to be able to detect and interact with your Android device, you need to enable USB debugging. To do so, follow these steps:
16 |
17 | 1. Open Settings on your Android device.
18 | 2. Go to "About phone".
19 | 3. Scroll down to the bottom and tap "Build Number" seven times until you get a message saying that you're a developer (on some phones, "Build Number" might be under "Software Information").
20 | 4. Go back to the main settings screen, then go to System > Advanced > Developer Settings and enable USB debugging.
21 |
22 | # Usage
23 | When AndroidDrive is running and you connect an Android device, AndroidDrive will automatically mount a drive containing that device's internal storage.
24 |
25 | If you *don't* want AndroidDrive to mound a drive for a specific Android device, you can right click on the AndroidDrive icon in the task bar, go to Devices and click "Device settings", then uncheck "Automatically connect drive". To actually disconnect it, you also need to click "Disconnect drive" in the Devices window. You can also temporarily disconnect a drive by clicking "Disconnect drive" without changing the device settings.
26 |
27 | When AndroidDrive detects a new Android device, it will automatically be assigned the first available drive letter after C (for example if your only drive is the hard drive, this will mean that it will be assigned the letter D). You can change the drive letter assigned to a specific Android device by right clicking on the AndroidDrive icon in the task bar, going to Devices, clicking "Device settings" and selecting a drive letter under "Drive letter". If you do this to a drive that's already connected, you will need to disconnect and re-connect the drive for the changes to take effect.
28 |
29 |
30 | # Files app
31 | I've also created a [Files app for Android](https://play.google.com/store/apps/details?id=io.github.gustavlindberg99.files) that you can install on your Android device. This app and AndroidDrive can be used independently (none of the two is necessary for the other one to work), but this app is made specifically to work well with AndroidDrive.
32 |
33 | For example, you can change the icon of a folder in the app and the new icon will be visible on your computer on the drive connected with AndroidDrive. You can also create a shortcut (LNK file) on the Android drive on with your computer and use the shortcut on your phone.
34 |
35 |
36 | # Languages
37 |
38 | AndroidDrive is currently available in the following languages:
39 |
40 | * English
41 | * French
42 | * German (translation by [flaviusgh](https://github.com/flaviusgh))
43 | * Hungarian (translation by [gidano](https://github.com/gidano))
44 | * Italian (translation by [bovirus](https://github.com/bovirus))
45 | * Swedish
46 |
47 | If your language is not listed above and you would like to help translate it, you can find instructions for how to do that [here](https://github.com/GustavLindberg99/AndroidDrive/blob/main/sources/translations/contribute.md).
48 |
49 |
50 | # Compatibility
51 |
52 | AndroidDrive works on any 64-bit computer with Windows 10 or later.
53 |
54 | Windows 7 is no longer supported since upgrading to Qt 6, but if you want it to work on Windows 7, you can download an older version of AndroidDrive (version 2.0.6) as an installer [here](https://github.com/GustavLindberg99/AndroidDrive/raw/a36e464a665bafd11866644507b5e900ef8c0e90/AndroidDrive-setup.exe) or a ZIP file [here](https://github.com/GustavLindberg99/AndroidDrive/raw/a36e464a665bafd11866644507b5e900ef8c0e90/AndroidDrive.zip). Note that this is an old version of AndroidDrive, so it doesn't have the latest features and won't be updated. If you have Windows 10 or later, it's highly recommended that you instead use the latest version as described in the Setup section above.
55 |
56 |
57 | # FAQ
58 |
59 | ## AndroidDrive is slow and/or uses a lot of CPU
60 |
61 | What's happening is that Windows Explorer is waiting for the driver to respond, and the driver needs to make requests to ADB, which is slower than just reading a regular hard drive. So unfortunately there is no way to make it as fast as a hard drive.
62 |
63 | There are already a few optimizations so that it isn't too slow, and if I find ways to optimize it more in the future I will do so. But there is no easy fix that I can do right now to make it faster.
64 |
65 | ## AndroidDrive isn't detecting my device
66 |
67 | First of all, make sure that you enabled USB debugging as described above under "Setup" (by following all the steps 1-4, just enabling developer options isn't enough).
68 |
69 | If you're still having issues, try running this in the command prompt:
70 |
71 | ```
72 | cd "C:\Program Files\AndroidDrive"
73 | adb.exe devices
74 | ```
75 |
76 | If your device isn't listed in the command prompt after running this, that's a problem with ADB, not AndroidDrive. You may be able to find solutions [here](https://stackoverflow.com/q/21170392/4284627).
77 |
78 | If your device *is* listed in the command prompt when running the commands above but *not* in AndroidDrive, you can report that as a bug [here](https://github.com/GustavLindberg99/AndroidDrive/issues/new/choose). If you do, please include the output that you got when running `adb.exe devices`.
79 |
80 | ## Can I use AndroidDrive together with file recovery software to recover deleted files on my phone?
81 |
82 | No, unfortunately that won't work. File recovery software works by reading parts of the disk that aren't currently assigned to any file. AndroidDrive doesn't have direct access to a disk, instead it receives requests to read from files from Dokan which it forwards to ADB through specific commands that can only read files that currently exist. So there's no way for AndroidDrive to access parts of the disk that aren't written to.
83 |
84 | To recover permanently deleted files on your phone, you would probably need to root your phone and find a file recovery tool that uses the Linux command line (since Android is Linux-based). You can then access the Linux command line on your phone through ADB by running `adb.exe shell enter command here` in the Windows command line.
85 |
86 |
87 | # Credits
88 |
89 | Icons from https://www.iconfinder.com/ are made by [Alpár-Etele Méder](https://www.iconfinder.com/pocike) and [Tango](https://www.iconfinder.com/iconsets/tango-icon-library).
90 |
91 | This program uses [Qt](https://www.qt.io/), [ADB](https://android.googlesource.com/platform/packages/modules/adb/) and [Dokan](https://dokan-dev.github.io/).
92 |
--------------------------------------------------------------------------------
/dependencies/AdbWinApi.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/AdbWinApi.dll
--------------------------------------------------------------------------------
/dependencies/AdbWinUsbApi.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/AdbWinUsbApi.dll
--------------------------------------------------------------------------------
/dependencies/D3Dcompiler_47.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/D3Dcompiler_47.dll
--------------------------------------------------------------------------------
/dependencies/Qt6Core.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/Qt6Core.dll
--------------------------------------------------------------------------------
/dependencies/Qt6Gui.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/Qt6Gui.dll
--------------------------------------------------------------------------------
/dependencies/Qt6Network.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/Qt6Network.dll
--------------------------------------------------------------------------------
/dependencies/Qt6Pdf.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/Qt6Pdf.dll
--------------------------------------------------------------------------------
/dependencies/Qt6Svg.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/Qt6Svg.dll
--------------------------------------------------------------------------------
/dependencies/Qt6Widgets.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/Qt6Widgets.dll
--------------------------------------------------------------------------------
/dependencies/adb.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/adb.exe
--------------------------------------------------------------------------------
/dependencies/dokan2.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/dokan2.dll
--------------------------------------------------------------------------------
/dependencies/generic/qtuiotouchplugin.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/generic/qtuiotouchplugin.dll
--------------------------------------------------------------------------------
/dependencies/iconengines/qsvgicon.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/iconengines/qsvgicon.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qgif.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qgif.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qicns.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qicns.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qico.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qico.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qjpeg.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qjpeg.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qpdf.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qpdf.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qsvg.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qsvg.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qtga.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qtga.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qtiff.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qtiff.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qwbmp.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qwbmp.dll
--------------------------------------------------------------------------------
/dependencies/imageformats/qwebp.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/imageformats/qwebp.dll
--------------------------------------------------------------------------------
/dependencies/networkinformation/qnetworklistmanager.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/networkinformation/qnetworklistmanager.dll
--------------------------------------------------------------------------------
/dependencies/opengl32sw.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/opengl32sw.dll
--------------------------------------------------------------------------------
/dependencies/platforms/qwindows.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/platforms/qwindows.dll
--------------------------------------------------------------------------------
/dependencies/styles/qwindowsvistastyle.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/styles/qwindowsvistastyle.dll
--------------------------------------------------------------------------------
/dependencies/tls/qcertonlybackend.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/tls/qcertonlybackend.dll
--------------------------------------------------------------------------------
/dependencies/tls/qopensslbackend.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/tls/qopensslbackend.dll
--------------------------------------------------------------------------------
/dependencies/tls/qschannelbackend.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/dependencies/tls/qschannelbackend.dll
--------------------------------------------------------------------------------
/sources/AndroidDrive.pro:
--------------------------------------------------------------------------------
1 | QT += widgets
2 | QT += network
3 |
4 | CONFIG += c++20
5 |
6 | SOURCES += main.cpp \
7 | androiddevice.cpp \
8 | androiddrive.cpp \
9 | debuglogger.cpp \
10 | devicelistmodel.cpp \
11 | devicelistwindow.cpp \
12 | dokanoperations.cpp \
13 | helperfunctions.cpp \
14 | settings.cpp \
15 | settingswindow.cpp \
16 | temporaryfile.cpp \
17 | updates.cpp
18 |
19 | HEADERS += \
20 | androiddevice.hpp \
21 | androiddrive.hpp \
22 | debuglogger.hpp \
23 | devicelistmodel.hpp \
24 | devicelistwindow.hpp \
25 | dokanoperations.hpp \
26 | helperfunctions.hpp \
27 | settings.hpp \
28 | settingswindow.hpp \
29 | temporaryfile.hpp \
30 | updates.hpp \
31 | version.h
32 |
33 | TRANSLATIONS = \
34 | translations/androiddrive_de.ts\
35 | translations/androiddrive_empty.ts\
36 | translations/androiddrive_fr.ts\
37 | translations/androiddrive_hu.ts\
38 | translations/androiddrive_it.ts\
39 | translations/androiddrive_sv.ts
40 |
41 | INCLUDEPATH += "C:/Program Files/Dokan/Dokan Library-2.0.6/include"
42 | LIBS += \
43 | -L"C:/Program Files/Dokan/Dokan Library-2.0.6/lib" -ldokan2 \
44 | -L"C:/Program Files (x86)/Windows Kits/10/Lib/10.0.22000.0/um/x64" -lAdvAPI32 -luser32
45 |
46 | win32:RC_FILE = resource.rc
47 |
48 | RESOURCES += \
49 | resource.qrc
50 |
--------------------------------------------------------------------------------
/sources/androiddevice.cpp:
--------------------------------------------------------------------------------
1 | #include "androiddevice.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #include "androiddrive.hpp"
10 | #include "debuglogger.hpp"
11 |
12 | QList AndroidDevice::_instances;
13 | bool AndroidDevice::_quitOnLastDeletedDevice = false;
14 |
15 | AndroidDevice::AndroidDevice(const QString &serialNumber):
16 | _serialNumber(serialNumber)
17 | {
18 | AndroidDevice::_instances.push_back(this);
19 |
20 | DebugLogger::getInstance().log("Constructing device '{}'", serialNumber);
21 |
22 | //Get the paths to all the SD cards
23 | const QString internalStoragePath = this->runAdbCommand("realpath /sdcard");
24 | DebugLogger::getInstance().log("Device '{}': Internal storage path: {}", std::make_tuple(serialNumber, internalStoragePath));
25 | static const QRegularExpression newlineRegex("[\r\n]+"), spaceRegex("\\s+");
26 | const QString internalStorageDfOutput = this->runAdbCommand("df /sdcard");
27 | DebugLogger::getInstance().log("Device '{}': output of `df /sdcard`: {}", std::make_tuple(serialNumber, internalStorageDfOutput));
28 | const QStringList splittedInternalStorageDfOutput = internalStorageDfOutput.split(newlineRegex);
29 | if(splittedInternalStorageDfOutput.size() > 1){
30 | const QString storageFilesystem = splittedInternalStorageDfOutput[1].split(spaceRegex)[0];
31 | DebugLogger::getInstance().log("Device '{}': Storage file system: {}", std::make_tuple(serialNumber, storageFilesystem));
32 | const QStringList allDfOutput = this->runAdbCommand("df").split(newlineRegex);
33 | for(const QString &dfOutput: allDfOutput){
34 | const QStringList values = dfOutput.split(spaceRegex);
35 | if(values.size() > 5){
36 | DebugLogger::getInstance().log("Device '{}': Reading df output line {}", std::make_tuple(serialNumber, dfOutput));
37 | const QString filesystem = values[0];
38 | static const QRegularExpression storageRegex("^/mnt/.*media");
39 | if(filesystem != storageFilesystem && !filesystem.contains(storageRegex)){
40 | DebugLogger::getInstance().log("Device '{}': File system '{}' does not match regex", std::make_tuple(serialNumber, filesystem));
41 | continue;
42 | }
43 | QString androidPath = values[5];
44 | if(internalStoragePath.contains(androidPath)){
45 | androidPath = "/sdcard";
46 | }
47 | DebugLogger::getInstance().log("Device '{}': Adding drive at Android path {}", std::make_tuple(serialNumber, androidPath));
48 | this->addDrive(androidPath);
49 | }
50 | else{
51 | DebugLogger::getInstance().log("Device '{}': Skipping df output line {}", std::make_tuple(serialNumber, dfOutput));
52 | }
53 | }
54 | }
55 |
56 | //If the above failed, just add /sdcard
57 | if(this->_drives.empty()){
58 | DebugLogger::getInstance().log("Device '{}': df output failed, adding /sdcard", serialNumber);
59 | this->addDrive("/sdcard");
60 | }
61 | }
62 |
63 | AndroidDevice::~AndroidDevice(){
64 | DebugLogger::getInstance().log("Deleting device '{}'", this->serialNumber());
65 | AndroidDevice::_instances.removeAll(this);
66 | if(AndroidDevice::_instances.isEmpty() && AndroidDevice::_quitOnLastDeletedDevice){
67 | DebugLogger::getInstance().log("Quitting");
68 | qApp->quit();
69 | }
70 | }
71 |
72 | void AndroidDevice::quitOnLastDeletedDevice(){
73 | DebugLogger::getInstance().log("Will quit on last deleted device");
74 | AndroidDevice::_quitOnLastDeletedDevice = true;
75 | if(AndroidDevice::_instances.isEmpty()){
76 | DebugLogger::getInstance().log("Quitting because device list is empty");
77 | qApp->quit();
78 | }
79 | }
80 |
81 | void AndroidDevice::connectAllDrives(){
82 | Settings settings;
83 | for(const std::unique_ptr &drive: this->_drives){
84 | DebugLogger::getInstance().log("Connecting drive '{}'", drive.get());
85 | drive->connectDrive(settings.driveLetter(drive.get()), this->shared_from_this());
86 | }
87 | }
88 |
89 | void AndroidDevice::autoconnectAllDrives(){
90 | Settings settings;
91 | for(const std::unique_ptr &drive: this->_drives){
92 | if(settings.autoConnect(drive.get())){
93 | DebugLogger::getInstance().log("Autoconnecting drive '{}'", drive.get());
94 | drive->connectDrive(settings.driveLetter(drive.get()), this->shared_from_this());
95 | }
96 | else{
97 | DebugLogger::getInstance().log("Not autoconnecting drive '{}', autoconnecting is disabled for this drive", drive.get());
98 | }
99 | }
100 | }
101 |
102 | void AndroidDevice::disconnectAllDrives(){
103 | for(const std::unique_ptr &drive: this->_drives){
104 | DebugLogger::getInstance().log("Disconnecting drive '{}'", drive.get());
105 | drive->disconnectDrive();
106 | }
107 | }
108 |
109 | int AndroidDevice::numberOfDrives() const{
110 | return static_cast(this->_drives.size());
111 | }
112 |
113 | int AndroidDevice::numberOfConnectedDrives() const{
114 | int result = 0;
115 | for(const std::unique_ptr &drive: this->_drives){
116 | result += drive->isConnected();
117 | }
118 | return result;
119 | }
120 |
121 | bool AndroidDevice::isParentOfDrive(const AndroidDrive *drive) const{
122 | for(const std::unique_ptr &otherDrive: this->_drives){
123 | if(drive == otherDrive.get()){
124 | return true;
125 | }
126 | }
127 | return false;
128 | }
129 |
130 | AndroidDrive *AndroidDevice::driveAt(int index) const{
131 | if(index < 0 || index > this->numberOfDrives()){
132 | return nullptr;
133 | }
134 | return this->_drives[index].get();
135 | }
136 |
137 | QString AndroidDevice::runAdbCommand(const QString &command, bool *ok, bool useCache) const{
138 | if(useCache && this->_adbCache.contains(command) && QDateTime::currentMSecsSinceEpoch() - this->_adbCache.value(command).second < 1000){
139 | if(ok != nullptr){
140 | *ok = true;
141 | }
142 | const QString result = this->_adbCache.value(command).first;
143 | DebugLogger::getInstance().log("Device '{}': ADB command `{}` is cached, using previous result: {}", std::make_tuple(this->serialNumber(), command, result));
144 | return result;
145 | }
146 |
147 | DebugLogger::getInstance().log("Running ADB command `{}` on device '{}'", std::make_tuple(command, this->serialNumber()));
148 | QProcess adb;
149 | adb.start("adb.exe", {"-s", this->_serialNumber, "shell", command});
150 | adb.waitForFinished(-1);
151 | const int exitCode = adb.exitCode();
152 | DebugLogger::getInstance().log("Finished running ADB command `{}` on device '{}'. Exit code: {}.", std::make_tuple(command, this->serialNumber(), exitCode));
153 | if(ok != nullptr){
154 | *ok = exitCode == 0;
155 | }
156 | if(exitCode == 0){
157 | const QString result = adb.readAllStandardOutput().trimmed();
158 | DebugLogger::getInstance().log("Standard output: {}", result);
159 | if(useCache){
160 | this->_adbCache[command] = QPair(result, QDateTime::currentMSecsSinceEpoch());
161 | }
162 | return result;
163 | }
164 | else{
165 | DebugLogger::getInstance().log("Returning empty string because ADB command failed");
166 | return "";
167 | }
168 | }
169 |
170 | bool AndroidDevice::pullFromAdb(const QString &remoteFile, const QString &localFile) const{
171 | QProcess adb;
172 | DebugLogger::getInstance().log("Pulling '{}' to '{}'", std::make_tuple(remoteFile, localFile));
173 | adb.start("adb.exe", {"-s", this->_serialNumber, "pull", remoteFile, localFile});
174 | adb.waitForFinished(-1);
175 | const int exitCode = adb.exitCode();
176 | DebugLogger::getInstance().log("Pulling finished. Exit code: {}, standard output: '{}', standard error: '{}'", std::make_tuple(exitCode, adb.readAllStandardOutput().toStdString(), adb.readAllStandardError().toStdString()));
177 | return exitCode == 0;
178 | }
179 |
180 | bool AndroidDevice::pushToAdb(const QString &localFile, const QString &remoteFile) const{
181 | QProcess adb;
182 | DebugLogger::getInstance().log("Pushing '{}' to '{}'", std::make_tuple(localFile, remoteFile));
183 | adb.start("adb.exe", {"-s", this->_serialNumber, "push", localFile, remoteFile});
184 | adb.waitForFinished(-1);
185 | const int exitCode = adb.exitCode();
186 | DebugLogger::getInstance().log("Pushing finished. Exit code: {}, standard output: '{}', standard error: '{}'", std::make_tuple(exitCode, adb.readAllStandardOutput().toStdString(), adb.readAllStandardError().toStdString()));
187 | return exitCode == 0;
188 | }
189 |
190 | QString AndroidDevice::model() const{
191 | if(this->_cachedModel.isEmpty()){
192 | this->_cachedModel = this->runAdbCommand("getprop ro.product.model");
193 | if(this->_cachedModel.isEmpty()){
194 | this->_cachedModel = this->_serialNumber;
195 | }
196 | }
197 | return this->_cachedModel;
198 | }
199 |
200 | QString AndroidDevice::serialNumber() const{
201 | return this->_serialNumber;
202 | }
203 |
204 | void AndroidDevice::addDrive(QString androidRootPath){
205 | std::unique_ptr drive = std::make_unique(std::move(androidRootPath), this->model(), this->serialNumber());
206 | QObject::connect(drive.get(), &AndroidDrive::driveConnected, this, [this, drive = drive.get()](){emit this->driveConnected(drive);});
207 | QObject::connect(drive.get(), &AndroidDrive::driveMounted, this, [this, drive = drive.get()](char driveLetter){emit this->driveMounted(drive, driveLetter);});
208 | QObject::connect(drive.get(), &AndroidDrive::driveUnmounted, this, [this, drive = drive.get()](){emit this->driveUnmounted(drive);});
209 | QObject::connect(drive.get(), &AndroidDrive::driveDisconnected, this, [this, drive = drive.get()](int status){emit this->driveDisconnected(drive, status);});
210 | this->_drives.push_back(std::move(drive));
211 | }
212 |
--------------------------------------------------------------------------------
/sources/androiddevice.hpp:
--------------------------------------------------------------------------------
1 | #ifndef ANDROIDDEVICE_H
2 | #define ANDROIDDEVICE_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | #include
11 | #include
12 |
13 | class AndroidDrive;
14 |
15 | class AndroidDevice: public QObject, public std::enable_shared_from_this {
16 | Q_OBJECT
17 |
18 | public:
19 | /**
20 | * Constructs a device object.
21 | *
22 | * @param serialNumber - The serial number of the device.
23 | */
24 | explicit AndroidDevice(const QString &serialNumber);
25 |
26 | /**
27 | * Destructor.
28 | */
29 | virtual ~AndroidDevice();
30 |
31 | /**
32 | * Quits the application as soon as the last device is deleted. If there are no devices, quits immediately.
33 | */
34 | static void quitOnLastDeletedDevice();
35 |
36 | /**
37 | * Disallow copying.
38 | */
39 | AndroidDevice(const AndroidDevice&) = delete;
40 | void operator=(const AndroidDevice&) = delete;
41 |
42 | /**
43 | * Connects all the device's drives.
44 | */
45 | void connectAllDrives();
46 |
47 | /**
48 | * Connects the device's drives that are supposed to be automatically connected according to the settings. Does nothing with the others.
49 | */
50 | void autoconnectAllDrives();
51 |
52 | /**
53 | * Disconnects all the device's drives.
54 | */
55 | void disconnectAllDrives();
56 |
57 | /**
58 | * @return The number of drives that the device has.
59 | */
60 | int numberOfDrives() const;
61 |
62 | /**
63 | * @return The number of the device's drives that are currently connected.
64 | */
65 | int numberOfConnectedDrives() const;
66 |
67 | /**
68 | * Checks if this device is the parent of the given drive.
69 | *
70 | * @param drive - The drive to check for.
71 | *
72 | * @return True if the drive's parent is this device, false otherwise.
73 | */
74 | bool isParentOfDrive(const AndroidDrive *drive) const;
75 |
76 | /**
77 | * Gets the drive at the given index.
78 | *
79 | * @param index - The index to get the drive at. 0 for the device's first drive, 1 for the device's second drive, etc.
80 | *
81 | * @return A non-owning pointer to the drive at the given index, or nullptr if the index is out of range.
82 | */
83 | AndroidDrive *driveAt(int index) const;
84 |
85 | /**
86 | * Runs a bash command through ADB on the given device.
87 | *
88 | * @param command - The bash command to run.
89 | * @param ok - Will be set to true if the command exits correctly and false otherwise.
90 | * @param useCache - If true and the result of the command has been cached less than a second ago, returns the cached value and doesn't run anything through ADB. If true but no recent cache exists, caches the result only if the command is successful.
91 | *
92 | * @return The standard output of the command, excluding leading and trailing whitespace.
93 | */
94 | QString runAdbCommand(const QString &command, bool *ok = nullptr, bool useCache = true) const;
95 |
96 | /**
97 | * Copies a file from Android to Windows.
98 | *
99 | * @param remoteFile - The path of the Android file to download.
100 | * @param localFile - The path of the Windows file to create. If the file already exists, overwrites it.
101 | *
102 | * @return True if the file was successfully copied, false otherwise.
103 | */
104 | bool pullFromAdb(const QString &remoteFile, const QString &localFile) const;
105 |
106 | /**
107 | * Copies a file from Windows to Android.
108 | *
109 | * @param localFile - The path of the Windows file to upload.
110 | * @param remoteFile - The path of the Android file to create. If the file already exists, overwrites it.
111 | *
112 | * @return True if the file was successfully copied, false otherwise.
113 | */
114 | bool pushToAdb(const QString &localFile, const QString &remoteFile) const;
115 |
116 | /**
117 | * @return The model of the device.
118 | */
119 | QString model() const;
120 |
121 | /**
122 | * @return The serial number of the device.
123 | */
124 | QString serialNumber() const;
125 |
126 | signals:
127 | /**
128 | * Emitted when the Dokan main loop starts.
129 | *
130 | * @param drive - The drive that's connected.
131 | */
132 | void driveConnected(AndroidDrive *drive);
133 |
134 | /**
135 | * Emitted when the drive appears in the This PC folder.
136 | *
137 | * @param drive - The drive that's connected.
138 | * @param driveLetter - The drive letter that the drive is connected as.
139 | */
140 | void driveMounted(AndroidDrive *drive, char driveLetter);
141 |
142 | /**
143 | * Emitted when the drive is removed from the This PC folder.
144 | *
145 | * @param drive - The drive that's connected.
146 | */
147 | void driveUnmounted(AndroidDrive *drive);
148 |
149 | /**
150 | * Emitted when the Dokan main loop finishes.
151 | *
152 | * @param drive - The drive that's connected.
153 | * @param status - The status code returned by Dokan.
154 | */
155 | void driveDisconnected(AndroidDrive *drive, int status);
156 |
157 | private:
158 | /**
159 | * Adds a drive to the device.
160 | *
161 | * @param androidRootPath - The root path of the drive to add.
162 | */
163 | void addDrive(QString androidRootPath);
164 |
165 | static QList _instances;
166 | static bool _quitOnLastDeletedDevice;
167 | bool _willBeDeleted = false;
168 |
169 | const QString _serialNumber;
170 |
171 | std::vector> _drives;
172 |
173 | //Cache for specific methods (it would be easier to declare these as static in their respective methods, but that would use the same cache accross instances, see https://stackoverflow.com/a/6223371/4284627)
174 | //These are mutable because the cache can be updated in const methods that don't really change the state of the object
175 | mutable QMap> _adbCache;
176 | mutable QString _cachedModel;
177 | };
178 |
179 | #endif // ANDROIDDEVICE_H
180 |
--------------------------------------------------------------------------------
/sources/androiddrive-install-languages.iss:
--------------------------------------------------------------------------------
1 | [CustomMessages]
2 |
3 | [Languages]
4 | Name: "english"; MessagesFile: "compiler:Default.isl"
5 | Name: "french"; MessagesFile: "compiler:Languages\French.isl"
6 | Name: "german"; MessagesFile: "compiler:Languages\German.isl"
7 | Name: "hungarian"; MessagesFile: "compiler:Languages\Hungarian.isl"
8 | Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl"
9 | ;For the Swedish translation to work, download the unofficial Swedish translation from https://jrsoftware.org/files/istrans/ and save it in C:\Program Files (x86)\Inno Setup 6\Languages
10 | Name: "swedish"; MessagesFile: "compiler:Languages\Swedish.isl"
11 |
12 |
--------------------------------------------------------------------------------
/sources/androiddrive-install.iss:
--------------------------------------------------------------------------------
1 | ; Script generated by the Inno Setup Script Wizard.
2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3 |
4 | #include "androiddrive-install-languages.iss"
5 | #define MyAppName "AndroidDrive"
6 | #define MyAppExeName MyAppName + ".exe"
7 | #define MyAppExePath "..\build\release"
8 | #define MyDepenciesPath "..\dependencies"
9 | #define MyAppVersion GetVersionNumbersString(MyAppExePath + "\" + MyAppExeName)
10 | #define MyAppURL "https://github.com/GustavLindberg99/AndroidDrive"
11 | #define MyAppOutputDir ".."
12 | #define MyAppOutputExe "AndroidDrive-install"
13 | #define MyAppLicenseFile "license.rtf"
14 | #define MyAppCompany "Gustav Lindberg"
15 | #define MyAppStartingYear "2021"
16 | #define CurrentYear GetDateTimeString('yyyy','','')
17 |
18 | [Setup]
19 | ; NOTE: The value of AppId uniquely identifies this application.
20 | ; Do not use the same AppId value in installers for other applications.
21 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
22 | AppId={{C370AD59-EF9D-4DE1-B2F5-CD4D87123B11}
23 | AppName={#MyAppName}
24 | AppVersion={#MyAppVersion}
25 | AppVerName={#MyAppName} {#MyAppVersion}
26 |
27 | AppCopyright=(c) {#MyAppStartingYear}-{#CurrentYear} {#MyAppCompany}
28 | AppPublisher={#MyAppCompany}
29 | AppPublisherURL={#MyAppURL}
30 | AppSupportURL={#MyAppURL}
31 | AppUpdatesURL={#MyAppURL}
32 |
33 | VersionInfoDescription={#MyAppName} installer
34 | VersionInfoProductName={#MyAppName}
35 | VersionInfoVersion={#MyAppVersion}
36 |
37 | UninstallDisplayName={#MyAppName}
38 | UninstallDisplayIcon={app}\{#MyAppExeName}
39 |
40 | LicenseFile={#MyAppLicenseFile}
41 |
42 | ShowLanguageDialog=yes
43 | UsePreviousLanguage=no
44 | LanguageDetectionMethod=uilanguage
45 |
46 | WizardStyle=modern
47 |
48 | DefaultDirName={commonpf64}\{#MyAppName}
49 | DisableProgramGroupPage=yes
50 | OutputDir={#MyAppOutputDir}
51 | OutputBaseFilename={#MyAppOutputExe}
52 | Compression=lzma
53 | SolidCompression=yes
54 |
55 |
56 | [Tasks]
57 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
58 |
59 | [Files]
60 | Source: "{#MyAppExePath}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
61 | Source: "{#MyDepenciesPath}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
62 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
63 |
64 | [Icons]
65 | Name: "{commonprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
66 | Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
67 |
68 | [Run]
69 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
70 |
--------------------------------------------------------------------------------
/sources/androiddrive.cpp:
--------------------------------------------------------------------------------
1 | #include "androiddrive.hpp"
2 |
3 | #include
4 | #include
5 |
6 | #include "androiddevice.hpp"
7 | #include "debuglogger.hpp"
8 | #include "dokanoperations.hpp"
9 |
10 | QList AndroidDrive::_instances;
11 | DWORD AndroidDrive::_internalLogicalDrives = 0;
12 |
13 | AndroidDrive::AndroidDrive(QString androidRootPath, QString model, QString serialNumber):
14 | _androidRootPath(std::move(androidRootPath)),
15 | _model(std::move(model)),
16 | _serialNumber(std::move(serialNumber)),
17 | _settingsWindow(this)
18 | {
19 | AndroidDrive::_instances.push_back(this);
20 |
21 | ZeroMemory(&this->_dokanOptions, sizeof(DOKAN_OPTIONS));
22 | this->_dokanOptions.Version = DOKAN_VERSION;
23 | this->_dokanOptions.MountPoint = this->_mountPoint; //this->_dokanOptions.MountPoint and this->_mountPoint will point to the same memory address, which will be used in connectDrive() and fromDokanFileInfo()
24 | this->_dokanOptions.Options |= DOKAN_OPTION_ALT_STREAM;
25 |
26 | ZeroMemory(&this->_dokanOperations, sizeof(DOKAN_OPERATIONS));
27 | this->_dokanOperations.ZwCreateFile = createFile;
28 | this->_dokanOperations.CloseFile = closeFile;
29 | this->_dokanOperations.Cleanup = cleanup;
30 | this->_dokanOperations.ReadFile = readFile;
31 | this->_dokanOperations.WriteFile = writeFile;
32 | this->_dokanOperations.FlushFileBuffers = flushFileBuffers;
33 | this->_dokanOperations.GetFileInformation = getFileInformation;
34 | this->_dokanOperations.FindFiles = findFiles;
35 | this->_dokanOperations.SetFileAttributes = setFileAttributes;
36 | this->_dokanOperations.SetFileTime = setFileTime;
37 | this->_dokanOperations.DeleteFile = deleteFile;
38 | this->_dokanOperations.DeleteDirectory = deleteDirectory;
39 | this->_dokanOperations.MoveFile = moveFile;
40 | this->_dokanOperations.SetEndOfFile = this->_dokanOperations.SetAllocationSize = setAllocationSize;
41 | this->_dokanOperations.GetDiskFreeSpace = getDiskFreeSpace;
42 | this->_dokanOperations.GetVolumeInformation = getVolumeInformation;
43 | this->_dokanOperations.Unmounted = unmounted;
44 | this->_dokanOperations.Mounted = mounted;
45 |
46 | QObject::connect(this, &AndroidDrive::driveMounted, this, [this](){
47 | this->_mounted = true;
48 | DebugLogger::getInstance().log("Drive '{}' mounted", this);
49 | if(this->_shouldBeDisconnected){
50 | DebugLogger::getInstance().log("Disconnecting drive '{}'", this);
51 | this->disconnectDrive();
52 | }
53 | });
54 | QObject::connect(this, &AndroidDrive::driveUnmounted, this, [this](){
55 | this->_mounted = false;
56 | DebugLogger::getInstance().log("Drive '{}' unmounted", this);
57 | });
58 |
59 | //Do the cleanup after Dokan exit here to make sure that it's run on the main thread instead of the Dokan thread (otherwise the destructors will be called in the Dokan thread so the thread will try to join itself and crash).
60 | //Qt::ConnectionType::AutoConnection is the default so this will be run on the main thread since the connection is done on the main thread.
61 | QObject::connect(this, &AndroidDrive::driveDisconnected, this, [this](){
62 | DebugLogger::getInstance().log("Disconnecting drive '{}', waiting for mutex", this);
63 | std::lock_guard lockGuard(this->_mutex);
64 | DebugLogger::getInstance().log("Disconnecting drive '{}', mutex locked", this);
65 | this->_temporaryFiles.clear();
66 | this->_temporaryDir = nullptr;
67 | this->_device = nullptr;
68 | AndroidDrive::_internalLogicalDrives &= ~(1 << (this->_mountPoint[0] - 'A'));
69 | });
70 | }
71 |
72 | AndroidDrive::~AndroidDrive(){
73 | this->disconnectDrive();
74 | if(this->_thread.joinable()){
75 | DebugLogger::getInstance().log("Joining Dokan thread for drive '{}'", this);
76 | this->_thread.join();
77 | }
78 | DebugLogger::getInstance().log("Deleting drive '{}'", this);
79 | AndroidDrive::_instances.removeAll(this);
80 |
81 | //Sometimes the mutex is already locked by the main thread when it gets destroyed. If that's the case, it must be unlocked otherwise it crashes. It can't be locked by another thread because the other thread was just joined.
82 | (void) this->_mutex.try_lock();
83 | this->_mutex.unlock();
84 | }
85 |
86 | AndroidDrive *AndroidDrive::fromDokanFileInfo(PDOKAN_FILE_INFO dokanFileInfo){
87 | const QList instances = AndroidDrive::_instances;
88 | for(AndroidDrive *drive: instances){
89 | //device->_mountPoint and dokanOptions->MountPoint point to the same memory address if they belong to the same device, so comparing them by reference as below is a reliable way to tell which device goes with which DokanOptions object
90 | if(drive->_mountPoint == dokanFileInfo->DokanOptions->MountPoint){
91 | return drive;
92 | }
93 | }
94 | return nullptr;
95 | }
96 |
97 | void AndroidDrive::connectDrive(char driveLetter, const std::shared_ptr &device){
98 | if(!this->isConnected()){
99 | if(this->_thread.joinable()){
100 | DebugLogger::getInstance().log("Joining Dokan thread for drive '{}'", this);
101 | this->_thread.join();
102 | }
103 |
104 | DebugLogger::getInstance().log("Attempting to connect drive '{}' with drive letter '{}'", std::make_tuple(this, driveLetter));
105 | const DWORD driveLetters = GetLogicalDrives() | AndroidDrive::_internalLogicalDrives; //Bitmask where 1 means the drive is occupied and 0 means the drive is available. The least significant bit corresponds to A:\, the second least significant bit corresponds to B:\, etc. For example, 14 = 0b1110 means that B:\, C:\ and D:\ are occupied and everything else is available.
106 | for(int i = 0; (driveLetters & (1 << (driveLetter - 'A'))) && i < 26; i++){
107 | driveLetter++;
108 | if(driveLetter == 'Z' + 1){
109 | driveLetter = 'A';
110 | }
111 | }
112 | DebugLogger::getInstance().log("Connecting drive '{}' with drive letter '{}'", std::make_tuple(this, driveLetter));
113 |
114 | AndroidDrive::_internalLogicalDrives |= 1 << (driveLetter - 'A');
115 | this->_shouldBeDisconnected = false;
116 | this->_temporaryDir = std::make_unique();
117 | DebugLogger::getInstance().log("Creating temporary folder '{}' for drive '{}'", std::make_tuple(this->_temporaryDir->path(), this));
118 | this->_device = device;
119 | this->_mountPoint[0] = driveLetter; //We don't need to change dokanOptions.MountPoint because it points to the same memory address as this->_mountPoint
120 |
121 | const QString output = this->device()->runAdbCommand("mount | grep $(df /sdcard | sed \"s/.* //g\" | tail -n +2)");
122 | static const QRegularExpression fileSystemRegex("\\S+\\s+on\\s+\\S+\\s+type\\s+([a-zA-Z0-9]+)\\s+");
123 | const QRegularExpressionMatch match = fileSystemRegex.match(output);
124 | this->_fileSystem = match.hasMatch() ? match.captured(1) : "";
125 |
126 | this->_thread = std::thread([this](){
127 | DebugLogger::getInstance().log("Starting Dokan thread for drive '{}'", this);
128 | const int status = DokanMain(&this->_dokanOptions, &this->_dokanOperations);
129 | emit this->driveDisconnected(status);
130 | DebugLogger::getInstance().log("Exiting Dokan thread for drive '{}'", this);
131 | });
132 | emit this->driveConnected();
133 | }
134 | }
135 |
136 | void AndroidDrive::disconnectDrive(){
137 | if(this->isConnected()){
138 | this->_shouldBeDisconnected = true;
139 | DebugLogger::getInstance().log("Removing Dokan mount point for drive '{}'", this);
140 | DokanRemoveMountPoint(this->_mountPoint);
141 | }
142 | }
143 |
144 | bool AndroidDrive::isConnected() const{
145 | return this->_temporaryDir != nullptr;
146 | }
147 |
148 | bool AndroidDrive::mountingInProgress() const{
149 | return !this->_mounted && !this->_shouldBeDisconnected && this->isConnected();
150 | }
151 |
152 | bool AndroidDrive::unmountingInProgress() const{
153 | return this->_shouldBeDisconnected && this->isConnected();
154 | }
155 |
156 | std::shared_ptr AndroidDrive::device() const{
157 | return this->_device;
158 | }
159 |
160 | bool AndroidDrive::isInternalStorage() const{
161 | return this->_androidRootPath == "/sdcard";
162 | }
163 |
164 | QString AndroidDrive::fileSystem() const{
165 | return this->_fileSystem;
166 | }
167 |
168 | QString AndroidDrive::name() const{
169 | if(this->isInternalStorage()){
170 | return QObject::tr("Internal storage");
171 | }
172 | const QString sdCardName = QFileInfo(this->_androidRootPath).baseName();
173 | return QObject::tr("SD card %1").arg(sdCardName);
174 | }
175 |
176 | QString AndroidDrive::completeName() const{
177 | if(this->device() != nullptr && this->device()->numberOfDrives() == 1){
178 | return this->_model;
179 | }
180 | return this->_model + " " + this->name();
181 | }
182 |
183 | QString AndroidDrive::id() const{
184 | return this->_serialNumber + this->androidRootPath();
185 | }
186 |
187 | QString AndroidDrive::androidRootPath() const{
188 | return this->_androidRootPath;
189 | }
190 |
191 | QString AndroidDrive::localPath(const QString &remotePath) const{
192 | static const QRegularExpression leadingSlashes("^[/\\\\]+");
193 | const QString remoteRelativePath = QString(remotePath).replace(leadingSlashes, "");
194 | const QString result = QDir::toNativeSeparators(this->_temporaryDir->filePath(remoteRelativePath));
195 | QFileInfo(result).dir().mkpath(".");
196 | return result;
197 | }
198 |
199 | QString AndroidDrive::windowsPathToAndroidPath(LPCWSTR windowsPath) const{
200 | QString androidPath = this->_androidRootPath + QString::fromWCharArray(windowsPath).replace("\\", "/").split(":")[0];
201 | //Use the same trick as WSL for characters that are allowed on Android but not on Windows (this trick consists in replacing a special character with a Unicode version by adding 0xf000 to its char code)
202 | const char specialCharacters[] = {'\\', ':', '*', '?', '"', '<', '>', '|'};
203 | for(const char character: specialCharacters){
204 | androidPath.replace(QChar(character + 0xf000), QChar(character));
205 | }
206 | return androidPath;
207 | }
208 |
209 | QString AndroidDrive::mountPoint() const{
210 | return QString::fromWCharArray(this->_mountPoint);
211 | }
212 |
213 | NTSTATUS AndroidDrive::addTemporaryFile(PDOKAN_FILE_INFO dokanFileInfo, const QString &remotePath, DWORD creationDisposition, ULONG shareAccess, ACCESS_MASK desiredAccess, ULONG fileAttributes, ULONG createOptions, ULONG createDisposition, bool exists, const QString &altStream){
214 | DebugLogger::getInstance().log("Adding temporary file for '{}', waiting for mutex", remotePath);
215 | std::lock_guard lockGuard(this->_mutex);
216 | DebugLogger::getInstance().log("Adding temporary file for '{}', mutex locked", remotePath);
217 | if(this->_device == nullptr){
218 | DebugLogger::getInstance().log("Not adding temporary file, device is disconnected");
219 | return STATUS_ALREADY_DISCONNECTED;
220 | }
221 | std::unique_ptr temporaryFile = std::make_unique(this, remotePath, creationDisposition, shareAccess, desiredAccess, fileAttributes, createOptions, createDisposition, exists, altStream);
222 | const NTSTATUS errorCode = temporaryFile->errorCode();
223 |
224 | //If there's no error, move the unique_ptr to the list of temporary files to keep it in memory, otherwise do nothing and it will be deleted at the end of the scope.
225 | if(errorCode == STATUS_SUCCESS){
226 | DebugLogger::getInstance().log("Temporary file for '{}' created successfully", remotePath);
227 | dokanFileInfo->Context = reinterpret_cast(temporaryFile.get());
228 | this->_temporaryFiles.push_back(std::move(temporaryFile));
229 | }
230 | else{
231 | DebugLogger::getInstance().log("Failed to create temporary file for '{}': error {}", std::make_tuple(remotePath, errorCode));
232 | }
233 |
234 | return errorCode;
235 | }
236 |
237 | void AndroidDrive::deleteTemporaryFile(PDOKAN_FILE_INFO dokanFileInfo){
238 | DebugLogger::getInstance().log("Deleting temporary file on drive '{}', waiting for mutex", this);
239 | std::lock_guard lockGuard(this->_mutex);
240 | DebugLogger::getInstance().log("Deleting temporary file on drive '{}', mutex locked", this);
241 | TemporaryFile *temporaryFile = reinterpret_cast(dokanFileInfo->Context);
242 | dokanFileInfo->Context = 0;
243 | for(size_t i = 0; i < this->_temporaryFiles.size(); i++){
244 | if(this->_temporaryFiles[i].get() == temporaryFile){
245 | DebugLogger::getInstance().log("Temporary file found in list of temporary files");
246 | this->_temporaryFiles.erase(this->_temporaryFiles.begin() + i);
247 | break;
248 | }
249 | }
250 | }
251 |
252 | void AndroidDrive::openSettingsWindow(){
253 | this->_settingsWindow.show();
254 | }
255 |
--------------------------------------------------------------------------------
/sources/androiddrive.hpp:
--------------------------------------------------------------------------------
1 | #ifndef ANDROIDDRIVE_H
2 | #define ANDROIDDRIVE_H
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | #include "settingswindow.hpp"
14 | #include "temporaryfile.hpp"
15 |
16 | class AndroidDevice;
17 |
18 | class AndroidDrive : public QObject {
19 | Q_OBJECT
20 |
21 | public:
22 | /**
23 | * Constructs a drive object.
24 | *
25 | * @param androidRootPath - The path to folder on Android that should be used as the root of the drive.
26 | * @param model - The model of the device that the drive belongs to.
27 | * @param serialNumber - The serial number of the device that the drive belongs to.
28 | */
29 | explicit AndroidDrive(QString androidRootPath, QString model, QString serialNumber);
30 |
31 | /**
32 | * Destructor, shuts down Dokan if needed and doesn't return until Dokan is shut down.
33 | */
34 | virtual ~AndroidDrive();
35 |
36 | /**
37 | * Disallow copying.
38 | */
39 | AndroidDrive(const AndroidDrive&) = delete;
40 | void operator=(const AndroidDrive&) = delete;
41 |
42 | /**
43 | * Gets a drive from a PDOKAN_FILE_INFO object.
44 | *
45 | * @param dokanFileInfo - The Dokan file info to get the drive from.
46 | *
47 | * @return A non-owning pointer to the drive that the file info refers to.
48 | */
49 | static AndroidDrive *fromDokanFileInfo(PDOKAN_FILE_INFO dokanFileInfo);
50 |
51 | /**
52 | * Connects the drive.
53 | *
54 | * Can't just be called connect because QObject already has a connect method.
55 | *
56 | * @param driveLetter - The drive letter to use.
57 | * @param device - The device that the drive belongs to. The drive should co-own the device as long as it's mounted so that the device doens't get deleted until Dokan has had time to shut down.
58 | */
59 | void connectDrive(char driveLetter, const std::shared_ptr &device);
60 |
61 | /**
62 | * Disconnects the drive.
63 | *
64 | * Can't just be called disconnect because QObject already has a connect method.
65 | */
66 | void disconnectDrive();
67 |
68 | /**
69 | * Checks if the drive is connected. A drive is considered connected even while it's mounting/unmounting.
70 | *
71 | * @return True if the drive is connected, false otherwise.
72 | */
73 | bool isConnected() const;
74 |
75 | /**
76 | * Checks if the drive is has received a request to connect but isn't mounted yet.
77 | *
78 | * @return True if mounting is in progress, false otherwise.
79 | */
80 | bool mountingInProgress() const;
81 |
82 | /**
83 | * Checks if the drive is has received a request to disconnect but isn't unmounted yet.
84 | *
85 | * @return True if unmounting is in progress, false otherwise.
86 | */
87 | bool unmountingInProgress() const;
88 |
89 | /**
90 | * Gets the device that the drive belongs to if the drive is connected.
91 | *
92 | * @return The device if the drive is connected, nullptr otherwise. To get the device even if the drive isn't connected, use DeviceListModel::parentDevice.
93 | */
94 | std::shared_ptr device() const;
95 |
96 | /**
97 | * Checks whether this drive is the interal storage drive.
98 | *
99 | * @return True if it's the device's internal storage drive, false if it's an external SD card.
100 | */
101 | bool isInternalStorage() const;
102 |
103 | /**
104 | * Gets the name of the file system. Don't use on disconnected drives.
105 | *
106 | * @return The name of the file system.
107 | */
108 | QString fileSystem() const;
109 |
110 | /**
111 | * Gets the name as it shows up in AndroidDrive's list of devices (doesn't need as much context since the name of the device will be displayed above it).
112 | *
113 | * @return The name of the drive.
114 | */
115 | QString name() const;
116 |
117 | /**
118 | * Gets the name as it shows up in the This PC folder (needs more context since it will be displayed as its own drive).
119 | *
120 | * @return The complete name of the drive.
121 | */
122 | QString completeName() const;
123 |
124 | /**
125 | * A unique id for the drive, used internally to store settings related to it.
126 | *
127 | * @return The id of the drive.
128 | */
129 | QString id() const;
130 |
131 | /**
132 | * Gets the path to folder on Android that should be used as the root of the drive.
133 | *
134 | * @return The Android path to use as root.
135 | */
136 | QString androidRootPath() const;
137 |
138 | /**
139 | * Converts an Android path to a Windows path.
140 | *
141 | * @param remotePath - The Android path.
142 | *
143 | * @return The Windows path.
144 | */
145 | QString localPath(const QString &remotePath) const;
146 |
147 | /**
148 | * Converts a Windows path to an Android path.
149 | *
150 | * @param windowsPath - The Windows path.
151 | *
152 | * @return The Android path.
153 | */
154 | QString windowsPathToAndroidPath(LPCWSTR windowsPath) const;
155 |
156 | /**
157 | * Gets the mount point, i.e. the drive letter followed by ":\".
158 | *
159 | * @return The mount point.
160 | */
161 | QString mountPoint() const;
162 |
163 | /**
164 | * Downloads a file from the Android device to a local temporary file.
165 | *
166 | * @param dokanFileInfo - The Dokan file info object that the temporary file should be added to as Context.
167 | * @param remotePath - The path of the Android file to download.
168 | * @param creationDisposition - The creation disposition to use when creating the handle.
169 | * @param shareAccess - The share access to use when creating the handle.
170 | * @param desiredAccess - The desired access to use when creating the handle.
171 | * @param fileAttributes - The file attributes to use when creating the handle.
172 | * @param createOptions - The create options to use when creating the handle.
173 | * @param createDisposition - The create disposition to use when creating the handle.
174 | * @param exists - True if opening an existing file (in which case it should be downloaded), false when creating a new file (in which case it should just be created locally).
175 | * @param altStream - The alt stream to use when creating the handle.
176 | *
177 | * @return STATUS_SUCCESS on success, an error status on failure.
178 | */
179 | NTSTATUS addTemporaryFile(PDOKAN_FILE_INFO dokanFileInfo, const QString &remotePath, DWORD creationDisposition, ULONG shareAccess, ACCESS_MASK desiredAccess, ULONG fileAttributes, ULONG createOptions, ULONG createDisposition, bool exists, const QString &altStream);
180 |
181 | /**
182 | * Deletes the temporary file.
183 | *
184 | * @param dokanFileInfo - The Dokan file info that contains the temporary file as Context, the Context will be set to null.
185 | */
186 | void deleteTemporaryFile(PDOKAN_FILE_INFO dokanFileInfo);
187 |
188 | /**
189 | * Opens the drive's settings window.
190 | */
191 | void openSettingsWindow();
192 |
193 | signals:
194 | /**
195 | * Emitted when the Dokan main loop starts.
196 | */
197 | void driveConnected();
198 |
199 | /**
200 | * Emitted when the drive appears in the This PC folder.
201 | *
202 | * @param driveLetter - The drive letter that the drive is connected as.
203 | */
204 | void driveMounted(char driveLetter);
205 |
206 | /**
207 | * Emitted when the drive is removed from the This PC folder.
208 | */
209 | void driveUnmounted();
210 |
211 | /**
212 | * Emitted when the Dokan main loop finishes.
213 | *
214 | * @param status - The status code returned by Dokan.
215 | */
216 | void driveDisconnected(int status);
217 |
218 | private:
219 | static QList _instances;
220 | static DWORD _internalLogicalDrives; //Bitmask similar to Windows API GetLogicalDrives() but which keeps track of which drives are used internally. Needed to avoid race conditions in case two drives are connected at the same time.
221 |
222 | DOKAN_OPERATIONS _dokanOperations;
223 | DOKAN_OPTIONS _dokanOptions;
224 |
225 | const QString _androidRootPath;
226 | const QString _model;
227 | const QString _serialNumber;
228 | QString _fileSystem;
229 |
230 | std::shared_ptr _device = nullptr;
231 | std::thread _thread;
232 | std::mutex _mutex;
233 | std::unique_ptr _temporaryDir = nullptr;
234 | std::vector> _temporaryFiles;
235 |
236 | wchar_t _mountPoint[4] = L"?:\\";
237 | bool _mounted = false;
238 | bool _shouldBeDisconnected = true;
239 |
240 | SettingsWindow _settingsWindow;
241 | };
242 |
243 | template<>
244 | struct std::formatter: public std::formatter {
245 | auto format(const AndroidDrive *drive, std::format_context &ctx) const {
246 | const auto device = drive->device();
247 | const std::string serialNumber = device == nullptr ? "nullptr" : device->serialNumber().toStdString();
248 | return std::formatter::format(serialNumber + ":" + drive->androidRootPath().toStdString(), ctx);
249 | }
250 | };
251 |
252 | template<>
253 | struct std::formatter: public std::formatter {
254 | auto format(AndroidDrive *drive, std::format_context &ctx) const {
255 | return std::formatter::format(drive, ctx);
256 | }
257 | };
258 |
259 | #endif // ANDROIDDRIVE_H
260 |
--------------------------------------------------------------------------------
/sources/debuglogger.cpp:
--------------------------------------------------------------------------------
1 | #include "debuglogger.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #include "version.h"
10 |
11 | DebugLogger &DebugLogger::getInstance(){
12 | static DebugLogger instance;
13 | return instance;
14 | }
15 |
16 | bool DebugLogger::start(){
17 | const QString parentFolder = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
18 | QDir(parentFolder).mkpath(".");
19 | const QString path = parentFolder + "/AndroidDriveDebugLog" + QDateTime::currentDateTime().toString("yyyyMMddHHmmss") + ".log";
20 | this->_file = std::make_unique(path);
21 | if(!this->_file->open(QFile::WriteOnly)){
22 | this->_file = nullptr;
23 | return false;
24 | }
25 | this->_file->write("Log captured with AndroidDrive version " PROGRAMVERSION "\n");
26 | return true;
27 | }
28 |
29 | void DebugLogger::stop(){
30 | if(this->_file != nullptr){
31 | QProcess::startDetached("explorer.exe", {"/select," + this->logFilePath()});
32 | }
33 | this->_file = nullptr;
34 | }
35 |
36 | void DebugLogger::log(const QString &message, std::source_location location){
37 | if(this->_file == nullptr){
38 | return;
39 | }
40 |
41 | this->_file->write(QString("%1 %2:%3 %4\n")
42 | .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"))
43 | .arg(location.file_name())
44 | .arg(location.line())
45 | .arg(message)
46 | .toUtf8()
47 | );
48 | this->_file->flush(); //Very important so that it's possible to debug crashes
49 | }
50 |
51 | bool DebugLogger::isRecording() const{
52 | return this->_file != nullptr;
53 | }
54 |
55 | QString DebugLogger::logFilePath() const{
56 | if(this->_file == nullptr){
57 | return "";
58 | }
59 | return QDir::toNativeSeparators(this->_file->fileName());
60 | }
61 |
--------------------------------------------------------------------------------
/sources/debuglogger.hpp:
--------------------------------------------------------------------------------
1 | #ifndef DEBUGLOGGER_HPP
2 | #define DEBUGLOGGER_HPP
3 |
4 | #include
5 | #include
6 |
7 | #include
8 | #include
9 | #include
10 |
11 | template<>
12 | struct std::formatter: public std::formatter {
13 | auto format(const QString &str, std::format_context &ctx) const {
14 | return std::formatter::format(str.toStdString(), ctx);
15 | }
16 | };
17 |
18 | template
19 | constexpr bool isNotTupleHelper = true;
20 |
21 | template
22 | constexpr bool isNotTupleHelper> = false;
23 |
24 | template
25 | concept IsNotTuple = isNotTupleHelper;
26 |
27 | class DebugLogger final {
28 | public:
29 | /**
30 | * Gets the instance of the singleton class.
31 | *
32 | * @return The instance.
33 | */
34 | static DebugLogger& getInstance();
35 |
36 | /**
37 | * Not possible to copy a singleton.
38 | */
39 | DebugLogger(const DebugLogger&) = delete;
40 | DebugLogger &operator=(const DebugLogger&) = delete;
41 |
42 | /**
43 | * Starts recording debug logs.
44 | */
45 | bool start();
46 |
47 | /**
48 | * Saves the debug logs to a file.
49 | */
50 | void stop();
51 |
52 | /**
53 | * If logging is enabled, logs the message, otherwise does nothing.
54 | *
55 | * @param message The message to log.
56 | * @param location The location at which this function is being called at. Intended to always use the default argument, see documentation for std::source_location.
57 | */
58 | void log(const QString &message, std::source_location location = std::source_location::current());
59 |
60 | /**
61 | * If logging is enabled, logs the message, otherwise does nothing. Should be called the same way as std::format, but with the arguments in a tuple to be able to use std::source_location as an optional parameter.
62 | *
63 | * @param fmt The format string.
64 | * @param args Arguments to be formatted.
65 | * @param location The location at which this function is being called at. Intended to always use the default argument, see documentation for std::source_location.
66 | */
67 | template
68 | void log(const std::format_string &fmt, const std::tuple &args, std::source_location location = std::source_location::current()){
69 | if(this->_file == nullptr){
70 | return;
71 | }
72 | const std::string message = std::vformat(
73 | fmt.get(),
74 | std::apply([](auto&&... it){return std::make_format_args(it...);}, args) //Passes the content of the tuple args to std::make_format_args (see https://stackoverflow.com/a/37100646/4284627)
75 | );
76 | this->log(QString::fromStdString(message), location);
77 | }
78 |
79 | /**
80 | * Shorthand for log(fmt, std::make_tuple(arg)).
81 | *
82 | * Uses a concept to disallow T from being a tuple, otherwise calls to log(fmt, std::make_tuple(arg1, arg2, ...)) would be ambiguous.
83 | *
84 | * @param fmt The format string, containing one placeholder.
85 | * @param arg Argument to be formatted.
86 | * @param location The location at which this function is being called at. Intended to always use the default argument, see documentation for std::source_location.
87 | */
88 | template
89 | void log(const std::format_string &fmt, const T &arg, std::source_location location = std::source_location::current()){
90 | this->log(fmt, std::make_tuple(arg), location);
91 | }
92 |
93 | /**
94 | * Checks if debug logs are currently being recorded.
95 | *
96 | * @return True if they're currently being recorded, false otherwise.
97 | */
98 | bool isRecording() const;
99 |
100 | /**
101 | * Gets the absolute path of the log file currently being written to.
102 | *
103 | * @return The absolute path of the log file with backslashes as directory separators. If debug logs aren't currently being recorded, returns an empty string.
104 | */
105 | QString logFilePath() const;
106 |
107 | private:
108 | DebugLogger() = default;
109 |
110 | std::unique_ptr _file = nullptr;
111 | };
112 |
113 | #endif // DEBUGLOGGER_HPP
114 |
--------------------------------------------------------------------------------
/sources/devicelistmodel.cpp:
--------------------------------------------------------------------------------
1 | #include "devicelistmodel.hpp"
2 |
3 | #include
4 |
5 | #include
6 |
7 | #include "androiddrive.hpp"
8 |
9 | QModelIndex DeviceListModel::index(int row, int column, const QModelIndex &parent) const{
10 | if(!this->hasIndex(row, column, parent)){
11 | return QModelIndex();
12 | }
13 |
14 | //If the parent is the root item (represented by a null pointer), then the child is a device
15 | if(this->indexIsRoot(parent)){
16 | if(row >= 0 && row < this->_devices.size()){
17 | return this->createIndex(row, column, static_cast(this->_devices[row].get())); //This needs to be manually cast to a QObject* before being cast to a void*, see https://stackoverflow.com/a/5445220/4284627
18 | }
19 | }
20 |
21 | //If the parent is a device, then the child is a drive
22 | AndroidDevice *parentDevice = this->indexToDevice(parent);
23 | if(parentDevice != nullptr){
24 | if(row >= 0 && row < parentDevice->numberOfDrives()){
25 | AndroidDrive *childDrive = parentDevice->driveAt(row);
26 | return this->createIndex(row, column, static_cast(childDrive));
27 | }
28 | }
29 |
30 | //This will be run either if the parent is neither a device or a root item, meaning it's a drive, or if the child is out of bounds.
31 | //In both cases, return an invalid index, since drives don't have any children.
32 | return QModelIndex();
33 | }
34 |
35 | QModelIndex DeviceListModel::parent(const QModelIndex &index) const{
36 | QObject *item = nullptr;
37 | if(index.isValid()){
38 | item = static_cast(index.internalPointer());
39 | }
40 |
41 | AndroidDevice *device = dynamic_cast(item);
42 | AndroidDrive *drive = dynamic_cast(item);
43 |
44 | //If the item is a drive, the parent is a device
45 | if(drive != nullptr){
46 | return this->deviceToIndex(this->parentDevice(drive));
47 | }
48 |
49 | //If the item is a device, the parent is the root item
50 | else if(device != nullptr){
51 | return this->rootIndex();
52 | }
53 |
54 | //If the item is neither a drive nor a device, it's the root item which doesn't have any parent
55 | return QModelIndex();
56 | }
57 |
58 | int DeviceListModel::rowCount(const QModelIndex &parent) const{
59 | //If the parent is the root, it has a number of children equal to the number of devices
60 | if(this->indexIsRoot(parent)){
61 | return this->_devices.size();
62 | }
63 |
64 | //If the parent is a device, it has a number of children equal to the number of drives for that device
65 | AndroidDevice *parentDevice = this->indexToDevice(parent);
66 | if(parentDevice != nullptr){
67 | return parentDevice->numberOfDrives();
68 | }
69 |
70 | //Otherwise the parent is a drive, in which case it has no children
71 | return 0;
72 | }
73 |
74 | int DeviceListModel::columnCount(const QModelIndex&) const{
75 | return 2;
76 | }
77 |
78 | QVariant DeviceListModel::data(const QModelIndex &index, int role) const{
79 | if(!index.isValid()){
80 | return QVariant();
81 | }
82 |
83 | AndroidDevice *device = this->indexToDevice(index);
84 | AndroidDrive *drive = this->indexToDrive(index);
85 | switch(index.column()){
86 | case 0: //Column with the name of the device/drive
87 | switch(role){
88 | case Qt::DisplayRole: //Text
89 | if(this->indexIsRoot(index)){
90 | return QObject::tr("Device");
91 | }
92 | else if(device != nullptr){
93 | return device->model();
94 | }
95 | else if(drive != nullptr){
96 | return drive->name();
97 | }
98 | break;
99 |
100 | case Qt::DecorationRole: //Icon
101 | if(device != nullptr){
102 | return QIcon(":/phone.svg");
103 | }
104 | else if(drive != nullptr){
105 | if(drive->isInternalStorage()){
106 | return QIcon(":/systemdrive.svg");
107 | }
108 | else{
109 | return QIcon(":/drive.svg");
110 | }
111 | }
112 | break;
113 | }
114 | break;
115 |
116 | case 1: //Status column
117 | if(role == Qt::DisplayRole){
118 | if(this->indexIsRoot(index)){
119 | return QObject::tr("Status");
120 | }
121 | else if(drive != nullptr){
122 | if(drive->isConnected()){
123 | if(drive->mountingInProgress()){
124 | return QObject::tr("Mounting...");
125 | }
126 | else if(drive->unmountingInProgress()){
127 | return QObject::tr("Unmounting...");
128 | }
129 | else{
130 | return QObject::tr("Mounted as %1").arg(drive->mountPoint());
131 | }
132 | }
133 | else{
134 | return QObject::tr("Not mounted");
135 | }
136 | }
137 | }
138 | break;
139 | }
140 |
141 | return QVariant();
142 | }
143 |
144 | QVariant DeviceListModel::headerData(int section, Qt::Orientation orientation, int role) const{
145 | if (orientation == Qt::Horizontal && role == Qt::DisplayRole){
146 | return this->data(this->rootIndex(section), role);
147 | }
148 | return QVariant();
149 | }
150 |
151 | Qt::ItemFlags DeviceListModel::flags(const QModelIndex &index) const{
152 | if(!index.isValid()){
153 | return Qt::NoItemFlags;
154 | }
155 | Qt::ItemFlags result = QAbstractItemModel::flags(index);
156 | if(this->indexToDrive(index) != nullptr){
157 | result |= Qt::ItemNeverHasChildren;
158 | }
159 | return result;
160 | }
161 |
162 | void DeviceListModel::updateDevices(const QStringList &newSerialNumbers, const QStringList &newOfflineSerialNumbers){
163 | //Add new devices
164 | for(const QString &newSerialNumber: newSerialNumbers){
165 | const bool serialNumberExists = std::find_if(this->_devices.begin(), this->_devices.end(), [&newSerialNumber](const std::shared_ptr &device){
166 | return device->serialNumber() == newSerialNumber;
167 | }) != this->_devices.end();
168 | if(!serialNumberExists){
169 | const int index = this->_devices.size();
170 | this->beginInsertRows(this->rootIndex(), index, index);
171 | this->_devices.push_back(std::make_shared(newSerialNumber));
172 | this->endInsertRows();
173 | }
174 | }
175 |
176 | //Remove devices that no longer exist
177 | for(int i = 0; i < this->_devices.size(); i++){
178 | const std::shared_ptr oldDevice = this->_devices[i];
179 | if(!newSerialNumbers.contains(oldDevice->serialNumber())){
180 | this->beginRemoveRows(this->rootIndex(), i, i);
181 | this->_devices.removeAll(oldDevice);
182 | oldDevice->disconnectAllDrives();
183 | this->endRemoveRows();
184 | i--;
185 | }
186 | }
187 |
188 | //Update the list of offline serial numbers
189 | const QStringList allOfflineSerialNumbers = this->_offlineSerialNumbers.keys() + newOfflineSerialNumbers;
190 | for(const QString &offlineSerialNumber: allOfflineSerialNumbers){
191 | if(newOfflineSerialNumbers.contains(offlineSerialNumber)){
192 | this->_offlineSerialNumbers[offlineSerialNumber]++;
193 | }
194 | else{
195 | this->_offlineSerialNumbers.remove(offlineSerialNumber);
196 | }
197 | }
198 | }
199 |
200 | QList> DeviceListModel::devices() const{
201 | return this->_devices;
202 | /*QList> result;
203 | for(const auto& i: this->_devices) result.push_back(i.get());
204 | return result;*/
205 | }
206 |
207 | std::shared_ptr DeviceListModel::parentDevice(const AndroidDrive * drive) const{
208 | const auto parentDevice = std::find_if(this->_devices.begin(), this->_devices.end(), [&drive](const std::shared_ptr &device){
209 | return device->isParentOfDrive(drive);
210 | });
211 | return *parentDevice;
212 | }
213 |
214 | int DeviceListModel::timeSinceOffline(const QString &offlineSerialNumber) const{
215 | return this->_offlineSerialNumbers.value(offlineSerialNumber, 0);
216 | }
217 |
218 | QModelIndex DeviceListModel::rootIndex(int column) const{
219 | return this->createIndex(0, column, nullptr);
220 | }
221 |
222 | QModelIndex DeviceListModel::deviceToIndex(const std::shared_ptr &device, int column) const{
223 | const int row = this->_devices.indexOf(device);
224 | /*int row = -1;
225 | for(int i = 0; i < this->_devices.length(); i++) if(this->_devices[i].get() == device) row = i;*/
226 | return this->createIndex(row, column, static_cast(device.get()));
227 | }
228 |
229 | bool DeviceListModel::indexIsRoot(const QModelIndex &index) const{
230 | return this->indexToDevice(index) == nullptr && this->indexToDrive(index) == nullptr;
231 | }
232 |
233 | AndroidDevice *DeviceListModel::indexToDevice(const QModelIndex &index) const{
234 | if(!index.isValid()){
235 | return nullptr;
236 | }
237 | QObject *object = static_cast(index.internalPointer());
238 | return dynamic_cast(object);
239 | }
240 |
241 | AndroidDrive *DeviceListModel::indexToDrive(const QModelIndex &index) const{
242 | if(!index.isValid()){
243 | return nullptr;
244 | }
245 | QObject *object = static_cast(index.internalPointer());
246 | return dynamic_cast(object);
247 | }
248 |
--------------------------------------------------------------------------------
/sources/devicelistmodel.hpp:
--------------------------------------------------------------------------------
1 | #ifndef DEVICELISTMODEL_H
2 | #define DEVICELISTMODEL_H
3 |
4 | #include
5 |
6 | #include "androiddevice.hpp"
7 |
8 | /**
9 | * This model is a tree model with three levels: the root level (which contains titles for the columns), devices and drives.
10 | * The QModelIndex::internalPointer() returns a pointer to the object at that index.
11 | * For the root level, this pointer is always null.
12 | * For the device level, this is a pointer to the corresponding AndroidDevice object.
13 | * For the drive level, this is a pointer to the corresponding AndroidDrive object.
14 | */
15 | class DeviceListModel : public QAbstractItemModel {
16 | Q_OBJECT
17 |
18 | public:
19 | QModelIndex index(int row, int column, const QModelIndex &parent) const override;
20 | QModelIndex parent(const QModelIndex &index) const override;
21 |
22 | int rowCount(const QModelIndex &parent) const override;
23 | int columnCount(const QModelIndex &parent) const override;
24 |
25 | QVariant data(const QModelIndex &index, int role) const override; //Returns the text that will be displayed
26 | QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
27 | Qt::ItemFlags flags(const QModelIndex &index) const override;
28 |
29 | /**
30 | * Update the list of devices.
31 | *
32 | * @param newSerialNumbers - The serial numbers of the devices that are connected and work correctly (including those that already were connected).
33 | * @param newOfflineSerialNumbers - The serial numbers of offline devices (including those that already were offline).
34 | */
35 | void updateDevices(const QStringList &newSerialNumbers, const QStringList &newOfflineSerialNumbers);
36 |
37 | /**
38 | * Gets a list of all connected devices.
39 | *
40 | * @return All devices that are connected (not including offline devices).
41 | */
42 | QList> devices() const;
43 |
44 | /**
45 | * Gets the parent device of a given drive.
46 | *
47 | * @param drive - The drive to get the parent of.
48 | *
49 | * @return The parent device.
50 | */
51 | std::shared_ptr parentDevice(const AndroidDrive *drive) const;
52 |
53 | /**
54 | * Gets the number of times ADB has run since the given device has been offline.
55 | *
56 | * @param offlineSerialNumber - The serial number of the device to check.
57 | *
58 | * @return The number of times ADB has run since the device has been offline, or 0 if it's not offline or if it's disconnected.
59 | */
60 | int timeSinceOffline(const QString &offlineSerialNumber) const;
61 |
62 | /**
63 | * Gets the root QModelIndex in the tree structure, i.e. the one containing all devices.
64 | *
65 | * @param column - The column to get the index at.
66 | *
67 | * @return The root index.
68 | */
69 | QModelIndex rootIndex(int column = 0) const;
70 |
71 | /**
72 | * @brief Gets the QModelIndex corresponding to a specific device.
73 | *
74 | * @param device - The device to get the index of.
75 | * @param column - The column of the index.
76 | *
77 | * @return The QModelIndex corresponding to the given device.
78 | */
79 | QModelIndex deviceToIndex(const std::shared_ptr &device, int column = 0) const;
80 |
81 | /**
82 | * Checks if a given index is the root index, i.e. the one containing all the devices.
83 | *
84 | * @param index - The index to check if it's the root index.
85 | *
86 | * @return True if it's the root index, false if it isn't.
87 | */
88 | bool indexIsRoot(const QModelIndex &index) const;
89 |
90 | /**
91 | * Gets the device corresponding to the given QModelIndex.
92 | *
93 | * @param index - The index to convert to a device.
94 | *
95 | * @return A non-owning pointer to the corresponding device, or nullptr if this index doesn't correspond to a device.
96 | */
97 | AndroidDevice *indexToDevice(const QModelIndex &index) const;
98 |
99 | /**
100 | * Gets the drive corresponding to the given QModelIndex.
101 | *
102 | * @param index - The index to convert to a drive.
103 | *
104 | * @return A non-owning pointer to the corresponding drive, or nullptr if this index doesn't correspond to a drive.
105 | */
106 | AndroidDrive *indexToDrive(const QModelIndex &index) const;
107 |
108 | private:
109 | QList> _devices;
110 | QMap _offlineSerialNumbers;
111 | };
112 |
113 | #endif // DEVICELISTMODEL_H
114 |
--------------------------------------------------------------------------------
/sources/devicelistwindow.cpp:
--------------------------------------------------------------------------------
1 | #include "devicelistwindow.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | #include "debuglogger.hpp"
11 |
12 | DeviceListWindow::DeviceListWindow(){
13 | //Initialize the UI
14 | QGridLayout *layout = new QGridLayout(this);
15 |
16 | this->_mountButton->setWhatsThis(QObject::tr("Mounts a drive containing the internal storage of the selected Android device, as well as a drive for each external SD card that the selected device has, if any."));
17 | this->_settingsButton->setWhatsThis(QObject::tr("Allows you to change the settings for this device, for example select a new drive letter or choose whether it should be mounted automatically."));
18 | this->_openInExplorerButton->setWhatsThis(QObject::tr("Opens the selected drive in Windows Explorer."));
19 |
20 | this->setWindowTitle(QObject::tr("AndroidDrive - Devices"));
21 | this->setWindowIcon(QIcon(":/icon.svg"));
22 | this->setWindowFlag(Qt::WindowContextHelpButtonHint, true);
23 | this->setMinimumWidth(400);
24 |
25 | this->_view->setModel(&this->_model);
26 | this->_view->setEditTriggers(QTreeView::NoEditTriggers);
27 | this->_view->setColumnWidth(0, 200);
28 |
29 | layout->addWidget(this->_view, 0, 0, 1, 3);
30 | layout->addWidget(this->_mountButton, 1, 0);
31 | layout->addWidget(this->_settingsButton, 1, 1);
32 | layout->addWidget(this->_openInExplorerButton, 1, 2);
33 |
34 | this->setLayout(layout);
35 |
36 | //Handle the mount button pressed signal
37 | QObject::connect(this->_mountButton, &QPushButton::pressed, this, [this](){
38 | AndroidDevice *device = this->selectedDevice();
39 | if(device != nullptr){
40 | if(device->numberOfConnectedDrives() > 0){
41 | device->disconnectAllDrives();
42 | }
43 | else{
44 | device->connectAllDrives();
45 | }
46 | this->updateButtons();
47 | }
48 | else{
49 | AndroidDrive *drive = this->selectedDrive();
50 | if(drive != nullptr){
51 | if(drive->isConnected()){
52 | drive->disconnectDrive();
53 | }
54 | else{
55 | drive->connectDrive(Settings().driveLetter(drive), this->_model.parentDevice(drive));
56 | }
57 | this->updateButtons();
58 | }
59 | }
60 | });
61 |
62 | //Handle the settings button pressed signal
63 | QObject::connect(this->_settingsButton, &QPushButton::pressed, this, [this](){
64 | AndroidDrive *drive = this->selectedDrive();
65 | if(drive != nullptr){
66 | drive->openSettingsWindow();
67 | }
68 | });
69 |
70 | //Handle the open in explorer button pressed signal
71 | QObject::connect(this->_openInExplorerButton, &QPushButton::pressed, this, [this](){
72 | AndroidDrive *drive = this->selectedDrive();
73 | if(drive != nullptr){
74 | QProcess::startDetached("C:\\Windows\\explorer.exe", {drive->mountPoint()});
75 | }
76 | });
77 |
78 | //Update the buttons when the view is clicked, meaning that the user selected a new item
79 | QObject::connect(this->_view, &QTreeView::clicked, this, [this](){
80 | this->updateButtons();
81 | });
82 |
83 | //Handle when a new device is connected
84 | QObject::connect(&this->_model, &DeviceListModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last){
85 | //For some reason this is necessary for the view to update
86 | this->_view->hide();
87 | this->_view->show();
88 |
89 | //If the parent isn't the root, the child isn't a device, but this function only cares about when devices are connected
90 | if(parent != this->_model.rootIndex()){
91 | return;
92 | }
93 |
94 | const QList> devices = this->_model.devices();
95 | for(int i = first; i <= last; i++){
96 | const std::shared_ptr device = devices[i];
97 | Settings settings;
98 |
99 | //Mount automatically if the setting for that is enabled
100 | device->autoconnectAllDrives();
101 |
102 | //Update the buttons whenever the drive is connected or disconnected
103 | QObject::connect(device.get(), &AndroidDevice::driveConnected, this, &DeviceListWindow::updateButtons);
104 | QObject::connect(device.get(), &AndroidDevice::driveMounted, this, &DeviceListWindow::updateButtons);
105 | QObject::connect(device.get(), &AndroidDevice::driveUnmounted, this, &DeviceListWindow::updateButtons);
106 | QObject::connect(device.get(), &AndroidDevice::driveDisconnected, this, &DeviceListWindow::updateButtons);
107 |
108 | //Handle errors
109 | QObject::connect(device.get(), &AndroidDevice::driveDisconnected, this, &DeviceListWindow::handleDokanError);
110 |
111 | //Open in Explorer if the setting for that is enabled
112 | QObject::connect(device.get(), &AndroidDevice::driveMounted, [](AndroidDrive*, char driveLetter){
113 | if(Settings().openInExplorer()){
114 | QProcess::startDetached("C:\\Windows\\explorer.exe", {driveLetter + QString(":\\")});
115 | }
116 | });
117 |
118 | //Expand items by default
119 | this->_view->expand(this->_model.deviceToIndex(device));
120 | }
121 | });
122 |
123 | //Start ADB to see which devices there are
124 | QObject::connect(&this->_adb, &QProcess::finished, this, &DeviceListWindow::updateDevices);
125 | QObject::connect(&this->_adb, &QProcess::errorOccurred, this, &DeviceListWindow::handleAdbError);
126 | this->_adb.start("adb.exe", {"devices"});
127 |
128 | this->updateButtons();
129 | }
130 |
131 | DeviceListWindow::~DeviceListWindow(){
132 | for(const std::shared_ptr &device: this->_model.devices()){
133 | device->disconnectAllDrives();
134 | }
135 | this->_adb.disconnect();
136 | this->_adb.close();
137 | }
138 |
139 | AndroidDevice *DeviceListWindow::selectedDevice() const{
140 | const QModelIndex selectedIndex = this->_view->currentIndex();
141 | return this->_model.indexToDevice(selectedIndex);
142 | }
143 |
144 | AndroidDrive *DeviceListWindow::selectedDrive() const{
145 | const QModelIndex selectedIndex = this->_view->currentIndex();
146 | return this->_model.indexToDrive(selectedIndex);
147 | }
148 |
149 | void DeviceListWindow::updateButtons(){
150 | //For some reason this is necessary for the view to update
151 | this->_view->hide();
152 | this->_view->show();
153 |
154 | const AndroidDevice *device = this->selectedDevice();
155 | const AndroidDrive *drive = this->selectedDrive();
156 |
157 | this->_mountButton->setEnabled(device != nullptr || drive != nullptr);
158 | this->_settingsButton->setEnabled(drive != nullptr);
159 | this->_openInExplorerButton->setEnabled(drive != nullptr && drive->isConnected());
160 |
161 | if(device != nullptr){
162 | if(device->numberOfConnectedDrives() > 0){
163 | this->_mountButton->setText(QObject::tr("&Unmount all drives"));
164 | this->_mountButton->setWhatsThis(QObject::tr("Unmounts all drives corresponding to the selected Android device.
This only unmounts the drives, the Android device itself will remain connected, so you will still be able to access it for example through ADB."));
165 | }
166 | else{
167 | this->_mountButton->setText(QObject::tr("&Mount all drives"));
168 | this->_mountButton->setWhatsThis(QObject::tr("Mounts a drive containing the internal storage of the selected Android device, as well as a drive for each external SD card that the selected device has, if any."));
169 | }
170 | }
171 | else if(drive != nullptr){
172 | if(drive->isConnected()){
173 | this->_mountButton->setText(QObject::tr("&Unmount drive"));
174 | this->_mountButton->setWhatsThis(QObject::tr("Unmounts the selected drive.
This only unmounts the drive, the Android device itself will remain connected, so you will still be able to access it for example through ADB."));
175 | }
176 | else{
177 | this->_mountButton->setText(QObject::tr("&Mount drive"));
178 | this->_mountButton->setWhatsThis(QObject::tr("Mounts a drive containing the selected internal storage or external SD card."));
179 | }
180 | this->_mountButton->setEnabled(!drive->mountingInProgress() && !drive->unmountingInProgress());
181 | }
182 | }
183 |
184 | void DeviceListWindow::updateDevices(int exitCode, QProcess::ExitStatus){
185 | //Check that it exited correctly
186 | if(exitCode != 0){
187 | DebugLogger::getInstance().log("ADB failed.\nstdout: {}\nstderr: {}", std::make_tuple(this->_adb.readAllStandardOutput().toStdString(), this->_adb.readAllStandardError().toStdString()));
188 | if(this->_adbFailed){
189 | QMessageBox::critical(nullptr, "", QObject::tr("Fatal error: Could not list Android devices.
Try unlocking the device, then unplugging it and re-plugging it.
If this error persists, you may be able to find solutions here (any adb commands mentioned there can be run in the command prompt after running cd \"%3\").")
221 | .arg(serialNumber, "https://stackoverflow.com/q/14993855/4284627", QCoreApplication::applicationDirPath())
222 | );
223 | }
224 | else if(unauthorized){
225 | QMessageBox::warning(nullptr, "",
226 | QObject::tr("Device %1 is unauthorized.
Try unlocking your device. If it shows you a dialog asking if you want to allow this computer to access phone data, tap \"Allow\". If it doesn't show that dialog, disable and re-enable USB debugging as explained here.
If it still isn't working, try unplugging and then re-plugging your device.")
227 | .arg(serialNumber, "https://github.com/GustavLindberg99/AndroidDrive?tab=readme-ov-file#setup")
228 | );
229 | }
230 | }
231 | }
232 | else{
233 | DebugLogger::getInstance().log("Adding device '{}'", serialNumber);
234 | serialNumbers.push_back(serialNumber);
235 | }
236 | }
237 |
238 | //Update the model
239 | this->_model.updateDevices(serialNumbers, offlineSerialNumbers);
240 |
241 | //Start ADB again to continue updating
242 | this->_adb.start("adb.exe", {"devices"});
243 | }
244 |
245 | void DeviceListWindow::handleDokanError(AndroidDrive *drive, int status){
246 | if(status == DOKAN_SUCCESS){
247 | return;
248 | }
249 | QString errorMessage;
250 | QMessageBox::StandardButtons buttons = QMessageBox::Ok;
251 | switch(status){
252 | case DOKAN_DRIVE_LETTER_ERROR:
253 | errorMessage = QObject::tr("Could not create a drive with the given drive letter.");
254 | break;
255 | case DOKAN_DRIVER_INSTALL_ERROR:
256 | if(this->_dokanInstalling){
257 | drive->connectDrive(Settings().driveLetter(drive), this->_model.parentDevice(drive)); //Try connecting it again in case Dokan is finished installing
258 | return;
259 | }
260 | errorMessage = QObject::tr("Dokan doesn't seem to be installed.
Would you like to install it now?");
261 | buttons = QMessageBox::Yes | QMessageBox::No;
262 | break;
263 | case DOKAN_START_ERROR:
264 | errorMessage = QObject::tr("Could not start the driver.");
265 | break;
266 | case DOKAN_MOUNT_ERROR:
267 | case DOKAN_MOUNT_POINT_ERROR:
268 | errorMessage = QObject::tr("Could not assign a drive letter.
Try changing the drive letter in Device Settings to an available drive letter.");
269 | break;
270 | case DOKAN_VERSION_ERROR:
271 | errorMessage = QObject::tr("Dokan version error.");
272 | break;
273 | default:
274 | errorMessage = QObject::tr("An unknown error occurred.");
275 | break;
276 | }
277 | if(QMessageBox::critical(nullptr, "", QObject::tr("Could not mount drive %1: %2").arg(drive->completeName(), errorMessage), buttons) == QMessageBox::Yes){
278 | QDesktopServices::openUrl(QUrl("https://github.com/dokan-dev/dokany/releases/download/v2.0.6.1000/DokanSetup.exe"));
279 | this->_dokanInstalling = true;
280 | }
281 | };
282 |
283 | void DeviceListWindow::handleAdbError(QProcess::ProcessError error){
284 | if(!this->_adbFailed){ //If it only fails once in a while, don't bother the user with it. But if it fails twice in a row, there's probably something wrong, in which case we show an error message and exit.
285 | this->_adbFailed = true;
286 | this->_adb.start("adb.exe", {"devices"});
287 | return;
288 | }
289 |
290 | QString errorMessage;
291 | switch(error){
292 | case QProcess::Timedout:
293 | errorMessage = QObject::tr("ADB timed out.");
294 | break;
295 | case QProcess::ReadError:
296 | errorMessage = QObject::tr("An error occurred when attempting to read from the ADB process.");
297 | break;
298 | case QProcess::WriteError:
299 | errorMessage = QObject::tr("An error occurred when attempting to write to the ADB process.");
300 | break;
301 | case QProcess::FailedToStart:
302 | errorMessage = QObject::tr("ADB failed to start.
Either the adb.exe file is missing, or you may have insufficient permissions to invoke the program.");
303 | break;
304 | case QProcess::Crashed:
305 | errorMessage = QObject::tr("ADB crashed.");
306 | break;
307 | default:
308 | errorMessage = QObject::tr("ADB encountered an unknown error.");
309 | break;
310 | }
311 | QMessageBox::critical(nullptr, "", QObject::tr("Fatal error: Could not list Android devices: %1").arg(errorMessage));
312 | emit this->encounteredFatalError();
313 | }
314 |
--------------------------------------------------------------------------------
/sources/devicelistwindow.hpp:
--------------------------------------------------------------------------------
1 | #ifndef DEVICELISTWINDOW_H
2 | #define DEVICELISTWINDOW_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #include "androiddevice.hpp"
10 | #include "androiddrive.hpp"
11 | #include "devicelistmodel.hpp"
12 |
13 | class DeviceListWindow : public QDialog {
14 | Q_OBJECT
15 |
16 | public:
17 | /**
18 | * Constructs a device list window without opening it.
19 | */
20 | DeviceListWindow();
21 |
22 | /**
23 | * Destructor.
24 | */
25 | virtual ~DeviceListWindow();
26 |
27 | /**
28 | * Gets the selected device.
29 | *
30 | * @return A non-owning pointer to the selected device, or nullptr if no device is selected.
31 | */
32 | AndroidDevice *selectedDevice() const;
33 |
34 | /**
35 | * Gets the selected drive.
36 | *
37 | * @return A non-owning pointer to the selected drive, or nullptr if no device is selected.
38 | */
39 | AndroidDrive *selectedDrive() const;
40 |
41 | signals:
42 | /**
43 | * Emitted when a fatal error is encountered and the program needs to exit.
44 | */
45 | void encounteredFatalError();
46 |
47 | private slots:
48 | /**
49 | * Checks the state of the drives and updates the text and disabled status of the buttons accordingly.
50 | */
51 | void updateButtons();
52 |
53 | /**
54 | * Updates which devices are connected according to ADB.
55 | *
56 | * @param exitCode - ADB's exit code.
57 | * @param exitStatus - ADB's exit status.
58 | */
59 | void updateDevices(int exitCode, QProcess::ExitStatus exitStatus);
60 |
61 | /**
62 | * Handles errors that occur when DokanMain exits with a failure code. Can also be called when Dokan exits successfully, but does nothing in that case.
63 | *
64 | * @param drive - The drive that the error occurred on.
65 | * @param status - The status code returned by Dokan.
66 | */
67 | void handleDokanError(AndroidDrive *drive, int status);
68 |
69 | /**
70 | * Handles errors that occur when the ADB process fails.
71 | *
72 | * @param error - The error that ADB failed with.
73 | */
74 | void handleAdbError(QProcess::ProcessError error);
75 |
76 | private:
77 | QProcess _adb;
78 | bool _adbFailed = false;
79 | bool _dokanInstalling = false;
80 |
81 | DeviceListModel _model;
82 |
83 | QTreeView *const _view = new QTreeView(this);
84 |
85 | QPushButton *const _mountButton = new QPushButton(QObject::tr("&Mount drive"), this);
86 | QPushButton *const _settingsButton = new QPushButton(QObject::tr("Drive &settings"), this);
87 | QPushButton *const _openInExplorerButton = new QPushButton(QObject::tr("&Open in Explorer"), this);
88 | };
89 |
90 | #endif // DEVICELISTWINDOW_H
91 |
--------------------------------------------------------------------------------
/sources/dokanoperations.hpp:
--------------------------------------------------------------------------------
1 | #ifndef DOKANOPERATIONS_H
2 | #define DOKANOPERATIONS_H
3 |
4 | #include
5 |
6 | NTSTATUS DOKAN_CALLBACK createFile(LPCWSTR fileName, PDOKAN_IO_SECURITY_CONTEXT securityContext, ACCESS_MASK desiredAccess, ULONG fileAttributes, ULONG shareAccess, ULONG createDisposition, ULONG createOptions, PDOKAN_FILE_INFO dokanFileInfo);
7 | void DOKAN_CALLBACK closeFile(LPCWSTR fileName, PDOKAN_FILE_INFO dokanFileInfo);
8 | void DOKAN_CALLBACK cleanup(LPCWSTR fileName, PDOKAN_FILE_INFO dokanFileInfo);
9 | NTSTATUS DOKAN_CALLBACK readFile(LPCWSTR fileName, LPVOID buffer, DWORD bufferLength, LPDWORD readLength, LONGLONG offset, PDOKAN_FILE_INFO dokanFileInfo);
10 | NTSTATUS DOKAN_CALLBACK writeFile(LPCWSTR fileName, LPCVOID buffer, DWORD numberOfBytesToWrite, LPDWORD numberOfBytesWritten, LONGLONG offset, PDOKAN_FILE_INFO dokanFileInfo);
11 | NTSTATUS DOKAN_CALLBACK flushFileBuffers(LPCWSTR fileName, PDOKAN_FILE_INFO dokanFileInfo);
12 | NTSTATUS DOKAN_CALLBACK getFileInformation(LPCWSTR fileName, LPBY_HANDLE_FILE_INFORMATION handleFileInformation, PDOKAN_FILE_INFO dokanFileInfo);
13 | NTSTATUS DOKAN_CALLBACK findFiles(LPCWSTR fileName, PFillFindData fillFindData, PDOKAN_FILE_INFO dokanFileInfo);
14 | NTSTATUS DOKAN_CALLBACK setFileAttributes(LPCWSTR fileName, DWORD fileAttributes, PDOKAN_FILE_INFO dokanFileInfo);
15 | NTSTATUS DOKAN_CALLBACK setFileTime(LPCWSTR fileName, const FILETIME *creationTime, const FILETIME *lastAccessTime, const FILETIME *lastWriteTime, PDOKAN_FILE_INFO dokanFileInfo);
16 | NTSTATUS DOKAN_CALLBACK deleteFile(LPCWSTR fileName, PDOKAN_FILE_INFO dokanFileInfo);
17 | NTSTATUS DOKAN_CALLBACK deleteDirectory(LPCWSTR fileName, PDOKAN_FILE_INFO dokanFileInfo);
18 | NTSTATUS DOKAN_CALLBACK moveFile(LPCWSTR oldFileName, LPCWSTR newFileName, BOOL replaceIfExisting, PDOKAN_FILE_INFO dokanFileInfo);
19 | NTSTATUS DOKAN_CALLBACK setAllocationSize(LPCWSTR fileName, LONGLONG allocSize, PDOKAN_FILE_INFO dokanFileInfo);
20 | NTSTATUS DOKAN_CALLBACK getVolumeInformation(LPWSTR volumeNameBuffer, DWORD volumeNameSize, LPDWORD volumeSerialNumber, LPDWORD maximumComponentLength, LPDWORD fileSystemFlags, LPWSTR fileSystemNameBuffer, DWORD fileSystemNameSize, PDOKAN_FILE_INFO dokanFileInfo);
21 | NTSTATUS DOKAN_CALLBACK getDiskFreeSpace(PULONGLONG freeBytesAvailable, PULONGLONG totalNumberOfBytes, PULONGLONG totalNumberOfFreeBytes, PDOKAN_FILE_INFO dokanFileInfo);
22 | NTSTATUS DOKAN_CALLBACK unmounted(PDOKAN_FILE_INFO dokanFileInfo);
23 | NTSTATUS DOKAN_CALLBACK mounted(LPCWSTR mountPoint, PDOKAN_FILE_INFO dokanFileInfo);
24 |
25 | #endif // DOKANOPERATIONS_H
26 |
--------------------------------------------------------------------------------
/sources/drive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
56 |
--------------------------------------------------------------------------------
/sources/helperfunctions.cpp:
--------------------------------------------------------------------------------
1 | #include "helperfunctions.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #include "settings.hpp"
8 |
9 | QString escapeSpecialCharactersForBash(QString filePath){
10 | //List of characters that need to be escaped from https://stackoverflow.com/a/27817504/4284627
11 | const char specialCharacters[] = {'\\', ' ', '!', '"', '#', '$', '&', '\'', '(', ')', '*', ',', ';', '<', '>', '?', '[', ']', '^', '`', '{', '|', '}', '~'}; //It's important for the backslash to be first otherwise it will be escaped when already part of an escape sequence
12 | for(const char character: specialCharacters){
13 | filePath.replace(character, QString("\\") + character);
14 | }
15 | return filePath;
16 | }
17 |
18 | QString getAltStream(LPCWSTR windowsPath){
19 | static const QRegularExpression altStreamRegex("^[^:]+(:|$)");
20 | return QString::fromWCharArray(windowsPath).replace(altStreamRegex, "");
21 | }
22 |
23 | QString androidFileNameToWindowsFileName(QString fileName){
24 | //Use the same trick as WSL for characters that are allowed on Android but not on Windows (this trick consists in replacing a special character with a Unicode version by adding 0xf000 to its char code)
25 | const char specialCharacters[] = {'\\', ':', '*', '?', '"', '<', '>', '|'};
26 | for(const char character: specialCharacters){
27 | fileName.replace(QChar(character), QChar(character + 0xf000));
28 | }
29 | return fileName;
30 | }
31 |
32 | qlonglong microsoftTimeToUnixTime(FILETIME microsoftTime){
33 | LARGE_INTEGER mstLargeInt;
34 | mstLargeInt.LowPart = microsoftTime.dwLowDateTime;
35 | mstLargeInt.HighPart = microsoftTime.dwHighDateTime;
36 | const qlonglong secondsSinceMsEpoch = mstLargeInt.QuadPart / 10000000;
37 | constexpr qlonglong msEpochUnixTimestamp = -11644473600;
38 | return secondsSinceMsEpoch + msEpochUnixTimestamp;
39 | }
40 |
41 | FILETIME unixTimeToMicrosftTime(qlonglong unixTime){
42 | constexpr qlonglong msEpochUnixTimestamp = -11644473600;
43 | const qlonglong secondsSinceMsEpoch = unixTime - msEpochUnixTimestamp;
44 | LARGE_INTEGER mstLargeInt;
45 | mstLargeInt.QuadPart = secondsSinceMsEpoch * 10000000;
46 | FILETIME microsftTime;
47 | microsftTime.dwLowDateTime = mstLargeInt.LowPart;
48 | microsftTime.dwHighDateTime = mstLargeInt.HighPart;
49 | return microsftTime;
50 | }
51 |
52 | DWORD getFileAttributes(bool isDirectory, const QString &fileName){
53 | DWORD fileAttributes = 0;
54 | if(isDirectory){
55 | fileAttributes |= Attribute::Directory;
56 | }
57 | if((fileName.startsWith(".") && Settings().hideDotFiles()) || fileName == "desktop.ini"){
58 | fileAttributes |= Attribute::Hidden;
59 | }
60 | //This is needed for custom folder icons to be displayed correctly (source: https://superuser.com/q/882442/513819)
61 | if(isDirectory || fileName == "desktop.ini"){
62 | fileAttributes |= Attribute::System;
63 | }
64 | return fileAttributes;
65 | }
66 |
--------------------------------------------------------------------------------
/sources/helperfunctions.hpp:
--------------------------------------------------------------------------------
1 | #ifndef HELPERFUNCTIONS_H
2 | #define HELPERFUNCTIONS_H
3 |
4 | #include
5 |
6 | #include
7 |
8 | enum Attribute{
9 | ReadOnly = 0x0001,
10 | Hidden = 0x0002,
11 | System = 0x0004,
12 | VolumeLabel = 0x0008,
13 | Directory = 0x0010,
14 | Archive = 0x0020,
15 | NtfsEfs = 0x0040,
16 | Normal = 0x0080,
17 | Temporary = 0x0100,
18 | Sparse = 0x0200,
19 | ReparsePointData = 0x0400,
20 | Compressed = 0x0800,
21 | Offline = 0x1000
22 | };
23 |
24 | /**
25 | * Escapes special characters so that a file path can be passed to a bash command.
26 | *
27 | * @param filePath - The raw file path to be escaped.
28 | *
29 | * @return The escaped file path that can be passed to a bash command.
30 | */
31 | QString escapeSpecialCharactersForBash(QString filePath);
32 |
33 | /**
34 | * Gets the alt stream from a Windows path, i.e. the characters after the first ':' character.
35 | *
36 | * @param windowsPath - The path to get the alt stream from.
37 | *
38 | * @return The alt stream.
39 | */
40 | QString getAltStream(LPCWSTR windowsPath);
41 |
42 | /**
43 | * Use the same trick as WSL to escape characters that are allowed on Android/Linux but not on Windows (this trick consists in replacing a special character with a Unicode version by adding 0xf000 to its char code).
44 | *
45 | * @param fileName - The unescaped name of the file.
46 | *
47 | * @return The name of the file with all characters that are disallowed by Windows replaced with a Unicode version.
48 | */
49 | QString androidFileNameToWindowsFileName(QString fileName);
50 |
51 | /**
52 | * Converts a Microsoft time to a Unix time.
53 | *
54 | * @param microsoftTime - The Microsoft timestamp to convert.
55 | *
56 | * @return The corresponding Unix timestamp.
57 | */
58 | qlonglong microsoftTimeToUnixTime(FILETIME microsoftTime);
59 |
60 | /**
61 | * Converts a Unix time to a Microsoft time.
62 | *
63 | * @param unixTime - The Unix timestamp to convert.
64 | *
65 | * @return The corresponding Microsoft timestamp.
66 | */
67 | FILETIME unixTimeToMicrosftTime(qlonglong unixTime);
68 |
69 | /**
70 | * Gets the Windows file attributes of an Android file based on the file name and whether it's a directory.
71 | *
72 | * @param isDirectory - True if it's a directory, false otherwise.
73 | * @param fileName - The name of the file.
74 | *
75 | * @return A bitmask with the file attributes that Windows should use for this file.
76 | */
77 | DWORD getFileAttributes(bool isDirectory, const QString &fileName);
78 |
79 | #endif // HELPERFUNCTIONS_H
80 |
--------------------------------------------------------------------------------
/sources/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/icon.ico
--------------------------------------------------------------------------------
/sources/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
70 |
--------------------------------------------------------------------------------
/sources/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | #include "androiddevice.hpp"
8 | #include "debuglogger.hpp"
9 | #include "devicelistwindow.hpp"
10 | #include "settingswindow.hpp"
11 | #include "updates.hpp"
12 | #include "version.h"
13 |
14 | #include
15 | #include
16 | #include
17 |
18 | /**
19 | * Checks if another instance of this process is already running.
20 | *
21 | * @return True if it is, false if it isn't.
22 | */
23 | bool isAlreadyRunning(){
24 | DWORD pids[1024], cbNeeded;
25 | if(!EnumProcesses(pids, sizeof(pids), &cbNeeded)){
26 | return false;
27 | }
28 |
29 | bool alreadyFound = false;
30 | const DWORD numberOfProcesses = cbNeeded / sizeof(DWORD);
31 | for(unsigned int i = 0; i < numberOfProcesses; i++){
32 | const DWORD pid = pids[i];
33 | if(pid == 0){
34 | continue;
35 | }
36 | HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
37 | if(handle == nullptr){
38 | continue;
39 | }
40 | HMODULE hMod;
41 | DWORD cbNeeded;
42 | if(EnumProcessModules(handle, &hMod, sizeof(hMod), &cbNeeded)){
43 | wchar_t currentProcessPath[MAX_PATH] = L"";
44 | GetModuleFileName(hMod, currentProcessPath, MAX_PATH);
45 | if(QDir::toNativeSeparators(QApplication::applicationFilePath()) == QString::fromWCharArray(currentProcessPath)){
46 | //If another instance of the process is already running, there will be at least 2 instances listed (this one and the other one), in which case return true. Otherwise, there will still be one instance (the one currently doing the check), in which case return false.
47 | if(alreadyFound){
48 | return true;
49 | }
50 | else{
51 | alreadyFound = true;
52 | }
53 | }
54 | }
55 | }
56 |
57 | return false;
58 | }
59 |
60 | int main(int argc, char **argv){
61 | QApplication app(argc, argv);
62 | if(isAlreadyRunning()){
63 | QMessageBox::information(nullptr, "", QObject::tr("AndroidDrive is already running.
If you're trying to restart AndroidDrive, you can close the existing process by right clicking on the AndroidDrive icon in the task bar and selecting Exit."));
64 | return ERROR_SERVICE_ALREADY_RUNNING;
65 | }
66 | app.setQuitOnLastWindowClosed(false);
67 | DokanInit();
68 |
69 |
70 | //Load translations
71 | QTranslator translator, baseTranslator;
72 | QString language = Settings().language();
73 | if(language == "auto"){
74 | language = QLocale::system().name().section('_', 0, 0);
75 | }
76 | SettingsWindow::systemLanguageAvailable = translator.load(":/translations/androiddrive_" + language) || language == "en";
77 | (void) baseTranslator.load(":/translations/qtbase_" + language);
78 | app.installTranslator(&translator);
79 | app.installTranslator(&baseTranslator);
80 |
81 |
82 | //Create the tray icon and the windows
83 | auto trayIcon = std::make_unique(QIcon(":/icon.svg"));
84 | auto deviceListWindow = std::make_unique();
85 | auto settingsWindow = std::make_unique(nullptr);
86 | const auto quit = [&trayIcon, &deviceListWindow, &settingsWindow](){
87 | AndroidDevice::quitOnLastDeletedDevice();
88 | trayIcon.reset();
89 | deviceListWindow.reset();
90 | settingsWindow.reset();
91 | };
92 | checkForUpdates(
93 | QUrl("https://github.com/GustavLindberg99/AndroidDrive"),
94 | QUrl("https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/main/sources/version.h"),
95 | QUrl("https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/main/AndroidDrive-portable.zip"),
96 | QUrl("https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/main/AndroidDrive-install.exe"),
97 | quit
98 | );
99 |
100 |
101 | //Initialize the device list window
102 | QObject::connect(deviceListWindow.get(), &DeviceListWindow::encounteredFatalError, quit);
103 |
104 |
105 | //Initialize the tray icon
106 | QMenu contextMenu;
107 |
108 | QAction *deviceListAction = contextMenu.addAction(QObject::tr("&Devices"));
109 | QObject::connect(deviceListAction, &QAction::triggered, deviceListWindow.get(), &QWidget::show);
110 |
111 | QAction *settingsAction = contextMenu.addAction(QObject::tr("&Settings"));
112 | QObject::connect(settingsAction, &QAction::triggered, settingsWindow.get(), &QWidget::show);
113 |
114 | QAction *debugAction = contextMenu.addAction(QObject::tr("Record Debug &Logs"));
115 | QObject::connect(debugAction, &QAction::triggered, [debugAction](){
116 | DebugLogger &logger = DebugLogger::getInstance();
117 | if(logger.isRecording()){
118 | logger.stop();
119 | debugAction->setText(QObject::tr("Record Debug &Logs"));
120 | }
121 | else{
122 | if(logger.start()){
123 | debugAction->setText(QObject::tr("Finish Recording Debug &Logs"));
124 | QMessageBox::information(nullptr, "", QObject::tr("Recording of debug logs has started.
You will be able to find the log file in %1.
If you're planning on attaching the log file to a bug report, keep in mind that the log file will contain the names of the files on your phone, so make sure that the filenames don't contain any sensitive information (The debug logs will only contain the file names, they won't contain the contents of any file).").arg(DebugLogger::getInstance().logFilePath()));
125 | }
126 | else{
127 | QMessageBox::critical(nullptr, "", QObject::tr("Failed to create log file."));
128 | }
129 | }
130 | });
131 |
132 | QAction *aboutAction = contextMenu.addAction(QObject::tr("&About AndroidDrive"));
133 | QObject::connect(aboutAction, &QAction::triggered, aboutAction, [](){
134 | QMessageBox msg;
135 | msg.setIconPixmap(QPixmap(":/icon.svg"));
136 | msg.setWindowIcon(QIcon(":/icon.svg"));
137 | msg.setWindowTitle(QObject::tr("About AndroidDrive"));
138 | msg.setText(QObject::tr("AndroidDrive version %1 by Gustav Lindberg.").arg(PROGRAMVERSION) + "
" + QObject::tr("This program uses %1 and %2.").arg("ADB", "Dokan"));
139 | msg.exec();
140 | });
141 |
142 | QAction *aboutQtAction = contextMenu.addAction(QObject::tr("About &Qt"));
143 | QObject::connect(aboutQtAction, &QAction::triggered, [](){
144 | QMessageBox::aboutQt(nullptr);
145 | });
146 |
147 | QAction *exitAction = contextMenu.addAction(QObject::tr("E&xit"));
148 | QObject::connect(exitAction, &QAction::triggered, quit);
149 | trayIcon->setContextMenu(&contextMenu);
150 |
151 | trayIcon->setToolTip("AndroidDrive");
152 | trayIcon->show();
153 |
154 |
155 | //Run the program
156 | const int status = app.exec();
157 | DokanShutdown();
158 | return status;
159 | }
160 |
--------------------------------------------------------------------------------
/sources/phone.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
56 |
--------------------------------------------------------------------------------
/sources/resource.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | translations/androiddrive_fr.qm
4 | translations/androiddrive_hu.qm
5 | translations/androiddrive_it.qm
6 | translations/androiddrive_sv.qm
7 | translations/qtbase_fr.qm
8 | translations/qtbase_hu.qm
9 | translations/qtbase_it.qm
10 | translations/qtbase_sv.qm
11 | drive.svg
12 | phone.svg
13 | systemdrive.svg
14 | icon.svg
15 | icon.ico
16 | translations/androiddrive_de.qm
17 | translations/qtbase_de.qm
18 |
19 |
20 |
--------------------------------------------------------------------------------
/sources/resource.rc:
--------------------------------------------------------------------------------
1 | #include
2 | //Icons
3 | 0 ICON "icon.ico"
4 |
5 | #include "version.h"
6 | VS_VERSION_INFO VERSIONINFO
7 | FILEVERSION PROGRAMVERSION_NUMBER
8 | PRODUCTVERSION PROGRAMVERSION_NUMBER
9 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
10 | FILEOS VOS__WINDOWS32
11 | FILETYPE VFT_APP
12 | FILESUBTYPE VFT2_UNKNOWN
13 | BEGIN
14 | BLOCK "StringFileInfo"
15 | BEGIN
16 | BLOCK "040904E4"
17 | BEGIN
18 | VALUE "FileDescription", "AndroidDrive"
19 | VALUE "ProductName", "AndroidDrive"
20 | VALUE "FileVersion", PROGRAMVERSION
21 | VALUE "ProductVersion", PROGRAMVERSION
22 | END
23 | END
24 | BLOCK "VarFileInfo"
25 | BEGIN
26 | VALUE "Translation", 0x0409, 1252
27 | END
28 | END
29 |
--------------------------------------------------------------------------------
/sources/settings.cpp:
--------------------------------------------------------------------------------
1 | #include "settings.hpp"
2 |
3 | #include "androiddrive.hpp"
4 |
5 | char Settings::driveLetter(const AndroidDrive *drive) const{
6 | return this->_settings.value(drive->id() + "_driveLetter", 'D').toChar().toLatin1();
7 | }
8 |
9 | void Settings::setDriveLetter(const AndroidDrive *drive, char driveLetter){
10 | this->_settings.setValue(drive->id() + "_driveLetter", driveLetter);
11 | }
12 |
13 | QString Settings::driveName(const AndroidDrive *drive) const{
14 | return this->_settings.value(drive->id() + "_driveName", drive->completeName()).toString();
15 | }
16 |
17 | void Settings::setDriveName(const AndroidDrive *drive, const QString &driveName){
18 | this->_settings.setValue(drive->id() + "_driveName", driveName);
19 | }
20 |
21 | bool Settings::autoConnect(const AndroidDrive *drive) const{
22 | return this->_settings.value(drive->id() + "_connectAutomatically", true).toBool();
23 | }
24 |
25 | void Settings::setAutoConnect(const AndroidDrive *drive, bool autoConnect){
26 | this->_settings.setValue(drive->id() + "_connectAutomatically", autoConnect);
27 | }
28 |
29 | bool Settings::openInExplorer() const{
30 | return this->_settings.value("openInExplorer", true).toBool();
31 | }
32 |
33 | void Settings::setOpenInExplorer(bool openInExplorer){
34 | this->_settings.setValue("openInExplorer", openInExplorer);
35 | }
36 |
37 | bool Settings::hideDotFiles() const{
38 | return this->_settings.value("hideDotFiles", true).toBool();
39 | }
40 |
41 | void Settings::setHideDotFiles(bool hideDotFiles){
42 | this->_settings.setValue("hideDotFiles", hideDotFiles);
43 | }
44 |
45 | QString Settings::language() const{
46 | return this->_settings.value("language", "auto").toString();
47 | }
48 |
49 | void Settings::setLanguage(const QString &language){
50 | this->_settings.setValue("language", language);
51 | }
52 |
--------------------------------------------------------------------------------
/sources/settings.hpp:
--------------------------------------------------------------------------------
1 | #ifndef SETTINGS_H
2 | #define SETTINGS_H
3 |
4 | #include
5 |
6 | class AndroidDrive;
7 |
8 | class Settings final {
9 | public:
10 | /**
11 | * Constructs a settings object containing the currently saved settings.
12 | */
13 | Settings() = default;
14 |
15 | /**
16 | * Gets the drive letter that the user prefers for the given drive.
17 | *
18 | * @param drive - The drive to get the drive letter for.
19 | *
20 | * @return The drive letter for the given drive.
21 | */
22 | char driveLetter(const AndroidDrive *drive) const;
23 |
24 | /**
25 | * Sets the drive letter that the user prefers for the given drive (does not change the drive object itself, only the settings).
26 | *
27 | * @param drive - The drive to set the drive letter for.
28 | * @param driveLetter - The drive letter to set.
29 | */
30 | void setDriveLetter(const AndroidDrive *drive, char driveLetter);
31 |
32 | /**
33 | * Gets the name that the user set for the given drive.
34 | *
35 | * @param drive - The drive to get the name of.
36 | *
37 | * @return The name of the given drive.
38 | */
39 | QString driveName(const AndroidDrive *drive) const;
40 |
41 | /**
42 | * Sets the name for the given drive (does not change the drive object itself, only the settings).
43 | *
44 | * @param drive - The drive to set the name of.
45 | * @param driveName - The name to set.
46 | */
47 | void setDriveName(const AndroidDrive *drive, const QString &driveName);
48 |
49 | /**
50 | * Gets whether the given drive should be automatically mounted when the device is connected.
51 | *
52 | * @param drive - The drive to get if it should be automatically mounted.
53 | *
54 | * @return True if it should be automatically mounted, false if it shouldn't.
55 | */
56 | bool autoConnect(const AndroidDrive *drive) const;
57 |
58 | /**
59 | * Aets whether the given drive should be automatically mounted when the device is connected (does not change the drive object itself, only the settings).
60 | *
61 | * @param drive - The drive to set if it should be automatically mounted.
62 | * @param autoConnect - True if it should be automatically mounted, false if it shouldn't.
63 | */
64 | void setAutoConnect(const AndroidDrive *drive, bool autoConnect);
65 |
66 | /**
67 | * Checks if newly connected drives should be opened in Windows Explorer.
68 | *
69 | * @return True if newly connected drives should be opened in Windows Explorer, false if they shouldn't.
70 | */
71 | bool openInExplorer() const;
72 |
73 | /**
74 | * Sets if newly connected drives should be opened in Windows Explorer.
75 | *
76 | * @param openInExplorer - True if newly connected drives should be opened in Windows Explorer, false if they shouldn't.
77 | */
78 | void setOpenInExplorer(bool openInExplorer);
79 |
80 | /**
81 | * Checks whether files beginning with a dot should be hidden.
82 | *
83 | * @return True if files beginning with a dot should be hidden, false if they shouldn't.
84 | */
85 | bool hideDotFiles() const;
86 |
87 | /**
88 | * Sets whether files beginning with a dot should be hidden.
89 | *
90 | * @return True if files beginning with a dot should be hidden, false if they shouldn't.
91 | */
92 | void setHideDotFiles(bool hideDotFiles);
93 |
94 | /**
95 | * Gets the language that the tray icon settings windows should be displayed in.
96 | *
97 | * @return The two-letter code of the language, or "auto" if the language should be automatically detected based on the system lanugage.
98 | */
99 | QString language() const;
100 |
101 | /**
102 | * Sets the language that the tray icon settings windows should be displayed in.
103 | *
104 | * @return The two-letter code of the language, or "auto" if the language should be automatically detected based on the system lanugage.
105 | */
106 | void setLanguage(const QString &language);
107 |
108 | private:
109 | QSettings _settings = QSettings("Gustav Lindberg", "AndroidDrive");
110 | };
111 |
112 | #endif // SETTINGS_H
113 |
--------------------------------------------------------------------------------
/sources/settingswindow.cpp:
--------------------------------------------------------------------------------
1 | #include "settingswindow.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include "androiddrive.hpp"
9 |
10 | QSet SettingsWindow::_instances;
11 | bool SettingsWindow::systemLanguageAvailable = true;
12 | const QStringList SettingsWindow::_languageNames{"", "Deutsch", "English", "Français", "Magyar", "Italiano", "Svenska"};
13 | const QStringList SettingsWindow::_languageAbbreviations{"auto", "de", "en", "fr", "hu", "it", "sv"};
14 |
15 | SettingsWindow::SettingsWindow(const AndroidDrive *drive):
16 | _drive(drive)
17 | {
18 | SettingsWindow::_instances.insert(this);
19 |
20 | this->setWindowTitle(QObject::tr("AndroidDrive - Settings"));
21 | this->setWindowIcon(QIcon(":/icon.svg"));
22 | this->setWindowFlag(Qt::WindowContextHelpButtonHint, true);
23 |
24 | QGridLayout *layout = new QGridLayout(this);
25 |
26 | const auto enableApplyButton = [this](){this->_applyButton->setEnabled(true);};
27 |
28 | if(this->_drive != nullptr){
29 | QGroupBox *driveSettingsBox = new QGroupBox(QObject::tr("Drive settings for %1").arg(drive->completeName()), this);
30 | QFormLayout *driveSettingsLayout = new QFormLayout(driveSettingsBox);
31 |
32 | this->_driveLetter = new QComboBox(driveSettingsBox);
33 | QObject::connect(this->_driveLetter, &QComboBox::currentIndexChanged, this->_applyButton, enableApplyButton);
34 | this->_driveLetter->setWhatsThis(QObject::tr("Allows you to select a preferred drive letter for the selected Android drive.
If the preferred drive letter is unavailable when this drive is connected, it will use the next available drive letter in alphabetical order.
If you change the drive letter while this drive is connected, you will have to unmount it and re-mount it again for the changes to take effect."));
35 | driveSettingsLayout->addRow(QObject::tr("Drive &letter"), this->_driveLetter);
36 |
37 | QWidget *driveNameContainer = new QWidget(driveSettingsBox);
38 | QHBoxLayout *driveNameLayout = new QHBoxLayout(driveNameContainer);
39 |
40 | this->_driveName = new QLineEdit(driveNameContainer);
41 | QObject::connect(this->_driveName, &QLineEdit::textEdited, this->_applyButton, enableApplyButton);
42 | this->_driveName->setWhatsThis(QObject::tr("Allows you to select a name for the selected Android drive."));
43 | driveNameLayout->addWidget(this->_driveName);
44 |
45 | QPushButton *resetDriveName = new QPushButton(QObject::tr("&Reset"), driveNameContainer);
46 | QObject::connect(resetDriveName, &QPushButton::pressed, this->_driveName, [this](){
47 | const QString name = this->_drive->completeName();
48 | this->_driveName->setText(name);
49 | emit this->_driveName->textEdited(name);
50 | });
51 | resetDriveName->setWhatsThis(QObject::tr("Resets the name of the drive to the default value."));
52 | driveNameLayout->addWidget(resetDriveName);
53 |
54 | driveNameLayout->setContentsMargins(0, 0, 0, 0);
55 | driveNameContainer->setLayout(driveNameLayout);
56 | driveSettingsLayout->addRow(QObject::tr("Drive &name"), driveNameContainer);
57 |
58 | this->_autoConnect = new QCheckBox(QObject::tr("Automatically mount &drive"), driveSettingsBox);
59 | QObject::connect(this->_autoConnect, &QCheckBox::clicked, this->_applyButton, enableApplyButton);
60 | this->_autoConnect->setWhatsThis(QObject::tr("If this checkbox is checked, the selected drive will be automatically connected as a drive whenever you plug it into your computer.
Otherwise, you will have to mount it manually by going into Devices > Mount drive."));
61 | driveSettingsLayout->addRow(this->_autoConnect);
62 |
63 | driveSettingsBox->setWhatsThis(QObject::tr("These settings only affect the selected drive."));
64 | driveSettingsBox->setLayout(driveSettingsLayout);
65 | layout->addWidget(driveSettingsBox, 0, 0, 1, 3);
66 | }
67 |
68 | QGroupBox *globalSettingsBox = new QGroupBox(QObject::tr("Global settings"), this);
69 | QFormLayout *globalSettingsLayout = new QFormLayout(globalSettingsBox);
70 |
71 | this->_openInExplorer = new QCheckBox(QObject::tr("Open newly connected drives in &Explorer"), globalSettingsBox);
72 | QObject::connect(this->_openInExplorer, &QCheckBox::clicked, this->_applyButton, enableApplyButton);
73 | this->_openInExplorer->setWhatsThis(QObject::tr("If this checkbox is checked, whenever AndroidDrive is finished connecting a drive, it will open that drive in Windows Explorer."));
74 | globalSettingsLayout->addRow(this->_openInExplorer);
75 |
76 | this->_hideDotFiles = new QCheckBox(QObject::tr("&Hide files beginning with a dot"), globalSettingsBox);
77 | QObject::connect(this->_hideDotFiles, &QCheckBox::clicked, this->_applyButton, enableApplyButton);
78 | this->_hideDotFiles->setWhatsThis(QObject::tr("If this checkbox is checked, files that begin with a dot will be treated as hidden files, and will only be visible in Windows Explorer if Windows Explorer's \"Show hidden files\" option is activated."));
79 | globalSettingsLayout->addRow(this->_hideDotFiles);
80 |
81 | this->_language = new QComboBox(globalSettingsBox);
82 | QObject::connect(this->_language, &QComboBox::currentIndexChanged, this->_applyButton, enableApplyButton);
83 | for(const QString &language: SettingsWindow::_languageNames){
84 | if(language.isEmpty()){
85 | //QObject::tr("Use system language") can't go directly in _languageNames because otherwise it will be initialized before the translations are loaded
86 | this->_language->addItem(QObject::tr("Use system language"));
87 | }
88 | else{
89 | this->_language->addItem(language);
90 | }
91 | }
92 | this->_language->setWhatsThis(QObject::tr("Allows you to select which language AndroidDrive's GUI will be displayed in.
If you select \"Use system language\" but AndroidDrive isn't availiable in your system language, English will be used.
This setting has no effect on how the actual drive works.
You must restart AndroidDrive for this change to take effect."));
93 | globalSettingsLayout->addRow(QObject::tr("&Language"), this->_language);
94 |
95 | if(!systemLanguageAvailable){
96 | QLabel *contributeToTranslation = new QLabel(QObject::tr("AndroidDrive doesn't seem to be available in your system language.
Click here if you would like to help translate it.").arg("href=\"https://github.com/GustavLindberg99/AndroidDrive/blob/main/sources/translations/contribute.md\""), this);
97 | contributeToTranslation->setWordWrap(true);
98 | contributeToTranslation->setTextFormat(Qt::RichText);
99 | contributeToTranslation->setTextInteractionFlags(Qt::TextBrowserInteraction);
100 | contributeToTranslation->setOpenExternalLinks(true);
101 | globalSettingsLayout->addRow(contributeToTranslation);
102 | }
103 |
104 | globalSettingsBox->setWhatsThis(QObject::tr("These settings affect all drives connected with AndroidDrive."));
105 | globalSettingsBox->setLayout(globalSettingsLayout);
106 | layout->addWidget(globalSettingsBox, drive != nullptr, 0, 1, 3);
107 |
108 | QPushButton *okButton = new QPushButton(QObject::tr("&OK"), this);
109 | QPushButton *cancelButton = new QPushButton(QObject::tr("&Cancel"), this);
110 |
111 | layout->addWidget(okButton, 1 + (drive != nullptr), 0);
112 | layout->addWidget(cancelButton, 1 + (drive != nullptr), 1);
113 | layout->addWidget(this->_applyButton, 1 + (drive != nullptr), 2);
114 |
115 | this->setLayout(layout);
116 |
117 | Settings() >> this;
118 |
119 | QObject::connect(okButton, &QPushButton::clicked, this, [this](){
120 | this->_applyButton->click();
121 | this->close();
122 | });
123 | QObject::connect(cancelButton, &QPushButton::clicked, this, [this](){
124 | Settings() >> this;
125 | this->close();
126 | });
127 | QObject::connect(this->_applyButton, &QPushButton::clicked, this, [this](){
128 | Settings settings;
129 | settings << this;
130 | for(SettingsWindow *settingsWindow: qAsConst(SettingsWindow::_instances)){
131 | settings >> settingsWindow;
132 | }
133 | });
134 | }
135 |
136 | SettingsWindow::~SettingsWindow(){
137 | SettingsWindow::_instances.remove(this);
138 | }
139 |
140 | Settings &operator<<(Settings &settings, const SettingsWindow *settingsWindow){
141 | if(settingsWindow->_drive != nullptr){
142 | settings.setDriveLetter(settingsWindow->_drive, settingsWindow->_driveLetter->currentText().at(0).toLatin1());
143 | settings.setDriveName(settingsWindow->_drive, settingsWindow->_driveName->text());
144 | settings.setAutoConnect(settingsWindow->_drive, settingsWindow->_autoConnect->isChecked());
145 | }
146 | settings.setOpenInExplorer(settingsWindow->_openInExplorer->isChecked());
147 | settings.setHideDotFiles(settingsWindow->_hideDotFiles->isChecked());
148 | settings.setLanguage(SettingsWindow::_languageAbbreviations[settingsWindow->_language->currentIndex()]);
149 | settingsWindow->_applyButton->setEnabled(false);
150 | return settings;
151 | }
152 |
153 | const Settings &operator>>(const Settings &settings, SettingsWindow *settingsWindow){
154 | if(settingsWindow->_drive != nullptr){
155 | settingsWindow->_driveLetter->clear();
156 | for(char letter = 'A'; letter <= 'Z'; letter++){
157 | settingsWindow->_driveLetter->addItem(letter + QString(":"));
158 | if(letter == settings.driveLetter(settingsWindow->_drive)){
159 | settingsWindow->_driveLetter->setCurrentIndex(settingsWindow->_driveLetter->count() - 1);
160 | }
161 | }
162 | settingsWindow->_driveName->setText(settings.driveName(settingsWindow->_drive));
163 | settingsWindow->_autoConnect->setChecked(settings.autoConnect(settingsWindow->_drive));
164 | }
165 | settingsWindow->_openInExplorer->setChecked(settings.openInExplorer());
166 | settingsWindow->_hideDotFiles->setChecked(settings.hideDotFiles());
167 | settingsWindow->_language->setCurrentIndex(SettingsWindow::_languageAbbreviations.indexOf(settings.language()));
168 | settingsWindow->_applyButton->setEnabled(false);
169 | return settings;
170 | }
171 |
--------------------------------------------------------------------------------
/sources/settingswindow.hpp:
--------------------------------------------------------------------------------
1 | #ifndef SETTINGSWINDOW_H
2 | #define SETTINGSWINDOW_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include "settings.hpp"
13 |
14 | class SettingsWindow : public QDialog {
15 | Q_OBJECT
16 |
17 | public:
18 | /**
19 | * Constructs a settings window for a drive (without opening the window).
20 | *
21 | * @param drive - A non-owning pointer to the drive that these settings are for (the settings window should be owned by the drive to ensure that it's closed when the drive is destroyed). Can be null to show only general settings.
22 | */
23 | explicit SettingsWindow(const AndroidDrive *drive);
24 |
25 | /**
26 | * Destructor.
27 | */
28 | virtual ~SettingsWindow();
29 |
30 | /**
31 | * Saves the settings specified in a settings window to a settings object.
32 | *
33 | * @param settings - The settings object to save the settings to.
34 | * @param settingsWindow - The settings window to get the settings from.
35 | *
36 | * @return The settings object to enable chaining.
37 | */
38 | friend Settings &operator<<(Settings &settings, const SettingsWindow *settingsWindow);
39 |
40 | /**
41 | * Displays the settings from a settings object in a settings window.
42 | *
43 | * @param settings - The settings object to load the settings from.
44 | * @param settingsWindow - The settings window to display the settings in.
45 | *
46 | * @return The settings object to enable chaining.
47 | */
48 | friend const Settings &operator>>(const Settings &settings, SettingsWindow *settingsWindow);
49 |
50 | static bool systemLanguageAvailable;
51 |
52 | private:
53 | static QSet _instances;
54 | static const QStringList _languageNames, _languageAbbreviations;
55 |
56 | const AndroidDrive *const _drive;
57 |
58 | QComboBox *_driveLetter = nullptr;
59 | QLineEdit *_driveName = nullptr;
60 | QCheckBox *_autoConnect = nullptr;
61 |
62 | QCheckBox *_openInExplorer;
63 | QCheckBox *_hideDotFiles;
64 | QComboBox *_language;
65 |
66 | QPushButton *const _applyButton = new QPushButton(QObject::tr("&Apply"), this);
67 | };
68 |
69 | #endif // SETTINGSWINDOW_H
70 |
--------------------------------------------------------------------------------
/sources/systemdrive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
82 |
--------------------------------------------------------------------------------
/sources/temporaryfile.cpp:
--------------------------------------------------------------------------------
1 | #include "temporaryfile.hpp"
2 |
3 | #include "androiddrive.hpp"
4 | #include "debuglogger.hpp"
5 | #include "helperfunctions.hpp"
6 |
7 | //Since AndroidDrive reads and writes to files by copying them to local temporary files, a lot of this code is based on Dokan's Mirror example.
8 |
9 | TemporaryFile::TemporaryFile(const AndroidDrive *drive, const QString &remotePath, DWORD creationDisposition, ULONG shareAccess, ACCESS_MASK desiredAccess, ULONG fileAttributes, ULONG createOptions, ULONG createDisposition, bool exists, const QString &altStream):
10 | _localPath(drive->localPath(remotePath)),
11 | _remotePath(remotePath),
12 | _device(drive->device()),
13 | _handle(INVALID_HANDLE_VALUE),
14 | _errorCode(STATUS_SUCCESS),
15 | _modified(!exists)
16 | {
17 | DebugLogger::getInstance().log("Creating temporary file. localPath: '{}', remotePath: '{}', device: '{}', exists: {}, creationDisposition: {}, shareAccess: {}, desiredAccess: {}, fileAttributes: {}, createOptions: {}, createDisposition: {}, altStream: {}", std::make_tuple(this->_localPath, this->_remotePath, this->_device->serialNumber(), exists, creationDisposition, shareAccess, desiredAccess, fileAttributes, createOptions, createDisposition, altStream));
18 |
19 | if(exists && !this->_device->pullFromAdb(remotePath, this->_localPath) && !QFileInfo(this->_localPath).isFile()){
20 | DebugLogger::getInstance().log("Failed to pul temporary file '{}' from ADB", this->_remotePath);
21 | this->_errorCode = STATUS_UNSUCCESSFUL;
22 | return;
23 | }
24 |
25 | ACCESS_MASK genericDesiredAccess;
26 | DWORD fileAttributesAndFlags;
27 | DokanMapKernelToUserCreateFileFlags(desiredAccess, fileAttributes, createOptions, createDisposition, &genericDesiredAccess, &fileAttributesAndFlags, &creationDisposition);
28 | if(creationDisposition == TRUNCATE_EXISTING){
29 | genericDesiredAccess |= GENERIC_WRITE;
30 | }
31 | DebugLogger::getInstance().log("genericDesiredAccess: {}, fileAttributesAndFlags: {}, creationDisposition: {}", std::make_tuple(genericDesiredAccess, fileAttributesAndFlags, creationDisposition));
32 |
33 | this->_handle = CreateFile(this->localPathWithAltStream(altStream).c_str(), genericDesiredAccess, shareAccess, nullptr, creationDisposition, fileAttributesAndFlags, nullptr);
34 |
35 | if(this->_handle == INVALID_HANDLE_VALUE){
36 | this->_errorCode = DokanNtStatusFromWin32(GetLastError());
37 | DebugLogger::getInstance().log("Invalid handle, error code: {}", this->_errorCode);
38 | }
39 | else{
40 | DebugLogger::getInstance().log("Valid handle: {}", this->_handle);
41 | }
42 | }
43 |
44 | TemporaryFile::~TemporaryFile(){
45 | DebugLogger::getInstance().log("Destroying temporary file with remote path '{}'", this->_remotePath);
46 | if(this->_handle != INVALID_HANDLE_VALUE){
47 | CloseHandle(this->_handle);
48 | }
49 | }
50 |
51 | NTSTATUS TemporaryFile::errorCode() const{
52 | return this->_errorCode;
53 | }
54 |
55 | NTSTATUS TemporaryFile::read(LPVOID buffer, DWORD bufferLength, LPDWORD readLength, LONGLONG offset, const QString &altStream) const{
56 | DebugLogger::getInstance().log("Calling read on temporary file with local path '{}': bufferLength: {}, offset: {}, altStream: {}", std::make_tuple(this->_localPath, bufferLength, offset, altStream));
57 |
58 | NTSTATUS status = STATUS_SUCCESS;
59 | bool closeHandleWhenFinished = false;
60 |
61 | HANDLE handle = this->_handle;
62 | if(handle == INVALID_HANDLE_VALUE){
63 | handle = CreateFile(this->localPathWithAltStream(altStream).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
64 | if(handle == INVALID_HANDLE_VALUE){
65 | DebugLogger::getInstance().log("Failed to create handle during read");
66 | return DokanNtStatusFromWin32(GetLastError());
67 | }
68 | DebugLogger::getInstance().log("Handle not initialized, creating it during read");
69 | closeHandleWhenFinished = true;
70 | }
71 |
72 | LARGE_INTEGER distanceToMove;
73 | distanceToMove.QuadPart = offset;
74 | if(!SetFilePointerEx(handle, distanceToMove, nullptr, FILE_BEGIN) || !ReadFile(handle, buffer, bufferLength, readLength, nullptr)){
75 | status = DokanNtStatusFromWin32(GetLastError());
76 | DebugLogger::getInstance().log("Reading file '{}' failed. Error code: {}", std::make_tuple(this->_localPath, status));
77 | }
78 | else{
79 | DebugLogger::getInstance().log("Reading file '{}' succeeded", this->_localPath);
80 | }
81 |
82 | if(closeHandleWhenFinished){
83 | DebugLogger::getInstance().log("Closing handle after read");
84 | CloseHandle(handle);
85 | }
86 |
87 | return status;
88 | }
89 |
90 | NTSTATUS TemporaryFile::write(LPCVOID buffer, DWORD numberOfBytesToWrite, LPDWORD numberOfBytesWritten, LONGLONG offset, PDOKAN_FILE_INFO dokanFileInfo, const QString &altStream){
91 | DebugLogger::getInstance().log("Calling write on temporary file with local path '{}': numberOfBytesToWrite: {}, offset: {}, altStream: {}", std::make_tuple(this->_localPath, numberOfBytesToWrite, offset, altStream));
92 |
93 | NTSTATUS status = STATUS_SUCCESS;
94 | bool closeHandleWhenFinished = false;
95 |
96 | HANDLE handle = this->_handle;
97 | if(handle == INVALID_HANDLE_VALUE){
98 | handle = CreateFile(this->localPathWithAltStream(altStream).c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
99 | if(handle == INVALID_HANDLE_VALUE){
100 | DebugLogger::getInstance().log("Failed to create handle during write");
101 | return DokanNtStatusFromWin32(GetLastError());
102 | }
103 | DebugLogger::getInstance().log("Handle not initialized, creating it during write");
104 | closeHandleWhenFinished = true;
105 | }
106 |
107 | UINT64 fileSize = 0;
108 | DWORD fileSizeLow = 0;
109 | DWORD fileSizeHigh = 0;
110 | fileSizeLow = GetFileSize(handle, &fileSizeHigh);
111 | if(fileSizeLow == INVALID_FILE_SIZE){
112 | status = DokanNtStatusFromWin32(GetLastError());
113 | DebugLogger::getInstance().log("Failed to get file size. Error code: {}", status);
114 | }
115 | else{
116 | fileSize = (static_cast(fileSizeHigh) << 32) | fileSizeLow;
117 |
118 | LARGE_INTEGER distanceToMove;
119 | if(dokanFileInfo->WriteToEndOfFile){
120 | LARGE_INTEGER z;
121 | z.QuadPart = 0;
122 | if(!SetFilePointerEx(handle, z, nullptr, FILE_END)){
123 | status = DokanNtStatusFromWin32(GetLastError());
124 | DebugLogger::getInstance().log("Failed to write to end of file. Error code: {}", status);
125 | }
126 | }
127 | else{
128 | if(dokanFileInfo->PagingIo){
129 | if(static_cast(offset) >= fileSize){
130 | DebugLogger::getInstance().log("Offset {} greater than file size {}, no need to write anything", std::make_tuple(offset, fileSize));
131 | *numberOfBytesWritten = 0;
132 | if(closeHandleWhenFinished){
133 | DebugLogger::getInstance().log("Closing handle after write");
134 | CloseHandle(handle);
135 | }
136 | return STATUS_SUCCESS;
137 | }
138 | else if((static_cast(offset) + numberOfBytesToWrite) > fileSize){
139 | UINT64 bytes = fileSize - offset;
140 | if(bytes >> 32){
141 | numberOfBytesToWrite = static_cast(bytes & 0xFFFFFFFFUL);
142 | }
143 | else{
144 | numberOfBytesToWrite = static_cast(bytes);
145 | }
146 | }
147 | }
148 |
149 | DebugLogger::getInstance().log("Writing {} bytes to file '{}'", std::make_tuple(numberOfBytesToWrite, this->_localPath));
150 | distanceToMove.QuadPart = offset;
151 | if(!SetFilePointerEx(handle, distanceToMove, nullptr, FILE_BEGIN)){
152 | status = DokanNtStatusFromWin32(GetLastError());
153 | DebugLogger::getInstance().log("Error during SetFilePointerEx. Error code: {}");
154 | }
155 | }
156 |
157 | if(status == STATUS_SUCCESS){
158 | if(WriteFile(handle, buffer, numberOfBytesToWrite, numberOfBytesWritten, nullptr)){
159 | this->_modified = true;
160 | DebugLogger::getInstance().log("Write to file '{}' succeeded", this->_localPath);
161 | }
162 | else{
163 | status = DokanNtStatusFromWin32(GetLastError());
164 | DebugLogger::getInstance().log("Write to file '{}' failed. Error code: {}", std::make_tuple(this->_localPath, status));
165 | }
166 | }
167 | }
168 |
169 | if(closeHandleWhenFinished){
170 | DebugLogger::getInstance().log("Closing handle after write");
171 | CloseHandle(handle);
172 | }
173 |
174 | return status;
175 | }
176 |
177 | NTSTATUS TemporaryFile::setAllocationSize(LONGLONG allocSize){
178 | DebugLogger::getInstance().log("Calling setAllocationSize on temporary file with local path '{}': allocSize: {}", std::make_tuple(this->_localPath, allocSize));
179 |
180 | if(this->_handle == INVALID_HANDLE_VALUE){
181 | DebugLogger::getInstance().log("Cannot set allocation size of file '{}' because handle is not valid", this->_localPath);
182 | return STATUS_INVALID_HANDLE;
183 | }
184 |
185 | LARGE_INTEGER fileSize;
186 | if(!GetFileSizeEx(this->_handle, &fileSize)){
187 | DebugLogger::getInstance().log("Failed to get file size of file '{}'", this->_localPath);
188 | return DokanNtStatusFromWin32(GetLastError());
189 | }
190 |
191 | DebugLogger::getInstance().log("File size of '{}': {}", std::make_tuple(this->_localPath, fileSize.QuadPart));
192 | if(allocSize < fileSize.QuadPart){
193 | fileSize.QuadPart = allocSize;
194 | if(!SetFilePointerEx(this->_handle, fileSize, nullptr, FILE_BEGIN) || !SetEndOfFile(this->_handle)){
195 | DebugLogger::getInstance().log("Failed to set allocation size of file '{}'", this->_localPath);
196 | return DokanNtStatusFromWin32(GetLastError());
197 | }
198 | else{
199 | DebugLogger::getInstance().log("Successfully set allocation size of file '{}'", this->_localPath);
200 | }
201 | }
202 | else{
203 | DebugLogger::getInstance().log("No need to set allocation size of '{}': allocation size {} is less than file size {}", std::make_tuple(this->_localPath, allocSize, fileSize.QuadPart));
204 | }
205 |
206 | this->_modified = true;
207 | return STATUS_SUCCESS;
208 | }
209 |
210 | NTSTATUS TemporaryFile::push(){
211 | DebugLogger::getInstance().log("Calling push on temporary file with local path '{}' and remote path '{}'", std::make_tuple(this->_localPath, this->_remotePath));
212 |
213 | if(this->_modified){
214 | BY_HANDLE_FILE_INFORMATION fileInformation;
215 | this->getFileInformation(&fileInformation);
216 | if(!this->_device->pushToAdb(this->_localPath, this->_remotePath)){
217 | //If it failed while the handle is opened, close the handle because sometimes it fails because the open handle causes it to not have read permission
218 | CloseHandle(this->_handle);
219 | this->_handle = INVALID_HANDLE_VALUE;
220 | DebugLogger::getInstance().log("Failed to push with open handle, trying again with closed handle");
221 | if(!this->_device->pushToAdb(this->_localPath, this->_remotePath)){
222 | DebugLogger::getInstance().log("Failed to push local file '{}' to remote '{}'", std::make_tuple(this->_localPath, this->_remotePath));
223 | return STATUS_UNSUCCESSFUL;
224 | }
225 | else{
226 | DebugLogger::getInstance().log("Pushing local file '{}' to remote file '{}' succeeded after closing handle", std::make_tuple(this->_localPath, this->_remotePath));
227 | }
228 | }
229 | else{
230 | DebugLogger::getInstance().log("Pushing local file '{}' to remote file '{}' succeeded on first try", std::make_tuple(this->_localPath, this->_remotePath));
231 | }
232 | this->_device->runAdbCommand(QString("(test -d %1 || test -f %1) && touch -cm --date=\"@%2\" %1 && touch -ca --date=\"@%3\" %1").arg(escapeSpecialCharactersForBash(this->_remotePath), QString::number(microsoftTimeToUnixTime(fileInformation.ftLastWriteTime)), QString::number(microsoftTimeToUnixTime(fileInformation.ftLastAccessTime))), nullptr, false);
233 | this->_modified = false;
234 | }
235 | else{
236 | DebugLogger::getInstance().log("File '{}' not modified, no need to push", this->_localPath);
237 | }
238 | return STATUS_SUCCESS;
239 | }
240 |
241 | NTSTATUS TemporaryFile::getFileInformation(LPBY_HANDLE_FILE_INFORMATION handleFileInformation){
242 | const bool success = GetFileInformationByHandle(this->_handle, handleFileInformation);
243 | return success ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
244 | }
245 |
246 | NTSTATUS TemporaryFile::setFileTime(const FILETIME *creationTime, const FILETIME *lastAccessTime, const FILETIME *lastWriteTime){
247 | const bool success = SetFileTime(this->_handle, creationTime, lastAccessTime, lastWriteTime);
248 | return success ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL;
249 | }
250 |
251 | std::wstring TemporaryFile::localPathWithAltStream(const QString &altStream) const{
252 | if(altStream.isEmpty()){
253 | return this->_localPath.toStdWString();
254 | }
255 | return (this->_localPath + ":" + altStream).toStdWString();
256 | }
257 |
--------------------------------------------------------------------------------
/sources/temporaryfile.hpp:
--------------------------------------------------------------------------------
1 | #ifndef TEMPORARYFILE_H
2 | #define TEMPORARYFILE_H
3 |
4 | #include
5 |
6 | #include
7 |
8 | #include "androiddevice.hpp"
9 |
10 | class TemporaryFile final {
11 | public:
12 | /**
13 | * Downloads a file from the Android device to a local temporary file.
14 | *
15 | * @param drive - The Android drive that the file is on.
16 | * @param remotePath - The path of the Android file to download.
17 | * @param creationDisposition - The creation disposition to use when creating the handle.
18 | * @param shareAccess - The share access to use when creating the handle.
19 | * @param desiredAccess - The desired access to use when creating the handle.
20 | * @param fileAttributes - The file attributes to use when creating the handle.
21 | * @param createOptions - The create options to use when creating the handle.
22 | * @param createDisposition - The create disposition to use when creating the handle.
23 | * @param exists - True if opening an existing file (in which case it should be downloaded), false when creating a new file (in which case it should just be created locally).
24 | * @param altStream - The alt stream to use when creating the handle.
25 | */
26 | explicit TemporaryFile(const AndroidDrive *drive, const QString &remotePath, DWORD creationDisposition, ULONG shareAccess, ACCESS_MASK desiredAccess, ULONG fileAttributes, ULONG createOptions, ULONG createDisposition, bool exists, const QString &altStream);
27 |
28 | /**
29 | * Destructor, closes the handle.
30 | */
31 | ~TemporaryFile();
32 |
33 | /**
34 | * Disallow copying.
35 | */
36 | TemporaryFile(const TemporaryFile&) = delete;
37 | void operator=(const TemporaryFile&) = delete;
38 |
39 | /**
40 | * Gets the error code of the last error that occurred in the constructor.
41 | *
42 | * @return The error code of the last error, or STATUS_SUCCESS if no error has occurred in the constructor.
43 | */
44 | NTSTATUS errorCode() const;
45 |
46 | /**
47 | * Reads from the local copy of the file.
48 | *
49 | * @param buffer - Read buffer that will be filled with the read result.
50 | * @param bufferLength - Buffer length and read size to continue with.
51 | * @param readLength - Total data size that has been read.
52 | * @param offset - Offset from where the read has to be continued.
53 | * @param altStream - The alt stream of the file that was requested.
54 | *
55 | * @return STATUS_SUCCESS on success, an error code on failure.
56 | */
57 | NTSTATUS read(LPVOID buffer, DWORD bufferLength, LPDWORD readLength, LONGLONG offset, const QString &altStream) const;
58 |
59 | /**
60 | * Writes to the local copy of the file.
61 | *
62 | * @param buffer - Data to write.
63 | * @param numberOfBytesToWrite - Buffer length and write size to continue with.
64 | * @param numberOfBytesWritten - Total number of bytes that have been written.
65 | * @param offset - Offset from where the write has to be continued.
66 | * @param dokanFileInfo - The Dokan file info of the file.
67 | * @param altStream - The alt stream of the file that was requested.
68 | *
69 | * @return STATUS_SUCCESS on success, an error code on failure.
70 | */
71 | NTSTATUS write(LPCVOID buffer, DWORD numberOfBytesToWrite, LPDWORD numberOfBytesWritten, LONGLONG offset, PDOKAN_FILE_INFO dokanFileInfo, const QString &altStream);
72 |
73 | /**
74 | * Truncates or extends the local copy of the file.
75 | *
76 | * @param allocSize - File length to set.
77 | *
78 | * @return STATUS_SUCCESS on success, an error code on failure.
79 | */
80 | NTSTATUS setAllocationSize(LONGLONG allocSize);
81 |
82 | /**
83 | * Gets information about the local copy of the file.
84 | *
85 | * @param handleFileInformation The file information struct that will be filled.
86 | *
87 | * @return STATUS_SUCCESS on success, an error code on failure.
88 | */
89 | NTSTATUS getFileInformation(LPBY_HANDLE_FILE_INFORMATION handleFileInformation);
90 |
91 | /**
92 | * Sets the file time on the local file.
93 | *
94 | * @param creationTime - The creation time to set.
95 | * @param lastAccessTime - The last access time to set.
96 | * @param lastWriteTime - The last write time to set.
97 | *
98 | * @return STATUS_SUCCESS on success, an error code on failure.
99 | */
100 | NTSTATUS setFileTime(const FILETIME *creationTime, const FILETIME *lastAccessTime, const FILETIME *lastWriteTime);
101 |
102 | /**
103 | * Pushes the local copy to the Android device.
104 | *
105 | * @return STATUS_SUCCESS on success, an error code on failure.
106 | */
107 | NTSTATUS push();
108 |
109 | private:
110 | /**
111 | * Gets the path of the local copy of this file with the given alt stream.
112 | *
113 | * @param altStream - The alt stream to append to the path. If empty, just returns the local path as is.
114 | *
115 | * @return The local path and the alt stream separated by a ':' character.
116 | */
117 | std::wstring localPathWithAltStream(const QString &altStream) const;
118 |
119 | const QString _localPath, _remotePath;
120 | const std::shared_ptr _device;
121 | HANDLE _handle;
122 | NTSTATUS _errorCode;
123 | bool _modified;
124 | };
125 |
126 | #endif // TEMPORARYFILE_H
127 |
--------------------------------------------------------------------------------
/sources/translations/androiddrive_de.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/androiddrive_de.qm
--------------------------------------------------------------------------------
/sources/translations/androiddrive_empty.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QObject
6 |
7 |
8 | Internal storage
9 |
10 |
11 |
12 |
13 | SD card %1
14 |
15 |
16 |
17 |
18 | Device
19 |
20 |
21 |
22 |
23 | Status
24 |
25 |
26 |
27 |
28 | Mounting...
29 |
30 |
31 |
32 |
33 | Unmounting...
34 |
35 |
36 |
37 |
38 | Mounted as %1
39 |
40 |
41 |
42 |
43 | Not mounted
44 |
45 |
46 |
47 |
48 |
49 | Mounts a drive containing the internal storage of the selected Android device, as well as a drive for each external SD card that the selected device has, if any.
50 |
51 |
52 |
53 |
54 | Allows you to change the settings for this device, for example select a new drive letter or choose whether it should be mounted automatically.
55 |
56 |
57 |
58 |
59 | Opens the selected drive in Windows Explorer.
60 |
61 |
62 |
63 |
64 | AndroidDrive - Devices
65 |
66 |
67 |
68 |
69 | &Unmount all drives
70 |
71 |
72 |
73 |
74 | Unmounts all drives corresponding to the selected Android device.<br/><br/>This only unmounts the drives, the Android device itself will remain connected, so you will still be able to access it for example through ADB.
75 |
76 |
77 |
78 |
79 | &Mount all drives
80 |
81 |
82 |
83 |
84 | &Unmount drive
85 |
86 |
87 |
88 |
89 | Unmounts the selected drive.<br/><br/>This only unmounts the drive, the Android device itself will remain connected, so you will still be able to access it for example through ADB.
90 |
91 |
92 |
93 |
94 |
95 | &Mount drive
96 |
97 |
98 |
99 |
100 | Mounts a drive containing the selected internal storage or external SD card.
101 |
102 |
103 |
104 |
105 | Fatal error: Could not list Android devices.<br/><br/>ADB exited with code %1.
106 |
107 |
108 |
109 |
110 | Device %1 is offline.<br/><br/>Try unlocking the device, then unplugging it and re-plugging it.<br/><br/>If this error persists, you may be able to find solutions <a href="%2">here</a> (any adb commands mentioned there can be run in the command prompt after running <code>cd "%3"</code>).
111 |
112 |
113 |
114 |
115 | Device %1 is unauthorized.<br/><br/>Try unlocking your device. If it shows you a dialog asking if you want to allow this computer to access phone data, tap "Allow". If it doesn't show that dialog, disable and re-enable USB debugging as explained <a href="%2">here</a>.<br/><br/>If it still isn't working, try unplugging and then re-plugging your device.
116 |
117 |
118 |
119 |
120 | Could not create a drive with the given drive letter.
121 |
122 |
123 |
124 |
125 | Dokan doesn't seem to be installed.<br/><br/>Would you like to install it now?
126 |
127 |
128 |
129 |
130 | Could not start the driver.
131 |
132 |
133 |
134 |
135 | Could not assign a drive letter.<br/><br/>Try changing the drive letter in Device Settings to an available drive letter.
136 |
137 |
138 |
139 |
140 | Dokan version error.
141 |
142 |
143 |
144 |
145 | An unknown error occurred.
146 |
147 |
148 |
149 |
150 | Could not mount drive %1: %2
151 |
152 |
153 |
154 |
155 | ADB timed out.
156 |
157 |
158 |
159 |
160 | An error occurred when attempting to read from the ADB process.
161 |
162 |
163 |
164 |
165 | An error occurred when attempting to write to the ADB process.
166 |
167 |
168 |
169 |
170 | ADB failed to start.<br/><br/>Either the adb.exe file is missing, or you may have insufficient permissions to invoke the program.
171 |
172 |
173 |
174 |
175 | ADB crashed.
176 |
177 |
178 |
179 |
180 | ADB encountered an unknown error.
181 |
182 |
183 |
184 |
185 | Fatal error: Could not list Android devices: %1
186 |
187 |
188 |
189 |
190 | Drive &settings
191 |
192 |
193 |
194 |
195 | &Open in Explorer
196 |
197 |
198 |
199 |
200 | AndroidDrive is already running.<br/><br/>If you're trying to restart AndroidDrive, you can close the existing process by right clicking on the AndroidDrive icon in the task bar and selecting Exit.
201 |
202 |
203 |
204 |
205 | &Devices
206 |
207 |
208 |
209 |
210 | &Settings
211 |
212 |
213 |
214 |
215 |
216 | Record Debug &Logs
217 |
218 |
219 |
220 |
221 | Finish Recording Debug &Logs
222 |
223 |
224 |
225 |
226 | Recording of debug logs has started.<br/><br/>You will be able to find the log file in %1.<br/><br/>If you're planning on attaching the log file to a bug report, keep in mind that the log file will contain the names of the files on your phone, so make sure that the filenames don't contain any sensitive information (The debug logs will only contain the file names, they won't contain the contents of any file).
227 |
228 |
229 |
230 |
231 | Failed to create log file.
232 |
233 |
234 |
235 |
236 | &About AndroidDrive
237 |
238 |
239 |
240 |
241 | About AndroidDrive
242 |
243 |
244 |
245 |
246 | AndroidDrive version %1 by Gustav Lindberg.
247 |
248 |
249 |
250 |
251 | Icons made by %3 and %4 from %1 are licensed by %2.
252 |
253 |
254 |
255 |
256 | This program uses %1 and %2.
257 |
258 |
259 |
260 |
261 | About &Qt
262 |
263 |
264 |
265 |
266 | E&xit
267 |
268 |
269 |
270 |
271 | AndroidDrive - Settings
272 |
273 |
274 |
275 |
276 | Drive settings for %1
277 |
278 |
279 |
280 |
281 | Allows you to select a preferred drive letter for the selected Android drive.<br/><br/>If the preferred drive letter is unavailable when this drive is connected, it will use the next available drive letter in alphabetical order.<br/><br/>If you change the drive letter while this drive is connected, you will have to unmount it and re-mount it again for the changes to take effect.
282 |
283 |
284 |
285 |
286 | Drive &letter
287 |
288 |
289 |
290 |
291 | Allows you to select a name for the selected Android drive.
292 |
293 |
294 |
295 |
296 | &Reset
297 |
298 |
299 |
300 |
301 | Resets the name of the drive to the default value.
302 |
303 |
304 |
305 |
306 | Drive &name
307 |
308 |
309 |
310 |
311 | Automatically mount &drive
312 |
313 |
314 |
315 |
316 | If this checkbox is checked, the selected drive will be automatically connected as a drive whenever you plug it into your computer.<br/><br/>Otherwise, you will have to mount it manually by going into Devices > Mount drive.
317 |
318 |
319 |
320 |
321 | These settings only affect the selected drive.
322 |
323 |
324 |
325 |
326 | Global settings
327 |
328 |
329 |
330 |
331 | Open newly connected drives in &Explorer
332 |
333 |
334 |
335 |
336 | If this checkbox is checked, whenever AndroidDrive is finished connecting a drive, it will open that drive in Windows Explorer.
337 |
338 |
339 |
340 |
341 | &Hide files beginning with a dot
342 |
343 |
344 |
345 |
346 | If this checkbox is checked, files that begin with a dot will be treated as hidden files, and will only be visible in Windows Explorer if Windows Explorer's "Show hidden files" option is activated.
347 |
348 |
349 |
350 |
351 | Use system language
352 |
353 |
354 |
355 |
356 | Allows you to select which language AndroidDrive's GUI will be displayed in.<br/><br/>If you select "Use system language" but AndroidDrive isn't availiable in your system language, English will be used.<br/><br/>This setting has no effect on how the actual drive works.<br/><br/>You must restart AndroidDrive for this change to take effect.
357 |
358 |
359 |
360 |
361 | &Language
362 |
363 |
364 |
365 |
366 | AndroidDrive doesn't seem to be available in your system language.<br/><br/><a %1>Click here</a> if you would like to help translate it.
367 |
368 |
369 |
370 |
371 | These settings affect all drives connected with AndroidDrive.
372 |
373 |
374 |
375 |
376 | &OK
377 |
378 |
379 |
380 |
381 | &Cancel
382 |
383 |
384 |
385 |
386 | &Apply
387 |
388 |
389 |
390 |
391 | Installing updates...
392 |
393 |
394 |
395 |
396 | An update is available.<br/><br/>Do you want to install it now?
397 |
398 |
399 |
400 |
401 |
--------------------------------------------------------------------------------
/sources/translations/androiddrive_fr.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/androiddrive_fr.qm
--------------------------------------------------------------------------------
/sources/translations/androiddrive_hu.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/androiddrive_hu.qm
--------------------------------------------------------------------------------
/sources/translations/androiddrive_it.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/androiddrive_it.qm
--------------------------------------------------------------------------------
/sources/translations/androiddrive_sv.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/androiddrive_sv.qm
--------------------------------------------------------------------------------
/sources/translations/contribute.md:
--------------------------------------------------------------------------------
1 | # Translating AndroidDrive
2 |
3 | AndroidDrive is currently available in the following languages:
4 |
5 | * English
6 | * French
7 | * Hungarian (translation by [gidano](https://github.com/gidano))
8 | * Italian (translation by [bovirus](https://github.com/bovirus))
9 | * Swedish
10 |
11 | If your language is not listed above, you can help translate it. Do do so, follow the instructions below:
12 |
13 | 1. Install Qt Linguist. If you already have Qt installed, Qt Linguist is already installed with it, so you don't have to do anything. Otherwise, you can install Qt Linguist as a standalone program by downloading the [portable zip file](https://raw.githubusercontent.com/GustavLindberg99/QtLinguistWeb/main/qt-linguist-portable.zip) or the [installer](https://raw.githubusercontent.com/GustavLindberg99/QtLinguistWeb/main/qt-linguist-web-install.exe) (in the installer, when it asks you to select components, you only need Qt Linguist, the other stuff is for translating websites and isn't needed here). If you plan on developing your own programs with Qt, you can also download Qt as a whole [here](https://www.qt.io/download-qt-installer-oss).
14 | 2. Download [this file](https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/main/sources/translations/androiddrive_empty_translation.ts) and save it as `androiddrive_.ts`, where `` is the two-letter code of the language you're translating to. For example, this is `fr` for French, `it` for Italian, `sv` for Swedish, etc.
15 | 3. Open the file in Qt Linguist. You will get a dialog where you can select languages. Normally it auto-detects everything correctly and you just need to click OK:
16 |
17 |
18 |
19 | The source language (the one on the top) should always be English (United States). The target language (the one on the bottom) should be the language you're translating to. If you saved the file with the correct name, normally Qt Linguist should auto-detect the language and you don't need to do anything there either. If you want, though, you can select a country or region.
20 |
21 | 4. Next you need to do the actual translation. To do so, click on an English source text in "Strings" and translate it to your language in the text box below. Then click on the question mark to the left of the English source text, and it should turn into a green checkmark. The following screenshot is what it should look like when you're done translating the first two strings:
22 |
23 |
24 |
25 | Pay attention to the warnings on the bottom right under "Warnings" to make sure that your translation has the same puctuation, keyboard shortcuts (`&`) and placeholders (`%1`, `%2`, etc) as the original.
26 |
27 | If it says "File ... not available" on the top right under "Sources and Forms", don't worry about it, that's perfectly normal. You will still be able to translate it just as easily and your translation will still be useable.
28 |
29 | 6. When you're done translating, to publish your translation, you need to start by forking this repository by clicking [here](https://github.com/GustavLindberg99/AndroidDrive/fork). Then go to `https://github.com//AndroidDrive/upload/main/sources/translations` and upload your translation.
30 | 7. Next create a pull request by going to `https://github.com/GustavLindberg99/AndroidDrive/compare/main...:AndroidDrive:main` and click Create pull request. Make sure to allow edits by maintainers so that I can make the necessary changes to the source code.
31 | 8. Now all you need to do is wait for me to approve your pull request and publish your translation.
32 |
--------------------------------------------------------------------------------
/sources/translations/qtbase_de.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/qtbase_de.qm
--------------------------------------------------------------------------------
/sources/translations/qtbase_fr.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/qtbase_fr.qm
--------------------------------------------------------------------------------
/sources/translations/qtbase_hu.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/qtbase_hu.qm
--------------------------------------------------------------------------------
/sources/translations/qtbase_it.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/qtbase_it.qm
--------------------------------------------------------------------------------
/sources/translations/qtbase_sv.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GustavLindberg99/AndroidDrive/a49fb002ccd93219fcf4a9c9c55203d237240dcc/sources/translations/qtbase_sv.qm
--------------------------------------------------------------------------------
/sources/updates.cpp:
--------------------------------------------------------------------------------
1 | #include "updates.hpp"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include "version.h"
15 |
16 | //This file is shared between projects, which is why there is both Windows-specific and Linux-specific code regardless of whether or not the project is cross-platform.
17 |
18 | void installUpdates(const QUrl &githubRepo, const QUrl &zipFileOrLinuxBinaryOrInstaller, const std::function &quit){
19 | #ifdef _WIN32
20 | if(!zipFileOrLinuxBinaryOrInstaller.toString().endsWith(".exe") && !QFile::exists("C:\\Windows\\system32\\tar.exe")){
21 | //tar.exe is needed to unzip the contents of the update when installing it on Windows, so if it doesn't exist then just open the GitHub page so that the user can install the update manually
22 | QDesktopServices::openUrl(githubRepo);
23 | return;
24 | }
25 | #endif
26 |
27 | QProgressBar *progressBar = new QProgressBar();
28 | progressBar->setWindowTitle(QObject::tr("Installing updates..."));
29 | progressBar->setWindowFlags(Qt::WindowSystemMenuHint | Qt::WindowTitleHint);
30 | progressBar->setWindowIcon(QIcon(":/icon.ico"));
31 | progressBar->setFixedWidth(300);
32 | progressBar->setMaximum(0);
33 | progressBar->setTextVisible(false);
34 | progressBar->show();
35 |
36 | QNetworkAccessManager *manager = new QNetworkAccessManager();
37 | QObject::connect(manager, &QNetworkAccessManager::finished, manager, [=](QNetworkReply *reply){
38 | manager->deleteLater();
39 |
40 | if(reply->error()){
41 | QDesktopServices::openUrl(githubRepo);
42 | delete progressBar;
43 | return;
44 | }
45 |
46 | //Create a temporary file to put the newly downloaded files in
47 | QTemporaryDir temp;
48 | temp.setAutoRemove(false);
49 |
50 | //Create the ZIP file or Linux binary with the downloaded contents
51 | QFile downloadeFile(temp.filePath(zipFileOrLinuxBinaryOrInstaller.fileName()));
52 | if(!downloadeFile.open(QIODevice::WriteOnly)){
53 | QDesktopServices::openUrl(githubRepo);
54 | delete progressBar;
55 | return;
56 | }
57 | downloadeFile.write(reply->readAll());
58 | downloadeFile.close();
59 |
60 | #ifdef _WIN32
61 | //If we use the installer, just launch the installer
62 | if(zipFileOrLinuxBinaryOrInstaller.toString().endsWith(".exe")){
63 | QProcess::startDetached(downloadeFile.fileName());
64 | delete progressBar;
65 | quit();
66 | return;
67 | }
68 |
69 | //Unzip the ZIP file
70 | QProcess tar;
71 | tar.setWorkingDirectory(temp.path());
72 | tar.start("tar.exe", {"-xf", downloadeFile.fileName()});
73 | tar.waitForFinished();
74 | if(tar.exitCode() != 0){
75 | QDesktopServices::openUrl(githubRepo);
76 | delete progressBar;
77 | return;
78 | }
79 | downloadeFile.remove();
80 | #endif
81 |
82 | //Create a vbscript or bash file that copies the new version to the current folder. We have to do this in a seperate process otherwise the currently running executable file will be locked because it's in use.
83 | QFile vbsOrBashFile(temp.filePath("copyupdate"
84 | #ifdef _WIN32
85 | ".vbs"
86 | #else
87 | ".sh"
88 | #endif
89 | ));
90 | if(!vbsOrBashFile.open(QIODevice::WriteOnly | QIODevice::Text)){
91 | QDesktopServices::openUrl(githubRepo);
92 | delete progressBar;
93 | return;
94 | }
95 | const QString mainExecutable = QFileInfo(qApp->arguments()[0]).absoluteFilePath();
96 | const QString temporaryFolder = temp.path();
97 | #ifdef _WIN32
98 | const QString destinationFolder = QDir(QApplication::applicationDirPath()).absolutePath();
99 | const QString vbscript(
100 | //Test if the destination folder can be written to without admin rights
101 | "set fso = createObject(\"Scripting.FileSystemObject\")\n"
102 | "on error resume next\n"
103 | "testFolder = \"" + destinationFolder + "/permissiontest\"\n"
104 | "fso.CreateFolder testFolder\n"
105 | "if fso.folderExists(testFolder) then\n"
106 | "fso.deleteFolder testFolder, true\n"
107 | "end if\n"
108 |
109 | //If it can't, restart the script with admin rights
110 | "if err.number <> 0 then\n"
111 | "createObject(\"Shell.Application\").shellExecute wScript.fullName _\n"
112 | ", \"\"\"\" & wScript.scriptFullName & \"\"\"\", \"\", \"runas\", 1\n"
113 | "wScript.quit\n"
114 | "end if\n"
115 |
116 | //Delete the current .vbs file so that it doens't go in the installation folder with the other files
117 | "fso.deleteFile \"" + temporaryFolder + "/copyupdate.vbs\"\n"
118 |
119 | //As long as the program is still running, we can't do anything because the .exe file is locked
120 | "do\n"
121 | "err.clear\n"
122 | "fso.openTextFile \"" + mainExecutable + "\", 8, false\n"
123 | "loop while err.number <> 0\n"
124 | "err.clear\n"
125 |
126 | //Copy the new version from the temporary folder to the folder where the old version is installed
127 | "fso.copyFolder \"" + temporaryFolder + "\", \"" + destinationFolder + "\"\n"
128 | "set objShell = wScript.createObject(\"wScript.Shell\")\n"
129 | "if err.number <> 0 then\n"
130 | //If there was an error, open GitHub so that the user can install the update manually
131 | "objShell.run \"" + githubRepo.toString() + "\"\n"
132 | "else\n"
133 | //Restart the program
134 | "objShell.run \"\"\"" + qApp->arguments().join("\"\" \"\"") + "\"\"\""
135 | "end if\n"
136 |
137 | //We don't need the temporary folder anymore, so delete it
138 | "fso.deleteFolder \"" + temporaryFolder + "\"\n"
139 | );
140 | vbsOrBashFile.write(vbscript.toUtf8());
141 | vbsOrBashFile.close();
142 |
143 | QProcess::startDetached("wscript.exe", {vbsOrBashFile.fileName()});
144 | #else
145 | const QString bashScript(
146 | "mainExecutable=$(printf '%q\n' \"$1\")\n"
147 | "temporaryFolder=$(printf '%q\n' \"$2\")\n"
148 | "downloadedFileName=$(printf '%q\n' \"$3\")\n"
149 | "githubRepo=$(printf '%q\n' \"$4\")\n"
150 | "if mv \"$downloadedFileName\" \"$mainExecutable\"; then\n"
151 | "\"$mainExecutable\"\n"
152 | "else\n"
153 | "python -m webbrowser \"$githubRepo\"\n"
154 | "fi\n"
155 | "rm -rf \"$temporaryFolder\""
156 | );
157 | vbsOrBashFile.write(bashScript.toUtf8());
158 | vbsOrBashFile.setPermissions(static_cast(0x777));
159 | vbsOrBashFile.close();
160 |
161 | QProcess::startDetached("bash", {vbsOrBashFile.fileName(), mainExecutable, temporaryFolder, downloadeFile.fileName(), githubRepo.toString()});
162 | #endif
163 |
164 | delete progressBar;
165 | quit();
166 | });
167 | manager->get(QNetworkRequest(zipFileOrLinuxBinaryOrInstaller));
168 | }
169 |
170 | void checkForUpdates(const QUrl &githubRepo, const QUrl &versionHeader, const QUrl &zipFileOrLinuxBinary, const QUrl &windowsInstaller, const std::function &quit){
171 | QNetworkAccessManager *manager = new QNetworkAccessManager();
172 | QObject::connect(manager, &QNetworkAccessManager::finished, manager, [=](QNetworkReply *reply){
173 | manager->deleteLater();
174 |
175 | if(reply->error()){
176 | //If there's an error, it's probably because the user isn't connected to internet, in which case we can check for updates another time instead
177 | return;
178 | }
179 |
180 | const QString versionHeaderContents = QString::fromUtf8(reply->readAll());
181 |
182 | //clazy:excludeall=use-static-qregularexpression
183 | //Don't use static QRegularExpressions here because this function will only be executed once
184 | const QRegularExpressionMatch majorVersionMatch = QRegularExpression("#define\\s+MAJORVERSION\\s+([0-9]+)\\s*[\r\n]").match(versionHeaderContents);
185 | const QRegularExpressionMatch minorVersionMatch = QRegularExpression("#define\\s+MINORVERSION\\s+([0-9]+)\\s*[\r\n]").match(versionHeaderContents);
186 | const QRegularExpressionMatch patchVersionMatch = QRegularExpression("#define\\s+PATCHVERSION\\s+([0-9]+)\\s*[\r\n]").match(versionHeaderContents);
187 | if(!majorVersionMatch.hasMatch() || !minorVersionMatch.hasMatch() || !patchVersionMatch.hasMatch()){
188 | return;
189 | }
190 | const int mostRecentMajorVersion = majorVersionMatch.captured(1).toInt();
191 | const int mostRecentMinorVersion = minorVersionMatch.captured(1).toInt();
192 | const int mostRecentPatchVersion = patchVersionMatch.captured(1).toInt();
193 |
194 | const bool updateIsAvailable = mostRecentMajorVersion > MAJORVERSION || (mostRecentMajorVersion == MAJORVERSION && (
195 | mostRecentMinorVersion > MINORVERSION || (mostRecentMinorVersion == MINORVERSION &&
196 | mostRecentPatchVersion > PATCHVERSION)));
197 |
198 | if(updateIsAvailable && QMessageBox::question(nullptr, "", QObject::tr("An update is available.