Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
56
app/src/main/java/ani/dantotsu/subcriptions/AlarmReceiver.kt
Normal file
56
app/src/main/java/ani/dantotsu/subcriptions/AlarmReceiver.kt
Normal file
|
@ -0,0 +1,56 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AlarmReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> tryWith(true) {
|
||||
logger("Starting Dantotsu Subscription Service on Boot")
|
||||
context?.startSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val con = context ?: currContext() ?: return@launch
|
||||
if (isOnline(con)) Subscription.perform(con)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun alarm(context: Context) {
|
||||
val alarmIntent = Intent(context, AlarmReceiver::class.java)
|
||||
alarmIntent.action = "ani.dantotsu.ACTION_ALARM"
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, 0, alarmIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val curTime = loadData<Int>("subscriptions_time", context) ?: defaultTime
|
||||
|
||||
if (timeMinutes[curTime] > 0)
|
||||
alarmManager.setRepeating(
|
||||
AlarmManager.RTC,
|
||||
System.currentTimeMillis(),
|
||||
(timeMinutes[curTime] * 60 * 1000),
|
||||
pendingIntent
|
||||
)
|
||||
else alarmManager.cancel(pendingIntent)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
161
app/src/main/java/ani/dantotsu/subcriptions/Notifications.kt
Normal file
161
app/src/main/java/ani/dantotsu/subcriptions/Notifications.kt
Normal file
|
@ -0,0 +1,161 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationChannelGroup
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Context.NOTIFICATION_SERVICE
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.NotificationCompat
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.UrlMedia
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
class Notifications {
|
||||
enum class Group(val title: String, val icon: Int) {
|
||||
ANIME_GROUP("New Episodes", R.drawable.ic_round_movie_filter_24),
|
||||
MANGA_GROUP("New Chapters", R.drawable.ic_round_menu_book_24)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun openSettings(context: Context, channelId: String?): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val intent = Intent(
|
||||
if (channelId != null) Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
|
||||
else Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
fun getIntent(context: Context, mediaId: Int): PendingIntent {
|
||||
val notifyIntent = Intent(context, UrlMedia::class.java)
|
||||
.putExtra("media", mediaId)
|
||||
.setAction(mediaId.toString())
|
||||
.apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context, 0, notifyIntent,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
|
||||
} else {
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun createChannel(context: Context, group: Group?, id: String, name: String, silent: Boolean = false) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val importance = if (!silent) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_LOW
|
||||
val mChannel = NotificationChannel(id, name, importance)
|
||||
|
||||
val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (group != null) {
|
||||
notificationManager.createNotificationChannelGroup(NotificationChannelGroup(group.name, group.title))
|
||||
mChannel.group = group.name
|
||||
}
|
||||
|
||||
notificationManager.createNotificationChannel(mChannel)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChannel(context: Context, id: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.deleteNotificationChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNotification(
|
||||
context: Context,
|
||||
group: Group?,
|
||||
channelId: String,
|
||||
title: String,
|
||||
text: String?,
|
||||
silent: Boolean = false
|
||||
): NotificationCompat.Builder {
|
||||
createChannel(context, group, channelId, title, silent)
|
||||
return NotificationCompat.Builder(context, channelId)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setSmallIcon(group?.icon ?: R.drawable.monochrome)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
suspend fun getNotification(
|
||||
context: Context,
|
||||
group: Group?,
|
||||
channelId: String,
|
||||
title: String,
|
||||
text: String,
|
||||
img: FileUrl?,
|
||||
silent: Boolean = false,
|
||||
largeImg: FileUrl?
|
||||
): NotificationCompat.Builder {
|
||||
val builder = getNotification(context, group, channelId, title, text, silent)
|
||||
return if (img != null) {
|
||||
val bitmap = withContext(Dispatchers.IO) {
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(GlideUrl(img.url) { img.headers })
|
||||
.submit()
|
||||
.get()
|
||||
}
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val largeBitmap = if (largeImg != null) Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(GlideUrl(largeImg.url) { largeImg.headers })
|
||||
.submit()
|
||||
.get()
|
||||
else null
|
||||
|
||||
if(largeBitmap!=null) builder.setStyle(
|
||||
NotificationCompat
|
||||
.BigPictureStyle()
|
||||
.bigPicture(largeBitmap)
|
||||
.bigLargeIcon(bitmap)
|
||||
)
|
||||
|
||||
builder.setLargeIcon(bitmap)
|
||||
} else builder
|
||||
}
|
||||
|
||||
suspend fun getNotification(
|
||||
context: Context,
|
||||
group: Group?,
|
||||
channelId: String,
|
||||
title: String,
|
||||
text: String,
|
||||
img: String? = null,
|
||||
silent: Boolean = false,
|
||||
largeImg: FileUrl? = null
|
||||
): NotificationCompat.Builder {
|
||||
return getNotification(
|
||||
context,
|
||||
group,
|
||||
channelId,
|
||||
title,
|
||||
text,
|
||||
if (img != null) FileUrl(img) else null,
|
||||
silent,
|
||||
largeImg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
138
app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt
Normal file
138
app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt
Normal file
|
@ -0,0 +1,138 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.parsers.Episode
|
||||
import ani.dantotsu.parsers.MangaChapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class Subscription {
|
||||
companion object {
|
||||
const val defaultTime = 8
|
||||
val timeMinutes = arrayOf(0L, 5, 10, 15, 30, 45, 60, 90, 120, 180, 240, 360, 480, 720, 1440)
|
||||
|
||||
private var alreadyStarted = false
|
||||
fun Context.startSubscription(force: Boolean = false) {
|
||||
if (!alreadyStarted || force) {
|
||||
alreadyStarted = true
|
||||
SubscriptionWorker.enqueue(this)
|
||||
AlarmReceiver.alarm(this)
|
||||
} else logger("Already Subscribed")
|
||||
}
|
||||
|
||||
private var currentlyPerforming = false
|
||||
|
||||
suspend fun perform(context: Context) {
|
||||
if (!currentlyPerforming) tryWithSuspend {
|
||||
currentlyPerforming = true
|
||||
App.context = context
|
||||
|
||||
val subscriptions = SubscriptionHelper.getSubscriptions(context)
|
||||
var i = 0
|
||||
val index = subscriptions.map { i++; it.key to i }.toMap()
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
val progressEnabled = loadData("subscription_checking_notifications", context) ?: true
|
||||
val progressNotification = if (progressEnabled) getProgressNotification(
|
||||
context,
|
||||
subscriptions.size
|
||||
) else null
|
||||
if (progressNotification != null) {
|
||||
notificationManager.notify(progressNotificationId, progressNotification.build())
|
||||
//Seems like if the parent coroutine scope gets cancelled, the notification stays
|
||||
//So adding this as a safeguard? dk if this will be useful
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(5 * subscriptions.size * 1000L)
|
||||
notificationManager.cancel(progressNotificationId)
|
||||
}
|
||||
}
|
||||
|
||||
fun progress(progress: Int, parser: String, media: String) {
|
||||
if (progressNotification != null)
|
||||
notificationManager.notify(
|
||||
progressNotificationId,
|
||||
progressNotification
|
||||
.setProgress(subscriptions.size, progress, false)
|
||||
.setContentText("$media on $parser")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
subscriptions.toList().map {
|
||||
val media = it.second
|
||||
val text = if (media.isAnime) {
|
||||
val parser = SubscriptionHelper.getAnimeParser(context, media.isAdult, media.id)
|
||||
progress(index[it.first]!!, parser.name, media.name)
|
||||
val ep: Episode? = SubscriptionHelper.getEpisode(context, parser, media.id, media.isAdult)
|
||||
if (ep != null) currActivity()!!.getString(R.string.episode)+"${ep.number}${
|
||||
if (ep.title != null) " : ${ep.title}" else ""
|
||||
}${
|
||||
if (ep.isFiller) " [Filler]" else ""
|
||||
} "+ currActivity()!!.getString(R.string.just_released) to ep.thumbnail
|
||||
else null
|
||||
} else {
|
||||
val parser = SubscriptionHelper.getMangaParser(context, media.isAdult, media.id)
|
||||
progress(index[it.first]!!, parser.name, media.name)
|
||||
val ep: MangaChapter? =
|
||||
SubscriptionHelper.getChapter(context, parser, media.id, media.isAdult)
|
||||
if (ep != null) currActivity()!!.getString(R.string.chapter)+"${ep.number}${
|
||||
if (ep.title != null) " : ${ep.title}" else ""
|
||||
} "+ currActivity()!!.getString(R.string.just_released) to null
|
||||
else null
|
||||
} ?: return@map
|
||||
createNotification(context.applicationContext, media, text.first, text.second)
|
||||
}
|
||||
|
||||
if (progressNotification != null) notificationManager.cancel(progressNotificationId)
|
||||
currentlyPerforming = false
|
||||
}
|
||||
}
|
||||
|
||||
fun getChannelId(isAnime: Boolean, mediaId: Int) = "${if (isAnime) "anime" else "manga"}_${mediaId}"
|
||||
|
||||
private suspend fun createNotification(
|
||||
context: Context,
|
||||
media: SubscriptionHelper.Companion.SubscribeMedia,
|
||||
text: String,
|
||||
thumbnail: FileUrl?
|
||||
) {
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
val notification = Notifications.getNotification(
|
||||
context,
|
||||
if (media.isAnime) Notifications.Group.ANIME_GROUP else Notifications.Group.MANGA_GROUP,
|
||||
getChannelId(media.isAnime, media.id),
|
||||
media.name,
|
||||
text,
|
||||
media.image,
|
||||
false,
|
||||
thumbnail
|
||||
).setContentIntent(Notifications.getIntent(context, media.id)).build()
|
||||
|
||||
notification.flags = Notification.FLAG_AUTO_CANCEL
|
||||
//+100 to have extra ids for other notifications?
|
||||
notificationManager.notify(100 + media.id, notification)
|
||||
}
|
||||
|
||||
private const val progressNotificationId = 100
|
||||
|
||||
private fun getProgressNotification(context: Context, size: Int): NotificationCompat.Builder {
|
||||
return Notifications.getNotification(
|
||||
context,
|
||||
null,
|
||||
"subscription_checking",
|
||||
currContext()!!.getString(R.string.checking_subscriptions_title),
|
||||
null,
|
||||
true
|
||||
).setOngoing(true).setProgress(size, 0, false).setAutoCancel(false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.Selected
|
||||
import ani.dantotsu.parsers.*
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.R
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class SubscriptionHelper {
|
||||
companion object {
|
||||
private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected {
|
||||
return loadData<Selected>("${mediaId}-select", context) ?: Selected().let {
|
||||
it.source =
|
||||
if (isAdult) 0
|
||||
else if (isAnime) loadData("settings_def_anime_source", context) ?: 0
|
||||
else loadData("settings_def_manga_source", context) ?: 0
|
||||
it.preferDub = loadData("settings_prefer_dub", context) ?: false
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSelected(context: Context, mediaId: Int, data: Selected) {
|
||||
saveData("$mediaId-select", data, context)
|
||||
}
|
||||
|
||||
fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser {
|
||||
val sources = if (isAdult) HAnimeSources else AnimeSources
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
val parser = sources[selected.source]
|
||||
parser.selectDub = selected.preferDub
|
||||
return parser
|
||||
}
|
||||
|
||||
suspend fun getEpisode(context: Context, parser: AnimeParser, id: Int, isAdult: Boolean): Episode? {
|
||||
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
val ep = withTimeoutOrNull(10 * 1000) {
|
||||
tryWithSuspend {
|
||||
val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id))
|
||||
show.sAnime?.let {
|
||||
parser.getLatestEpisode(show.link, show.extra,
|
||||
it, selected.latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ep?.apply {
|
||||
selected.latest = number.toFloat()
|
||||
saveSelected(context, id, selected)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser {
|
||||
val sources = if (isAdult) HMangaSources else MangaSources
|
||||
val selected = loadSelected(context, id, isAdult, false)
|
||||
return sources[selected.source]
|
||||
}
|
||||
|
||||
suspend fun getChapter(context: Context, parser: MangaParser, id: Int, isAdult: Boolean): MangaChapter? {
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
val chp = withTimeoutOrNull(10 * 1000) {
|
||||
tryWithSuspend {
|
||||
val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id))
|
||||
parser.getLatestChapter(show.link, show.extra, selected.latest)
|
||||
}
|
||||
}
|
||||
|
||||
return chp?.apply {
|
||||
selected.latest = number.toFloat()
|
||||
saveSelected(context, id, selected)
|
||||
}
|
||||
}
|
||||
|
||||
data class SubscribeMedia(
|
||||
val isAnime: Boolean,
|
||||
val isAdult: Boolean,
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val image: String?
|
||||
) : java.io.Serializable
|
||||
|
||||
private const val subscriptions = "subscriptions"
|
||||
fun getSubscriptions(context: Context): Map<Int, SubscribeMedia> = loadData(subscriptions, context)
|
||||
?: mapOf<Int, SubscribeMedia>().also { saveData(subscriptions, it, context) }
|
||||
|
||||
fun saveSubscription(context: Context, media: Media, subscribed: Boolean) {
|
||||
val data = loadData<Map<Int, SubscribeMedia>>(subscriptions, context)!!.toMutableMap()
|
||||
if (subscribed) {
|
||||
if (!data.containsKey(media.id)) {
|
||||
val new = SubscribeMedia(
|
||||
media.anime != null,
|
||||
media.isAdult,
|
||||
media.id,
|
||||
media.userPreferredName,
|
||||
media.cover
|
||||
)
|
||||
data[media.id] = new
|
||||
}
|
||||
} else {
|
||||
data.remove(media.id)
|
||||
}
|
||||
saveData(subscriptions, data, context)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.*
|
||||
|
||||
class SubscriptionWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
withContext(Dispatchers.IO){
|
||||
Subscription.perform(context)
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val SUBSCRIPTION_WORK_NAME = "work_subscription"
|
||||
fun enqueue(context: Context) {
|
||||
val curTime = loadData<Int>("subscriptions_time") ?: defaultTime
|
||||
if(timeMinutes[curTime]>0L) {
|
||||
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||
val periodicSyncDataWork = PeriodicWorkRequest.Builder(
|
||||
SubscriptionWorker::class.java, 6, TimeUnit.HOURS
|
||||
).apply {
|
||||
addTag(SUBSCRIPTION_WORK_NAME)
|
||||
setConstraints(constraints)
|
||||
}.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
SUBSCRIPTION_WORK_NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, periodicSyncDataWork
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue