package code.name.monkey.retromusic.helper import android.content.Context import android.os.Environment import android.widget.Toast import code.name.monkey.retromusic.App import code.name.monkey.retromusic.BuildConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.* import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream object BackupHelper { suspend fun createBackup(context: Context, name: String) { val backupFile = File(backupRootPath + File.separator + name + APPEND_EXTENSION) if (backupFile.parentFile?.exists() != true) { backupFile.parentFile?.mkdirs() } val zipItems = mutableListOf() zipItems.addAll(getDatabaseZipItems(context)) zipItems.addAll(getSettingsZipItems(context)) getUserImageZipItems(context)?.let { zipItems.addAll(it) } zipItems.addAll(getCustomArtistZipItems(context)) zipAll(zipItems, backupFile) } private suspend fun zipAll(zipItems: List, backupFile: File) { withContext(Dispatchers.IO) { kotlin.runCatching { ZipOutputStream(BufferedOutputStream(FileOutputStream(backupFile))).use { out -> for (zipItem in zipItems) { FileInputStream(zipItem.filePath).use { fi -> BufferedInputStream(fi).use { origin -> val entry = ZipEntry(zipItem.zipPath) out.putNextEntry(entry) origin.copyTo(out) } } } } }.onFailure { it.printStackTrace() withContext(Dispatchers.Main) { Toast.makeText(App.getContext(), "Couldn't create backup", Toast.LENGTH_SHORT) .show() } } withContext(Dispatchers.Main) { Toast.makeText(App.getContext(), "Backup created successfully", Toast.LENGTH_SHORT) .show() } } } private fun getDatabaseZipItems(context: Context): List { return context.databaseList().filter { it.endsWith(".db") }.map { ZipItem(context.getDatabasePath(it).absolutePath, "$DATABASES_PATH${File.separator}$it") } } private fun getSettingsZipItems(context: Context): List { val sharedPrefPath = context.filesDir.parentFile?.absolutePath + "/shared_prefs/" return listOf( "${BuildConfig.APPLICATION_ID}_preferences.xml", // App settings pref path "$THEME_PREFS_KEY_DEFAULT.xml" // appthemehelper pref path ).map { ZipItem(sharedPrefPath + it, "$SETTINGS_PATH${File.separator}$it") } } private fun getUserImageZipItems(context: Context): List? { return context.filesDir.listFiles { _, name -> name.endsWith(".jpg") }?.map { ZipItem(it.absolutePath, "$IMAGES_PATH${File.separator}${it.name}") } } private fun getCustomArtistZipItems(context: Context): List { val zipItemList = mutableListOf() val sharedPrefPath = context.filesDir.parentFile?.absolutePath + "/shared_prefs/" zipItemList.addAll( File(context.filesDir, "custom_artist_images") .listFiles()?.map { ZipItem( it.absolutePath, "$CUSTOM_ARTISTS_PATH${File.separator}custom_artist_images${File.separator}${it.name}" ) }?.toList() ?: listOf() ) zipItemList.add( ZipItem( sharedPrefPath + File.separator + "custom_artist_image.xml", "$CUSTOM_ARTISTS_PATH${File.separator}prefs${File.separator}custom_artist_image.xml" ) ) return zipItemList } suspend fun restoreBackup(context: Context, inputStream: InputStream?) { withContext(Dispatchers.IO) { ZipInputStream(inputStream).use { var entry = it.nextEntry while (entry != null) { if (entry.isDatabaseEntry()) restoreDatabase(context, it, entry) if (entry.isPreferenceEntry()) restorePreferences(context, it, entry) if (entry.isImageEntry()) restoreImages(context, it, entry) if (entry.isCustomArtistImageEntry()) restoreCustomArtistImages( context, it, entry ) if (entry.isCustomArtistPrefEntry()) restoreCustomArtistPrefs( context, it, entry ) entry = it.nextEntry } } withContext(Dispatchers.Main) { Toast.makeText(context, "Restore Completed Successfully", Toast.LENGTH_SHORT).show() } } } private fun restoreImages(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) { val filePath = context.filesDir.path + File.separator + zipEntry.getFileName() BufferedOutputStream(FileOutputStream(filePath)).use { bos -> val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE) var read: Int while (zipIn.read(bytesIn).also { read = it } != -1) { bos.write(bytesIn, 0, read) } } } private fun restorePreferences(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) { val file = File( context.filesDir.parent!! + File.separator + "shared_prefs" + File.separator + zipEntry.getFileName() ) if (file.exists()) { file.delete() } BufferedOutputStream(FileOutputStream(file)).use { bos -> val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE) var read: Int while (zipIn.read(bytesIn).also { read = it } != -1) { bos.write(bytesIn, 0, read) } } } private fun restoreDatabase(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) { val filePath = context.filesDir.parent!! + File.separator + DATABASES_PATH + File.separator + zipEntry.getFileName() BufferedOutputStream(FileOutputStream(filePath)).use { bos -> val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE) var read: Int while (zipIn.read(bytesIn).also { read = it } != -1) { bos.write(bytesIn, 0, read) } } } private fun restoreCustomArtistImages( context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry ) { val parentFolder = File(context.filesDir, "custom_artist_images") if (!parentFolder.exists()) { parentFolder.mkdirs() } BufferedOutputStream( FileOutputStream( File( parentFolder, zipEntry.getFileName() ) ) ).use { bos -> val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE) var read: Int while (zipIn.read(bytesIn).also { read = it } != -1) { bos.write(bytesIn, 0, read) } } } private fun restoreCustomArtistPrefs( context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry ) { val filePath = context.filesDir.parentFile?.absolutePath + "/shared_prefs/" + zipEntry.getFileName() BufferedOutputStream(FileOutputStream(filePath)).use { bos -> val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE) var read: Int while (zipIn.read(bytesIn).also { read = it } != -1) { bos.write(bytesIn, 0, read) } } } val backupRootPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) .toString() + "/RetroMusic/Backups/" const val BACKUP_EXTENSION = "rmbak" const val APPEND_EXTENSION = ".$BACKUP_EXTENSION" private const val DATABASES_PATH = "databases" private const val SETTINGS_PATH = "prefs" private const val IMAGES_PATH = "userImages" private const val CUSTOM_ARTISTS_PATH = "artistImages" private const val THEME_PREFS_KEY_DEFAULT = "[[kabouzeid_app-theme-helper]]" private fun ZipEntry.isDatabaseEntry(): Boolean { return name.startsWith(DATABASES_PATH) } private fun ZipEntry.isPreferenceEntry(): Boolean { return name.startsWith(SETTINGS_PATH) } private fun ZipEntry.isImageEntry(): Boolean { return name.startsWith(IMAGES_PATH) } private fun ZipEntry.isCustomArtistImageEntry(): Boolean { return name.startsWith(CUSTOM_ARTISTS_PATH) && name.contains("custom_artist_images") } private fun ZipEntry.isCustomArtistPrefEntry(): Boolean { return name.startsWith(CUSTOM_ARTISTS_PATH) && name.contains("prefs") } private fun ZipEntry.getFileName(): String { return name.substring(name.lastIndexOf(File.separator)) } } data class ZipItem(val filePath: String, val zipPath: String) fun CharSequence.sanitize(): String { return toString().replace("/", "_") .replace(":", "_") .replace("*", "_") .replace("?", "_") .replace("\"", "_") .replace("<", "_") .replace(">", "_") .replace("|", "_") .replace("\\", "_") .replace("&", "_") }