├── .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.

ADB exited with code %1.").arg(exitCode)); 190 | emit this->encounteredFatalError(); 191 | } 192 | else{ 193 | this->_adbFailed = true; 194 | this->_adb.start("adb.exe", {"devices"}); 195 | } 196 | return; 197 | } 198 | this->_adbFailed = false; 199 | 200 | //Find which devices are connected 201 | static const QRegularExpression newlineRegex("[\r\n]+"); 202 | const QStringList result = QString::fromUtf8(this->_adb.readAllStandardOutput()).trimmed().split(newlineRegex); 203 | QStringList serialNumbers, offlineSerialNumbers; 204 | static const QRegularExpression spaceRegex("\\s+"); 205 | for(const QString &line: result){ 206 | if(line == "List of devices attached" || line.isEmpty()){ 207 | DebugLogger::getInstance().log("Skipping line '{}'", line); 208 | continue; 209 | } 210 | const QStringList splittedLine = line.split(spaceRegex); 211 | const QString serialNumber = splittedLine[0]; 212 | const bool offline = splittedLine[1] == "offline"; 213 | const bool unauthorized = splittedLine[1] == "unauthorized"; 214 | if(offline || unauthorized){ 215 | DebugLogger::getInstance().log("Offline/unauthorized device '{}'", serialNumber); 216 | offlineSerialNumbers.push_back(serialNumber); 217 | if(this->_model.timeSinceOffline(serialNumber) == 3){ 218 | if(offline){ 219 | QMessageBox::warning(nullptr, "", 220 | QObject::tr("Device %1 is offline.

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 | 12 | 14 | 21 | 25 | 29 | 30 | 31 | 34 | 38 | 42 | 46 | 50 | 54 | 55 | 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("Icons made by %3 and %4 from %1 are licensed by %2.").arg("www.iconfinder.com", "CC 3.0 BY", "Alpár-Etele Méder", "Tango") + "

" + 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 | 16 | 34 | 36 | 45 | 50 | 55 | 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 | 16 | 34 | 36 | 43 | 47 | 51 | 52 | 53 | 56 | 60 | 64 | 68 | 72 | 76 | 77 | 81 | 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.

Do you want to install it now?")) == QMessageBox::Yes){ 199 | #ifdef _WIN32 200 | const bool useInstaller = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", QSettings::NativeFormat).childGroups().contains(GUID "_is1", Qt::CaseInsensitive) 201 | || QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", QSettings::NativeFormat).childGroups().contains(GUID "_is1", Qt::CaseInsensitive); 202 | #else 203 | const bool useInstaller = false; 204 | #endif 205 | installUpdates(githubRepo, useInstaller ? windowsInstaller : zipFileOrLinuxBinary, quit); 206 | } 207 | }); 208 | manager->get(QNetworkRequest(versionHeader)); 209 | } 210 | -------------------------------------------------------------------------------- /sources/updates.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UPDATES_H 2 | #define UPDATES_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | void checkForUpdates(const QUrl &githubRepo, const QUrl &versionHeader, const QUrl &zipFileOrLinuxBinary, const QUrl &windowsInstaller, const std::function &quit = [](){qApp->quit();}); 10 | 11 | #endif // UPDATES_H 12 | -------------------------------------------------------------------------------- /sources/version.h: -------------------------------------------------------------------------------- 1 | #ifndef VERSION_H_ 2 | #define VERSION_H_ 3 | 4 | #ifndef QT_STRINGIFY 5 | #define QT_STRINGIFY2(x) #x 6 | #define QT_STRINGIFY(x) QT_STRINGIFY2(x) 7 | #endif 8 | 9 | //Version 10 | #define MAJORVERSION 2 11 | #define MINORVERSION 4 12 | #define PATCHVERSION 1 13 | #define PROGRAMVERSION QT_STRINGIFY(MAJORVERSION.MINORVERSION.PATCHVERSION) 14 | #define PROGRAMVERSION_NUMBER MAJORVERSION,MINORVERSION,PATCHVERSION 15 | #define GUID "{C370AD59-EF9D-4DE1-B2F5-CD4D87123B11}" 16 | 17 | #endif // VERSION_H_ 18 | --------------------------------------------------------------------------------