diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e5f63d91..44cdd08c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,6 +22,11 @@ jobs:
distribution: 'temurin'
cache: gradle
+ - name: Load Google Services file
+ env:
+ DATA: ${{ secrets.HA1_GOOGLE_SERVICES_JSON_BASE64 }}
+ run: echo $DATA | base64 -di > app/google-services.json
+
- name: Grant execute permission for gradlew
run: chmod +x gradlew
@@ -30,6 +35,7 @@ jobs:
env:
HA1_KEYSTORE_PASSWORD: ${{ secrets.HA1_KEYSTORE_PASSWORD }}
HA1_GITHUB_TOKEN: ${{ secrets.HA1_GITHUB_TOKEN }}
+ HA1_VERSION_SOURCE: 'ci'
- name: Upload APK
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml
index 1b6da50b..0d764eec 100644
--- a/.github/workflows/pr_check.yml
+++ b/.github/workflows/pr_check.yml
@@ -22,6 +22,11 @@ jobs:
distribution: 'temurin'
cache: gradle
+ - name: Load Google Services file
+ env:
+ DATA: ${{ secrets.HA1_GOOGLE_SERVICES_JSON_BASE64 }}
+ run: echo $DATA | base64 -di > app/google-services.json
+
- name: Grant execute permission for gradlew
run: chmod +x gradlew
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5eea1115..bf384686 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,6 +2,8 @@
import Config.Version.createVersionCode
import Config.Version.createVersionName
+import Config.Version.source
+import Config.isRelease
import Config.lastCommitSha
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
@@ -11,11 +13,10 @@ plugins {
alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize)
alias(libs.plugins.org.jetbrains.kotlin.plugin.serialization)
alias(libs.plugins.com.google.devtools.ksp)
+ alias(libs.plugins.com.google.gms.google.services)
+ alias(libs.plugins.com.google.firebase.crashlytics)
}
-val isRelease: Boolean
- get() = gradle.startParameter.taskNames.any { it.contains("Release") }
-
android {
compileSdk = property("compile.sdk")?.toString()?.toIntOrNull()
@@ -52,6 +53,7 @@ android {
buildConfigField("String", "VERSION_NAME", "\"${versionName}\"")
buildConfigField("int", "VERSION_CODE", "$versionCode")
buildConfigField("String", "HA1_GITHUB_TOKEN", "\"${githubToken}\"")
+ buildConfigField("String", "HA1_VERSION_SOURCE", "\"${source}\"")
buildConfigField("int", "SEARCH_YEAR_RANGE_END", "${Config.thisYear}")
}
@@ -147,6 +149,12 @@ dependencies {
implementation(libs.statelayout)
implementation(libs.circular.reveal.switch)
+ // firebase
+
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.analytics)
+ implementation(libs.firebase.crashlytics)
+
ksp(libs.room.compiler)
coreLibraryDesugaring(libs.desugar.jdk.libs)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index ff59496d..cc606597 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,6 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keepattributes SourceFile, LineNumberTable
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 50e74f9c..87ddea59 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,17 @@
android:foregroundServiceType="dataSync"
tools:node="merge" />
+
+
+
+
(), DrawerListener {
)
}
+ override val fragmentOnAttachListener: (Fragment) -> Unit = { fragment ->
+ logScreenViewEvent(fragment)
+ }
+
/**
* 初始化数据
*/
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SearchActivity.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SearchActivity.kt
index e3a7a8d0..2d0cf5aa 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SearchActivity.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SearchActivity.kt
@@ -18,6 +18,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
@@ -44,6 +45,7 @@ import com.yenaly.han1meviewer.ui.fragment.search.HMultiChoicesDialog
import com.yenaly.han1meviewer.ui.fragment.search.SearchOptionsPopupFragment
import com.yenaly.han1meviewer.ui.viewmodel.MyListViewModel
import com.yenaly.han1meviewer.ui.viewmodel.SearchViewModel
+import com.yenaly.han1meviewer.util.logScreenViewEvent
import com.yenaly.yenaly_libs.base.YenalyActivity
import com.yenaly.yenaly_libs.utils.dp
import com.yenaly.yenaly_libs.utils.intentExtra
@@ -91,6 +93,10 @@ class SearchActivity : YenalyActivity(), StateLayoutMixin
override fun getViewBinding(layoutInflater: LayoutInflater): ActivitySearchBinding =
ActivitySearchBinding.inflate(layoutInflater)
+ override val fragmentOnAttachListener: (Fragment) -> Unit = { fragment ->
+ logScreenViewEvent(fragment)
+ }
+
override fun setUiStyle() {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SettingsActivity.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SettingsActivity.kt
index c630bec8..31f24c73 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SettingsActivity.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/activity/SettingsActivity.kt
@@ -11,11 +11,13 @@ import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.yenaly.han1meviewer.R
import com.yenaly.han1meviewer.databinding.ActivitySettingsBinding
import com.yenaly.han1meviewer.ui.viewmodel.SettingsViewModel
+import com.yenaly.han1meviewer.util.logScreenViewEvent
import com.yenaly.yenaly_libs.base.YenalyActivity
import com.yenaly.yenaly_libs.utils.intentExtra
@@ -41,6 +43,10 @@ class SettingsActivity : YenalyActivity() {
override fun getViewBinding(layoutInflater: LayoutInflater): ActivitySettingsBinding =
ActivitySettingsBinding.inflate(layoutInflater)
+ override val fragmentOnAttachListener: (Fragment) -> Unit = { fragment ->
+ logScreenViewEvent(fragment)
+ }
+
override fun setUiStyle() {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/activity/VideoActivity.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/activity/VideoActivity.kt
index 8e7752f2..b40f37a5 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/activity/VideoActivity.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/activity/VideoActivity.kt
@@ -14,13 +14,19 @@ import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
+import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import cn.jzvd.Jzvd
import coil.load
+import com.google.firebase.analytics.FirebaseAnalytics
+import com.google.firebase.analytics.ktx.analytics
+import com.google.firebase.analytics.logEvent
+import com.google.firebase.ktx.Firebase
import com.yenaly.han1meviewer.COMMENT_TYPE
+import com.yenaly.han1meviewer.FirebaseConstants
import com.yenaly.han1meviewer.Preferences
import com.yenaly.han1meviewer.R
import com.yenaly.han1meviewer.VIDEO_CODE
@@ -38,6 +44,7 @@ import com.yenaly.han1meviewer.ui.view.video.HanimeDataSource
import com.yenaly.han1meviewer.ui.viewmodel.CommentViewModel
import com.yenaly.han1meviewer.ui.viewmodel.VideoViewModel
import com.yenaly.han1meviewer.util.getOrCreateBadgeOnTextViewAt
+import com.yenaly.han1meviewer.util.logScreenViewEvent
import com.yenaly.han1meviewer.util.showAlertDialog
import com.yenaly.yenaly_libs.base.YenalyActivity
import com.yenaly.yenaly_libs.utils.OrientationManager
@@ -69,6 +76,10 @@ class VideoActivity : YenalyActivity(),
override fun getViewBinding(layoutInflater: LayoutInflater): ActivityVideoBinding =
ActivityVideoBinding.inflate(layoutInflater)
+ override val fragmentOnAttachListener: (Fragment) -> Unit = { fragment ->
+ logScreenViewEvent(fragment)
+ }
+
override fun setUiStyle() {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
@@ -265,6 +276,17 @@ class VideoActivity : YenalyActivity(),
prompt = null // 這裏不要給太多負擔,保存就行了沒必要寫comment
)
)
+ // 使用到这里说明用户可能是关键H帧目标用户
+ Firebase.analytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT) {
+ param(
+ FirebaseAnalytics.Param.ITEM_ID,
+ FirebaseConstants.H_KEYFRAMES
+ )
+ param(
+ FirebaseAnalytics.Param.CONTENT_TYPE,
+ FirebaseConstants.H_KEYFRAMES
+ )
+ }
}
setNegativeButton(R.string.cancel, null)
}
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/HKeyframesRvAdapter.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/HKeyframesRvAdapter.kt
index ea137f8e..06e8a65c 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/HKeyframesRvAdapter.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/HKeyframesRvAdapter.kt
@@ -1,12 +1,12 @@
package com.yenaly.han1meviewer.ui.adapter
import android.content.Context
-import android.text.method.LinkMovementMethod
import android.util.Base64
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
+import androidx.core.text.method.LinkMovementMethodCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -68,7 +68,7 @@ class HKeyframesRvAdapter : BaseDifferAdapter(
holder.setText(R.id.tv_title, item.title)
holder.setGone(R.id.btn_edit, item.author != null)
holder.getView(R.id.tv_video_code).apply {
- movementMethod = LinkMovementMethod.getInstance()
+ movementMethod = LinkMovementMethodCompat.getInstance()
text = spannable {
context.getString(R.string.h_keyframe_title_prefix).text()
item.videoCode.span {
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/SharedHKeyframesRvAdapter.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/SharedHKeyframesRvAdapter.kt
index 90a2ed44..9d59d8e2 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/SharedHKeyframesRvAdapter.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/adapter/SharedHKeyframesRvAdapter.kt
@@ -2,9 +2,9 @@ package com.yenaly.han1meviewer.ui.adapter
import android.annotation.SuppressLint
import android.content.Context
-import android.text.method.LinkMovementMethod
import android.view.ViewGroup
import android.widget.TextView
+import androidx.core.text.method.LinkMovementMethodCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -76,7 +76,7 @@ class SharedHKeyframesRvAdapter : BaseDifferAdapter(R.id.tv_video_code).apply {
- movementMethod = LinkMovementMethod.getInstance()
+ movementMethod = LinkMovementMethodCompat.getInstance()
text = spannable {
context.getString(R.string.h_keyframe_title_prefix).text()
item.videoCode.span {
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/search/HMultiChoicesDialog.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/search/HMultiChoicesDialog.kt
index 56336066..969eefa6 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/search/HMultiChoicesDialog.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/search/HMultiChoicesDialog.kt
@@ -90,6 +90,12 @@ class HMultiChoicesDialog(
onReset = action
}
+ fun setOnDismissListener(action: (AlertDialog) -> Unit) {
+ dialog.setOnDismissListener {
+ action(it as AlertDialog)
+ }
+ }
+
fun loadSavedTags(saved: SparseArray>) {
for (i in 0..()
val myListViewModel by activityViewModels()
+ /**
+ * 是否用户真正使用了高级搜索里面的功能
+ *
+ * 用于 Firebase 统计
+ */
+ private var isUserUsed = false
+
private var genres: Array? = null
private var sortOptions: Array? = null
private var durations: Array? = null
@@ -95,11 +107,12 @@ class SearchOptionsPopupFragment :
viewModel.month = null
}
}
+ isUserUsed = true
initOptionsChecked()
}
})
}
- return XPopup.Builder(requireContext()).setOptionsCheckedCallback()
+ return XPopup.Builder(requireContext()).setOptionsCheckedCallback("release_dates")
.borderRadius(POP_UP_BORDER_RADIUS)
.isDarkTheme(true)
.asCustom(popup) as TimePickerPopup
@@ -142,6 +155,7 @@ class SearchOptionsPopupFragment :
setTitle(R.string.type)
setSingleChoiceItems(genres, index) { _, which ->
viewModel.genre = viewModel.genres.getOrNull(which)?.searchKey
+ isUserUsed = true
initOptionsChecked()
}
setPositiveButton(R.string.save, null)
@@ -149,8 +163,8 @@ class SearchOptionsPopupFragment :
viewModel.genre = null
initOptionsChecked()
}
- setOnDismissListener {
- initOptionsChecked()
+ setOnCancelListener {
+ logAdvSearchEvent("genres")
}
}
}
@@ -162,6 +176,7 @@ class SearchOptionsPopupFragment :
return@lc true
}
}
+ // deprecated
binding.brand.apply {
setOnClickListener {
HMultiChoicesDialog(context, R.string.brand, hasSingleItem = true).apply {
@@ -221,12 +236,16 @@ class SearchOptionsPopupFragment :
setOnSaveListener {
viewModel.tagMap = collectCheckedTags()
initOptionsChecked()
+ isUserUsed = true
it.dismiss()
}
setOnResetListener {
clearAllChecks()
initOptionsChecked()
}
+ setOnDismissListener {
+ logAdvSearchEvent("tags")
+ }
}.show()
}
setOnLongClickListener lc@{
@@ -250,6 +269,7 @@ class SearchOptionsPopupFragment :
setTitle(R.string.sort_option)
setSingleChoiceItems(sortOptions, index) { _, which ->
viewModel.sort = viewModel.sortOptions.getOrNull(which)?.searchKey
+ isUserUsed = true
initOptionsChecked()
}
setPositiveButton(R.string.save, null)
@@ -257,8 +277,8 @@ class SearchOptionsPopupFragment :
viewModel.sort = null
initOptionsChecked()
}
- setOnDismissListener {
- initOptionsChecked()
+ setOnCancelListener {
+ logAdvSearchEvent("sort_options")
}
}
}
@@ -270,6 +290,7 @@ class SearchOptionsPopupFragment :
return@lc true
}
}
+ // deprecated
binding.duration.apply {
setOnClickListener {
// durationPopup.show()
@@ -371,11 +392,26 @@ class SearchOptionsPopupFragment :
}
}
- private fun XPopup.Builder.setOptionsCheckedCallback() = apply {
+ private fun XPopup.Builder.setOptionsCheckedCallback(type: String) = apply {
setPopupCallback(object : SimpleCallback() {
override fun beforeDismiss(popupView: BasePopupView?) {
initOptionsChecked()
}
+
+ override fun onDismiss(popupView: BasePopupView?) {
+ logAdvSearchEvent(type)
+ }
})
}
+
+ private fun logAdvSearchEvent(type: String, used: Boolean = isUserUsed) {
+ Firebase.analytics.logEvent(FirebaseConstants.ADV_SEARCH_OPT) {
+ // 判断当前点击类型
+ param(FirebaseAnalytics.Param.CONTENT_TYPE, type)
+ // 判断用户是否真正使用了高级搜索
+ param("used", used.toString())
+ }
+ // 重置状态
+ isUserUsed = false
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/settings/HomeSettingsFragment.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/settings/HomeSettingsFragment.kt
index 3dd30abe..4bedf61b 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/settings/HomeSettingsFragment.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/fragment/settings/HomeSettingsFragment.kt
@@ -7,8 +7,13 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
+import androidx.core.text.method.LinkMovementMethodCompat
+import androidx.core.text.parseAsHtml
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -16,6 +21,8 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat
+import com.google.firebase.Firebase
+import com.google.firebase.analytics.analytics
import com.itxca.spannablex.spannable
import com.yenaly.han1meviewer.BuildConfig
import com.yenaly.han1meviewer.HA1_GITHUB_FORUM_URL
@@ -29,11 +36,13 @@ import com.yenaly.han1meviewer.ui.activity.SettingsActivity
import com.yenaly.han1meviewer.ui.fragment.IToolbarFragment
import com.yenaly.han1meviewer.ui.view.pref.MaterialDialogPreference
import com.yenaly.han1meviewer.ui.viewmodel.AppViewModel
+import com.yenaly.han1meviewer.util.createAlertDialog
import com.yenaly.han1meviewer.util.hanimeVideoLocalFolder
import com.yenaly.han1meviewer.util.showAlertDialog
import com.yenaly.han1meviewer.util.showUpdateDialog
import com.yenaly.yenaly_libs.ActivityManager
import com.yenaly.yenaly_libs.base.preference.LongClickablePreference
+import com.yenaly.yenaly_libs.base.preference.MaterialSwitchPreference
import com.yenaly.yenaly_libs.base.settings.YenalySettingsFragment
import com.yenaly.yenaly_libs.utils.browse
import com.yenaly.yenaly_libs.utils.copyToClipboard
@@ -73,6 +82,8 @@ class HomeSettingsFragment : YenalySettingsFragment(R.xml.settings_home),
const val LAST_UPDATE_POPUP_TIME = "last_update_popup_time"
const val UPDATE_POPUP_INTERVAL_DAYS = "update_popup_interval_days"
const val USE_CI_UPDATE_CHANNEL = "use_ci_update_channel"
+
+ const val USE_ANALYTICS = "use_analytics"
}
private val videoLanguage
@@ -101,6 +112,8 @@ class HomeSettingsFragment : YenalySettingsFragment(R.xml.settings_home),
by safePreference(NETWORK_SETTINGS)
private val applyDeepLinks
by safePreference(APPLY_DEEP_LINKS)
+ private val useAnalytics
+ by safePreference(USE_ANALYTICS)
private var checkUpdateTimes = 0
@@ -255,6 +268,16 @@ class HomeSettingsFragment : YenalySettingsFragment(R.xml.settings_home),
return@setOnPreferenceChangeListener true
}
}
+ useAnalytics.apply {
+ setOnPreferenceChangeListener { _, newValue ->
+ Firebase.analytics.setAnalyticsCollectionEnabled(newValue as Boolean)
+ return@setOnPreferenceChangeListener true
+ }
+ setOnPreferenceLongClickListener {
+ showAnalyticsDialog(it.context)
+ return@setOnPreferenceLongClickListener true
+ }
+ }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -342,6 +365,24 @@ class HomeSettingsFragment : YenalySettingsFragment(R.xml.settings_home),
}
}
+ private fun showAnalyticsDialog(context: Context) {
+ context.createAlertDialog {
+ setTitle(R.string.about_analytics)
+ setMessage(getString(R.string.about_analytics_summary).parseAsHtml())
+ setPositiveButton(R.string.ok, null)
+ }.apply {
+ setOnShowListener {
+ // 另辟蹊径,我不信我访问不到
+ val ad = it as AlertDialog
+ val anchorView = ad.getButton(AlertDialog.BUTTON_POSITIVE)
+ val contentView = anchorView.rootView as ViewGroup
+ contentView.findViewById(android.R.id.message).apply {
+ movementMethod = LinkMovementMethodCompat.getInstance()
+ }
+ }
+ }.show()
+ }
+
private fun generateClearCacheSummary(size: Long): CharSequence {
return spannable {
size.formatFileSize().span {
diff --git a/app/src/main/java/com/yenaly/han1meviewer/ui/viewmodel/AppViewModel.kt b/app/src/main/java/com/yenaly/han1meviewer/ui/viewmodel/AppViewModel.kt
index 747f3ce8..817bf145 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/ui/viewmodel/AppViewModel.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/ui/viewmodel/AppViewModel.kt
@@ -1,7 +1,13 @@
package com.yenaly.han1meviewer.ui.viewmodel
+import android.util.Log
import androidx.lifecycle.viewModelScope
import androidx.work.WorkManager
+import com.google.firebase.crashlytics.crashlytics
+import com.google.firebase.crashlytics.setCustomKeys
+import com.google.firebase.Firebase
+import com.yenaly.han1meviewer.FirebaseConstants
+import com.yenaly.han1meviewer.Preferences
import com.yenaly.han1meviewer.logic.NetworkRepo
import com.yenaly.han1meviewer.logic.model.github.Latest
import com.yenaly.han1meviewer.logic.state.WebsiteState
@@ -27,6 +33,8 @@ object AppViewModel : YenalyViewModel(application), IHCsrfToken {
*/
override var csrfToken: String? = null
+ val loginStateFlow = MutableStateFlow(Preferences.isAlreadyLogin)
+
private val _versionFlow = MutableStateFlow>(WebsiteState.Loading)
val versionFlow = _versionFlow.asStateFlow()
@@ -34,6 +42,15 @@ object AppViewModel : YenalyViewModel(application), IHCsrfToken {
// 取消,防止每次启动都有残留的更新任务
WorkManager.getInstance(application).pruneWork()
+ viewModelScope.launch {
+ loginStateFlow.collect { isLogin ->
+ Log.d("LoginState", "isLogin: $isLogin")
+ Firebase.crashlytics.setCustomKeys {
+ key(FirebaseConstants.LOGIN_STATE, isLogin)
+ }
+ }
+ }
+
viewModelScope.launch(Dispatchers.Main) {
HUpdateWorker.collectOutput(application)
}
diff --git a/app/src/main/java/com/yenaly/han1meviewer/util/Firebases.kt b/app/src/main/java/com/yenaly/han1meviewer/util/Firebases.kt
new file mode 100644
index 00000000..907cd493
--- /dev/null
+++ b/app/src/main/java/com/yenaly/han1meviewer/util/Firebases.kt
@@ -0,0 +1,18 @@
+package com.yenaly.han1meviewer.util
+
+import android.app.Activity
+import androidx.fragment.app.Fragment
+import com.google.firebase.Firebase
+import com.google.firebase.analytics.FirebaseAnalytics
+import com.google.firebase.analytics.analytics
+import com.google.firebase.analytics.logEvent
+
+fun Activity.logScreenViewEvent(fragment: Fragment) {
+ Firebase.analytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
+ // example: MainActivity-HomePageFragment
+ val screenName = this@logScreenViewEvent.javaClass.simpleName +
+ "-" + fragment.javaClass.simpleName
+ param(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
+ param(FirebaseAnalytics.Param.SCREEN_CLASS, fragment.javaClass.name)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/yenaly/han1meviewer/util/TextViews.kt b/app/src/main/java/com/yenaly/han1meviewer/util/TextViews.kt
index 85d8d48f..74e5e0ec 100644
--- a/app/src/main/java/com/yenaly/han1meviewer/util/TextViews.kt
+++ b/app/src/main/java/com/yenaly/han1meviewer/util/TextViews.kt
@@ -7,11 +7,11 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.StaticLayout
import android.text.TextPaint
-import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import androidx.core.text.HtmlCompat
+import androidx.core.text.method.LinkMovementMethodCompat
import com.yenaly.han1meviewer.R
import com.yenaly.yenaly_libs.utils.getThemeColor
import kotlin.math.roundToInt
@@ -36,7 +36,7 @@ fun TextView.setResizableText(
}
return
}
- movementMethod = LinkMovementMethod.getInstance()
+ movementMethod = LinkMovementMethodCompat.getInstance()
// Since we take the string character by character, we don't want to break up the Windows-style
// line endings.
val adjustedText = fullText.replace("\r\n", "\n")
diff --git a/app/src/main/res/drawable/baseline_data_usage_24.xml b/app/src/main/res/drawable/baseline_data_usage_24.xml
new file mode 100644
index 00000000..cd730ad2
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_data_usage_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index bf4ced95..83faf23f 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -335,5 +335,12 @@ Recommendations are as follows:\n
Are you sure you want to redownload?
Subscription
Login expired, automatically logged out
+ After enabling, crash logs will be automatically reported without manual submission
+ Long press to understand the necessity of enabling for developers
+ Automatic Crash Logs Reporting
+ Usage Statistics
+ Privacy
+ Would you mind if the developer collected your device information and your usage? It will help the developer a lot.<br><br>Data analysis service is provided by Google Analytics for Firebase. Its privacy policy can be found at <a href="https://www.google.com/policies/privacy/">https://www.google.com/policies/privacy/</a>.<br><br>If you agree to join data analysis, your device information and your usage will be collected, including but not limited to Android API version, equipment model, language residence, the application version, conversion time, function times. The developer promise that these information will NOT be collected, including phone number, e-mail address, IMEI.<br><br>Your information will not be collected until you agree to join data analysis.<br><br>The collected information will be analyzed by Google Analytics for Firebase. The analysis report can only be viewed by the developer of this application.
+ About Analytics
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 417241ab..cdea77cc 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -341,5 +341,12 @@
确定要重新下载吗?
关注
登录已过期,已经自动登出
+ 开启后,将会自动上报崩溃日志,无须手动提交
+ 长按了解开启对开发者的必要性
+ 崩溃日志自动上报
+ 使用情况统计
+ 隐私
+ 您同意让开发者收集您的设备信息与使用情况吗?这将对开发有很大帮助。<br><br>数据统计服务由 Google Analytics for Firebase 提供,其隐私政策可在 <a href="https://www.google.com/policies/privacy/">https://www.google.com/policies/privacy/</a> 查看。<br><br>若您同意参与数据统计,您的设备信息与应用使用情况将会被记录,其包括但不限于:Android API 版本、设备型号、语言所在地、本应用版本号、会话时长、应用功能使用次数。开发者承诺以下信息并不会被记录:电话号码、电子邮件地址、IMEI。<br><br>在您同意参与数据统计之前,您的信息不会被记录。<br><br>被记录的数据会由 Google Analytics for Firebase 进行分析,分析报表仅可被本应用开发者访问。
+ 关于使用情况统计
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eab8c531..a27035ca 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -345,5 +345,12 @@
確定要重新下載嗎?
訂閱
登入已過期,已自動登出
+ 開啟後,將會自動上報崩潰日誌,無須手動提交
+ 長按了解開啟對開發者的必要性
+ 崩潰日誌自動上報
+ 使用情況統計
+ 隱私
+ 您同意讓開發者收集您的裝置資訊與使用方式嗎?這對開發有很大幫助。<br><br>資料統計服務由 Google Analytics for Firebase 提供,其隱私政策可在 <a href="https://www.google.com/policies/privacy/">https://www.google.com/policies/privacy/</a> 檢視。<br><br>若您同意參與資料統計,您的裝置資訊與應用使用情況將會被記錄,其包括但不限於:Android API 版本、裝置型號、語言所在地、本應用版本號、會話時長、應用功能使用次數。開發者承諾以下資訊並不會被記錄:電話號碼、電子郵件地址、IMEI。<br><br>在您同意參與資料統計之前,您的資訊不會被記錄。<br><br>被記錄的資料會由 Google Analytics for Firebase 進行分析,分析報表僅由此應用程式開發者存取。
+ 關於使用情況統計
diff --git a/app/src/main/res/xml/settings_home.xml b/app/src/main/res/xml/settings_home.xml
index eb32f039..7f3d198c 100644
--- a/app/src/main/res/xml/settings_home.xml
+++ b/app/src/main/res/xml/settings_home.xml
@@ -70,6 +70,17 @@
+
+
+
+
+
+
("clean") {
diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt
index b674d957..b0733808 100644
--- a/buildSrc/src/main/java/Config.kt
+++ b/buildSrc/src/main/java/Config.kt
@@ -12,6 +12,9 @@ import java.time.format.DateTimeFormatter
*/
object Config {
+ val Project.isRelease: Boolean
+ get() = gradle.startParameter.taskNames.any { it.contains("Release") }
+
object Version {
fun Int?.createVersionName(
major: Int,
@@ -28,6 +31,16 @@ object Config {
fun createVersionCode() = LocalDateTime.now(Clock.systemUTC()).format(
DateTimeFormatter.ofPattern("yyMMddHH")
).toInt().also { println("Version Code: $it") }
+
+ /**
+ * 版本来源,用于区分不同的版本
+ *
+ * @return ci 或 official 或 debug
+ */
+ val Project.source: String
+ get() = System.getenv("HA1_VERSION_SOURCE") ?: kotlin.run {
+ return if (isRelease) "official" else "debug"
+ }
}
val Project.lastCommitSha: String
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 529865c1..69edd06f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -63,6 +63,7 @@ circularRevealSwitch = "0.4.6"
desugarJdkLibs = "2.1.2"
asynclayoutinflater = "1.0.0"
flexbox = "3.0.0"
+firebaseBom = "33.4.0"
[libraries]
# Android
@@ -141,6 +142,11 @@ circular-reveal-switch = { group = "com.github.YenalyLiew", name = "CircularReve
leak-canary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakCanary" } # debugImplementation
desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarJdkLibs" }
+# firebase
+firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
+firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
+
[bundles]
android-base = [
@@ -167,4 +173,6 @@ com-android-library = { id = "com.android.library", version.ref = "androidGradle
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
org-jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serializationPlugin" }
-org-jetbrains-kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
\ No newline at end of file
+org-jetbrains-kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
+com-google-gms-google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
+com-google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" }
\ No newline at end of file
diff --git a/yenaly_libs/build.gradle.kts b/yenaly_libs/build.gradle.kts
index 2cf40d6e..5d65a27a 100644
--- a/yenaly_libs/build.gradle.kts
+++ b/yenaly_libs/build.gradle.kts
@@ -47,6 +47,7 @@ dependencies {
implementation(libs.material)
implementation(libs.coroutines.android)
+ implementation(libs.navigation.fragment.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.preference.ktx)
diff --git a/yenaly_libs/proguard-rules.pro b/yenaly_libs/proguard-rules.pro
index ff59496d..cc606597 100644
--- a/yenaly_libs/proguard-rules.pro
+++ b/yenaly_libs/proguard-rules.pro
@@ -18,4 +18,6 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keepattributes SourceFile, LineNumberTable
\ No newline at end of file
diff --git a/yenaly_libs/src/main/AndroidManifest.xml b/yenaly_libs/src/main/AndroidManifest.xml
index 785d3a6c..c597df64 100644
--- a/yenaly_libs/src/main/AndroidManifest.xml
+++ b/yenaly_libs/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
diff --git a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyApplication.kt b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyApplication.kt
index d06b453c..65fe3b05 100644
--- a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyApplication.kt
+++ b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyApplication.kt
@@ -17,11 +17,13 @@ import java.lang.ref.WeakReference
*/
open class YenalyApplication : Application(), Application.ActivityLifecycleCallbacks {
+ open val isDefaultCrashHandlerEnabled: Boolean = true
+
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(this)
// do not forget to register the crash dialog activity!
- if (!isDebugEnabled) YenalyCrashHandler.instance.init(this)
+ if (isDefaultCrashHandlerEnabled && !isDebugEnabled) YenalyCrashHandler.instance.init(this)
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
diff --git a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyInitializer.kt b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyInitializer.kt
index 0bab4963..184423d9 100644
--- a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyInitializer.kt
+++ b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/YenalyInitializer.kt
@@ -3,6 +3,7 @@
package com.yenaly.yenaly_libs.base
import android.content.Context
+import androidx.annotation.CallSuper
import androidx.startup.Initializer
import com.yenaly.yenaly_libs.utils.applicationContext
@@ -12,7 +13,9 @@ import com.yenaly.yenaly_libs.utils.applicationContext
* @Time : 2022/04/21 021 14:04
* @Description : Description...
*/
-class YenalyInitializer : Initializer {
+open class YenalyInitializer : Initializer {
+
+ @CallSuper
override fun create(context: Context) {
applicationContext = context
}
diff --git a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/frame/FrameActivity.kt b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/frame/FrameActivity.kt
index 7cc6dd01..d25f73d5 100644
--- a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/frame/FrameActivity.kt
+++ b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/frame/FrameActivity.kt
@@ -1,6 +1,7 @@
package com.yenaly.yenaly_libs.base.frame
import android.os.Bundle
+import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@@ -11,8 +12,12 @@ import androidx.annotation.MenuRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentOnAttachListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
+import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.yenaly.yenaly_libs.R
import com.yenaly.yenaly_libs.utils.dp
@@ -25,6 +30,7 @@ abstract class FrameActivity : AppCompatActivity() {
private lateinit var loadingDialog: AlertDialog
+ @Deprecated("狗都不用")
@JvmOverloads
open fun showLoadingDialog(
loadingText: String = getString(R.string.yenaly_loading),
@@ -43,6 +49,7 @@ abstract class FrameActivity : AppCompatActivity() {
loadingDialog.window?.setLayout(dialogWidth, dialogHeight)
}
+ @Deprecated("狗都不用")
open fun hideLoadingDialog() {
if (this::loadingDialog.isInitialized) {
loadingDialog.hide()
@@ -55,10 +62,27 @@ abstract class FrameActivity : AppCompatActivity() {
open fun setUiStyle() {
}
+ /**
+ * 能够监听该 Activity 旗下所有 Fragment 的 onAttach 事件
+ */
+ open val fragmentOnAttachListener: ((Fragment) -> Unit)? = null
override fun onCreate(savedInstanceState: Bundle?) {
setUiStyle()
super.onCreate(savedInstanceState)
+ if (fragmentOnAttachListener != null) {
+ supportFragmentManager.addFragmentOnAttachListener(object : FragmentOnAttachListener {
+ override fun onAttachFragment(fm: FragmentManager, fragment: Fragment) {
+ if (fragment is NavHostFragment) {
+ fm.removeFragmentOnAttachListener(this)
+ fragment.childFragmentManager.addFragmentOnAttachListener(this)
+ return
+ }
+ fragmentOnAttachListener?.invoke(fragment)
+ Log.d("FrameActivity", "onAttachFragment: $fragment")
+ }
+ })
+ }
}
diff --git a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/preference/MaterialSwitchPreference.kt b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/preference/MaterialSwitchPreference.kt
index 6b47c39d..269b8c89 100644
--- a/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/preference/MaterialSwitchPreference.kt
+++ b/yenaly_libs/src/main/java/com/yenaly/yenaly_libs/base/preference/MaterialSwitchPreference.kt
@@ -2,13 +2,39 @@ package com.yenaly.yenaly_libs.base.preference
import android.content.Context
import android.util.AttributeSet
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreferenceCompat
import com.yenaly.yenaly_libs.R
open class MaterialSwitchPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
-): SwitchPreferenceCompat(context, attrs) {
+) : SwitchPreferenceCompat(context, attrs) {
init {
widgetLayoutResource = R.layout.yenaly_preference_switch_widget
}
+
+ private var onPreferenceLongClickListener: OnPreferenceLongClickListener? = null
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ holder.itemView.setOnLongClickListener {
+ performLongClick()
+ }
+ }
+
+ fun setOnPreferenceLongClickListener(onPreferenceLongClickListener: OnPreferenceLongClickListener) {
+ this.onPreferenceLongClickListener = onPreferenceLongClickListener
+ }
+
+ private fun performLongClick(): Boolean {
+ if (!isEnabled || !isSelectable) {
+ return false
+ }
+ return onPreferenceLongClickListener?.onPreferenceLongClick(this) ?: false
+ }
+
+ fun interface OnPreferenceLongClickListener {
+ fun onPreferenceLongClick(preference: Preference): Boolean
+ }
}
\ No newline at end of file