diff --git a/scripts/android/files/AndroidManifest.xml b/scripts/android/files/AndroidManifest.xml index 8f7e705b8..2b146b55b 100644 --- a/scripts/android/files/AndroidManifest.xml +++ b/scripts/android/files/AndroidManifest.xml @@ -9,10 +9,6 @@ - - - - diff --git a/src/android/android_main.cpp b/src/android/android_main.cpp index 0668b30dd..633c30443 100644 --- a/src/android/android_main.cpp +++ b/src/android/android_main.cpp @@ -1,160 +1,231 @@ -#include - -#ifdef CONF_PLATFORM_ANDROID -#include -#include +#include "android_main.h" #include #include +#include #include + #include + #include #include -extern "C" __attribute__((visibility("default"))) void InitAndroid(); - -static int gs_AndroidStarted = false; - -void InitAndroid() +static bool UnpackAsset(const char *pFilename) { - if(gs_AndroidStarted) + char aAssetFilename[IO_MAX_PATH_LENGTH]; + str_copy(aAssetFilename, "asset_integrity_files/"); + str_append(aAssetFilename, pFilename); + + // This uses SDL_RWFromFile because it can read Android assets, + // which are files stored in the app's APK file. All data files + // are stored as assets and unpacked to the external storage. + SDL_RWops *pAssetFile = SDL_RWFromFile(aAssetFilename, "rb"); + if(!pAssetFile) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "DDNet", "The app was started, but not closed properly, this causes bugs. Please restart or manually delete this task.", SDL_GL_GetCurrentWindow()); - std::exit(0); + log_error("android", "Failed to open asset '%s' for reading", pFilename); + return false; } - gs_AndroidStarted = true; - - // change current path to a writable directory - const char *pPath = SDL_AndroidGetExternalStoragePath(); - chdir(pPath); - dbg_msg("client", "changed path to %s", pPath); - - // copy integrity files + const long int FileLength = SDL_RWsize(pAssetFile); + if(FileLength < 0) { - SDL_RWops *pF = SDL_RWFromFile("asset_integrity_files/integrity.txt", "rb"); - if(!pF) - { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "DDNet", "integrity.txt not found, consider reinstalling", SDL_GL_GetCurrentWindow()); - std::exit(0); - } - - long int length; - SDL_RWseek(pF, 0, RW_SEEK_END); - length = SDL_RWtell(pF); - SDL_RWseek(pF, 0, RW_SEEK_SET); - - char *pAl = (char *)malloc(length); - SDL_RWread(pF, pAl, 1, length); - - SDL_RWclose(pF); - - mkdir("data", 0755); - - dbg_msg("integrity", "copying integrity.txt with size: %ld", length); - - IOHANDLE IntegrityFileWrite = io_open("integrity.txt", IOFLAG_WRITE); - io_write(IntegrityFileWrite, pAl, length); - io_close(IntegrityFileWrite); - - free(pAl); + SDL_RWclose(pAssetFile); + log_error("android", "Failed to determine length of asset '%s'", pFilename); + return false; } - IOHANDLE IntegrityFileRead = io_open("integrity.txt", IOFLAG_READ); - CLineReader IntegrityFileLineReader; - IntegrityFileLineReader.Init(IntegrityFileRead); - const char *pReadLine = NULL; - std::vector vLines; - while((pReadLine = IntegrityFileLineReader.Get())) - { - vLines.push_back(pReadLine); - } - io_close(IntegrityFileRead); + char *pData = static_cast(malloc(FileLength)); + const size_t ReadLength = SDL_RWread(pAssetFile, pData, 1, FileLength); + SDL_RWclose(pAssetFile); - // first line is the whole hash - std::string AllAsOne; - for(size_t i = 1; i < vLines.size(); ++i) + if(ReadLength != (size_t)FileLength) { - AllAsOne.append(vLines[i]); - AllAsOne.append("\n"); - } - SHA256_DIGEST ShaAll; - bool GotSHA = false; - { - IOHANDLE IntegritySaveFileRead = io_open("integrity_save.txt", IOFLAG_READ); - if(IntegritySaveFileRead != NULL) - { - CLineReader IntegritySaveLineReader; - IntegritySaveLineReader.Init(IntegritySaveFileRead); - const char *pLine = IntegritySaveLineReader.Get(); - if(pLine != NULL) - { - sha256_from_str(&ShaAll, pLine); - GotSHA = true; - } - io_close(IntegritySaveFileRead); - } + free(pData); + log_error("android", "Failed to read asset '%s' (read %" PRIzu ", wanted %ld)", pFilename, ReadLength, FileLength); + return false; } - SHA256_DIGEST ShaAllFile; - sha256_from_str(&ShaAllFile, vLines[0].c_str()); - - // TODO: check files individually - if(!GotSHA || ShaAllFile != ShaAll) + IOHANDLE TargetFile = io_open(pFilename, IOFLAG_WRITE); + if(!TargetFile) { - // then the files - for(size_t i = 1; i < vLines.size(); ++i) - { - std::string FileName, Hash; - std::string::size_type n = 0; - std::string::size_type c = 0; - while((c = vLines[i].find(' ', n)) != std::string::npos) - n = c + 1; - FileName = vLines[i].substr(0, n - 1); - Hash = vLines[i].substr(n + 1); - - std::string AssetFileName = std::string("asset_integrity_files/") + FileName; - SDL_RWops *pF = SDL_RWFromFile(AssetFileName.c_str(), "rb"); - - dbg_msg("Integrity", "Copying from assets: %s", FileName.c_str()); - - std::string FileNamePath = FileName; - std::string FileNamePathSub; - c = 0; - while((c = FileNamePath.find('/', c)) != std::string::npos) - { - FileNamePathSub = FileNamePath.substr(0, c); - fs_makedir(FileNamePathSub.c_str()); - ++c; - } - - long int length; - SDL_RWseek(pF, 0, RW_SEEK_END); - length = SDL_RWtell(pF); - SDL_RWseek(pF, 0, RW_SEEK_SET); - - char *pAl = (char *)malloc(length); - SDL_RWread(pF, pAl, 1, length); - - SDL_RWclose(pF); - - IOHANDLE AssetFileWrite = io_open(FileName.c_str(), IOFLAG_WRITE); - io_write(AssetFileWrite, pAl, length); - io_close(AssetFileWrite); - - free(pAl); - } - - IOHANDLE IntegritySaveFileWrite = io_open("integrity_save.txt", IOFLAG_WRITE); - if(IntegritySaveFileWrite != NULL) - { - char aFileSHA[SHA256_MAXSTRSIZE]; - sha256_str(ShaAllFile, aFileSHA, sizeof(aFileSHA)); - io_write(IntegritySaveFileWrite, aFileSHA, str_length(aFileSHA)); - io_close(IntegritySaveFileWrite); - } + free(pData); + log_error("android", "Failed to open '%s' for writing", pFilename); + return false; } + + const size_t WriteLength = io_write(TargetFile, pData, FileLength); + io_close(TargetFile); + free(pData); + + if(WriteLength != (size_t)FileLength) + { + log_error("android", "Failed to write data to '%s' (wrote %" PRIzu ", wanted %ld)", pFilename, WriteLength, FileLength); + return false; + } + + return true; } -#endif +constexpr const char *INTEGRITY_INDEX = "integrity.txt"; +constexpr const char *INTEGRITY_INDEX_SAVE = "integrity_save.txt"; + +// The first line of each integrity file contains the combined hash for all files, +// if the hashes match then we assume that the unpacked data folder is up-to-date. +static bool EqualIntegrityFiles(const char *pAssetFilename, const char *pStorageFilename) +{ + IOHANDLE StorageFile = io_open(pStorageFilename, IOFLAG_READ); + if(!StorageFile) + { + return false; + } + + char aStorageMainSha256[SHA256_MAXSTRSIZE]; + const size_t StorageReadLength = io_read(StorageFile, aStorageMainSha256, sizeof(aStorageMainSha256) - 1); + io_close(StorageFile); + if(StorageReadLength != sizeof(aStorageMainSha256) - 1) + { + return false; + } + aStorageMainSha256[sizeof(aStorageMainSha256) - 1] = '\0'; + + char aAssetFilename[IO_MAX_PATH_LENGTH]; + str_copy(aAssetFilename, "asset_integrity_files/"); + str_append(aAssetFilename, pAssetFilename); + + SDL_RWops *pAssetFile = SDL_RWFromFile(aAssetFilename, "rb"); + if(!pAssetFile) + { + return false; + } + + char aAssetMainSha256[SHA256_MAXSTRSIZE]; + const size_t AssetReadLength = SDL_RWread(pAssetFile, aAssetMainSha256, 1, sizeof(aAssetMainSha256) - 1); + SDL_RWclose(pAssetFile); + if(AssetReadLength != sizeof(aAssetMainSha256) - 1) + { + return false; + } + aAssetMainSha256[sizeof(aAssetMainSha256) - 1] = '\0'; + + return str_comp(aStorageMainSha256, aAssetMainSha256) == 0; +} + +class CIntegrityFileLine +{ +public: + char m_aFilename[IO_MAX_PATH_LENGTH]; + SHA256_DIGEST m_Sha256; +}; + +static std::vector ReadIntegrityFile(const char *pFilename) +{ + IOHANDLE IntegrityFile = io_open(pFilename, IOFLAG_READ); + if(!IntegrityFile) + { + return {}; + } + + CLineReader LineReader; + LineReader.Init(IntegrityFile); + const char *pReadLine; + std::vector vLines; + while((pReadLine = LineReader.Get())) + { + const char *pSpaceInLine = str_rchr(pReadLine, ' '); + CIntegrityFileLine Line; + char aSha256[SHA256_MAXSTRSIZE]; + if(pSpaceInLine == nullptr) + { + if(!vLines.empty()) + { + // Only the first line is allowed to not contain a filename + log_error("android", "Failed to parse line %" PRIzu " of '%s': line does not contain space", vLines.size() + 1, pFilename); + return {}; + } + Line.m_aFilename[0] = '\0'; + str_copy(aSha256, pReadLine); + } + else + { + str_truncate(Line.m_aFilename, sizeof(Line.m_aFilename), pReadLine, pSpaceInLine - pReadLine); + str_copy(aSha256, pSpaceInLine + 1); + } + if(sha256_from_str(&Line.m_Sha256, aSha256) != 0) + { + log_error("android", "Failed to parse line %" PRIzu " of '%s': invalid SHA256 string", vLines.size() + 1, pFilename); + return {}; + } + vLines.emplace_back(std::move(Line)); + } + + io_close(IntegrityFile); + return vLines; +} + +const char *InitAndroid() +{ + // Change current working directory to our external storage location + const char *pPath = SDL_AndroidGetExternalStoragePath(); + if(pPath == nullptr) + { + return "The external storage is not available."; + } + if(fs_chdir(pPath) != 0) + { + return "Failed to change current directory to external storage."; + } + log_info("android", "Changed current directory to '%s'", pPath); + + if(fs_makedir("data") != 0 || fs_makedir("user") != 0) + { + return "Failed to create 'data' and 'user' directories in external storage."; + } + + if(EqualIntegrityFiles(INTEGRITY_INDEX, INTEGRITY_INDEX_SAVE)) + { + return nullptr; + } + + if(!UnpackAsset(INTEGRITY_INDEX)) + { + return "Failed to unpack the integrity index file. Consider reinstalling the app."; + } + + std::vector vIntegrityLines = ReadIntegrityFile(INTEGRITY_INDEX); + if(vIntegrityLines.empty()) + { + return "Failed to load the integrity index file. Consider reinstalling the app."; + } + + std::vector vIntegritySaveLines = ReadIntegrityFile(INTEGRITY_INDEX_SAVE); + + // The remaining lines of each integrity file list all assets and their hashes + for(size_t i = 1; i < vIntegrityLines.size(); ++i) + { + const CIntegrityFileLine &IntegrityLine = vIntegrityLines[i]; + + // Check if the asset is unchanged from the last unpacking + const auto IntegritySaveLine = std::find_if(vIntegritySaveLines.begin(), vIntegritySaveLines.end(), [&](const CIntegrityFileLine &Line) { + return str_comp(Line.m_aFilename, IntegrityLine.m_aFilename) == 0; + }); + if(IntegritySaveLine != vIntegritySaveLines.end() && IntegritySaveLine->m_Sha256 == IntegrityLine.m_Sha256) + { + continue; + } + + if(fs_makedir_rec_for(IntegrityLine.m_aFilename) != 0 || !UnpackAsset(IntegrityLine.m_aFilename)) + { + return "Failed to unpack game assets, consider reinstalling the app."; + } + } + + // The integrity file will be unpacked every time when launching, + // so we can simply rename it to update the saved integrity file. + if((fs_is_file(INTEGRITY_INDEX_SAVE) && fs_remove(INTEGRITY_INDEX_SAVE) != 0) || fs_rename(INTEGRITY_INDEX, INTEGRITY_INDEX_SAVE) != 0) + { + return "Failed to update the saved integrity index file."; + } + + return nullptr; +} diff --git a/src/android/android_main.h b/src/android/android_main.h new file mode 100644 index 000000000..29a2ee402 --- /dev/null +++ b/src/android/android_main.h @@ -0,0 +1,23 @@ +#ifndef ANDROID_ANDROID_MAIN_H +#define ANDROID_ANDROID_MAIN_H + +#include +#if !defined(CONF_PLATFORM_ANDROID) +#error "This header should only be included when compiling for Android" +#endif + +/** + * Initializes the Android storage. Must be called on Android-systems + * before using any of the I/O and storage functions. + * + * This will change the current working directory to the app specific external + * storage location and unpack the assets from the APK file to the `data` folder. + * The folder `user` is created in the external storage to store the user data. + * + * Failure must be handled by exiting the app. + * + * @return `nullptr` on success, error message on failure. + */ +const char *InitAndroid(); + +#endif // ANDROID_ANDROID_MAIN_H diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index faae3c182..4eefceb12 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -55,6 +55,10 @@ #include "video.h" #endif +#if defined(CONF_PLATFORM_ANDROID) +#include +#endif + #include "SDL.h" #ifdef main #undef main @@ -4262,9 +4266,8 @@ static void ShowMessageBox(const char *pTitle, const char *pMessage, IClient::EM #if defined(CONF_PLATFORM_MACOS) extern "C" int TWMain(int argc, const char **argv) #elif defined(CONF_PLATFORM_ANDROID) +static int gs_AndroidStarted = false; extern "C" __attribute__((visibility("default"))) int SDL_main(int argc, char *argv[]); -extern "C" void InitAndroid(); - int SDL_main(int argc, char *argv2[]) #else int main(int argc, const char **argv) @@ -4274,15 +4277,19 @@ int main(int argc, const char **argv) #if defined(CONF_PLATFORM_ANDROID) const char **argv = const_cast(argv2); + // Android might not unload the library from memory, causing globals like gs_AndroidStarted + // not to be initialized correctly when starting the app again. + if(gs_AndroidStarted) + { + ::ShowMessageBox("Android Error", "The app was started, but not closed properly, this causes bugs. Please restart or manually close this task."); + std::exit(0); + } + gs_AndroidStarted = true; #elif defined(CONF_FAMILY_WINDOWS) CWindowsComLifecycle WindowsComLifecycle(true); #endif CCmdlineFix CmdlineFix(&argc, &argv); -#if defined(CONF_PLATFORM_ANDROID) - InitAndroid(); -#endif - #if defined(CONF_EXCEPTION_HANDLING) init_exception_handler(); #endif @@ -4317,6 +4324,17 @@ int main(int argc, const char **argv) vpLoggers.push_back(pFutureAssertionLogger); log_set_global_logger(log_logger_collection(std::move(vpLoggers)).release()); +#if defined(CONF_PLATFORM_ANDROID) + // Initialize Android after logger is available + const char *pAndroidInitError = InitAndroid(); + if(pAndroidInitError != nullptr) + { + log_error("android", "%s", pAndroidInitError); + ::ShowMessageBox("Android Error", pAndroidInitError); + std::exit(0); + } +#endif + std::stack> CleanerFunctions; std::function PerformCleanup = [&CleanerFunctions]() mutable { while(!CleanerFunctions.empty()) @@ -4327,7 +4345,17 @@ int main(int argc, const char **argv) }; std::function PerformFinalCleanup = []() { #ifdef CONF_PLATFORM_ANDROID - // properly close this native thread, so globals are destructed + // Forcefully terminate the entire process, to ensure that static variables + // will be initialized correctly when the app is started again after quitting. + // Returning from the main function is not enough, as this only results in the + // native thread terminating, but the Java thread will continue. Java does not + // support unloading libraries once they have been loaded, so all static + // variables will not have their expected initial values anymore when the app + // is started again after quitting. The variable gs_AndroidStarted above is + // used to check that static variables have been initialized properly. + // TODO: This is not the correct way to close an activity on Android, as it + // ignores the activity lifecycle entirely, which may cause issues if + // we ever used any global resources like the camera. std::exit(0); #endif }; diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index 052a43f0c..44401fbf2 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -39,8 +39,13 @@ public: int Init(int StorageType, int NumArgs, const char **ppArguments) { -#if !defined(CONF_PLATFORM_ANDROID) - // get userdir, just use data directory on android +#if defined(CONF_PLATFORM_ANDROID) + // See InitAndroid in android_main.cpp for details about Android storage handling. + // The current working directory is set to the app specific external storage location + // on Android. The user data is stored within a folder "user" in the external storage. + str_copy(m_aUserdir, "user"); +#else + // get userdir char aFallbackUserdir[IO_MAX_PATH_LENGTH]; if(fs_storage_path("DDNet", m_aUserdir, sizeof(m_aUserdir))) {