diff --git a/gradle.properties b/gradle.properties index c6b9841..3e04b6f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,6 @@ #Fri Jul 07 00:04:56 CST 2017 org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 #org.gradle.jvmargs=-Xmx1536m -android.enableD8=true android.useAndroidX=true android.enableJetifier=true android.debug.obsoleteApi=true diff --git a/xkcd/build.gradle b/xkcd/build.gradle index 571c440..ca6ed35 100644 --- a/xkcd/build.gradle +++ b/xkcd/build.gradle @@ -69,7 +69,7 @@ android { defaultConfig { applicationId "xyz.jienan.xkcd" minSdkVersion 16 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 76 versionName "2.8.2" vectorDrawables.useSupportLibrary = true @@ -218,7 +218,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.8.0-beta02' implementation 'androidx.work:work-rxjava2:2.8.0-beta02' - proprietaryImplementation 'com.google.firebase:firebase-analytics:21.2.0' + proprietaryImplementation 'com.google.firebase:firebase-analytics:19.0.2' //noinspection GradleDependency proprietaryImplementation('com.google.firebase:firebase-messaging:22.0.0') { // 23.0.0 will require API level 19, Android 4.4 higher. diff --git a/xkcd/src/main/AndroidManifest.xml b/xkcd/src/main/AndroidManifest.xml index bc443aa..2fb504c 100644 --- a/xkcd/src/main/AndroidManifest.xml +++ b/xkcd/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + val archive: Single @GET("{article_id}/") - fun getArticle(@Path("article_id") id: Long): Observable + fun getArticle(@Path("article_id") id: Long): Single @FormUrlEncoded @POST - fun thumbsUpWhatIf(@Url url: String, @Field("what_if_id") whatIfId: Int): Observable + fun thumbsUpWhatIf(@Url url: String, @Field("what_if_id") whatIfId: Int): Single @Headers("$HEADER_CACHEABLE: 60") @GET - fun getTopWhatIfs(@Url url: String, @Query("sortby") sortby: String): Observable> + fun getTopWhatIfs(@Url url: String, @Query("sortby") sortby: String): Single> } diff --git a/xkcd/src/main/java/xyz/jienan/xkcd/comics/fragment/SingleComicFragment.kt b/xkcd/src/main/java/xyz/jienan/xkcd/comics/fragment/SingleComicFragment.kt index 11844cd..6b48c58 100644 --- a/xkcd/src/main/java/xyz/jienan/xkcd/comics/fragment/SingleComicFragment.kt +++ b/xkcd/src/main/java/xyz/jienan/xkcd/comics/fragment/SingleComicFragment.kt @@ -201,7 +201,7 @@ class SingleComicFragment : BaseFragment(), SingleComicContract.View { if (currentPic == null) { false } else { - showInfoDialog(showAltText = comicOnly) + showInfoDialog(showAltText = comicOnly && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) it.performHapticFeedback(LONG_PRESS) true } diff --git a/xkcd/src/main/java/xyz/jienan/xkcd/list/presenter/WhatIfListPresenter.kt b/xkcd/src/main/java/xyz/jienan/xkcd/list/presenter/WhatIfListPresenter.kt index cce4dc2..6712486 100644 --- a/xkcd/src/main/java/xyz/jienan/xkcd/list/presenter/WhatIfListPresenter.kt +++ b/xkcd/src/main/java/xyz/jienan/xkcd/list/presenter/WhatIfListPresenter.kt @@ -1,7 +1,9 @@ package xyz.jienan.xkcd.list.presenter import android.view.View +import io.reactivex.Flowable import io.reactivex.Observable +import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import timber.log.Timber @@ -39,14 +41,14 @@ class WhatIfListPresenter(private val view: WhatIfListContract.View) : ListPrese WhatIfModel.thumbUpList .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { view.setLoading(true) } - .flatMapSingle { whatIfArticles -> - Observable.fromIterable(whatIfArticles) + .flatMap { whatIfArticles -> + Flowable.fromIterable(whatIfArticles) .map { it.num } .filter { num -> num <= latest } .map { WhatIfModel.loadArticleFromDB(it)!! } .toList() } - .doOnNext { view.setLoading(false) } + .doOnSuccess { view.setLoading(false) } .subscribe({ view.updateData(it) }, { e -> Timber.e(e, "get top what if error") }) .also { compositeDisposable.add(it) } diff --git a/xkcd/src/main/java/xyz/jienan/xkcd/model/WhatIfModel.kt b/xkcd/src/main/java/xyz/jienan/xkcd/model/WhatIfModel.kt index ddcde91..d5db678 100644 --- a/xkcd/src/main/java/xyz/jienan/xkcd/model/WhatIfModel.kt +++ b/xkcd/src/main/java/xyz/jienan/xkcd/model/WhatIfModel.kt @@ -2,6 +2,8 @@ package xyz.jienan.xkcd.model import android.text.TextUtils import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers @@ -26,7 +28,7 @@ object WhatIfModel { val favWhatIf: List get() = BoxManager.favWhatIf - val thumbUpList: Observable> + val thumbUpList: Single> get() = whatIfAPI.getTopWhatIfs(WHAT_IF_TOP, XKCD_TOP_SORT_BY_THUMB_UP) .subscribeOn(Schedulers.io()) @@ -52,7 +54,6 @@ object WhatIfModel { fun loadAllWhatIf(): Single> { return whatIfAPI.archive .subscribeOn(Schedulers.io()) - .singleOrError() .map { WhatIfArticleUtil.getArticlesFromArchive(it) } .map { BoxManager.updateAndSaveWhatIf(it.toMutableList()) } } @@ -65,7 +66,6 @@ object WhatIfModel { fun loadArticle(id: Long): Single { return loadArticleContentFromDB(id) .switchIfEmpty(loadArticleFromAPI(id)) - .singleOrError() .observeOn(AndroidSchedulers.mainThread()) } @@ -90,7 +90,7 @@ object WhatIfModel { * @param index * @return thumb up count */ - fun thumbsUp(index: Long): Observable { + fun thumbsUp(index: Long): Single { return whatIfAPI.thumbsUpWhatIf(WHAT_IF_THUMBS_UP, index.toInt()) .subscribeOn(Schedulers.io()) .doOnSubscribe { BoxManager.likeWhatIf(index) } @@ -98,25 +98,29 @@ object WhatIfModel { } fun fastLoadWhatIfs(index: Long): Completable = - Observable.just(index) - .flatMap { + Flowable.just(index) + .flatMap { if (index == 0L) { loadLatest().map { it.num } - .flatMapObservable { Observable.rangeLong(1, it) } + .toFlowable() + .flatMap { Flowable.rangeLong(1, it) } } else { - Observable.rangeLong(1, index) + Flowable.rangeLong(1, index) } - }.flatMap { + }.flatMapSingle { if (loadArticleFromDB(it) == null || loadArticleFromDB(it)!!.content.isNullOrBlank()) { loadArticleFromAPI(it) + .map { index } + .onErrorReturnItem(index) } else { - Observable.just(it) + Single.just(it) } } .toList() .ignoreElement() + .onErrorComplete() - private fun loadArticleFromAPI(id: Long): Observable { + private fun loadArticleFromAPI(id: Long): Single { return whatIfAPI.getArticle(id) .subscribeOn(Schedulers.io()) .map { WhatIfArticleUtil.getArticleFromHtml(it) } @@ -124,8 +128,8 @@ object WhatIfModel { .map { BoxManager.updateAndSaveWhatIf(id, it) } } - private fun loadArticleContentFromDB(id: Long): Observable { + private fun loadArticleContentFromDB(id: Long): Maybe { val article = BoxManager.getWhatIf(id) - return if (article == null || TextUtils.isEmpty(article.content)) Observable.empty() else Observable.just(article) + return if (article == null || TextUtils.isEmpty(article.content)) Maybe.empty() else Maybe.just(article) } } diff --git a/xkcd/src/main/java/xyz/jienan/xkcd/settings/SettingsFragment.kt b/xkcd/src/main/java/xyz/jienan/xkcd/settings/SettingsFragment.kt index e6f376a..4f81a12 100644 --- a/xkcd/src/main/java/xyz/jienan/xkcd/settings/SettingsFragment.kt +++ b/xkcd/src/main/java/xyz/jienan/xkcd/settings/SettingsFragment.kt @@ -3,15 +3,36 @@ package xyz.jienan.xkcd.settings import android.content.Context import android.content.Intent import android.content.res.Configuration +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment +import android.provider.Settings import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatDelegate -import androidx.preference.* -import androidx.work.* +import androidx.core.app.NotificationManagerCompat +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager import timber.log.Timber -import xyz.jienan.xkcd.Const.* +import xyz.jienan.xkcd.Const.PREF_ARROW +import xyz.jienan.xkcd.Const.PREF_DARK_THEME +import xyz.jienan.xkcd.Const.PREF_FONT +import xyz.jienan.xkcd.Const.PREF_NOTIFICATION +import xyz.jienan.xkcd.Const.PREF_XKCD_STORAGE +import xyz.jienan.xkcd.Const.PREF_XKCD_TRANSLATION +import xyz.jienan.xkcd.Const.PREF_ZOOM import xyz.jienan.xkcd.R import xyz.jienan.xkcd.model.WhatIfModel import xyz.jienan.xkcd.model.XkcdModel @@ -33,10 +54,20 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan private val storagePref by lazy { findPreference(PREF_XKCD_STORAGE) } + private val notificationPref by lazy { findPreference(PREF_NOTIFICATION) } + + private val notificationManager by lazy { NotificationManagerCompat.from(requireContext()) } + + private lateinit var permissionLauncher: ActivityResultLauncher + + // android.Manifest.permission.POST_NOTIFICATIONS + private val notificationPermission = "android.permission.POST_NOTIFICATIONS" + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { Timber.d("onCreatePreferences") setPreferencesFromResource(R.xml.prefs, rootKey) + findPreference(PREF_FONT)?.onPreferenceChangeListener = this arrowPref?.summary = resources.getQuantityString(R.plurals.pref_arrow_summary, @@ -46,6 +77,7 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan darkPref?.onPreferenceChangeListener = this storagePref?.onPreferenceChangeListener = this + notificationPref?.onPreferenceChangeListener = this if (XkcdModel.localizedUrl.isBlank()) { findPreference("pref_key_xkcd")?.removePreference(findPreference(PREF_XKCD_TRANSLATION)!!) @@ -64,6 +96,13 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan startActivity(Intent(context, ManageSpaceActivity::class.java)) true } + permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + notificationPref?.isChecked = isGranted + Timber.d("isGranted $isGranted") + if (!crashFreeShouldShowRequestPermissionRationale(notificationPermission) && !isGranted) { + notificationPref?.setSummaryOff(R.string.pref_notification_summary_enable_from_settings) + } + } } override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { @@ -94,10 +133,52 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan ToastUtils.showToast(requireContext(), getString(R.string.pref_xkcd_storage_toast), duration = Toast.LENGTH_LONG) return true } + PREF_NOTIFICATION -> { + Timber.d("$PREF_NOTIFICATION, set to $newValue") + + val notificationEnabled = notificationManager.areNotificationsEnabled() + + if (notificationEnabled) { + (preference as SwitchPreferenceCompat).isChecked = (newValue == true) + } else { + if (newValue == true) { + if (crashFreeShouldShowRequestPermissionRationale(notificationPermission)) { + permissionLauncher.launch(notificationPermission) + } else { + showNotificationSettings(requireContext()) + } + } else { + (preference as SwitchPreferenceCompat).isChecked = false + } + } + } } return false } + override fun onResume() { + super.onResume() + if (notificationManager.areNotificationsEnabled() && notificationPref?.isChecked == false) { + notificationPref?.setSummaryOff(R.string.pref_notification_summary_disabled) + } else if (!notificationManager.areNotificationsEnabled()) { + notificationPref?.isChecked = false + if (crashFreeShouldShowRequestPermissionRationale(notificationPermission)) { + notificationPref?.setSummaryOff(R.string.pref_notification_summary_disabled) + } else { + notificationPref?.setSummaryOff(R.string.pref_notification_summary_enable_from_settings) + } + } + } + + @Suppress("SameParameterValue") + private fun crashFreeShouldShowRequestPermissionRationale(permission: String): Boolean { + return try { + shouldShowRequestPermissionRationale(permission) + } catch (e: Exception) { + false + } + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -131,6 +212,36 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan } } + private fun showNotificationSettings(context: Context, channelId: String? = null) { + val notificationSettingsIntent = when { + // TODO test 26 and above + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O /*26*/ -> Intent().apply { + action = when (channelId) { + null -> Settings.ACTION_APP_NOTIFICATION_SETTINGS + else -> Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS + } + channelId?.let { putExtra(Settings.EXTRA_CHANNEL_ID, it) } + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P /*28*/) { + flags += Intent.FLAG_ACTIVITY_NEW_TASK + } + } + // TODO test 21 - 25 + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /*21*/ -> Intent().apply { + action = "android.settings.APP_NOTIFICATION_SETTINGS" + putExtra("app_package", context.packageName) + putExtra("app_uid", context.applicationInfo.uid) + } + // TODO test 20 and below + else -> Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addCategory(Intent.CATEGORY_DEFAULT) + data = Uri.parse("package:" + context.packageName) + } + } + startActivity(notificationSettingsIntent) + } + companion object { private const val RES_DARK = 101 } diff --git a/xkcd/src/main/java/xyz/jienan/xkcd/ui/NotificationUtils.kt b/xkcd/src/main/java/xyz/jienan/xkcd/ui/NotificationUtils.kt index 46b1181..650f248 100644 --- a/xkcd/src/main/java/xyz/jienan/xkcd/ui/NotificationUtils.kt +++ b/xkcd/src/main/java/xyz/jienan/xkcd/ui/NotificationUtils.kt @@ -12,7 +12,9 @@ import android.media.RingtoneManager import android.os.Build import android.text.TextUtils import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.graphics.createBitmap +import androidx.preference.PreferenceManager import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import io.reactivex.Maybe @@ -20,6 +22,7 @@ import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import timber.log.Timber +import xyz.jienan.xkcd.Const import xyz.jienan.xkcd.Const.* import xyz.jienan.xkcd.R import xyz.jienan.xkcd.home.MainActivity @@ -58,6 +61,16 @@ object NotificationUtils { @SuppressLint("CheckResult") fun showNotification(context: Context, article: WhatIfArticle) { + + val allowNotification = PreferenceManager + .getDefaultSharedPreferences(context) + .getBoolean(PREF_NOTIFICATION, true) + && NotificationManagerCompat.from(context).areNotificationsEnabled() + + if (!allowNotification) { + return + } + val width = 400 val height = 400 val num = article.num.toInt() diff --git a/xkcd/src/main/java/xyz/jienan/xkcd/whatif/presenter/WhatIfMainPresenter.kt b/xkcd/src/main/java/xyz/jienan/xkcd/whatif/presenter/WhatIfMainPresenter.kt index e7d3780..882f558 100644 --- a/xkcd/src/main/java/xyz/jienan/xkcd/whatif/presenter/WhatIfMainPresenter.kt +++ b/xkcd/src/main/java/xyz/jienan/xkcd/whatif/presenter/WhatIfMainPresenter.kt @@ -36,7 +36,6 @@ class WhatIfMainPresenter constructor(private val view: WhatIfMainContract.View) } WhatIfModel.thumbsUp(currentIndex) - .debounce(100, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ view.showThumbUpCount(it) }, { e -> Timber.e(e, "Thumbs up failed") }) diff --git a/xkcd/src/main/res/values-b+zh/strings.xml b/xkcd/src/main/res/values-b+zh/strings.xml index e703882..f0cc6d7 100644 --- a/xkcd/src/main/res/values-b+zh/strings.xml +++ b/xkcd/src/main/res/values-b+zh/strings.xml @@ -127,4 +127,9 @@ 数据可用: %d xkcd extra what if 文章 + + 通知 + 点击前往设置开启应用通知 + 每周一、三、五,4月1日,以及大概每四年收到更新通知 + 开启后,在有更新时会收到通知 \ No newline at end of file diff --git a/xkcd/src/main/res/values/strings.xml b/xkcd/src/main/res/values/strings.xml index f813ea6..2747c4d 100644 --- a/xkcd/src/main/res/values/strings.xml +++ b/xkcd/src/main/res/values/strings.xml @@ -127,6 +127,13 @@ info ready: %d xkcd extra what if articles + Love & Archive Archive comics and articles when added to favorites + Archive + + Notification + Enable notification from system settings + Receive notifications on Monday, Wednesday, Friday, April 1, and every 4 years. + Turn ON to get notified of latest xkcd and what if diff --git a/xkcd/src/main/res/xml/prefs.xml b/xkcd/src/main/res/xml/prefs.xml index 24efca7..e089deb 100644 --- a/xkcd/src/main/res/xml/prefs.xml +++ b/xkcd/src/main/res/xml/prefs.xml @@ -59,6 +59,15 @@ + +