diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionFileObserver.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionFileObserver.kt deleted file mode 100644 index 30ad7548..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionFileObserver.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ani.dantotsu.parsers.novel - -import android.os.FileObserver -import ani.dantotsu.parsers.novel.FileObserver.fileObserver -import ani.dantotsu.util.Logger -import java.io.File - - -class NovelExtensionFileObserver(private val listener: Listener, private val path: String) : - FileObserver(path, CREATE or DELETE or MOVED_FROM or MOVED_TO or MODIFY) { - - init { - fileObserver = this - } - - /** - * Starts observing the file changes in the directory. - */ - fun register() { - startWatching() - } - - - override fun onEvent(event: Int, file: String?) { - Logger.log("Event: $event") - if (file == null) return - - val fullPath = File(path, file) - - when (event) { - CREATE -> { - Logger.log("File created: $fullPath") - listener.onExtensionFileCreated(fullPath) - } - - DELETE -> { - Logger.log("File deleted: $fullPath") - listener.onExtensionFileDeleted(fullPath) - } - - MODIFY -> { - Logger.log("File modified: $fullPath") - listener.onExtensionFileModified(fullPath) - } - } - } - - interface Listener { - fun onExtensionFileCreated(file: File) - fun onExtensionFileDeleted(file: File) - fun onExtensionFileModified(file: File) - } -} - -object FileObserver { - var fileObserver: FileObserver? = null -} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt index a209881e..0a225c9d 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt @@ -8,6 +8,7 @@ import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult +import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.awaitSuccess @@ -87,7 +88,7 @@ class NovelExtensionGithubApi { } } - val installedExtensions = NovelExtensionLoader.loadExtensions(context) + val installedExtensions = ExtensionLoader.loadNovelExtensions(context) .filterIsInstance() .map { it.extension } diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt deleted file mode 100644 index 1b0a9ad3..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt +++ /dev/null @@ -1,379 +0,0 @@ -package ani.dantotsu.parsers.novel - -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService -import androidx.core.net.toUri -import ani.dantotsu.snackString -import ani.dantotsu.util.Logger -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.extension.InstallStep -import eu.kanade.tachiyomi.util.storage.getUriCompat -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.nio.channels.FileChannel -import java.nio.file.Files -import java.util.concurrent.TimeUnit - -/** - * The installer which installs, updates and uninstalls the extensions. - * - * @param context The application context. - */ -internal class NovelExtensionInstaller(private val context: Context) { - - /** - * The system's download manager - */ - private val downloadManager = context.getSystemService()!! - - /** - * The broadcast receiver which listens to download completion events. - */ - private val downloadReceiver = DownloadCompletionReceiver() - - /** - * The currently requested downloads, with the package name (unique id) as key, and the id - * returned by the download manager. - */ - private val activeDownloads = hashMapOf() - - /** - * Relay used to notify the installation step of every download. - */ - private val downloadsRelay = PublishRelay.create>() - - /** - * Adds the given extension to the downloads queue and returns an observable containing its - * step in the installation process. - * - * @param url The url of the apk. - * @param extension The extension to install. - */ - fun downloadAndInstall(url: String, extension: NovelExtension): Observable = - Observable.defer { - val pkgName = extension.pkgName - - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } - - val sourcePath = - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath - //if the file is already downloaded, remove it - val fileToDelete = File("$sourcePath/${url.toUri().lastPathSegment}") - if (fileToDelete.exists()) { - if (fileToDelete.delete()) { - Logger.log("APK file deleted successfully.") - } else { - Logger.log("Failed to delete APK file.") - } - } else { - Logger.log("APK file not found.") - } - - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() - - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - downloadUri.lastPathSegment - ) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - downloadsRelay.filter { it.first == id } - .map { it.second } - // Poll download status - .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors - .takeUntil { it.isCompleted() } - // Always notify on main thread - .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed - .doOnUnsubscribe { deleteDownload(pkgName) } - } - - /** - * Returns an observable that polls the given download id for its status every second, as the - * manager doesn't have any notification system. It'll stop once the download finishes. - * - * @param id The id of the download to poll. - */ - private fun pollStatus(id: Long): Observable { - val query = DownloadManager.Query().setFilterById(id) - - return Observable.interval(0, 1, TimeUnit.SECONDS) - // Get the current download status - .map { - downloadManager.query(query).use { cursor -> - if (cursor.moveToFirst()) { - cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) - } else { - DownloadManager.STATUS_FAILED - } - } - } - // Ignore duplicate results - .distinctUntilChanged() - // Stop polling when the download fails or finishes - .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED } - // Map to our model - .flatMap { status -> - when (status) { - DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending) - DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading) - DownloadManager.STATUS_SUCCESSFUL -> Observable.just(InstallStep.Installing) - else -> Observable.empty() - } - } - } - - fun installApk(downloadId: Long, uri: Uri, context: Context, pkgName: String): InstallStep { - val sourcePath = - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/" + uri.lastPathSegment - val destinationPath = - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk" - - // Check if source path is obtained correctly - if (!sourcePath.startsWith(FILE_SCHEME)) { - Logger.log("Source APK path not found.") - downloadsRelay.call(downloadId to InstallStep.Error) - return InstallStep.Error - } - - // Create the destination directory if it doesn't exist - val destinationDir = File(destinationPath).parentFile - if (destinationDir?.exists() == false) { - destinationDir.mkdirs() - } - if (destinationDir?.setWritable(true) == false) { - Logger.log("Failed to set destinationDir to writable.") - downloadsRelay.call(downloadId to InstallStep.Error) - return InstallStep.Error - } - - // Copy the file to the new location - copyFileToInternalStorage(sourcePath, destinationPath) - Logger.log("APK moved to $destinationPath") - downloadsRelay.call(downloadId to InstallStep.Installed) - return InstallStep.Installed - } - - /** - * Cancels extension install and remove from download manager and installer. - */ - fun cancelInstall(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) ?: return - downloadManager.remove(downloadId) - } - - fun uninstallApk(pkgName: String, context: Context) { - val apkPath = - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk" - val fileToDelete = File(apkPath) - //give write permission to the file - if (fileToDelete.exists() && !fileToDelete.canWrite()) { - Logger.log("File is not writable. Giving write permission.") - val a = fileToDelete.setWritable(true) - Logger.log("Success: $a") - } - //set the directory to writable - val destinationDir = File(apkPath).parentFile - if (destinationDir?.exists() == false) { - destinationDir.mkdirs() - } - val s = destinationDir?.setWritable(true) - Logger.log("Success destinationDir: $s") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - Files.delete(fileToDelete.toPath()) - } catch (e: Exception) { - Logger.log("Failed to delete APK file.") - Logger.log(e) - snackString("Failed to delete APK file.") - } - } else { - if (fileToDelete.exists()) { - if (fileToDelete.delete()) { - Logger.log("APK file deleted successfully.") - snackString("APK file deleted successfully.") - } else { - Logger.log("Failed to delete APK file.") - snackString("Failed to delete APK file.") - } - } else { - Logger.log("APK file not found.") - snackString("APK file not found.") - } - } - } - - private fun copyFileToInternalStorage(sourcePath: String, destinationPath: String) { - val source = File(sourcePath) - val destination = File(destinationPath) - destination.setWritable(true) - - //delete the file if it already exists - if (destination.exists()) { - if (destination.delete()) { - Logger.log("File deleted successfully.") - } else { - Logger.log("Failed to delete file.") - } - } - - var inputChannel: FileChannel? = null - var outputChannel: FileChannel? = null - try { - inputChannel = FileInputStream(source).channel - outputChannel = FileOutputStream(destination).channel - inputChannel.transferTo(0, inputChannel.size(), outputChannel) - destination.setWritable(false) - } catch (e: Exception) { - e.printStackTrace() - } finally { - inputChannel?.close() - outputChannel?.close() - } - - Logger.log("File copied to internal storage.") - } - - @Suppress("unused") - private fun getRealPathFromURI(context: Context, contentUri: Uri): String? { - var cursor: Cursor? = null - try { - val proj = arrayOf(MediaStore.Images.Media.DATA) - cursor = context.contentResolver.query(contentUri, proj, null, null, null) - val columnIndex = cursor?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - if (cursor != null && cursor.moveToFirst() && columnIndex != null) { - return cursor.getString(columnIndex) - } - } finally { - cursor?.close() - } - return null - } - - /** - * Sets the step of the installation of an extension. - * - * @param downloadId The id of the download. - * @param step New install step. - */ - fun updateInstallStep(downloadId: Long, step: InstallStep) { - downloadsRelay.call(downloadId to step) - } - - /** - * Deletes the download for the given package name. - * - * @param pkgName The package name of the download to delete. - */ - private fun deleteDownload(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) - if (downloadId != null) { - downloadManager.remove(downloadId) - } - if (activeDownloads.isEmpty()) { - downloadReceiver.unregister() - } - } - - /** - * Receiver that listens to download status events. - */ - private inner class DownloadCompletionReceiver : BroadcastReceiver() { - - /** - * Whether this receiver is currently registered. - */ - private var isRegistered = false - - /** - * Registers this receiver if it's not already. - */ - fun register() { - if (isRegistered) return - isRegistered = true - - val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED) - } - - /** - * Unregisters this receiver if it's not already. - */ - fun unregister() { - if (!isRegistered) return - isRegistered = false - - context.unregisterReceiver(this) - } - - /** - * Called when a download event is received. It looks for the download in the current active - * downloads and notifies its installation step. - */ - override fun onReceive(context: Context, intent: Intent?) { - val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return - - // Avoid events for downloads we didn't request - if (id !in activeDownloads.values) return - - val uri = downloadManager.getUriForDownloadedFile(id) - - // Set next installation step - if (uri == null) { - Logger.log("Couldn't locate downloaded APK") - downloadsRelay.call(id to InstallStep.Error) - return - } - - val query = DownloadManager.Query().setFilterById(id) - downloadManager.query(query).use { cursor -> - if (cursor.moveToFirst()) { - val localUri = cursor.getString( - cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), - ).removePrefix(FILE_SCHEME) - val pkgName = extractPkgNameFromUri(localUri) - installApk(id, File(localUri).getUriCompat(context), context, pkgName) - } - } - } - - private fun extractPkgNameFromUri(localUri: String): String { - val uri = Uri.parse(localUri) - val path = uri.path - val pkgName = path?.substring(path.lastIndexOf('/') + 1)?.removeSuffix(".apk") - Logger.log("Package name: $pkgName") - return pkgName ?: "" - } - } - - companion object { - const val APK_MIME = "application/vnd.android.package-archive" - const val FILE_SCHEME = "file://" - } -} diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt deleted file mode 100644 index 7c814880..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt +++ /dev/null @@ -1,154 +0,0 @@ -package ani.dantotsu.parsers.novel - -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager.GET_SIGNATURES -import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES -import android.os.Build -import ani.dantotsu.connections.crashlytics.CrashlyticsInterface -import ani.dantotsu.parsers.NovelInterface -import ani.dantotsu.snackString -import ani.dantotsu.util.Logger -import dalvik.system.PathClassLoader -import eu.kanade.tachiyomi.util.lang.Hash -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.util.Locale - -internal object NovelExtensionLoader { - - private const val officialSignature = - "a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5" //dan's key - - fun loadExtensions(context: Context): List { - val installDir = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/" - val results = mutableListOf() - //the number of files - Logger.log("Loading extensions from $installDir") - Logger.log( - "Loading extensions from ${File(installDir).listFiles()?.size}" - ) - File(installDir).setWritable(false) - File(installDir).listFiles()?.forEach { - //set the file to read only - it.setWritable(false) - Logger.log("Loading extension ${it.name}") - val extension = loadExtension(context, it) - if (extension is NovelLoadResult.Success) { - results.add(extension) - } else { - Logger.log("Failed to load extension ${it.name}") - } - } - return results - } - - /** - * Attempts to load an extension from the given package name. It checks if the extension - * contains the required feature flag before trying to load it. - */ - @Suppress("unused") - fun loadExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult { - val path = - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk" - //make /extensions/novel read only - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/".let { - File(it).setWritable(false) - File(it).setReadable(true) - } - try { - context.packageManager.getPackageArchiveInfo(path, 0) - } catch (error: Exception) { - // Unlikely, but the package may have been uninstalled at this point - Logger.log("Failed to load extension $pkgName") - return NovelLoadResult.Error(Exception("Failed to load extension")) - } - return loadExtension(context, File(path)) - } - - @Suppress("DEPRECATION") - fun loadExtension(context: Context, file: File): NovelLoadResult { - val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - context.packageManager.getPackageArchiveInfo( - file.absolutePath, - GET_SIGNATURES or GET_SIGNING_CERTIFICATES - ) - ?: return NovelLoadResult.Error(Exception("Failed to load extension")) - } else { - context.packageManager.getPackageArchiveInfo(file.absolutePath, GET_SIGNATURES) - ?: return NovelLoadResult.Error(Exception("Failed to load extension")) - } - val appInfo = packageInfo.applicationInfo - ?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")) - appInfo.sourceDir = file.absolutePath - appInfo.publicSourceDir = file.absolutePath - - val signatureHash = getSignatureHash(packageInfo) - - if ((signatureHash == null) || !signatureHash.contains(officialSignature)) { - Logger.log("Package ${packageInfo.packageName} isn't signed") - Logger.log("signatureHash: $signatureHash") - snackString("Package ${packageInfo.packageName} isn't signed") - //return NovelLoadResult.Error(Exception("Extension not signed")) - } - - val extension = NovelExtension.Installed( - packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString() - ?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")), - packageInfo.packageName - ?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")), - packageInfo.versionName ?: "", - packageInfo.versionCode.toLong(), - loadSources( - context, file, - packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString()!! - ), - packageInfo.applicationInfo?.loadIcon(context.packageManager) - ) - - return NovelLoadResult.Success(extension) - } - - @Suppress("DEPRECATION") - private fun getSignatureHash(pkgInfo: PackageInfo): List? { - val signatures = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) { - pkgInfo.signingInfo.apkContentsSigners - } else { - pkgInfo.signatures - } - return if (!signatures.isNullOrEmpty()) { - signatures.map { Hash.sha256(it.toByteArray()) } - } else { - null - } - } - - private fun loadSources(context: Context, file: File, className: String): List { - return try { - Logger.log("isFileWritable: ${file.canWrite()}") - if (file.canWrite()) { - val a = file.setWritable(false) - Logger.log("success: $a") - } - Logger.log("isFileWritable: ${file.canWrite()}") - val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader) - val extensionClassName = - "some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className" - val loadedClass = classLoader.loadClass(extensionClassName) - val instance = loadedClass.getDeclaredConstructor().newInstance() - val novelInterfaceInstance = instance as? NovelInterface - listOfNotNull(novelInterfaceInstance) - } catch (e: Exception) { - e.printStackTrace() - Injekt.get().logException(e) - emptyList() - } - } -} - -sealed class NovelLoadResult { - data class Success(val extension: NovelExtension.Installed) : NovelLoadResult() - data class Error(val error: Exception) : NovelLoadResult() -} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt index e55b238c..8ea2f620 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt @@ -2,14 +2,17 @@ package ani.dantotsu.parsers.novel import android.content.Context import android.graphics.drawable.Drawable +import ani.dantotsu.media.MediaType import ani.dantotsu.snackString import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.InstallStep +import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver +import eu.kanade.tachiyomi.extension.util.ExtensionInstaller +import eu.kanade.tachiyomi.extension.util.ExtensionLoader import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import rx.Observable import tachiyomi.core.util.lang.withUIContext -import java.io.File class NovelExtensionManager(private val context: Context) { var isInitialized = false @@ -24,7 +27,7 @@ class NovelExtensionManager(private val context: Context) { /** * The installer which installs, updates and uninstalls the Novel extensions. */ - private val installer by lazy { NovelExtensionInstaller(context) } + private val installer by lazy { ExtensionInstaller(context) } private val iconMap = mutableMapOf() @@ -49,12 +52,11 @@ class NovelExtensionManager(private val context: Context) { init { initNovelExtensions() - val path = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/" - NovelExtensionFileObserver(NovelInstallationListener(), path).register() + ExtensionInstallReceiver().setNovelListener(NovelInstallationListener()).register(context) } private fun initNovelExtensions() { - val novelExtensions = NovelExtensionLoader.loadExtensions(context) + val novelExtensions = ExtensionLoader.loadNovelExtensions(context) _installedNovelExtensionsFlow.value = novelExtensions .filterIsInstance() @@ -117,7 +119,8 @@ class NovelExtensionManager(private val context: Context) { * @param extension The anime extension to be installed. */ fun installExtension(extension: NovelExtension.Available): Observable { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) + return installer.downloadAndInstall(api.getApkUrl(extension), extension.pkgName, + extension.name, MediaType.NOVEL) } /** @@ -157,7 +160,7 @@ class NovelExtensionManager(private val context: Context) { * @param pkgName The package name of the application to uninstall. */ fun uninstallExtension(pkgName: String, context: Context) { - installer.uninstallApk(pkgName, context) + installer.uninstallApk(pkgName) } /** @@ -202,28 +205,18 @@ class NovelExtensionManager(private val context: Context) { /** * Listener which receives events of the novel extensions being installed, updated or removed. */ - private inner class NovelInstallationListener : NovelExtensionFileObserver.Listener { - - override fun onExtensionFileCreated(file: File) { - NovelExtensionLoader.loadExtension(context, file).let { - if (it is NovelLoadResult.Success) { - registerNewExtension(it.extension.withUpdateCheck()) - } - } + private inner class NovelInstallationListener : ExtensionInstallReceiver.NovelListener { + override fun onExtensionInstalled(extension: NovelExtension.Installed) { + registerNewExtension(extension.withUpdateCheck()) } - override fun onExtensionFileDeleted(file: File) { - val pkgName = file.nameWithoutExtension + override fun onExtensionUpdated(extension: NovelExtension.Installed) { + registerUpdatedExtension(extension.withUpdateCheck()) + } + + override fun onPackageUninstalled(pkgName: String) { unregisterNovelExtension(pkgName) } - - override fun onExtensionFileModified(file: File) { - NovelExtensionLoader.loadExtension(context, file).let { - if (it is NovelLoadResult.Success) { - registerUpdatedExtension(it.extension.withUpdateCheck()) - } - } - } } /** diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelLoadResult.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelLoadResult.kt new file mode 100644 index 00000000..59ca96a5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelLoadResult.kt @@ -0,0 +1,7 @@ +package ani.dantotsu.parsers.novel + + +sealed class NovelLoadResult { + data class Success(val extension: NovelExtension.Installed) : NovelLoadResult() + data class Error(val error: Exception) : NovelLoadResult() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index ec7d56e2..f0bbe7c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -6,6 +6,8 @@ import android.content.Intent import android.content.IntentFilter import androidx.core.content.ContextCompat import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelLoadResult import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult @@ -28,6 +30,7 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { private var animeListener: AnimeListener? = null private var mangaListener: MangaListener? = null + private var novelListener: NovelListener? = null private var type: MediaType? = null /** @@ -50,6 +53,12 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { return this } + fun setNovelListener(listener: NovelListener): ExtensionInstallReceiver { + this.type = MediaType.NOVEL + novelListener = listener + return this + } + /** * Called when one of the events of the [filter] is received. When the package is an extension, * it's loaded in background and it notifies the [listener] when finished. @@ -92,6 +101,16 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { } } + MediaType.NOVEL -> { + when (val result = getNovelExtensionFromIntent(context, intent)) { + is NovelLoadResult.Success -> novelListener?.onExtensionInstalled( + result.extension + ) + + else -> {} + } + } + else -> {} } } @@ -120,6 +139,16 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { } } + MediaType.NOVEL -> { + when (val result = getNovelExtensionFromIntent(context, intent)) { + is NovelLoadResult.Success -> novelListener?.onExtensionUpdated( + result.extension + ) + + else -> {} + } + } + else -> {} } } @@ -139,6 +168,10 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { mangaListener?.onPackageUninstalled(pkgName) } + MediaType.NOVEL -> { + novelListener?.onPackageUninstalled(pkgName) + } + else -> {} } } @@ -188,6 +221,23 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { }.await() } + @OptIn(DelicateCoroutinesApi::class) + private suspend fun getNovelExtensionFromIntent( + context: Context, + intent: Intent? + ): NovelLoadResult { + val pkgName = getPackageNameFromIntent(intent) + if (pkgName == null) { + Logger.log("Package name not found") + return NovelLoadResult.Error(Exception("Package name not found")) + } + return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { + ExtensionLoader.loadNovelExtensionFromPkgName( + context, + pkgName, + ) + }.await() + } /** * Listener that receives extension installation events. @@ -206,6 +256,12 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { fun onPackageUninstalled(pkgName: String) } + interface NovelListener { + fun onExtensionInstalled(extension: NovelExtension.Installed) + fun onExtensionUpdated(extension: NovelExtension.Installed) + fun onPackageUninstalled(pkgName: String) + } + companion object { /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index d83553ec..4d7a0fed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -87,6 +87,8 @@ class ExtensionInstaller(private val context: Context) { downloadUri.lastPathSegment ) .setDescription(type.asText()) + .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + .setAllowedOverRoaming(true) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) val id = downloadManager.enqueue(request) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index fed98c98..d7b454a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -6,6 +6,9 @@ import android.content.pm.PackageManager import android.os.Build import androidx.core.content.pm.PackageInfoCompat import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.NovelInterface +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelLoadResult import ani.dantotsu.util.Logger import dalvik.system.PathClassLoader import eu.kanade.domain.source.service.SourcePreferences @@ -24,6 +27,7 @@ import eu.kanade.tachiyomi.util.system.getApplicationIcon import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.injectLazy +import java.util.Locale /** * Class that handles the loading of the extensions. Supports two kinds of extensions: @@ -77,6 +81,10 @@ internal object ExtensionLoader { private const val officialSignatureManga = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" + //dan's key + private const val officialSignature = + "a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5" + /** * List of the trusted signatures. */ @@ -133,6 +141,28 @@ internal object ExtensionLoader { } } + fun loadNovelExtensions(context: Context): List { + val pkgManager = context.packageManager + + val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + pkgManager.getInstalledPackages(PACKAGE_FLAGS) + } + + val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.NOVEL, it) } + + if (extPkgs.isEmpty()) return emptyList() + + // Load each extension concurrently and wait for completion + return runBlocking { + val deferred = extPkgs.map { + async { loadNovelExtension(context, it.packageName, it) } + } + deferred.map { it.await() } + } + } + /** * Attempts to load an extension from the given package name. It checks if the extension * contains the required feature flag before trying to load it. @@ -167,6 +197,21 @@ internal object ExtensionLoader { return loadMangaExtension(context, pkgName, pkgInfo) } + fun loadNovelExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult { + val pkgInfo = try { + context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) + } catch (error: PackageManager.NameNotFoundException) { + // Unlikely, but the package may have been uninstalled at this point + Logger.log(error) + return NovelLoadResult.Error(error) + } + if (!isPackageAnExtension(MediaType.NOVEL, pkgInfo)) { + Logger.log("Tried to load a package that wasn't a extension ($pkgName)") + return NovelLoadResult.Error(Exception("Tried to load a package that wasn't a extension ($pkgName)")) + } + return loadNovelExtension(context, pkgName, pkgInfo) + } + /** * Loads an extension given its package name. * @@ -400,17 +445,75 @@ internal object ExtensionLoader { return MangaLoadResult.Success(extension) } + private fun loadNovelExtension( + context: Context, + pkgName: String, + pkgInfo: PackageInfo + ): NovelLoadResult { + val pkgManager = context.packageManager + + val appInfo = try { + pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) + } catch (error: PackageManager.NameNotFoundException) { + // Unlikely, but the package may have been uninstalled at this point + Logger.log(error) + return NovelLoadResult.Error(error) + } + + val extName = + pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") + val versionName = pkgInfo.versionName + val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo) + + if (versionName.isNullOrEmpty()) { + Logger.log("Missing versionName for extension $extName") + return NovelLoadResult.Error(Exception("Missing versionName for extension $extName")) + } + + val signatureHash = getSignatureHash(pkgInfo) + + val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) + val novelInterfaceInstance = try { + val className = appInfo.loadLabel(context.packageManager).toString() + val extensionClassName = + "some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className" + val loadedClass = classLoader.loadClass(extensionClassName) + val instance = loadedClass.getDeclaredConstructor().newInstance() + instance as? NovelInterface + } catch (e: Throwable) { + Logger.log("Extension load error: $extName") + return NovelLoadResult.Error(e as Exception) + } + + val extension = NovelExtension.Installed( + name = extName, + pkgName = pkgName, + versionName = versionName, + versionCode = versionCode, + sources = listOfNotNull(novelInterfaceInstance), + isUnofficial = signatureHash != officialSignatureManga, + icon = context.getApplicationIcon(pkgName), + ) + return NovelLoadResult.Success(extension) + } + + /** * Returns true if the given package is an extension. * * @param pkgInfo The package info of the application. */ private fun isPackageAnExtension(type: MediaType, pkgInfo: PackageInfo): Boolean { - return pkgInfo.reqFeatures.orEmpty().any { - it.name == when (type) { - MediaType.ANIME -> ANIME_PACKAGE - MediaType.MANGA -> MANGA_PACKAGE - else -> "" + + return if (type == MediaType.NOVEL) { + pkgInfo.packageName.startsWith("some.random") + } else { + pkgInfo.reqFeatures.orEmpty().any { + it.name == when (type) { + MediaType.ANIME -> ANIME_PACKAGE + MediaType.MANGA -> MANGA_PACKAGE + else -> "" + } } } }