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