chore: lint performance optimization
This includes shadowed variables, unnecessary parameters, layouts with string literals, items that cause performance bottlenecks, and the merge of extension types into only the necessary separate classes.
This commit is contained in:
parent
958aa634b1
commit
37ec165319
111 changed files with 1561 additions and 2091 deletions
|
@ -19,7 +19,7 @@
|
|||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||
|
@ -53,11 +53,13 @@
|
|||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:roundIcon="${icon_placeholder_round}"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup">
|
||||
tools:ignore="AllowBackup"
|
||||
tools:targetApi="tiramisu">
|
||||
<receiver
|
||||
android:name=".widgets.CurrentlyAiringWidget"
|
||||
android:exported="false">
|
||||
|
@ -300,11 +302,7 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
|
@ -355,11 +353,7 @@
|
|||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
|
|
|
@ -86,7 +86,7 @@ class App : MultiDexApplication() {
|
|||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||
Logger.log("App: Logging started")
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
initializeNetwork()
|
||||
|
||||
setupNotificationChannels()
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
|
|
|
@ -159,16 +159,16 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val backgroundDrawable = bottomNavBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
bottomNavBar.background = backgroundDrawable
|
||||
}
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
val offset = try {
|
||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
|
@ -337,7 +337,7 @@ class MainActivity : AppCompatActivity() {
|
|||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) { it ->
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
|
@ -362,7 +362,7 @@ class MainActivity : AppCompatActivity() {
|
|||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
if (mainViewPager.getCurrentItem() != selectedOption) {
|
||||
if (mainViewPager.currentItem != selectedOption) {
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ani.dantotsu
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.others.webview.CloudFlare
|
||||
|
@ -35,7 +34,7 @@ lateinit var defaultHeaders: Map<String, String>
|
|||
lateinit var okHttpClient: OkHttpClient
|
||||
lateinit var client: Requests
|
||||
|
||||
fun initializeNetwork(context: Context) {
|
||||
fun initializeNetwork() {
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
|
|
|
@ -387,6 +387,7 @@ class AnilistQueries {
|
|||
returnArray.addAll(map.values)
|
||||
return returnArray
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
|
@ -544,6 +545,7 @@ class AnilistQueries {
|
|||
returnMap["current$type"] = returnArray
|
||||
return
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
|
@ -573,6 +575,7 @@ class AnilistQueries {
|
|||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<Int>(),
|
||||
|
@ -734,7 +737,7 @@ class AnilistQueries {
|
|||
}
|
||||
|
||||
sorted["All"] = all
|
||||
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||
val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
||||
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
|
|
|
@ -112,8 +112,8 @@ class AnilistHomeViewModel : ViewModel() {
|
|||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
Anilist.getSavedToken()
|
||||
MAL.getSavedToken(context)
|
||||
Discord.getSavedToken(context)
|
||||
MAL.getSavedToken()
|
||||
Discord.getSavedToken()
|
||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
searchVal: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
|
@ -167,7 +167,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
|||
animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
|
@ -231,7 +231,7 @@ class AnilistMangaViewModel : ViewModel() {
|
|||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
searchVal: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
|
@ -239,7 +239,7 @@ class AnilistMangaViewModel : ViewModel() {
|
|||
mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
|
|
|
@ -20,14 +20,14 @@ object Discord {
|
|||
var avatar: String? = null
|
||||
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
fun getSavedToken(): Boolean {
|
||||
token = PrefManager.getVal(
|
||||
PrefName.DiscordToken, null as String?
|
||||
)
|
||||
return token != null
|
||||
}
|
||||
|
||||
fun saveToken(context: Context, token: String) {
|
||||
fun saveToken(token: String) {
|
||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,16 +5,12 @@ import android.app.NotificationChannel
|
|||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
@ -37,7 +33,6 @@ import okhttp3.Response
|
|||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.io.File
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
class DiscordService : Service() {
|
||||
private var heartbeat: Int = 0
|
||||
|
@ -162,8 +157,8 @@ class DiscordService : Service() {
|
|||
|
||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||
|
||||
var retryAttempts = 0
|
||||
val maxRetryAttempts = 10
|
||||
private var retryAttempts = 0
|
||||
private val maxRetryAttempts = 10
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
super.onOpen(webSocket, response)
|
||||
this@DiscordService.webSocket = webSocket
|
||||
|
@ -232,7 +227,7 @@ class DiscordService : Service() {
|
|||
resume()
|
||||
resume = false
|
||||
} else {
|
||||
identify(webSocket, baseContext)
|
||||
identify(webSocket)
|
||||
log("WebSocket: Identified")
|
||||
}
|
||||
}
|
||||
|
@ -245,13 +240,13 @@ class DiscordService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
fun identify(webSocket: WebSocket, context: Context) {
|
||||
private fun identify(webSocket: WebSocket) {
|
||||
val properties = JsonObject()
|
||||
properties.addProperty("os", "linux")
|
||||
properties.addProperty("browser", "unknown")
|
||||
properties.addProperty("device", "unknown")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(context))
|
||||
d.addProperty("token", getToken())
|
||||
d.addProperty("intents", 0)
|
||||
d.add("properties", properties)
|
||||
val payload = JsonObject()
|
||||
|
@ -311,7 +306,7 @@ class DiscordService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getToken(context: Context): String {
|
||||
fun getToken(): String {
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
return if (token == null) {
|
||||
log("WebSocket: Token not found")
|
||||
|
@ -375,10 +370,10 @@ class DiscordService : Service() {
|
|||
log("WebSocket: Simple Test Presence Saved")
|
||||
}
|
||||
|
||||
fun setPresence(String: String) {
|
||||
fun setPresence(string: String) {
|
||||
log("WebSocket: Sending Presence payload")
|
||||
log(String)
|
||||
webSocket.send(String)
|
||||
log(string)
|
||||
webSocket.send(string)
|
||||
}
|
||||
|
||||
fun log(string: String) {
|
||||
|
@ -388,7 +383,7 @@ class DiscordService : Service() {
|
|||
fun resume() {
|
||||
log("Sending Resume payload")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(baseContext))
|
||||
d.addProperty("token", getToken())
|
||||
d.addProperty("session_id", sessionId)
|
||||
d.addProperty("seq", sequence)
|
||||
val json = JsonObject()
|
||||
|
@ -404,8 +399,7 @@ class DiscordService : Service() {
|
|||
Thread.sleep(heartbeat.toLong())
|
||||
heartbeatSend(webSocket, sequence)
|
||||
log("WebSocket: Heartbeat Sent")
|
||||
} catch (e: InterruptedException) {
|
||||
}
|
||||
} catch (ignored: InterruptedException) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ class Login : AppCompatActivity() {
|
|||
}
|
||||
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
saveToken(this, token)
|
||||
saveToken(token)
|
||||
startMainActivity(this@Login)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
|
@ -64,7 +63,7 @@ object MAL {
|
|||
}
|
||||
|
||||
|
||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||
suspend fun getSavedToken(): Boolean {
|
||||
return tryWithSuspend(false) {
|
||||
var res: ResponseToken =
|
||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
|
@ -77,7 +76,7 @@ object MAL {
|
|||
} ?: false
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
fun removeSavedToken() {
|
||||
token = null
|
||||
username = null
|
||||
userid = null
|
||||
|
|
|
@ -3,6 +3,7 @@ package ani.dantotsu.download
|
|||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.gson.Gson
|
||||
|
@ -15,11 +16,11 @@ class DownloadsManager(private val context: Context) {
|
|||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||
get() = downloadsList.filter { it.type == MediaType.MANGA }
|
||||
val animeDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||
get() = downloadsList.filter { it.type == MediaType.ANIME }
|
||||
val novelDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||
get() = downloadsList.filter { it.type == MediaType.NOVEL }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
|
@ -47,14 +48,8 @@ class DownloadsManager(private val context: Context) {
|
|||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
fun removeMedia(title: String, type: MediaType) {
|
||||
val subDirectory = type.asText()
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
|
@ -71,53 +66,45 @@ class DownloadsManager(private val context: Context) {
|
|||
cleanDownloads()
|
||||
}
|
||||
when (type) {
|
||||
DownloadedType.Type.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||
MediaType.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
|
||||
}
|
||||
|
||||
DownloadedType.Type.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||
MediaType.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
|
||||
}
|
||||
|
||||
DownloadedType.Type.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||
MediaType.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun cleanDownloads() {
|
||||
cleanDownload(DownloadedType.Type.MANGA)
|
||||
cleanDownload(DownloadedType.Type.ANIME)
|
||||
cleanDownload(DownloadedType.Type.NOVEL)
|
||||
cleanDownload(MediaType.MANGA)
|
||||
cleanDownload(MediaType.ANIME)
|
||||
cleanDownload(MediaType.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: DownloadedType.Type) {
|
||||
private fun cleanDownload(type: MediaType) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val subDirectory = type.asText()
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||
mangaDownloadedTypes
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
animeDownloadedTypes
|
||||
} else {
|
||||
novelDownloadedTypes
|
||||
val downloadsSubLists = when (type) {
|
||||
MediaType.MANGA -> mangaDownloadedTypes
|
||||
MediaType.ANIME -> animeDownloadedTypes
|
||||
else -> novelDownloadedTypes
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +140,7 @@ class DownloadsManager(private val context: Context) {
|
|||
return downloadsList.contains(downloadedType)
|
||||
}
|
||||
|
||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
|
||||
return if (type == null) {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||
} else {
|
||||
|
@ -162,21 +149,25 @@ class DownloadsManager(private val context: Context) {
|
|||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
val directory = when (downloadedType.type) {
|
||||
MediaType.MANGA -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
MediaType.ANIME -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the directory exists and delete it recursively
|
||||
|
@ -193,21 +184,25 @@ class DownloadsManager(private val context: Context) {
|
|||
}
|
||||
|
||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
val directory = when (downloadedType.type) {
|
||||
MediaType.MANGA -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
MediaType.ANIME -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
}
|
||||
val destination = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
|
@ -225,13 +220,17 @@ class DownloadsManager(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: DownloadedType.Type) {
|
||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
fun purgeDownloads(type: MediaType) {
|
||||
val directory = when (type) {
|
||||
MediaType.MANGA -> {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
}
|
||||
MediaType.ANIME -> {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
}
|
||||
else -> {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
}
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
|
@ -255,56 +254,53 @@ class DownloadsManager(private val context: Context) {
|
|||
|
||||
fun getDirectory(
|
||||
context: Context,
|
||||
type: DownloadedType.Type,
|
||||
type: MediaType,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): File {
|
||||
return if (type == DownloadedType.Type.MANGA) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
return when (type) {
|
||||
MediaType.MANGA -> {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title"
|
||||
)
|
||||
MediaType.ANIME -> {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
else -> {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
NOVEL
|
||||
}
|
||||
}
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: MediaType) : Serializable
|
||||
|
|
|
@ -27,14 +27,15 @@ import ani.dantotsu.download.DownloadedType
|
|||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
@ -242,7 +243,7 @@ class AnimeDownloaderService : Service() {
|
|||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -273,7 +274,7 @@ class AnimeDownloaderService : Service() {
|
|||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
|
@ -302,7 +303,7 @@ class AnimeDownloaderService : Service() {
|
|||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
|
|
|
@ -34,15 +34,16 @@ import ani.dantotsu.currContext
|
|||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
@ -188,8 +189,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val type: DownloadedType.Type =
|
||||
DownloadedType.Type.ANIME
|
||||
val type: MediaType = MediaType.ANIME
|
||||
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
|
@ -293,11 +293,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
|
@ -327,11 +323,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||
}
|
||||
|
||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
|
|
|
@ -21,8 +21,8 @@ import ani.dantotsu.R
|
|||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||
|
@ -30,6 +30,7 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
|||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
|
@ -211,8 +212,7 @@ class MangaDownloaderService : Service() {
|
|||
while (bitmap == null && retryCount < task.retries) {
|
||||
bitmap = image.fetchAndProcessImage(
|
||||
image.page,
|
||||
image.source,
|
||||
this@MangaDownloaderService
|
||||
image.source
|
||||
)
|
||||
retryCount++
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ class MangaDownloaderService : Service() {
|
|||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
|
|
|
@ -31,15 +31,16 @@ import ani.dantotsu.currContext
|
|||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
@ -179,11 +180,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: DownloadedType.Type =
|
||||
val type: MediaType =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
} else {
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
|
@ -289,11 +290,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
|
@ -317,11 +314,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
}
|
||||
|
||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = when (downloadedType.type) {
|
||||
DownloadedType.Type.MANGA -> "Manga"
|
||||
DownloadedType.Type.ANIME -> "Anime"
|
||||
else -> "Novel"
|
||||
}
|
||||
val type = downloadedType.type.asText()
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
|
|
|
@ -20,10 +20,11 @@ import ani.dantotsu.R
|
|||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -335,7 +336,7 @@ class NovelDownloaderService : Service() {
|
|||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.content.Intent
|
|||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -37,6 +36,7 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
|
|||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
|
@ -49,13 +49,14 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
object Helper {
|
||||
|
||||
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource =
|
||||
|
@ -157,16 +158,14 @@ object Helper {
|
|||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Logger.log("Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Logger.log("Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Logger.log("Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Logger.log("Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Logger.log("Download Downloading")
|
||||
when (download.state) {
|
||||
Download.STATE_COMPLETED -> Logger.log("Download Completed")
|
||||
Download.STATE_FAILED -> Logger.log("Download Failed")
|
||||
Download.STATE_STOPPED -> Logger.log("Download Stopped")
|
||||
Download.STATE_QUEUED -> Logger.log("Download Queued")
|
||||
Download.STATE_DOWNLOADING -> Logger.log("Download Downloading")
|
||||
Download.STATE_REMOVING -> Logger.log("Download Removing")
|
||||
Download.STATE_RESTARTING -> Logger.log("Download Restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +219,7 @@ object Helper {
|
|||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||
.queryDownload(title, episode, MediaType.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
|
@ -243,7 +242,7 @@ object Helper {
|
|||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
DownloadedType.Type.ANIME
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
|
|
|
@ -167,8 +167,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.animeTrendingViewPager.currentItem =
|
||||
binding.animeTrendingViewPager.currentItem + 1
|
||||
binding.animeTrendingViewPager.currentItem += 1
|
||||
}
|
||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
|
|
|
@ -66,8 +66,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.util.TypedValue
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -16,8 +15,8 @@ import androidx.lifecycle.lifecycleScope
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
|
@ -73,10 +72,7 @@ class CalendarActivity : AppCompatActivity() {
|
|||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
hideSystemBarsExtendView()
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package ani.dantotsu.media
|
|||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.content.res.Configuration
|
||||
|
@ -13,9 +12,7 @@ import android.view.GestureDetector
|
|||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -243,13 +240,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
@SuppressLint("ResourceType")
|
||||
fun total() {
|
||||
val text = SpannableStringBuilder().apply {
|
||||
val typedValue = TypedValue()
|
||||
val mediaTypedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue,
|
||||
mediaTypedValue,
|
||||
true
|
||||
)
|
||||
val white = typedValue.data
|
||||
val white = mediaTypedValue.data
|
||||
if (media.userStatus != null) {
|
||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||
val typedValue = TypedValue()
|
||||
|
|
|
@ -52,12 +52,16 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
it
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = if (media.anime != null) {
|
||||
AnimeSources.list.size - 1
|
||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||
MangaSources.list.size - 1
|
||||
} else {
|
||||
NovelSources.list.size - 1
|
||||
data.sourceIndex = when {
|
||||
media.anime != null -> {
|
||||
AnimeSources.list.size - 1
|
||||
}
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
||||
MangaSources.list.size - 1
|
||||
}
|
||||
else -> {
|
||||
NovelSources.list.size - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
@ -152,10 +156,10 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
watchSources?.get(i)?.apply {
|
||||
if (!post && !allowsPreloading) return@apply
|
||||
ep.sEpisode?.let {
|
||||
loadByVideoServers(link, ep.extra, it) {
|
||||
if (it.videos.isNotEmpty()) {
|
||||
list.add(it)
|
||||
ep.extractorCallback?.invoke(it)
|
||||
loadByVideoServers(link, ep.extra, it) { extractor ->
|
||||
if (extractor.videos.isNotEmpty()) {
|
||||
list.add(extractor)
|
||||
ep.extractorCallback?.invoke(extractor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
26
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
26
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
enum class MediaType {
|
||||
ANIME,
|
||||
MANGA,
|
||||
NOVEL;
|
||||
|
||||
fun asText(): String {
|
||||
return when (this) {
|
||||
ANIME -> "Anime"
|
||||
MANGA -> "Manga"
|
||||
NOVEL -> "Novel"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string : String): MediaType {
|
||||
return when (string) {
|
||||
"Anime" -> ANIME
|
||||
"Manga" -> MANGA
|
||||
"Novel" -> NOVEL
|
||||
else -> { ANIME }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
|||
i = media!!.selected!!.sourceIndex
|
||||
|
||||
val source = if (media!!.anime != null) {
|
||||
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
|
||||
(if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
|
||||
} else {
|
||||
anime = false
|
||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||
|
|
|
@ -17,7 +17,7 @@ class SubtitleDownloader {
|
|||
|
||||
companion object {
|
||||
//doesn't really download the subtitles -\_(o_o)_/-
|
||||
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
||||
suspend fun loadSubtitleType(url: String): SubtitleType =
|
||||
withContext(Dispatchers.IO) {
|
||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
@ -60,7 +60,7 @@ class SubtitleDownloader {
|
|||
if (!directory.exists()) { //just in case
|
||||
directory.mkdirs()
|
||||
}
|
||||
val type = loadSubtitleType(context, url)
|
||||
val type = loadSubtitleType(url)
|
||||
val subtiteFile = File(directory, "subtitle.${type}")
|
||||
if (subtiteFile.exists()) {
|
||||
subtiteFile.delete()
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.util.regex.Pattern
|
|||
class AnimeNameAdapter {
|
||||
companion object {
|
||||
const val episodeRegex =
|
||||
"(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
"(episode|episodio|ep|e)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
const val failedEpisodeNumberRegex =
|
||||
"(?<!part\\s)\\b(\\d+)\\b"
|
||||
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
|
@ -114,7 +114,7 @@ class AnimeNameAdapter {
|
|||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern: Regex =
|
||||
val failedEpisodeNumberPattern =
|
||||
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
|
|
|
@ -29,19 +29,24 @@ import androidx.recyclerview.widget.ConcatAdapter
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.setNavigationTheme
|
||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
@ -433,7 +438,7 @@ class AnimeWatchFragment : Fragment() {
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.ANIME
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
episodeAdapter.purgeDownload(i)
|
||||
|
@ -445,7 +450,7 @@ class AnimeWatchFragment : Fragment() {
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.ANIME
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||
|
|
|
@ -13,14 +13,16 @@ import androidx.lifecycle.coroutineScope
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
|
@ -427,7 +429,7 @@ class EpisodeAdapter(
|
|||
if (bytes < 0) return null
|
||||
val unit = 1000
|
||||
if (bytes < unit) return "$bytes B"
|
||||
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||
val pre = ("KMGTPE")[exp - 1]
|
||||
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,10 @@ import android.graphics.Color
|
|||
import android.graphics.drawable.Animatable
|
||||
import android.hardware.SensorManager
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.*
|
||||
import android.media.AudioManager.AUDIOFOCUS_GAIN
|
||||
import android.media.AudioManager.AUDIOFOCUS_LOSS
|
||||
import android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
|
||||
import android.media.AudioManager.STREAM_MUSIC
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -27,8 +30,18 @@ import android.provider.Settings.System
|
|||
import android.util.AttributeSet
|
||||
import android.util.Rational
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.KeyEvent.*
|
||||
import android.view.GestureDetector
|
||||
import android.view.KeyEvent
|
||||
import android.view.KeyEvent.ACTION_UP
|
||||
import android.view.KeyEvent.KEYCODE_B
|
||||
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
|
||||
import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
|
||||
import android.view.KeyEvent.KEYCODE_N
|
||||
import android.view.KeyEvent.KEYCODE_SPACE
|
||||
import android.view.MotionEvent
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ImageButton
|
||||
|
@ -46,27 +59,43 @@ import androidx.core.view.updateLayoutParams
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.util.EventLogger
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.ui.*
|
||||
import androidx.media3.ui.CaptionStyleCompat.*
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.CaptionStyleCompat
|
||||
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_DEPRESSED
|
||||
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
|
||||
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE
|
||||
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.mediarouter.app.MediaRouteButton
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.NoPaddingArrayAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.brightnessConverter
|
||||
import ani.dantotsu.circularReveal
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
|
@ -75,19 +104,38 @@ import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
|||
import ani.dantotsu.connections.discord.RPC
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.getCurrentBrightnessValue
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.AniSkip.getType
|
||||
import ani.dantotsu.others.ResettableTimer
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.*
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.PlayerSettingsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
|
@ -103,8 +151,11 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.Calendar
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
@ -344,15 +395,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this)
|
||||
castContext = CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
|
||||
castPlayer = CastPlayer(castContext!!)
|
||||
castPlayer!!.setSessionAvailabilityListener(this)
|
||||
} catch (e: Exception) {
|
||||
isCastApiAvailable = false
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
hideSystemBars()
|
||||
hideSystemBarsExtendView()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
finishAndRemoveTask()
|
||||
|
@ -397,21 +447,25 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
orientationListener =
|
||||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if (orientation in 45..135) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
when (orientation) {
|
||||
in 45..135 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
} else if (orientation in 225..315) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
in 225..315 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
} else if (orientation in 315..360 || orientation in 0..45) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
in 315..360, in 0..45 -> {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -943,7 +997,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
episodeArr = episodes.keys.toList()
|
||||
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
|
||||
|
||||
episodeTitleArr = arrayListOf<String>()
|
||||
episodeTitleArr = arrayListOf()
|
||||
episodes.forEach {
|
||||
val episode = it.value
|
||||
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
|
||||
|
@ -1054,7 +1108,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||
RPC.Link(
|
||||
"Stream on Dantotsu",
|
||||
"https://github.com/rebelonion/Dantotsu/"
|
||||
getString(R.string.github)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -1266,6 +1320,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
media.anime!!.selectedEpisode!!
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
|
||||
if (list.contains(media.id)) list.remove(media.id)
|
||||
list.add(media.id)
|
||||
|
@ -1303,9 +1358,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
if (subtitle != null) {
|
||||
//var localFile: String? = null
|
||||
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
||||
val context = this
|
||||
runBlocking {
|
||||
val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
|
||||
val type = SubtitleDownloader.loadSubtitleType(subtitle!!.file.url)
|
||||
val fileUri = Uri.parse(subtitle!!.file.url)
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
.Builder(fileUri)
|
||||
|
@ -1360,8 +1414,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
}
|
||||
dataSource
|
||||
}
|
||||
val dafuckDataSourceFactory =
|
||||
DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
|
||||
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
|
||||
cacheFactory = CacheDataSource.Factory().apply {
|
||||
setCache(Helper.getSimpleCache(this@ExoplayerView))
|
||||
if (ext.server.offline) {
|
||||
|
@ -1737,28 +1790,26 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
timer = null
|
||||
return
|
||||
}
|
||||
if (timer == null) {
|
||||
timer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
if (new == null){
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
timer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
if (new == null) {
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
disappeared = true
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
}
|
||||
}
|
||||
timer?.start()
|
||||
|
||||
override fun onFinish() {
|
||||
skipTimeButton.visibility = View.GONE
|
||||
exoSkip.visibility = View.VISIBLE
|
||||
disappeared = true
|
||||
functionstarted = false
|
||||
cancelTimer()
|
||||
}
|
||||
}
|
||||
timer?.start()
|
||||
|
||||
}
|
||||
if (PrefManager.getVal(PrefName.ShowTimeStampButton)) {
|
||||
|
|
|
@ -29,6 +29,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
@ -258,6 +259,7 @@ class CommentItem(val comment: Comment,
|
|||
|
||||
private fun removeSubCommentIds(){
|
||||
subCommentIds.forEach { id ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
||||
val commentToRemove = parentComments.find { it.comment.commentId == id }
|
||||
commentToRemove?.let {
|
||||
|
@ -290,7 +292,7 @@ class CommentItem(val comment: Comment,
|
|||
@SuppressLint("SimpleDateFormat")
|
||||
private fun formatTimestamp(timestamp: String): String {
|
||||
return try {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsedDate = dateFormat.parse(timestamp)
|
||||
val currentDate = Date()
|
||||
|
@ -315,7 +317,7 @@ class CommentItem(val comment: Comment,
|
|||
companion object {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun timestampToMillis(timestamp: String): Long {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsedDate = dateFormat.parse(timestamp)
|
||||
return parsedDate?.time ?: 0
|
||||
|
|
|
@ -2,7 +2,6 @@ package ani.dantotsu.media.manga
|
|||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
|
@ -10,8 +9,8 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.LruCache
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -25,8 +24,7 @@ data class ImageData(
|
|||
) {
|
||||
suspend fun fetchAndProcessImage(
|
||||
page: Page,
|
||||
httpSource: HttpSource,
|
||||
context: Context
|
||||
httpSource: HttpSource
|
||||
): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
|
@ -5,8 +5,8 @@ import java.util.regex.Pattern
|
|||
|
||||
class MangaNameAdapter {
|
||||
companion object {
|
||||
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
|
||||
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
private const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||
private const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
|
|
@ -30,21 +30,25 @@ import androidx.recyclerview.widget.ConcatAdapter
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.setNavigationTheme
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
@ -492,7 +496,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.deleteDownload(i)
|
||||
|
@ -510,7 +514,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
chapterAdapter.purgeDownload(i)
|
||||
|
|
|
@ -13,10 +13,14 @@ import androidx.core.view.GestureDetectorCompat
|
|||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
|
@ -118,13 +122,13 @@ abstract class BaseImageAdapter(
|
|||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||
|
||||
companion object {
|
||||
suspend fun Context.loadBitmap_old(
|
||||
suspend fun Context.loadBitmapOld(
|
||||
link: FileUrl,
|
||||
transforms: List<BitmapTransformation>
|
||||
): Bitmap? { //still used in some places
|
||||
return tryWithSuspend {
|
||||
withContext(Dispatchers.IO) {
|
||||
Glide.with(this@loadBitmap_old)
|
||||
Glide.with(this@loadBitmapOld)
|
||||
.asBitmap()
|
||||
.let {
|
||||
if (link.url.startsWith("file://")) {
|
||||
|
@ -168,8 +172,7 @@ abstract class BaseImageAdapter(
|
|||
mangaCache.get(link.url)?.let { imageData ->
|
||||
val bitmap = imageData.fetchAndProcessImage(
|
||||
imageData.page,
|
||||
imageData.source,
|
||||
context = this@loadBitmap
|
||||
imageData.source
|
||||
)
|
||||
it.load(bitmap)
|
||||
.skipMemoryCache(true)
|
||||
|
|
|
@ -10,8 +10,19 @@ import android.content.res.Resources
|
|||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.KeyEvent.*
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.KeyEvent
|
||||
import android.view.KeyEvent.ACTION_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_DPAD_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_DPAD_UP
|
||||
import android.view.KeyEvent.KEYCODE_PAGE_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_PAGE_UP
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AdapterView
|
||||
import android.widget.CheckBox
|
||||
|
@ -27,7 +38,9 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.NoPaddingArrayAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
|
@ -35,7 +48,12 @@ import ani.dantotsu.connections.discord.DiscordService
|
|||
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||
import ani.dantotsu.connections.discord.RPC
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
||||
import ani.dantotsu.dp
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaSingleton
|
||||
|
@ -46,14 +64,25 @@ import ani.dantotsu.others.ImageViewDialog
|
|||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaImage
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.CurrentReaderSettings
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Companion.applyWebtoon
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.*
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.BOTTOM_TO_TOP
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.LEFT_TO_RIGHT
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.RIGHT_TO_LEFT
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.TOP_TO_BOTTOM
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.Automatic
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.Force
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.No
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.CONTINUOUS_PAGED
|
||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.PAGED
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.showSystemBarsRetractView
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.tryWith
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
|
@ -66,7 +95,8 @@ import java.io.FileInputStream
|
|||
import java.io.FileOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.math.min
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
@ -88,7 +118,6 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
|
||||
private var isContVisible = false
|
||||
private var showProgressDialog = true
|
||||
private var hidescrollbar = false
|
||||
|
||||
private var maxChapterPage = 0L
|
||||
private var currentChapterPage = 0L
|
||||
|
@ -123,7 +152,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun hideSystemBars() {
|
||||
if (PrefManager.getVal<Boolean>(PrefName.ShowSystemBars))
|
||||
if (PrefManager.getVal(PrefName.ShowSystemBars))
|
||||
showSystemBarsRetractView()
|
||||
else
|
||||
hideSystemBarsExtendView()
|
||||
|
@ -368,7 +397,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||
RPC.Link(
|
||||
"Stream on Dantotsu",
|
||||
"https://github.com/rebelonion/Dantotsu/"
|
||||
getString(R.string.github)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -740,12 +769,12 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
goneTimer.schedule(timerTask, controllerDuration)
|
||||
}
|
||||
|
||||
enum class pressPos {
|
||||
enum class PressPos {
|
||||
LEFT, RIGHT, CENTER
|
||||
}
|
||||
|
||||
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
|
||||
var pressLocation = pressPos.CENTER
|
||||
var pressLocation = PressPos.CENTER
|
||||
if (!sliding) {
|
||||
if (event != null && defaultSettings.layout == PAGED) {
|
||||
if (event.action != MotionEvent.ACTION_UP) return
|
||||
|
@ -755,23 +784,23 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||
if (screenWidth / 5 in x + 1..<y) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
pressPos.RIGHT
|
||||
PressPos.RIGHT
|
||||
} else {
|
||||
pressPos.LEFT
|
||||
PressPos.LEFT
|
||||
}
|
||||
}
|
||||
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
||||
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
pressPos.LEFT
|
||||
PressPos.LEFT
|
||||
} else {
|
||||
pressPos.RIGHT
|
||||
PressPos.RIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if pressLocation is left or right go to previous or next page (paged mode only)
|
||||
if (pressLocation == pressPos.LEFT) {
|
||||
if (pressLocation == PressPos.LEFT) {
|
||||
|
||||
if (binding.mangaReaderPager.currentItem > 0) {
|
||||
//if the current images zoomed in, go back to normal before going to previous page
|
||||
|
@ -782,7 +811,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
} else if (pressLocation == pressPos.RIGHT) {
|
||||
} else if (pressLocation == PressPos.RIGHT) {
|
||||
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
|
||||
//if the current images zoomed in, go back to normal before going to next page
|
||||
if (imageAdapter?.isZoomed() == true) {
|
||||
|
@ -960,7 +989,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
if (!incognito && PrefManager.getCustomVal(
|
||||
"${media.id}_save_progress",
|
||||
true
|
||||
) && if (media.isAdult) PrefManager.getVal<Boolean>(PrefName.UpdateForHReader) else true
|
||||
) && if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHReader) else true
|
||||
)
|
||||
updateProgress(
|
||||
media,
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.os.Environment
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -28,6 +27,7 @@ import ani.dantotsu.download.novel.NovelDownloaderService
|
|||
import ani.dantotsu.download.novel.NovelServiceDataSingleton
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
|
@ -90,7 +90,7 @@ class NovelReadFragment : Fragment(),
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
@ -122,7 +122,7 @@ class NovelReadFragment : Fragment(),
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ class NovelReadFragment : Fragment(),
|
|||
DownloadedType(
|
||||
media.mainName(),
|
||||
novel.name,
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.util.TypedValue
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
|
@ -17,7 +16,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
|
@ -71,10 +70,7 @@ class ListActivity : AppCompatActivity() {
|
|||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
hideSystemBarsExtendView()
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class AndroidBug5497Workaround private constructor(activity: Activity, private v
|
|||
private val frameLayoutParams: FrameLayout.LayoutParams
|
||||
|
||||
init {
|
||||
val content = activity.findViewById(android.R.id.content) as FrameLayout
|
||||
val content: FrameLayout = activity.findViewById(android.R.id.content)
|
||||
mChildOfContent = content.getChildAt(0)
|
||||
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener { possiblyResizeChildOfContent() }
|
||||
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
|
||||
|
|
|
@ -14,7 +14,7 @@ import ani.dantotsu.R
|
|||
import ani.dantotsu.databinding.BottomSheetImageBinding
|
||||
import ani.dantotsu.downloadsPermission
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap_old
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
|
||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.saveImageToDownloads
|
||||
|
@ -84,9 +84,9 @@ class ImageViewDialog : BottomSheetDialogFragment() {
|
|||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val binding = _binding ?: return@launch
|
||||
|
||||
var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
|
||||
var bitmap = context.loadBitmapOld(image, trans1 ?: listOf())
|
||||
var bitmap2 =
|
||||
if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
|
||||
if (image2 != null) context.loadBitmapOld(image2, trans2 ?: listOf()) else null
|
||||
if (bitmap == null) {
|
||||
bitmap = context.loadBitmap(image, trans1 ?: listOf())
|
||||
bitmap2 =
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import ani.dantotsu.R
|
||||
|
||||
|
@ -54,14 +56,14 @@ class OutlineTextView : AppCompatTextView {
|
|||
setStrokeWidth(strokeWidth)
|
||||
}
|
||||
|
||||
private val Float.toPx get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
|
||||
)
|
||||
|
||||
private fun setStrokeWidth(width: Float) {
|
||||
strokeWidth = width.toPx(context)
|
||||
strokeWidth = width.toPx
|
||||
}
|
||||
|
||||
private fun Float.toPx(context: Context) =
|
||||
(this * context.resources.displayMetrics.scaledDensity + 0.5F)
|
||||
|
||||
override fun invalidate() {
|
||||
if (isDrawing) return
|
||||
super.invalidate()
|
||||
|
|
|
@ -12,19 +12,19 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class CookieCatcher : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
//get url from intent
|
||||
val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
val headers: Map<String, String> = intent.getSerializableExtra("headers") as? Map<String, String> ?: emptyMap()
|
||||
val url = intent.getStringExtra("url") ?: getString(R.string.cursed_yt)
|
||||
val headers: Map<String, String> = intent.getSerializableExtraCompat("headers") as? Map<String, String> ?: emptyMap()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = Application.getProcessName()
|
||||
|
|
|
@ -11,11 +11,11 @@ import android.os.Environment
|
|||
import android.provider.MediaStore
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
|
@ -59,8 +59,6 @@ class AniyomiAdapter {
|
|||
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
|
||||
return DynamicAnimeParser(extension)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
|
@ -192,7 +190,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
||||
val seasonGroups =
|
||||
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
||||
seasonGroups.keys.sortedBy { it.toInt() }
|
||||
seasonGroups.keys.sortedBy { it }
|
||||
.flatMap { season ->
|
||||
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
||||
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
||||
|
@ -209,7 +207,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
return sortedEpisodes.map { SEpisodeToEpisode(it) }
|
||||
return sortedEpisodes.map { sEpisodeToEpisode(it) }
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Exception: $e")
|
||||
}
|
||||
|
@ -244,7 +242,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
|
||||
return try {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
videos.map { VideoToVideoServer(it) }
|
||||
videos.map { videoToVideoServer(it) }
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Exception occurred: ${e.message}")
|
||||
emptyList()
|
||||
|
@ -296,7 +294,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
|
||||
private fun sEpisodeToEpisode(sEpisode: SEpisode): Episode {
|
||||
//if the float episode number is a whole number, convert it to an int
|
||||
val episodeNumberInt =
|
||||
if (sEpisode.episode_number % 1 == 0f) {
|
||||
|
@ -324,7 +322,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun VideoToVideoServer(video: Video): VideoServer {
|
||||
private fun videoToVideoServer(video: Video): VideoServer {
|
||||
return VideoServer(
|
||||
video.quality,
|
||||
video.url,
|
||||
|
@ -363,7 +361,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
return try {
|
||||
val res = source.getChapterList(sManga)
|
||||
val reversedRes = res.reversed()
|
||||
val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
|
||||
val chapterList = reversedRes.map { sChapterToMangaChapter(it) }
|
||||
Logger.log("chapterList size: ${chapterList.size}")
|
||||
Logger.log("chapterList: ${chapterList[1].title}")
|
||||
Logger.log("chapterList: ${chapterList[1].description}")
|
||||
|
@ -382,7 +380,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
sourceLanguage = 0
|
||||
extension.sources[sourceLanguage]
|
||||
} as? HttpSource ?: return emptyList()
|
||||
var imageDataList: List<ImageData> = listOf()
|
||||
val imageDataList: MutableList<ImageData> = mutableListOf()
|
||||
val ret = coroutineScope {
|
||||
try {
|
||||
Logger.log("source.name " + source.name)
|
||||
|
@ -632,7 +630,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|||
}
|
||||
|
||||
|
||||
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
|
||||
private fun sChapterToMangaChapter(sChapter: SChapter): MangaChapter {
|
||||
return MangaChapter(
|
||||
sChapter.name,
|
||||
sChapter.url,
|
||||
|
@ -676,8 +674,8 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
get() = videoServer
|
||||
|
||||
override suspend fun extract(): VideoContainer {
|
||||
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
|
||||
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
|
||||
val vidList = listOfNotNull(videoServer.video?.let { aniVideoToSaiVideo(it) })
|
||||
val subList = videoServer.video?.subtitleTracks?.map { trackToSubtitle(it) } ?: emptyList()
|
||||
|
||||
return if (vidList.isNotEmpty()) {
|
||||
VideoContainer(vidList, subList)
|
||||
|
@ -686,7 +684,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun AniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
|
||||
private fun aniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
|
||||
// Find the number value from the .quality string
|
||||
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
||||
|
||||
|
@ -789,9 +787,9 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|||
|
||||
}
|
||||
|
||||
private fun TrackToSubtitle(track: Track): Subtitle {
|
||||
private fun trackToSubtitle(track: Track): Subtitle {
|
||||
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
|
||||
var type: SubtitleType? = null
|
||||
var type: SubtitleType?
|
||||
runBlocking {
|
||||
type = findSubtitleType(track.url)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
@ -132,16 +133,16 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
|
|||
currContext()?.let {
|
||||
DownloadsManager.getDirectory(
|
||||
it,
|
||||
ani.dantotsu.download.DownloadedType.Type.ANIME,
|
||||
MediaType.ANIME,
|
||||
title,
|
||||
episode
|
||||
).listFiles()?.forEach {
|
||||
if (it.name.contains("subtitle")) {
|
||||
).listFiles()?.forEach { file ->
|
||||
if (file.name.contains("subtitle")) {
|
||||
return listOf(
|
||||
Subtitle(
|
||||
"Downloaded Subtitle",
|
||||
Uri.fromFile(it).toString(),
|
||||
determineSubtitletype(it.absolutePath)
|
||||
Uri.fromFile(file).toString(),
|
||||
determineSubtitletype(file.absolutePath)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ani.dantotsu.parsers.novel
|
||||
|
||||
import android.os.FileObserver
|
||||
import android.util.Log
|
||||
import ani.dantotsu.parsers.novel.FileObserver.fileObserver
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.io.File
|
|
@ -10,7 +10,6 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
|
@ -63,7 +62,7 @@ internal class NovelExtensionInstaller(private val context: Context) {
|
|||
* @param url The url of the apk.
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer {
|
||||
fun downloadAndInstall(url: String, extension: NovelExtension): Observable<InstallStep> = Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
|
|
|
@ -5,11 +5,10 @@ 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 android.util.Log
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.util.Logger
|
||||
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
|
||||
|
@ -134,10 +133,10 @@ internal object NovelExtensionLoader {
|
|||
}
|
||||
Logger.log("isFileWritable: ${file.canWrite()}")
|
||||
val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader)
|
||||
val className =
|
||||
val extensionClassName =
|
||||
"some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
|
||||
val loadedClass = classLoader.loadClass(className)
|
||||
val instance = loadedClass.newInstance()
|
||||
val loadedClass = classLoader.loadClass(extensionClassName)
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
val novelInterfaceInstance = instance as? NovelInterface
|
||||
listOfNotNull(novelInterfaceInstance)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -34,6 +34,7 @@ import ani.dantotsu.setSlideIn
|
|||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.util.AniMarkdown.Companion.getFullAniHTML
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -58,7 +59,7 @@ class ProfileFragment : Fragment() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
activity = requireActivity() as ProfileActivity
|
||||
|
||||
user = arguments?.getSerializable("user") as Query.UserProfile
|
||||
user = arguments?.getSerializableCompat<Query.UserProfile>("user") as Query.UserProfile
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.setData(user.id)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import ani.dantotsu.profile.ChartBuilder.Companion.StatType
|
|||
import ani.dantotsu.statusBarHeight
|
||||
import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -48,7 +49,7 @@ class StatsFragment :
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
activity = requireActivity() as ProfileActivity
|
||||
user = arguments?.getSerializable("user") as Query.UserProfile
|
||||
user = arguments?.getSerializableCompat<Query.UserProfile>("user") as Query.UserProfile
|
||||
|
||||
binding.statisticList.adapter = adapter
|
||||
binding.statisticList.recycledViewPool.setMaxRecycledViews(0, 0)
|
||||
|
@ -95,7 +96,7 @@ class StatsFragment :
|
|||
}
|
||||
} else {
|
||||
stats.removeAll(
|
||||
stats.filter { it?.id == Anilist.userid }
|
||||
stats.filter { it?.id == Anilist.userid }.toSet()
|
||||
)
|
||||
loadStats(type == MediaType.ANIME)
|
||||
}
|
||||
|
@ -445,6 +446,7 @@ class StatsFragment :
|
|||
}.toMutableList()
|
||||
chartPackets.clear()
|
||||
chartPackets.addAll(standardizedPackets)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val genreChart = ChartBuilder.buildChart(
|
||||
activity,
|
||||
ChartType.TwoDimensional,
|
||||
|
@ -499,6 +501,7 @@ class StatsFragment :
|
|||
}.toMutableList()
|
||||
chartPackets.clear()
|
||||
chartPackets.addAll(standardizedPackets)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val tagChart = ChartBuilder.buildChart(
|
||||
activity,
|
||||
ChartType.TwoDimensional,
|
||||
|
@ -553,6 +556,7 @@ class StatsFragment :
|
|||
}.toMutableList()
|
||||
chartPackets.clear()
|
||||
chartPackets.addAll(standardizedPackets)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val countryChart = ChartBuilder.buildChart(
|
||||
activity,
|
||||
ChartType.OneDimensional,
|
||||
|
@ -609,6 +613,7 @@ class StatsFragment :
|
|||
}.toMutableList()
|
||||
chartPackets.clear()
|
||||
chartPackets.addAll(standardizedPackets)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val voiceActorsChart = ChartBuilder.buildChart(
|
||||
activity,
|
||||
ChartType.TwoDimensional,
|
||||
|
@ -663,6 +668,7 @@ class StatsFragment :
|
|||
}.toMutableList()
|
||||
chartPackets.clear()
|
||||
chartPackets.addAll(standardizedPackets)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val studioChart = ChartBuilder.buildChart(
|
||||
activity,
|
||||
ChartType.TwoDimensional,
|
||||
|
@ -720,6 +726,7 @@ class StatsFragment :
|
|||
}.toMutableList()
|
||||
chartPackets.clear()
|
||||
chartPackets.addAll(standardizedPackets)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val staffChart = ChartBuilder.buildChart(
|
||||
activity,
|
||||
ChartType.TwoDimensional,
|
||||
|
|
|
@ -61,7 +61,7 @@ class FeedActivity : AppCompatActivity() {
|
|||
}
|
||||
})
|
||||
binding.listBack.setOnClickListener {
|
||||
onBackPressed()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ class NotificationActivity : AppCompatActivity() {
|
|||
binding.followerGrid.visibility = ViewGroup.GONE
|
||||
binding.followerList.visibility = ViewGroup.GONE
|
||||
binding.listBack.setOnClickListener {
|
||||
onBackPressed()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
binding.listProgressBar.visibility = ViewGroup.VISIBLE
|
||||
val activityId = intent.getIntExtra("activityId", -1)
|
||||
|
|
|
@ -46,7 +46,6 @@ import ani.dantotsu.databinding.ActivitySettingsAboutBinding
|
|||
import ani.dantotsu.databinding.ActivitySettingsAccountsBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsAnimeBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.databinding.ActivitySettingsCommonBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsExtensionsBinding
|
||||
import ani.dantotsu.databinding.ActivitySettingsMangaBinding
|
||||
|
@ -57,7 +56,7 @@ import ani.dantotsu.download.video.ExoplayerDownloadService
|
|||
import ani.dantotsu.downloadsPermission
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.TaskScheduler
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||
|
@ -81,6 +80,7 @@ import ani.dantotsu.startMainActivity
|
|||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import eltos.simpledialogfragment.SimpleDialog
|
||||
|
@ -151,13 +151,13 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
toast(getString(R.string.incorrect_password))
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
toast(getString(R.string.password_cannot_be_empty))
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
|
@ -165,11 +165,11 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Unknown file type")
|
||||
toast(getString(R.string.unknown_file_type))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("Error importing settings")
|
||||
toast(getString(R.string.error_importing_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
}
|
||||
|
||||
val tag = "colorPicker"
|
||||
CustomColorDialog().title("Custom Theme")
|
||||
CustomColorDialog().title(R.string.custom_theme)
|
||||
.colorPreset(originalColor)
|
||||
.colors(this, SimpleColorDialog.MATERIAL_COLOR_PALLET)
|
||||
.allowCustom(true)
|
||||
|
@ -271,7 +271,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
|
||||
val managers = arrayOf("Default", "1DM", "ADM")
|
||||
val downloadManagerDialog =
|
||||
AlertDialog.Builder(this, R.style.MyPopup).setTitle("Download Manager")
|
||||
AlertDialog.Builder(this, R.style.MyPopup).setTitle(R.string.download_manager)
|
||||
var downloadManager: Int = PrefManager.getVal(PrefName.DownloadManager)
|
||||
bindingCommon.settingsDownloadManager.setOnClickListener {
|
||||
val dialog = downloadManagerDialog.setSingleChoiceItems(
|
||||
|
@ -291,7 +291,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
val filteredLocations = Location.entries.filter { it.exportable }
|
||||
selectedArray.addAll(List(filteredLocations.size - 1) { false })
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Import/Export Settings")
|
||||
.setTitle(R.string.import_export_settings)
|
||||
.setMultiChoiceItems(
|
||||
filteredLocations.map { it.name }.toTypedArray(),
|
||||
selectedArray.toBooleanArray()
|
||||
|
@ -346,7 +346,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
.setMessage(getString(R.string.purge_confirm, getString(R.string.anime)))
|
||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(DownloadedType.Type.ANIME)
|
||||
downloadsManager.purgeDownloads(MediaType.ANIME)
|
||||
DownloadService.sendRemoveAllDownloads(
|
||||
this,
|
||||
ExoplayerDownloadService::class.java,
|
||||
|
@ -354,7 +354,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
|
@ -368,10 +368,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
.setMessage(getString(R.string.purge_confirm, getString(R.string.manga)))
|
||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(DownloadedType.Type.MANGA)
|
||||
downloadsManager.purgeDownloads(MediaType.MANGA)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
|
@ -385,10 +385,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
.setMessage(getString(R.string.purge_confirm, getString(R.string.novels)))
|
||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
downloadsManager.purgeDownloads(DownloadedType.Type.NOVEL)
|
||||
downloadsManager.purgeDownloads(MediaType.NOVEL)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
|
@ -423,16 +423,16 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("User Agent")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString())
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton("Reset") { dialog, _ ->
|
||||
.setNeutralButton(getString(R.string.reset)) { dialog, _ ->
|
||||
PrefManager.removeVal(PrefName.DefaultUserAgent)
|
||||
editText.setText("")
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
|
@ -613,7 +613,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
lifecycleScope.launch {
|
||||
it.pop()
|
||||
}
|
||||
openLinkInBrowser("https://www.buymeacoffee.com/rebelonion")
|
||||
openLinkInBrowser(getString(R.string.coffee))
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
binding.settingBuyMeCoffee.pop()
|
||||
|
@ -905,7 +905,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
if (MAL.token != null) {
|
||||
bindingAccounts.settingsMALLogin.setText(R.string.logout)
|
||||
bindingAccounts.settingsMALLogin.setOnClickListener {
|
||||
MAL.removeSavedToken(it.context)
|
||||
MAL.removeSavedToken()
|
||||
restartMainActivity.isEnabled = true
|
||||
reload()
|
||||
}
|
||||
|
@ -1060,7 +1060,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
box?.setSingleLine()
|
||||
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setTitle(getString(R.string.enter_password))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
|
@ -1076,7 +1076,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
dialog.dismiss()
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
toast(getString(R.string.password_cannot_be_empty))
|
||||
}
|
||||
}
|
||||
box?.setOnEditorActionListener { _, actionId, _ ->
|
||||
|
@ -1090,7 +1090,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
|||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
if (!isExporting)
|
||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
||||
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
|
|
|
@ -12,7 +12,6 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.view.isVisible
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.MainActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.BottomSheetSettingsBinding
|
||||
|
@ -25,13 +24,15 @@ import ani.dantotsu.home.MangaFragment
|
|||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.incognitoNotification
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.profile.activity.NotificationActivity
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.FeedActivity
|
||||
import ani.dantotsu.profile.activity.NotificationActivity
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
|
@ -42,7 +43,7 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
|
|||
private lateinit var pageType: PageType
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
pageType = arguments?.getSerializable("pageType") as? PageType ?: PageType.HOME
|
||||
pageType = arguments?.getSerializableCompat("pageType") as? PageType ?: PageType.HOME
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
|
|
@ -41,13 +41,14 @@ import kotlinx.coroutines.withContext
|
|||
class NovelExtensionsViewModelFactory(
|
||||
private val novelExtensionManager: NovelExtensionManager
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return NovelExtensionsViewModel(novelExtensionManager) as T
|
||||
}
|
||||
}
|
||||
|
||||
class NovelExtensionsViewModel(
|
||||
private val novelExtensionManager: NovelExtensionManager
|
||||
novelExtensionManager: NovelExtensionManager
|
||||
) : ViewModel() {
|
||||
private val searchQuery = MutableStateFlow("")
|
||||
private var currentPagingSource: NovelExtensionPagingSource? = null
|
||||
|
@ -102,21 +103,20 @@ class NovelExtensionPagingSource(
|
|||
} else {
|
||||
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||
}
|
||||
val filternfsw = filteredExtensions
|
||||
/*val filternfsw = if(isNsfwEnabled) { currently not implemented
|
||||
filteredExtensions
|
||||
} else {
|
||||
filteredExtensions.filterNot { it.isNsfw }
|
||||
}*/
|
||||
return try {
|
||||
val sublist = filternfsw.subList(
|
||||
val sublist = filteredExtensions.subList(
|
||||
fromIndex = position,
|
||||
toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
|
||||
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size)
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = sublist,
|
||||
prevKey = if (position == 0) null else position - params.loadSize,
|
||||
nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize
|
||||
nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
|
|
|
@ -6,7 +6,7 @@ import ani.dantotsu.util.ColorEditor.Companion.toHexColor
|
|||
class AniMarkdown { //istg anilist has the worst api
|
||||
companion object {
|
||||
private fun convertNestedImageToHtml(markdown: String): String {
|
||||
val regex = """\[\!\[(.*?)\]\((.*?)\)\]\((.*?)\)""".toRegex()
|
||||
val regex = """\[!\[(.*?)]\((.*?)\)]\((.*?)\)""".toRegex()
|
||||
return regex.replace(markdown) { matchResult ->
|
||||
val altText = matchResult.groupValues[1]
|
||||
val imageUrl = matchResult.groupValues[2]
|
||||
|
@ -16,7 +16,7 @@ class AniMarkdown { //istg anilist has the worst api
|
|||
}
|
||||
|
||||
private fun convertImageToHtml(markdown: String): String {
|
||||
val regex = """\!\[(.*?)\]\((.*?)\)""".toRegex()
|
||||
val regex = """!\[(.*?)]\((.*?)\)""".toRegex()
|
||||
return regex.replace(markdown) { matchResult ->
|
||||
val altText = matchResult.groupValues[1]
|
||||
val imageUrl = matchResult.groupValues[2]
|
||||
|
@ -25,7 +25,7 @@ class AniMarkdown { //istg anilist has the worst api
|
|||
}
|
||||
|
||||
private fun convertLinkToHtml(markdown: String): String {
|
||||
val regex = """\[(.*?)\]\((.*?)\)""".toRegex()
|
||||
val regex = """\[(.*?)]\((.*?)\)""".toRegex()
|
||||
return regex.replace(markdown) { matchResult ->
|
||||
val linkText = matchResult.groupValues[1]
|
||||
val linkUrl = matchResult.groupValues[2]
|
||||
|
@ -50,7 +50,7 @@ class AniMarkdown { //istg anilist has the worst api
|
|||
private fun underlineToHtml(html: String): String {
|
||||
return html.replace("(?s)___(.*?)___".toRegex(), "<br><em><strong>$1</strong></em><br>")
|
||||
.replace("(?s)__(.*?)__".toRegex(), "<br><strong>$1</strong><br>")
|
||||
.replace("(?s)[\\s]+_([^_]+)_[\\s]+".toRegex(), "<em>$1</em>")
|
||||
.replace("(?s)\\s+_([^_]+)_\\s+".toRegex(), "<em>$1</em>")
|
||||
}
|
||||
|
||||
fun getBasicAniHTML(html: String): String {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ani.dantotsu.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.widget.RemoteViews
|
||||
|
@ -12,7 +11,7 @@ import java.io.InputStream
|
|||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class CurrentlyAiringRemoteViewsFactory(private val context: Context, intent: Intent) :
|
||||
class CurrentlyAiringRemoteViewsFactory(private val context: Context) :
|
||||
RemoteViewsService.RemoteViewsFactory {
|
||||
private var widgetItems = mutableListOf<WidgetItem>()
|
||||
|
||||
|
|
|
@ -7,6 +7,6 @@ import ani.dantotsu.util.Logger
|
|||
class CurrentlyAiringRemoteViewsService : RemoteViewsService() {
|
||||
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
|
||||
Logger.log("CurrentlyAiringRemoteViewsFactory onGetViewFactory")
|
||||
return CurrentlyAiringRemoteViewsFactory(applicationContext, intent)
|
||||
return CurrentlyAiringRemoteViewsFactory(applicationContext)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ class ExtensionInstallerPreference(
|
|||
|
||||
|
||||
val entries
|
||||
get() = ExtensionInstaller.values().run {
|
||||
get() = ExtensionInstaller.entries.toTypedArray().run {
|
||||
if (context.hasMiuiPackageInstaller) {
|
||||
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
||||
} else {
|
||||
|
|
|
@ -58,8 +58,7 @@ interface AnimeSource {
|
|||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val list = fetchVideoList(episode).awaitSingle()
|
||||
return list
|
||||
return fetchVideoList(episode).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
|
|
|
@ -10,10 +10,11 @@ import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
|
|||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
|
||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
@ -51,7 +52,7 @@ class AnimeExtensionManager(
|
|||
/**
|
||||
* The installer which installs, updates and uninstalls the anime extensions.
|
||||
*/
|
||||
private val installer by lazy { AnimeExtensionInstaller(context) }
|
||||
private val installer by lazy { ExtensionInstaller(context) }
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
|
||||
|
@ -92,14 +93,14 @@ class AnimeExtensionManager(
|
|||
|
||||
init {
|
||||
initAnimeExtensions()
|
||||
AnimeExtensionInstallReceiver(AnimeInstallationListener()).register(context)
|
||||
ExtensionInstallReceiver().setAnimeListener(InstallationListener()).register(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and registers the installed animeextensions.
|
||||
*/
|
||||
private fun initAnimeExtensions() {
|
||||
val animeextensions = AnimeExtensionLoader.loadExtensions(context)
|
||||
val animeextensions = ExtensionLoader.loadAnimeExtensions(context)
|
||||
|
||||
_installedAnimeExtensionsFlow.value = animeextensions
|
||||
.filterIsInstance<AnimeLoadResult.Success>()
|
||||
|
@ -254,12 +255,13 @@ class AnimeExtensionManager(
|
|||
*
|
||||
* @param signature The signature to whitelist.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun trustSignature(signature: String) {
|
||||
val untrustedSignatures =
|
||||
_untrustedAnimeExtensionsFlow.value.map { it.signatureHash }.toSet()
|
||||
if (signature !in untrustedSignatures) return
|
||||
|
||||
AnimeExtensionLoader.trustedSignatures += signature
|
||||
ExtensionLoader.trustedSignaturesAnime += signature
|
||||
preferences.trustedSignatures() += signature
|
||||
|
||||
val nowTrustedAnimeExtensions =
|
||||
|
@ -271,7 +273,7 @@ class AnimeExtensionManager(
|
|||
nowTrustedAnimeExtensions
|
||||
.map { animeextension ->
|
||||
async {
|
||||
AnimeExtensionLoader.loadExtensionFromPkgName(
|
||||
ExtensionLoader.loadAnimeExtensionFromPkgName(
|
||||
ctx,
|
||||
animeextension.pkgName
|
||||
)
|
||||
|
@ -333,7 +335,7 @@ class AnimeExtensionManager(
|
|||
/**
|
||||
* Listener which receives events of the anime extensions being installed, updated or removed.
|
||||
*/
|
||||
private inner class AnimeInstallationListener : AnimeExtensionInstallReceiver.Listener {
|
||||
private inner class InstallationListener : ExtensionInstallReceiver.AnimeListener {
|
||||
|
||||
override fun onExtensionInstalled(extension: AnimeExtension.Installed) {
|
||||
registerNewExtension(extension.withUpdateCheck())
|
||||
|
|
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
|||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
|
||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
|
||||
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 +87,7 @@ internal class AnimeExtensionGithubApi {
|
|||
findExtensions().also { lastExtCheck.set(Date().time) }
|
||||
}
|
||||
|
||||
val installedExtensions = AnimeExtensionLoader.loadExtensions(context)
|
||||
val installedExtensions = ExtensionLoader.loadAnimeExtensions(context)
|
||||
.filterIsInstance<AnimeLoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
|
@ -115,7 +115,7 @@ internal class AnimeExtensionGithubApi {
|
|||
return this
|
||||
.filter {
|
||||
val libVersion = it.extractLibVersion()
|
||||
libVersion >= AnimeExtensionLoader.LIB_VERSION_MIN && libVersion <= AnimeExtensionLoader.LIB_VERSION_MAX
|
||||
libVersion >= ExtensionLoader.ANIME_LIB_VERSION_MIN && libVersion <= ExtensionLoader.ANIME_LIB_VERSION_MAX
|
||||
}
|
||||
.map {
|
||||
AnimeExtension.Available(
|
||||
|
|
|
@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.extension.anime.model
|
|||
sealed class AnimeLoadResult {
|
||||
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()
|
||||
class Untrusted(val extension: AnimeExtension.Untrusted) : AnimeLoadResult()
|
||||
object Error : AnimeLoadResult()
|
||||
data object Error : AnimeLoadResult()
|
||||
}
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Activity used to install extensions, because we can only receive the result of the installation
|
||||
* with [startActivityForResult], which we need to update the UI.
|
||||
*/
|
||||
class AnimeExtensionInstallActivity : Activity() {
|
||||
|
||||
// MIUI package installer bug workaround
|
||||
private var ignoreUntil = 0L
|
||||
private var ignoreResult = false
|
||||
private var hasIgnoredResult = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
.setDataAndType(intent.data, intent.type)
|
||||
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
if (hasMiuiPackageInstaller) {
|
||||
ignoreResult = true
|
||||
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
|
||||
}
|
||||
|
||||
try {
|
||||
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
|
||||
} catch (error: Exception) {
|
||||
// Either install package can't be found (probably bots) or there's a security exception
|
||||
// with the download manager. Nothing we can workaround.
|
||||
toast(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (ignoreResult && System.nanoTime() < ignoreUntil) {
|
||||
hasIgnoredResult = true
|
||||
return
|
||||
}
|
||||
if (requestCode == INSTALL_REQUEST_CODE) {
|
||||
checkInstallationResult(resultCode)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (hasIgnoredResult) {
|
||||
checkInstallationResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(AnimeExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager = Injekt.get<AnimeExtensionManager>()
|
||||
val newStep = when (resultCode) {
|
||||
RESULT_OK -> InstallStep.Installed
|
||||
RESULT_CANCELED -> InstallStep.Idle
|
||||
else -> InstallStep.Error
|
||||
}
|
||||
extensionManager.updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
}
|
||||
|
||||
private const val INSTALL_REQUEST_CODE = 500
|
|
@ -1,132 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
/**
|
||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
||||
* notifies the given [listener] when the package is an extension.
|
||||
*
|
||||
* @param listener The listener that should be notified of extension installation events.
|
||||
*/
|
||||
internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
||||
BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intent filter this receiver should subscribe to.
|
||||
*/
|
||||
private val filter
|
||||
get() = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
launchNow {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is AnimeLoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
||||
|
||||
is AnimeLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
launchNow {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is AnimeLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||
// Not needed as a package can't be upgraded if the signature is different
|
||||
// is LoadResult.Untrusted -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName != null) {
|
||||
listener.onPackageUninstalled(pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this package is performing an update.
|
||||
*
|
||||
* @param intent The intent that triggered the event.
|
||||
*/
|
||||
private fun isReplacing(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension triggered by the given intent.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param intent The intent containing the package name of the extension.
|
||||
*/
|
||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
Logger.log("Package name not found")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||
AnimeExtensionLoader.loadExtensionFromPkgName(
|
||||
context,
|
||||
pkgName,
|
||||
)
|
||||
}.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of the installed, updated or removed application.
|
||||
*/
|
||||
private fun getPackageNameFromIntent(intent: Intent?): String? {
|
||||
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that receives extension installation events.
|
||||
*/
|
||||
interface Listener {
|
||||
fun onExtensionInstalled(extension: AnimeExtension.Installed)
|
||||
fun onExtensionUpdated(extension: AnimeExtension.Installed)
|
||||
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.util
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
|
||||
import eu.kanade.tachiyomi.extension.anime.installer.PackageInstallerInstallerAnime
|
||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
|
||||
class AnimeExtensionInstallService : Service() {
|
||||
|
||||
private var installer: InstallerAnime? = null
|
||||
|
||||
override fun onCreate() {
|
||||
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setContentTitle("Installing Anime Extension...")
|
||||
setProgress(100, 100, true)
|
||||
}.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
Notifications.ID_EXTENSION_INSTALLER,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val uri = intent?.data
|
||||
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
||||
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
|
||||
EXTRA_INSTALLER,
|
||||
)
|
||||
if (uri == null || id == null || installerUsed == null) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (installer == null) {
|
||||
installer = when (installerUsed) {
|
||||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerAnime(
|
||||
this
|
||||
)
|
||||
|
||||
else -> {
|
||||
Logger.log("Not implemented for installer $installerUsed")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
installer!!.addToQueue(id, uri)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
installer?.onDestroy()
|
||||
installer = null
|
||||
}
|
||||
|
||||
override fun onBind(i: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
|
||||
|
||||
fun getIntent(
|
||||
context: Context,
|
||||
downloadId: Long,
|
||||
uri: Uri,
|
||||
installer: BasePreferences.ExtensionInstaller,
|
||||
): Intent {
|
||||
return Intent(context, AnimeExtensionInstallService::class.java)
|
||||
.setDataAndType(uri, AnimeExtensionInstaller.APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.putExtra(EXTRA_INSTALLER, installer)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,273 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
internal class AnimeExtensionInstaller(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The system's download manager
|
||||
*/
|
||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||
|
||||
/**
|
||||
* 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<String, Long>()
|
||||
|
||||
/**
|
||||
* Relay used to notify the installation step of every download.
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||
|
||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
||||
|
||||
/**
|
||||
* 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: AnimeExtension) = Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
|
||||
// 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<InstallStep> {
|
||||
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)
|
||||
else -> Observable.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an intent to install the extension at the given uri.
|
||||
*
|
||||
* @param uri The uri of the extension to install.
|
||||
*/
|
||||
fun installApk(downloadId: Long, uri: Uri) {
|
||||
when (val installer = extensionInstaller.get()) {
|
||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, AnimeExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val intent =
|
||||
AnimeExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels extension install and remove from download manager and installer.
|
||||
*/
|
||||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
InstallerAnime.cancelInstallQueue(context, downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an intent to uninstall the extension by the given package name.
|
||||
*
|
||||
* @param pkgName The package name of the extension to uninstall
|
||||
*/
|
||||
fun uninstallApk(pkgName: String) {
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
installApk(id, File(localUri).getUriCompat(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val EXTRA_DOWNLOAD_ID = "AnimeExtensionInstaller.extra.DOWNLOAD_ID"
|
||||
const val FILE_SCHEME = "file://"
|
||||
}
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
*/
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
internal object AnimeExtensionLoader {
|
||||
|
||||
private val preferences: SourcePreferences by injectLazy()
|
||||
private val loadNsfwSource by lazy {
|
||||
preferences.showNsfwSource().get()
|
||||
}
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.animeextension"
|
||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
|
||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
|
||||
private const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
|
||||
private const val METADATA_HAS_README = "tachiyomi.animeextension.hasReadme"
|
||||
private const val METADATA_HAS_CHANGELOG = "tachiyomi.animeextension.hasChangelog"
|
||||
const val LIB_VERSION_MIN = 12
|
||||
const val LIB_VERSION_MAX = 15
|
||||
|
||||
private const val PACKAGE_FLAGS =
|
||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||
|
||||
// jmir1's key
|
||||
private const val officialSignature =
|
||||
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
|
||||
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
var trustedSignatures =
|
||||
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun loadExtensions(context: Context): List<AnimeLoadResult> {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
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(it) }
|
||||
|
||||
if (extPkgs.isEmpty()) return emptyList()
|
||||
|
||||
// Load each extension concurrently and wait for completion
|
||||
return runBlocking {
|
||||
val deferred = extPkgs.map {
|
||||
async { loadExtension(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.
|
||||
*/
|
||||
fun loadExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
|
||||
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 AnimeLoadResult.Error
|
||||
}
|
||||
if (!isPackageAnExtension(pkgInfo)) {
|
||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
return loadExtension(context, pkgName, pkgInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension given its package name.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param pkgName The package name of the extension to load.
|
||||
* @param pkgInfo The package info of the extension.
|
||||
*/
|
||||
private fun loadExtension(
|
||||
context: Context,
|
||||
pkgName: String,
|
||||
pkgInfo: PackageInfo
|
||||
): AnimeLoadResult {
|
||||
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 AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
||||
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
Logger.log("Lib version is $libVersion, while only versions " +
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||
)
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
val signatureHash = getSignatureHash(pkgInfo)
|
||||
|
||||
if (signatureHash == null) {
|
||||
Logger.log("Package $pkgName isn't signed")
|
||||
return AnimeLoadResult.Error
|
||||
} else if (signatureHash !in trustedSignatures) {
|
||||
val extension = AnimeExtension.Untrusted(
|
||||
extName,
|
||||
pkgName,
|
||||
versionName,
|
||||
versionCode,
|
||||
libVersion,
|
||||
signatureHash
|
||||
)
|
||||
Logger.log("Extension $pkgName isn't trusted")
|
||||
return AnimeLoadResult.Untrusted(extension)
|
||||
}
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
if (!loadNsfwSource && isNsfw) {
|
||||
Logger.log("NSFW extension $pkgName not allowed")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
||||
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
|
||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||
.split(";")
|
||||
.map {
|
||||
val sourceClass = it.trim()
|
||||
if (sourceClass.startsWith(".")) {
|
||||
pkgInfo.packageName + sourceClass
|
||||
} else {
|
||||
sourceClass
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||
is AnimeSource -> listOf(obj)
|
||||
is AnimeSourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Extension load error: $extName ($it)")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
val langs = sources.filterIsInstance<AnimeCatalogueSource>()
|
||||
.map { it.lang }
|
||||
.toSet()
|
||||
val lang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extension = AnimeExtension.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
libVersion = libVersion,
|
||||
lang = lang,
|
||||
isNsfw = isNsfw,
|
||||
hasReadme = hasReadme,
|
||||
hasChangelog = hasChangelog,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
isUnofficial = signatureHash != officialSignature,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
return AnimeLoadResult.Success(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given package is an extension.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature hash of the package or null if it's not signed.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||
val signatures = pkgInfo.signatures
|
||||
return if (signatures != null && signatures.isNotEmpty()) {
|
||||
Hash.sha256(signatures.first().toByteArray())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.installer
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -8,8 +8,11 @@ import android.content.IntentFilter
|
|||
import android.net.Uri
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
@ -17,9 +20,11 @@ import java.util.concurrent.atomic.AtomicReference
|
|||
/**
|
||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||
*/
|
||||
abstract class InstallerAnime(private val service: Service) {
|
||||
abstract class Installer(private val service: Service) {
|
||||
|
||||
private val extensionManager: AnimeExtensionManager by injectLazy()
|
||||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
||||
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
||||
private val novelExtensionManager: NovelExtensionManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry>(null)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
@ -44,8 +49,8 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
fun addToQueue(downloadId: Long, uri: Uri) {
|
||||
queue.add(Entry(downloadId, uri))
|
||||
fun addToQueue(type: MediaType, downloadId: Long, uri: Uri) {
|
||||
queue.add(Entry(type, downloadId, uri))
|
||||
checkQueue()
|
||||
}
|
||||
|
||||
|
@ -58,7 +63,11 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
*/
|
||||
@CallSuper
|
||||
open fun processEntry(entry: Entry) {
|
||||
extensionManager.setInstalling(entry.downloadId)
|
||||
when (entry.type) {
|
||||
MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId)
|
||||
MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId)
|
||||
MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,7 +90,19 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
fun continueQueue(resultStep: InstallStep) {
|
||||
val completedEntry = waitingInstall.getAndSet(null)
|
||||
if (completedEntry != null) {
|
||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
when (completedEntry.type) {
|
||||
MediaType.ANIME -> {
|
||||
animeExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
}
|
||||
|
||||
MediaType.MANGA -> {
|
||||
mangaExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
}
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
novelExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
}
|
||||
}
|
||||
checkQueue()
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +134,19 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
@CallSuper
|
||||
open fun onDestroy() {
|
||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
||||
queue.forEach {
|
||||
when (it.type) {
|
||||
MediaType.ANIME -> {
|
||||
animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
mangaExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||
}
|
||||
MediaType.NOVEL -> {
|
||||
novelExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
queue.clear()
|
||||
waitingInstall.set(null)
|
||||
}
|
||||
|
@ -135,7 +168,17 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
this.waitingInstall.set(null)
|
||||
checkQueue()
|
||||
}
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
when (toCancel.type) {
|
||||
MediaType.ANIME -> {
|
||||
animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
mangaExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
}
|
||||
MediaType.NOVEL -> {
|
||||
novelExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,7 +188,7 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
data class Entry(val downloadId: Long, val uri: Uri)
|
||||
data class Entry(val type: MediaType, val downloadId: Long, val uri: Uri)
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
||||
|
@ -153,8 +196,8 @@ abstract class InstallerAnime(private val service: Service) {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_CANCEL_QUEUE = "InstallerAnime.action.CANCEL_QUEUE"
|
||||
private const val EXTRA_DOWNLOAD_ID = "InstallerAnime.extra.DOWNLOAD_ID"
|
||||
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
|
||||
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
|
||||
|
||||
/**
|
||||
* Attempts to cancel the installation entry for the provided download ID.
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.anime.installer
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
|
@ -9,6 +9,7 @@ import android.content.IntentFilter
|
|||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.IntentSanitizer
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
|
@ -16,7 +17,7 @@ import eu.kanade.tachiyomi.util.lang.use
|
|||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
|
||||
class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(service) {
|
||||
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||
|
||||
private val packageInstaller = service.packageManager.packageInstaller
|
||||
|
||||
|
@ -27,7 +28,18 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
|||
PackageInstaller.STATUS_FAILURE
|
||||
)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)?.run {
|
||||
IntentSanitizer.Builder()
|
||||
.allowAction(this.action!!)
|
||||
.allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second }
|
||||
.allowAnyComponent()
|
||||
.allowPackage {
|
||||
// There is no way to check the actual installer name so allow all.
|
||||
true
|
||||
}
|
||||
.build()
|
||||
.sanitizeByFiltering(this)
|
||||
}
|
||||
if (userAction == null) {
|
||||
Logger.log("Fatal error for $intent")
|
||||
continueQueue(InstallStep.Error)
|
||||
|
@ -78,7 +90,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
|||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
activeSession!!.second,
|
||||
Intent(INSTALL_ACTION),
|
||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
|
@ -88,8 +100,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
|||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}\n$e")
|
||||
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||
activeSession?.let { (_, sessionId) ->
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
|
@ -118,7 +129,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
|||
service,
|
||||
packageActionReceiver,
|
||||
IntentFilter(INSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
ContextCompat.RECEIVER_EXPORTED,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -10,9 +10,9 @@ import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
|
|||
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -51,7 +51,7 @@ class MangaExtensionManager(
|
|||
/**
|
||||
* The installer which installs, updates and uninstalls the extensions.
|
||||
*/
|
||||
private val installer by lazy { MangaExtensionInstaller(context) }
|
||||
private val installer by lazy { ExtensionInstaller(context) }
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
|
||||
|
@ -89,14 +89,14 @@ class MangaExtensionManager(
|
|||
|
||||
init {
|
||||
initExtensions()
|
||||
MangaExtensionInstallReceiver(InstallationListener()).register(context)
|
||||
ExtensionInstallReceiver().setMangaListener(InstallationListener()).register(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and registers the installed extensions.
|
||||
*/
|
||||
private fun initExtensions() {
|
||||
val extensions = MangaExtensionLoader.loadMangaExtensions(context)
|
||||
val extensions = ExtensionLoader.loadMangaExtensions(context)
|
||||
|
||||
_installedExtensionsFlow.value = extensions
|
||||
.filterIsInstance<MangaLoadResult.Success>()
|
||||
|
@ -254,7 +254,7 @@ class MangaExtensionManager(
|
|||
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
||||
if (signature !in untrustedSignatures) return
|
||||
|
||||
MangaExtensionLoader.trustedSignatures += signature
|
||||
ExtensionLoader.trustedSignaturesManga += signature
|
||||
preferences.trustedSignatures() += signature
|
||||
|
||||
val nowTrustedExtensions =
|
||||
|
@ -266,7 +266,7 @@ class MangaExtensionManager(
|
|||
nowTrustedExtensions
|
||||
.map { extension ->
|
||||
async {
|
||||
MangaExtensionLoader.loadMangaExtensionFromPkgName(
|
||||
ExtensionLoader.loadMangaExtensionFromPkgName(
|
||||
ctx,
|
||||
extension.pkgName
|
||||
)
|
||||
|
@ -326,7 +326,7 @@ class MangaExtensionManager(
|
|||
/**
|
||||
* Listener which receives events of the extensions being installed, updated or removed.
|
||||
*/
|
||||
private inner class InstallationListener : MangaExtensionInstallReceiver.Listener {
|
||||
private inner class InstallationListener : ExtensionInstallReceiver.MangaListener {
|
||||
|
||||
override fun onExtensionInstalled(extension: MangaExtension.Installed) {
|
||||
registerNewExtension(extension.withUpdateCheck())
|
||||
|
|
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
|||
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
|
||||
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 +87,7 @@ internal class MangaExtensionGithubApi {
|
|||
findExtensions().also { lastExtCheck.set(Date().time) }
|
||||
}
|
||||
|
||||
val installedExtensions = MangaExtensionLoader.loadMangaExtensions(context)
|
||||
val installedExtensions = ExtensionLoader.loadMangaExtensions(context)
|
||||
.filterIsInstance<MangaLoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
|
@ -114,7 +114,7 @@ internal class MangaExtensionGithubApi {
|
|||
return this
|
||||
.filter {
|
||||
val libVersion = it.extractLibVersion()
|
||||
libVersion >= MangaExtensionLoader.LIB_VERSION_MIN && libVersion <= MangaExtensionLoader.LIB_VERSION_MAX
|
||||
libVersion >= ExtensionLoader.MANGA_LIB_VERSION_MIN && libVersion <= ExtensionLoader.MANGA_LIB_VERSION_MAX
|
||||
}
|
||||
.map {
|
||||
MangaExtension.Available(
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.installer
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||
*/
|
||||
abstract class InstallerManga(private val service: Service) {
|
||||
|
||||
private val extensionManager: MangaExtensionManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry>(null)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
|
||||
cancelQueue(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installer readiness. If false, queue check will not run.
|
||||
*
|
||||
* @see checkQueue
|
||||
*/
|
||||
abstract var ready: Boolean
|
||||
|
||||
/**
|
||||
* Add an item to install queue.
|
||||
*
|
||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
fun addToQueue(downloadId: Long, uri: Uri) {
|
||||
queue.add(Entry(downloadId, uri))
|
||||
checkQueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
|
||||
* when the install process for this entry is finished to continue the queue.
|
||||
*
|
||||
* @param entry The [Entry] of item to process
|
||||
* @see continueQueue
|
||||
*/
|
||||
@CallSuper
|
||||
open fun processEntry(entry: Entry) {
|
||||
extensionManager.setInstalling(entry.downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before queue continues. Override this to handle when the removed entry is
|
||||
* currently being processed.
|
||||
*
|
||||
* @return true if this entry can be removed from queue.
|
||||
*/
|
||||
open fun cancelEntry(entry: Entry): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the queue to continue processing the next entry and updates the install step
|
||||
* of the completed entry ([waitingInstall]) to [MangaExtensionManager].
|
||||
*
|
||||
* @param resultStep new install step for the processed entry.
|
||||
* @see waitingInstall
|
||||
*/
|
||||
fun continueQueue(resultStep: InstallStep) {
|
||||
val completedEntry = waitingInstall.getAndSet(null)
|
||||
if (completedEntry != null) {
|
||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
checkQueue()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the queue. The provided service will be stopped if the queue is empty.
|
||||
* Will not be run when not ready.
|
||||
*
|
||||
* @see ready
|
||||
*/
|
||||
fun checkQueue() {
|
||||
if (!ready) {
|
||||
return
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
service.stopSelf()
|
||||
return
|
||||
}
|
||||
val nextEntry = queue.first()
|
||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||
queue.removeFirst()
|
||||
processEntry(nextEntry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method when the provided service is destroyed.
|
||||
*/
|
||||
@CallSuper
|
||||
open fun onDestroy() {
|
||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
||||
queue.clear()
|
||||
waitingInstall.set(null)
|
||||
}
|
||||
|
||||
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
||||
|
||||
/**
|
||||
* Cancels queue for the provided download ID if exists.
|
||||
*
|
||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
||||
*/
|
||||
private fun cancelQueue(downloadId: Long) {
|
||||
val waitingInstall = this.waitingInstall.get()
|
||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||
if (cancelEntry(toCancel)) {
|
||||
queue.remove(toCancel)
|
||||
if (waitingInstall == toCancel) {
|
||||
// Currently processing removed entry, continue queue
|
||||
this.waitingInstall.set(null)
|
||||
checkQueue()
|
||||
}
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install item to queue.
|
||||
*
|
||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
data class Entry(val downloadId: Long, val uri: Uri)
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
||||
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
|
||||
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
|
||||
|
||||
/**
|
||||
* Attempts to cancel the installation entry for the provided download ID.
|
||||
*
|
||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
||||
*/
|
||||
fun cancelInstallQueue(context: Context, downloadId: Long) {
|
||||
val intent = Intent(ACTION_CANCEL_QUEUE)
|
||||
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.installer
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
|
||||
class PackageInstallerInstallerManga(private val service: Service) : InstallerManga(service) {
|
||||
|
||||
private val packageInstaller = service.packageManager.packageInstaller
|
||||
|
||||
private val packageActionReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.getIntExtra(
|
||||
PackageInstaller.EXTRA_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
||||
if (userAction == null) {
|
||||
Logger.log("Fatal error for $intent")
|
||||
continueQueue(InstallStep.Error)
|
||||
return
|
||||
}
|
||||
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
service.startActivity(userAction)
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
continueQueue(InstallStep.Idle)
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
||||
else -> continueQueue(InstallStep.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var activeSession: Pair<Entry, Int>? = null
|
||||
|
||||
// Always ready
|
||||
override var ready = true
|
||||
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
activeSession = null
|
||||
try {
|
||||
val installParams =
|
||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
}
|
||||
activeSession = entry to packageInstaller.createSession(installParams)
|
||||
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||
installParams.setSize(fileSize)
|
||||
|
||||
val inputStream =
|
||||
service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
|
||||
val session = packageInstaller.openSession(activeSession!!.second)
|
||||
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
|
||||
session.use {
|
||||
arrayOf(inputStream, outputStream).use {
|
||||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
activeSession!!.second,
|
||||
Intent(INSTALL_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
} else 0
|
||||
).intentSender
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||
Logger.log(e)
|
||||
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||
activeSession?.let { (_, sessionId) ->
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
}
|
||||
continueQueue(InstallStep.Error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelEntry(entry: Entry): Boolean {
|
||||
activeSession?.let { (activeEntry, sessionId) ->
|
||||
if (activeEntry == entry) {
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
service.unregisterReceiver(packageActionReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(
|
||||
service,
|
||||
packageActionReceiver,
|
||||
IntentFilter(INSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"
|
|
@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.extension.manga.model
|
|||
sealed class MangaLoadResult {
|
||||
class Success(val extension: MangaExtension.Installed) : MangaLoadResult()
|
||||
class Untrusted(val extension: MangaExtension.Untrusted) : MangaLoadResult()
|
||||
object Error : MangaLoadResult()
|
||||
data object Error : MangaLoadResult()
|
||||
}
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
/**
|
||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
||||
* notifies the given [listener] when the package is an extension.
|
||||
*
|
||||
* @param listener The listener that should be notified of extension installation events.
|
||||
*/
|
||||
internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
||||
BroadcastReceiver() {
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intent filter this receiver should subscribe to.
|
||||
*/
|
||||
private val filter
|
||||
get() = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
launchNow {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is MangaLoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
||||
|
||||
is MangaLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
launchNow {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||
// Not needed as a package can't be upgraded if the signature is different
|
||||
// is LoadResult.Untrusted -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName != null) {
|
||||
listener.onPackageUninstalled(pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this package is performing an update.
|
||||
*
|
||||
* @param intent The intent that triggered the event.
|
||||
*/
|
||||
private fun isReplacing(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension triggered by the given intent.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param intent The intent containing the package name of the extension.
|
||||
*/
|
||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
Logger.log("Package name not found")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||
MangaExtensionLoader.loadMangaExtensionFromPkgName(
|
||||
context,
|
||||
pkgName,
|
||||
)
|
||||
}.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of the installed, updated or removed application.
|
||||
*/
|
||||
private fun getPackageNameFromIntent(intent: Intent?): String? {
|
||||
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that receives extension installation events.
|
||||
*/
|
||||
interface Listener {
|
||||
fun onExtensionInstalled(extension: MangaExtension.Installed)
|
||||
fun onExtensionUpdated(extension: MangaExtension.Installed)
|
||||
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
*/
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
internal object MangaExtensionLoader {
|
||||
|
||||
private val preferences: SourcePreferences by injectLazy()
|
||||
private val loadNsfwSource by lazy {
|
||||
preferences.showNsfwSource().get()
|
||||
}
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
|
||||
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
|
||||
const val LIB_VERSION_MIN = 1.2
|
||||
const val LIB_VERSION_MAX = 1.5
|
||||
|
||||
private const val PACKAGE_FLAGS =
|
||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||
|
||||
// inorichi's key
|
||||
private const val officialSignature =
|
||||
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
var trustedSignatures =
|
||||
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
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(it) }
|
||||
|
||||
if (extPkgs.isEmpty()) return emptyList()
|
||||
|
||||
// Load each extension concurrently and wait for completion
|
||||
return runBlocking {
|
||||
val deferred = extPkgs.map {
|
||||
async { loadMangaExtension(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.
|
||||
*/
|
||||
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
|
||||
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 MangaLoadResult.Error
|
||||
}
|
||||
if (!isPackageAnExtension(pkgInfo)) {
|
||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
return loadMangaExtension(context, pkgName, pkgInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension given its package name.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param pkgName The package name of the extension to load.
|
||||
* @param pkgInfo The package info of the extension.
|
||||
*/
|
||||
private fun loadMangaExtension(
|
||||
context: Context,
|
||||
pkgName: String,
|
||||
pkgInfo: PackageInfo
|
||||
): MangaLoadResult {
|
||||
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 MangaLoadResult.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 MangaLoadResult.Error
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
||||
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||
Logger.log("Lib version is $libVersion, while only versions " +
|
||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||
)
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
|
||||
val signatureHash = getSignatureHash(pkgInfo)
|
||||
|
||||
/* temporarily disabling signature check TODO: remove?
|
||||
if (signatureHash == null) {
|
||||
Logger.log("Package $pkgName isn't signed")
|
||||
return MangaLoadResult.Error
|
||||
} else if (signatureHash !in trustedSignatures) {
|
||||
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
||||
Logger.log("Extension $pkgName isn't trusted")
|
||||
return MangaLoadResult.Untrusted(extension)
|
||||
}
|
||||
*/
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
if (!loadNsfwSource && isNsfw) {
|
||||
Logger.log("NSFW extension $pkgName not allowed")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
|
||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
||||
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
|
||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||
.split(";")
|
||||
.map {
|
||||
val sourceClass = it.trim()
|
||||
if (sourceClass.startsWith(".")) {
|
||||
pkgInfo.packageName + sourceClass
|
||||
} else {
|
||||
sourceClass
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||
is MangaSource -> listOf(obj)
|
||||
is SourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Extension load error: $extName ($it)")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||
.map { it.lang }
|
||||
.toSet()
|
||||
val lang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extension = MangaExtension.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
libVersion = libVersion,
|
||||
lang = lang,
|
||||
isNsfw = isNsfw,
|
||||
hasReadme = hasReadme,
|
||||
hasChangelog = hasChangelog,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
isUnofficial = signatureHash != officialSignature,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
return MangaLoadResult.Success(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given package is an extension.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature hash of the package or null if it's not signed.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||
val signatures = pkgInfo.signatures
|
||||
return if (signatures != null && signatures.isNotEmpty()) {
|
||||
Hash.sha256(signatures.first().toByteArray())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.util
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -16,18 +22,24 @@ import kotlin.time.Duration.Companion.seconds
|
|||
* Activity used to install extensions, because we can only receive the result of the installation
|
||||
* with [startActivityForResult], which we need to update the UI.
|
||||
*/
|
||||
class MangaExtensionInstallActivity : Activity() {
|
||||
class ExtensionInstallActivity : AppCompatActivity() {
|
||||
|
||||
// MIUI package installer bug workaround
|
||||
private var ignoreUntil = 0L
|
||||
private var ignoreResult = false
|
||||
private var hasIgnoredResult = false
|
||||
|
||||
private var type: MediaType? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
if (intent.hasExtra(ExtensionInstaller.EXTRA_EXTENSION_TYPE))
|
||||
type = intent.getSerializableExtraCompat<MediaType>(ExtensionInstaller.EXTRA_EXTENSION_TYPE)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
.setDataAndType(intent.data, intent.type)
|
||||
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
|
@ -38,8 +50,19 @@ class MangaExtensionInstallActivity : Activity() {
|
|||
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
|
||||
}
|
||||
|
||||
val onInstallResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result: ActivityResult ->
|
||||
if (ignoreResult && System.nanoTime() < ignoreUntil) {
|
||||
hasIgnoredResult = true
|
||||
return@registerForActivityResult
|
||||
}
|
||||
checkInstallationResult(result.resultCode)
|
||||
finish()
|
||||
}
|
||||
|
||||
try {
|
||||
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
|
||||
onInstallResult.launch(installIntent)
|
||||
} catch (error: Exception) {
|
||||
// Either install package can't be found (probably bots) or there's a security exception
|
||||
// with the download manager. Nothing we can workaround.
|
||||
|
@ -47,17 +70,6 @@ class MangaExtensionInstallActivity : Activity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (ignoreResult && System.nanoTime() < ignoreUntil) {
|
||||
hasIgnoredResult = true
|
||||
return
|
||||
}
|
||||
if (requestCode == INSTALL_REQUEST_CODE) {
|
||||
checkInstallationResult(resultCode)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (hasIgnoredResult) {
|
||||
|
@ -67,15 +79,23 @@ class MangaExtensionInstallActivity : Activity() {
|
|||
}
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(MangaExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager = Injekt.get<MangaExtensionManager>()
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val newStep = when (resultCode) {
|
||||
RESULT_OK -> InstallStep.Installed
|
||||
RESULT_CANCELED -> InstallStep.Idle
|
||||
else -> InstallStep.Error
|
||||
}
|
||||
extensionManager.updateInstallStep(downloadId, newStep)
|
||||
when (type) {
|
||||
MediaType.ANIME -> {
|
||||
Injekt.get<AnimeExtensionManager>().updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
Injekt.get<MangaExtensionManager>().updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
MediaType.NOVEL -> {
|
||||
Injekt.get<NovelExtensionManager>().updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
null -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val INSTALL_REQUEST_CODE = 500
|
|
@ -0,0 +1,205 @@
|
|||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
/**
|
||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
||||
* notifies the given [listener] when the package is an extension.
|
||||
*
|
||||
* @param listener The listener that should be notified of extension installation events.
|
||||
*/
|
||||
internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||
|
||||
private var animeListener: AnimeListener? = null
|
||||
private var mangaListener: MangaListener? = null
|
||||
private var type: MediaType? = null
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun setAnimeListener(listener: AnimeListener) : ExtensionInstallReceiver {
|
||||
this.type = MediaType.ANIME
|
||||
animeListener = listener
|
||||
this.animeListener
|
||||
return this
|
||||
}
|
||||
|
||||
fun setMangaListener(listener: MangaListener) : ExtensionInstallReceiver {
|
||||
this.type = MediaType.MANGA
|
||||
mangaListener = listener
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intent filter this receiver should subscribe to.
|
||||
*/
|
||||
private val filter
|
||||
get() = IntentFilter().apply {
|
||||
priority = 100
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
launchNow {
|
||||
when (type) {
|
||||
MediaType.ANIME -> {
|
||||
when (val result = getAnimeExtensionFromIntent(context, intent)) {
|
||||
is AnimeLoadResult.Success -> animeListener?.onExtensionInstalled(result.extension)
|
||||
|
||||
is AnimeLoadResult.Untrusted -> animeListener?.onExtensionUntrusted(result.extension)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
when (val result = getMangaExtensionFromIntent(context, intent)) {
|
||||
is MangaLoadResult.Success -> mangaListener?.onExtensionInstalled(result.extension)
|
||||
|
||||
is MangaLoadResult.Untrusted -> mangaListener?.onExtensionUntrusted(result.extension)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
launchNow {
|
||||
when (type) {
|
||||
MediaType.ANIME -> {
|
||||
when (val result = getAnimeExtensionFromIntent(context, intent)) {
|
||||
is AnimeLoadResult.Success -> animeListener?.onExtensionUpdated(result.extension)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
when (val result = getMangaExtensionFromIntent(context, intent)) {
|
||||
is MangaLoadResult.Success -> mangaListener?.onExtensionUpdated(result.extension)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName != null) {
|
||||
when (type) {
|
||||
MediaType.ANIME -> {
|
||||
animeListener?.onPackageUninstalled(pkgName)
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
mangaListener?.onPackageUninstalled(pkgName)
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this package is performing an update.
|
||||
*
|
||||
* @param intent The intent that triggered the event.
|
||||
*/
|
||||
private fun isReplacing(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension triggered by the given intent.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param intent The intent containing the package name of the extension.
|
||||
*/
|
||||
private suspend fun getAnimeExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
Logger.log("Package name not found")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||
ExtensionLoader.loadAnimeExtensionFromPkgName(
|
||||
context,
|
||||
pkgName,
|
||||
)
|
||||
}.await()
|
||||
}
|
||||
|
||||
private suspend fun getMangaExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
Logger.log("Package name not found")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||
ExtensionLoader.loadMangaExtensionFromPkgName(
|
||||
context,
|
||||
pkgName,
|
||||
)
|
||||
}.await()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of the installed, updated or removed application.
|
||||
*/
|
||||
private fun getPackageNameFromIntent(intent: Intent?): String? {
|
||||
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that receives extension installation events.
|
||||
*/
|
||||
interface AnimeListener {
|
||||
fun onExtensionInstalled(extension: AnimeExtension.Installed)
|
||||
fun onExtensionUpdated(extension: AnimeExtension.Installed)
|
||||
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
|
||||
interface MangaListener {
|
||||
fun onExtensionInstalled(extension: MangaExtension.Installed)
|
||||
fun onExtensionUpdated(extension: MangaExtension.Installed)
|
||||
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.util
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
|
@ -8,18 +8,20 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
|
||||
import eu.kanade.tachiyomi.extension.manga.installer.PackageInstallerInstallerManga
|
||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_EXTENSION_TYPE
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
|
||||
class MangaExtensionInstallService : Service() {
|
||||
class ExtensionInstallService : Service() {
|
||||
|
||||
private var installer: InstallerManga? = null
|
||||
private var installer: Installer? = null
|
||||
|
||||
override fun onCreate() {
|
||||
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
||||
|
@ -43,18 +45,19 @@ class MangaExtensionInstallService : Service() {
|
|||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val uri = intent?.data
|
||||
val type = intent?.getSerializableExtraCompat<MediaType>(EXTRA_EXTENSION_TYPE)
|
||||
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
||||
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
|
||||
EXTRA_INSTALLER,
|
||||
EXTRA_INSTALLER
|
||||
)
|
||||
if (uri == null || id == null || installerUsed == null) {
|
||||
if (uri == null || type == null || id == null || installerUsed == null) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (installer == null) {
|
||||
installer = when (installerUsed) {
|
||||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerManga(
|
||||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(
|
||||
this
|
||||
)
|
||||
|
||||
|
@ -65,7 +68,7 @@ class MangaExtensionInstallService : Service() {
|
|||
}
|
||||
}
|
||||
}
|
||||
installer!!.addToQueue(id, uri)
|
||||
installer!!.addToQueue(type, id, uri)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
|
@ -81,13 +84,15 @@ class MangaExtensionInstallService : Service() {
|
|||
|
||||
fun getIntent(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
downloadId: Long,
|
||||
uri: Uri,
|
||||
installer: BasePreferences.ExtensionInstaller,
|
||||
): Intent {
|
||||
return Intent(context, MangaExtensionInstallService::class.java)
|
||||
.setDataAndType(uri, MangaExtensionInstaller.APK_MIME)
|
||||
return Intent(context, ExtensionInstallService::class.java)
|
||||
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||
.putExtra(EXTRA_INSTALLER, installer)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.manga.util
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -6,15 +6,19 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.novel.NovelExtension
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import rx.Observable
|
||||
|
@ -29,7 +33,7 @@ import java.util.concurrent.TimeUnit
|
|||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
internal class MangaExtensionInstaller(private val context: Context) {
|
||||
internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The system's download manager
|
||||
|
@ -61,7 +65,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
* @param url The url of the apk.
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: MangaExtension) = Observable.defer {
|
||||
fun downloadAndInstall(url: String, extension: AnimeExtension): Observable<InstallStep> = Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
|
@ -81,6 +85,83 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
Environment.DIRECTORY_DOWNLOADS,
|
||||
downloadUri.lastPathSegment
|
||||
)
|
||||
.setDescription(MediaType.ANIME.asText())
|
||||
.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) }
|
||||
}
|
||||
|
||||
fun downloadAndInstall(url: String, extension: MangaExtension): Observable<InstallStep> = Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
.setDescription(MediaType.MANGA.asText())
|
||||
.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) }
|
||||
}
|
||||
|
||||
fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
if (oldDownload != null) {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
.setDescription(MediaType.MANGA.asText())
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
|
@ -134,11 +215,12 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
*
|
||||
* @param uri The uri of the extension to install.
|
||||
*/
|
||||
fun installApk(downloadId: Long, uri: Uri) {
|
||||
fun installApk(type: MediaType, downloadId: Long, uri: Uri) {
|
||||
when (val installer = extensionInstaller.get()) {
|
||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, MangaExtensionInstallActivity::class.java)
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
|
@ -147,7 +229,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
|
||||
else -> {
|
||||
val intent =
|
||||
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||
ExtensionInstallService.getIntent(context, type, downloadId, uri, installer)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +241,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
InstallerManga.cancelInstallQueue(context, downloadId)
|
||||
Installer.cancelInstallQueue(context, downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -168,10 +250,13 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
* @param pkgName The package name of the extension to uninstall
|
||||
*/
|
||||
fun uninstallApk(pkgName: String) {
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
Intent(Intent.ACTION_DELETE).setData("package:$pkgName".toUri())
|
||||
else
|
||||
@Suppress("DEPRECATION")
|
||||
Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||
|
||||
context.startActivity(intent)
|
||||
context.startActivity(intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -255,8 +340,11 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
val localUri = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
||||
).removePrefix(FILE_SCHEME)
|
||||
val type = MediaType.fromText(cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION),
|
||||
))
|
||||
|
||||
installApk(id, File(localUri).getUriCompat(context))
|
||||
installApk(type, id, File(localUri).getUriCompat(context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -265,6 +353,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
|||
companion object {
|
||||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||
const val EXTRA_EXTENSION_TYPE = "ExtensionInstaller.extra.EXTENSION_TYPE"
|
||||
const val FILE_SCHEME = "file://"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,427 @@
|
|||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions. Supports two kinds of extensions:
|
||||
*
|
||||
* 1. Shared extension: This extension is installed to the system with package
|
||||
* installer, so other variants of Tachiyomi and its forks can also use this extension.
|
||||
*
|
||||
* 2. Private extension: This extension is put inside private data directory of the
|
||||
* running app, so this extension can only be used by the running app and not shared
|
||||
* with other apps.
|
||||
*
|
||||
* When both kinds of extensions are installed with a same package name, shared
|
||||
* extension will be used unless the version codes are different. In that case the
|
||||
* one with higher version code will be used.
|
||||
*/
|
||||
internal object ExtensionLoader {
|
||||
|
||||
private val preferences: SourcePreferences by injectLazy()
|
||||
private val loadNsfwSource by lazy {
|
||||
preferences.showNsfwSource().get()
|
||||
}
|
||||
|
||||
private const val ANIME_PACKAGE = "tachiyomi.animeextension"
|
||||
private const val MANGA_PACKAGE = "tachiyomi.extension"
|
||||
|
||||
private const val XX_METADATA_SOURCE_CLASS = ".class"
|
||||
private const val XX_METADATA_SOURCE_FACTORY = ".factory"
|
||||
private const val XX_METADATA_NSFW = "n.nsfw"
|
||||
private const val XX_METADATA_HAS_README = ".hasReadme"
|
||||
private const val XX_METADATA_HAS_CHANGELOG = ".hasChangelog"
|
||||
const val ANIME_LIB_VERSION_MIN = 12
|
||||
const val ANIME_LIB_VERSION_MAX = 15
|
||||
|
||||
const val MANGA_LIB_VERSION_MIN = 1.2
|
||||
const val MANGA_LIB_VERSION_MAX = 1.5
|
||||
|
||||
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
|
||||
PackageManager.GET_META_DATA or
|
||||
@Suppress ("DEPRECATION") PackageManager.GET_SIGNATURES or
|
||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
||||
|
||||
// jmir1's key
|
||||
private const val officialSignatureAnime =
|
||||
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
|
||||
|
||||
var trustedSignaturesAnime =
|
||||
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignatureAnime
|
||||
|
||||
// inorichi's key
|
||||
private const val officialSignatureManga =
|
||||
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||
|
||||
/**
|
||||
* List of the trusted signatures.
|
||||
*/
|
||||
var trustedSignaturesManga =
|
||||
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignatureManga
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun loadAnimeExtensions(context: Context): List<AnimeLoadResult> {
|
||||
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.ANIME, it) }
|
||||
|
||||
if (extPkgs.isEmpty()) return emptyList()
|
||||
|
||||
// Load each extension concurrently and wait for completion
|
||||
return runBlocking {
|
||||
val deferred = extPkgs.map {
|
||||
async { loadAnimeExtension(context, it.packageName, it) }
|
||||
}
|
||||
deferred.map { it.await() }
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
|
||||
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.MANGA, it) }
|
||||
|
||||
if (extPkgs.isEmpty()) return emptyList()
|
||||
|
||||
// Load each extension concurrently and wait for completion
|
||||
return runBlocking {
|
||||
val deferred = extPkgs.map {
|
||||
async { loadMangaExtension(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.
|
||||
*/
|
||||
fun loadAnimeExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
|
||||
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 AnimeLoadResult.Error
|
||||
}
|
||||
if (!isPackageAnExtension(MediaType.ANIME,pkgInfo)) {
|
||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
return loadAnimeExtension(context, pkgName, pkgInfo)
|
||||
}
|
||||
|
||||
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
|
||||
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 MangaLoadResult.Error
|
||||
}
|
||||
if (!isPackageAnExtension(MediaType.MANGA, pkgInfo)) {
|
||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
return loadMangaExtension(context, pkgName, pkgInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension given its package name.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param pkgName The package name of the extension to load.
|
||||
* @param pkgInfo The package info of the extension.
|
||||
*/
|
||||
private fun loadAnimeExtension(
|
||||
context: Context,
|
||||
pkgName: String,
|
||||
pkgInfo: PackageInfo
|
||||
): AnimeLoadResult {
|
||||
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 AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
||||
if (libVersion == null || libVersion < ANIME_LIB_VERSION_MIN || libVersion > ANIME_LIB_VERSION_MAX) {
|
||||
Logger.log("Lib version is $libVersion, while only versions " +
|
||||
"$ANIME_LIB_VERSION_MIN to $ANIME_LIB_VERSION_MAX are allowed"
|
||||
)
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
val signatureHash = getSignatureHash(pkgInfo)
|
||||
|
||||
if (signatureHash == null) {
|
||||
Logger.log("Package $pkgName isn't signed")
|
||||
return AnimeLoadResult.Error
|
||||
} else if (signatureHash !in trustedSignaturesAnime) {
|
||||
val extension = AnimeExtension.Untrusted(
|
||||
extName,
|
||||
pkgName,
|
||||
versionName,
|
||||
versionCode,
|
||||
libVersion,
|
||||
signatureHash
|
||||
)
|
||||
Logger.log("Extension $pkgName isn't trusted")
|
||||
return AnimeLoadResult.Untrusted(extension)
|
||||
}
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_NSFW") == 1
|
||||
if (!loadNsfwSource && isNsfw) {
|
||||
Logger.log("NSFW extension $pkgName not allowed")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
|
||||
val hasReadme = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_README", 0) == 1
|
||||
val hasChangelog = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
|
||||
val sources = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_CLASS")!!
|
||||
.split(";")
|
||||
.map {
|
||||
val sourceClass = it.trim()
|
||||
if (sourceClass.startsWith(".")) {
|
||||
pkgInfo.packageName + sourceClass
|
||||
} else {
|
||||
sourceClass
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
|
||||
is AnimeSource -> listOf(obj)
|
||||
is AnimeSourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Extension load error: $extName ($it)")
|
||||
return AnimeLoadResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
val langs = sources.filterIsInstance<AnimeCatalogueSource>()
|
||||
.map { it.lang }
|
||||
.toSet()
|
||||
val lang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extension = AnimeExtension.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
libVersion = libVersion,
|
||||
lang = lang,
|
||||
isNsfw = isNsfw,
|
||||
hasReadme = hasReadme,
|
||||
hasChangelog = hasChangelog,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_FACTORY"),
|
||||
isUnofficial = signatureHash != officialSignatureAnime,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
return AnimeLoadResult.Success(extension)
|
||||
}
|
||||
|
||||
private fun loadMangaExtension(
|
||||
context: Context,
|
||||
pkgName: String,
|
||||
pkgInfo: PackageInfo
|
||||
): MangaLoadResult {
|
||||
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 MangaLoadResult.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 MangaLoadResult.Error
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
||||
if (libVersion == null || libVersion < MANGA_LIB_VERSION_MIN || libVersion > MANGA_LIB_VERSION_MAX) {
|
||||
Logger.log("Lib version is $libVersion, while only versions " +
|
||||
"$MANGA_LIB_VERSION_MIN to $MANGA_LIB_VERSION_MAX are allowed"
|
||||
)
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
|
||||
val signatureHash = getSignatureHash(pkgInfo)
|
||||
|
||||
/* temporarily disabling signature check TODO: remove?
|
||||
if (signatureHash == null) {
|
||||
Logger.log("Package $pkgName isn't signed")
|
||||
return MangaLoadResult.Error
|
||||
} else if (signatureHash !in trustedSignatures) {
|
||||
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
||||
Logger.log("Extension $pkgName isn't trusted")
|
||||
return MangaLoadResult.Untrusted(extension)
|
||||
}
|
||||
*/
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_NSFW") == 1
|
||||
if (!loadNsfwSource && isNsfw) {
|
||||
Logger.log("NSFW extension $pkgName not allowed")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
|
||||
val hasReadme = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_README", 0) == 1
|
||||
val hasChangelog = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1
|
||||
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
|
||||
val sources = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_CLASS")!!
|
||||
.split(";")
|
||||
.map {
|
||||
val sourceClass = it.trim()
|
||||
if (sourceClass.startsWith(".")) {
|
||||
pkgInfo.packageName + sourceClass
|
||||
} else {
|
||||
sourceClass
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader)
|
||||
.getDeclaredConstructor().newInstance()) {
|
||||
is MangaSource -> listOf(obj)
|
||||
is SourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.log("Extension load error: $extName ($it)")
|
||||
return MangaLoadResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||
.map { it.lang }
|
||||
.toSet()
|
||||
val lang = when (langs.size) {
|
||||
0 -> ""
|
||||
1 -> langs.first()
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extension = MangaExtension.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
libVersion = libVersion,
|
||||
lang = lang,
|
||||
isNsfw = isNsfw,
|
||||
hasReadme = hasReadme,
|
||||
hasChangelog = hasChangelog,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_FACTORY"),
|
||||
isUnofficial = signatureHash != officialSignatureManga,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
return MangaLoadResult.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 -> ""
|
||||
} }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature hash of the package or null if it's not signed.
|
||||
*
|
||||
* @param pkgInfo The package info of the application.
|
||||
*/
|
||||
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
pkgInfo.signingInfo.signingCertificateHistory
|
||||
else
|
||||
@Suppress ("DEPRECATION") pkgInfo.signatures
|
||||
return if (signatures != null && signatures.isNotEmpty()) {
|
||||
Hash.sha256(signatures.first().toByteArray())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,8 +19,7 @@ fun GET(
|
|||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
val nUrl = url.toHttpUrl()
|
||||
val g = GET(nUrl, headers, cache)
|
||||
return g
|
||||
return GET(nUrl, headers, cache)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -82,7 +82,7 @@ class AndroidAnimeSourceManager(
|
|||
|
||||
}
|
||||
|
||||
private suspend fun createStubSource(id: Long): StubAnimeSource {
|
||||
private fun createStubSource(id: Long): StubAnimeSource {
|
||||
return StubAnimeSource(AnimeSourceData(id, "", ""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ class AndroidMangaSourceManager(
|
|||
|
||||
}
|
||||
|
||||
private suspend fun createStubSource(id: Long): StubMangaSource {
|
||||
private fun createStubSource(id: Long): StubMangaSource {
|
||||
return StubMangaSource(MangaSourceData(id, "", ""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.IntentCompat
|
||||
import java.io.Serializable
|
||||
|
||||
fun Uri.toShareIntent(context: Context, type: String = "image/*", message: String? = null): Intent {
|
||||
fun Uri.toShareIntent(type: String = "image/*", message: String? = null): Intent {
|
||||
val uri = this
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
|
@ -36,6 +36,15 @@ inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
|
|||
return IntentCompat.getParcelableExtra(this, name, T::class.java)
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Bundle.getSerializableCompat(name: String): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getSerializable(name, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getSerializable(name) as? T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(name: String): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getSerializableExtra(name, T::class.java)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -17,7 +16,7 @@ object LocaleHelper {
|
|||
/**
|
||||
* Returns display name of a string language code.
|
||||
*/
|
||||
fun getSourceDisplayName(lang: String?, context: Context): String {
|
||||
fun getSourceDisplayName(lang: String?): String {
|
||||
return when (lang) {
|
||||
LAST_USED_KEY -> "Last used"
|
||||
PINNED_KEY -> "Pinned"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package tachiyomi.domain.source.anime.model
|
||||
|
||||
sealed class Pin(val code: Int) {
|
||||
object Unpinned : Pin(0b00)
|
||||
object Pinned : Pin(0b01)
|
||||
object Actual : Pin(0b10)
|
||||
data object Unpinned : Pin(0b00)
|
||||
data object Pinned : Pin(0b01)
|
||||
data object Actual : Pin(0b10)
|
||||
}
|
||||
|
||||
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
||||
|
|
|
@ -21,8 +21,8 @@ class LocalAnimeSource(
|
|||
private val context: Context,
|
||||
) : AnimeCatalogueSource, UnmeteredSource {
|
||||
|
||||
private val POPULAR_FILTERS = AnimeFilterList(AnimeOrderBy.Popular(context))
|
||||
private val LATEST_FILTERS = AnimeFilterList(AnimeOrderBy.Latest(context))
|
||||
private val POPULAR_FILTERS = AnimeFilterList(AnimeOrderBy.Popular())
|
||||
private val LATEST_FILTERS = AnimeFilterList(AnimeOrderBy.Latest())
|
||||
|
||||
override val name = "Local anime source"
|
||||
|
||||
|
@ -61,7 +61,7 @@ class LocalAnimeSource(
|
|||
}
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = AnimeFilterList(AnimeOrderBy.Popular(context))
|
||||
override fun getFilterList() = AnimeFilterList(AnimeOrderBy.Popular())
|
||||
|
||||
// Unused stuff
|
||||
override suspend fun getVideoList(episode: SEpisode) =
|
||||
|
|
|
@ -19,8 +19,8 @@ class LocalMangaSource(
|
|||
) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(MangaOrderBy.Popular(context))
|
||||
private val LATEST_FILTERS = FilterList(MangaOrderBy.Latest(context))
|
||||
private val POPULAR_FILTERS = FilterList(MangaOrderBy.Popular())
|
||||
private val LATEST_FILTERS = FilterList(MangaOrderBy.Latest())
|
||||
|
||||
override val name: String = "Local manga source"
|
||||
|
||||
|
@ -56,7 +56,7 @@ class LocalMangaSource(
|
|||
}
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(MangaOrderBy.Popular(context))
|
||||
override fun getFilterList() = FilterList(MangaOrderBy.Popular())
|
||||
|
||||
// Unused stuff
|
||||
override suspend fun getPageList(chapter: SChapter) =
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package tachiyomi.source.local.filter.anime
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
sealed class AnimeOrderBy(context: Context, selection: Selection) : AnimeFilter.Sort(
|
||||
sealed class AnimeOrderBy(selection: Selection) : AnimeFilter.Sort(
|
||||
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection,
|
||||
) {
|
||||
class Popular(context: Context) : AnimeOrderBy(context, Selection(0, true))
|
||||
class Latest(context: Context) : AnimeOrderBy(context, Selection(1, false))
|
||||
class Popular : AnimeOrderBy(Selection(0, true))
|
||||
class Latest : AnimeOrderBy(Selection(1, false))
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
package tachiyomi.source.local.filter.manga
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
sealed class MangaOrderBy(context: Context, selection: Selection) : Filter.Sort(
|
||||
sealed class MangaOrderBy(selection: Selection) : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection,
|
||||
) {
|
||||
class Popular(context: Context) : MangaOrderBy(context, Selection(0, true))
|
||||
class Latest(context: Context) : MangaOrderBy(context, Selection(1, false))
|
||||
class Popular : MangaOrderBy(Selection(0, true))
|
||||
class Latest : MangaOrderBy(Selection(1, false))
|
||||
}
|
||||
|
|
|
@ -452,7 +452,7 @@
|
|||
android:minHeight="64dp"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:text="Auto Hide Time Stamps"
|
||||
android:text="@string/auto_hide_time_stamps"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/bg_opp"
|
||||
app:cornerRadius="0dp"
|
||||
|
@ -1175,7 +1175,7 @@
|
|||
android:minHeight="64dp"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:text="Show Rotate Button"
|
||||
android:text="@string/show_rotate_button"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/bg_opp"
|
||||
app:cornerRadius="0dp"
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
android:gravity="center_vertical"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:text="Default Manga Settings"
|
||||
android:text="@string/default_manga_settings"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
app:drawableEndCompat="@drawable/ic_round_arrow_drop_down_24"
|
||||
tools:ignore="TextContrastCheck" />
|
||||
|
@ -1134,7 +1134,7 @@
|
|||
android:minHeight="64dp"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:text="Use Dark Theme"
|
||||
android:text="@string/use_dark_theme"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
app:cornerRadius="0dp"
|
||||
|
@ -1155,7 +1155,7 @@
|
|||
android:minHeight="64dp"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:text="Use OLED Theme"
|
||||
android:text="@string/use_oled_theme"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
app:cornerRadius="0dp"
|
||||
|
|
|
@ -504,7 +504,7 @@
|
|||
android:elegantTextHeight="true"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:minHeight="64dp"
|
||||
android:text="Use Dark Theme"
|
||||
android:text="@string/use_dark_theme"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
app:cornerRadius="0dp"
|
||||
|
@ -523,7 +523,7 @@
|
|||
android:elegantTextHeight="true"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:minHeight="64dp"
|
||||
android:text="Use OLED Theme"
|
||||
android:text="@string/use_oled_theme"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
app:cornerRadius="0dp"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:theme="@style/Theme.Dantotsu.AppWidgetContainer">
|
||||
|
||||
<ImageView
|
||||
|
@ -8,7 +9,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/gradient_background" />
|
||||
android:src="@drawable/gradient_background"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/widgetContainer"
|
||||
|
@ -23,7 +25,8 @@
|
|||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:foregroundGravity="center_vertical"
|
||||
android:src="@drawable/ic_dantotsu_round" />
|
||||
android:src="@drawable/ic_dantotsu_round"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widgetTitle"
|
||||
|
@ -34,7 +37,7 @@
|
|||
android:layout_marginStart="0dp"
|
||||
android:layout_toEndOf="@+id/logoView"
|
||||
android:gravity="center_vertical"
|
||||
android:text="Currently Airing"
|
||||
android:text="@string/currently_airing"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
@ -44,12 +47,12 @@
|
|||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/widgetTitle" />
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<TextView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="No shows to display"
|
||||
android:text="@string/no_shows_to_display"
|
||||
android:textColor="#ffffff"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<!-- custom_dialog_layout.xml -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
|
@ -13,7 +14,7 @@
|
|||
android:fontFamily="@font/poppins_bold"
|
||||
android:gravity="center|start"
|
||||
android:singleLine="true"
|
||||
android:text="Scanlators"
|
||||
android:text="@string/scanlators"
|
||||
android:textColor="?attr/colorOnBackground"
|
||||
android:textSize="16sp" />
|
||||
|
||||
|
@ -40,7 +41,8 @@
|
|||
android:layout_marginEnd="5dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/untick_all_boxes"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
app:tint="?attr/colorPrimary"
|
||||
tools:ignore="ContentDescription" />
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
|
@ -12,6 +13,7 @@
|
|||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:maxLength="4"
|
||||
android:autofillHints="e.g. 1" />
|
||||
android:autofillHints="e.g. 1"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:alpha="0.58"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="Sort" />
|
||||
android:text="@string/sort" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sortText"
|
||||
|
@ -169,7 +169,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:alpha="0.58"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="Download" />
|
||||
android:text="@string/download" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloadNo"
|
||||
|
@ -215,7 +215,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:alpha="0.58"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="Scanlator" />
|
||||
android:text="@string/scanlator" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scanlatorNo"
|
||||
|
@ -263,13 +263,13 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:alpha="0.58"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="Set Cookies" />
|
||||
android:text="@string/set_cookies" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="Open Website"
|
||||
android:text="@string/open_website"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
tools:ignore="TextContrastCheck" />
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/poppins"
|
||||
android:text="Exporting credentials requires a password for encryption."
|
||||
android:text="@string/exporting_requires_encryption"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
|
|
@ -49,7 +49,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="horizontal"
|
||||
android:padding="32dp">
|
||||
android:padding="32dp"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Anime Queue(WIP)" />
|
||||
android:text="@string/anime_queue" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Manga Queue(WIP)" />
|
||||
android:text="@string/manga_queue" />
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue