diff --git a/app/build.gradle b/app/build.gradle index b573ff9..e51064d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "com.masterwok.shrimplesearch" minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion - versionCode 500053 - versionName "2.1.6" + versionCode 500054 + versionName "2.1.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -71,6 +71,9 @@ dependencies { def lifecycle_version = '2.2.0' def dagger_version = '2.28.1' + implementation 'com.google.android.play:core:1.10.0' + implementation 'com.google.android.play:core-ktx:1.8.1' + implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' diff --git a/app/src/main/java/com/masterwok/shrimplesearch/common/Config.kt b/app/src/main/java/com/masterwok/shrimplesearch/common/Config.kt index 1aff17f..51b9291 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/common/Config.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/common/Config.kt @@ -52,3 +52,8 @@ val DEFAULT_USER_SETTINGS = UserSettings( isExitDialogEnabled = true ) +/** + * The amount of times a user must tap a result item before being presented with an in-app review + * workflow. + */ +const val IN_APP_REVIEW_RESULT_ITEM_TAP_COUNT = 6 \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/JackettServiceImpl.kt b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/JackettServiceImpl.kt index 1e05994..8a7988e 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/JackettServiceImpl.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/JackettServiceImpl.kt @@ -2,16 +2,14 @@ package com.masterwok.shrimplesearch.common.data.repositories import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository -import com.masterwok.shrimplesearch.common.utils.notNull -import com.masterwok.xamarininterface.enums.QueryState import com.masterwok.xamarininterface.contracts.IJackettHarness import com.masterwok.xamarininterface.contracts.IJackettHarnessListener +import com.masterwok.xamarininterface.enums.QueryState import com.masterwok.xamarininterface.models.IndexerQueryResult import com.masterwok.xamarininterface.models.Query import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext -import java.lang.ref.WeakReference class JackettServiceImpl constructor( private val jackettHarness: IJackettHarness, @@ -19,7 +17,7 @@ class JackettServiceImpl constructor( private val indexerBlockList: List ) : JackettService { - private val jackettHarnessListener: IJackettHarnessListener = JackettHarnessListener(this) + private val jackettHarnessListener: IJackettHarnessListener = JackettHarnessListener() private val listeners = mutableListOf() @@ -78,29 +76,25 @@ class JackettServiceImpl constructor( listeners.remove(listener) } - private class JackettHarnessListener(jackettService: JackettServiceImpl) : - IJackettHarnessListener { - - private val weakJackettService = WeakReference(jackettService) + private inner class JackettHarnessListener : IJackettHarnessListener { - override fun onIndexersInitialized() = weakJackettService.get().notNull { jackettService -> - jackettService.listeners.forEach { it.onIndexersInitialized() } + override fun onIndexersInitialized() = listeners.forEach { + it.onIndexersInitialized() } - override fun onIndexerInitialized() = weakJackettService.get().notNull { jackettService -> - jackettService.listeners.forEach { it.onIndexerInitialized() } + override fun onIndexerInitialized() = listeners.forEach { + it.onIndexerInitialized() } - override fun onResultsUpdated() = weakJackettService.get().notNull { jackettService -> - if (jackettService.queryState != QueryState.Aborted) { - jackettService.listeners.forEach { it.onResultsUpdated() } + override fun onResultsUpdated() { + if (queryState != QueryState.Aborted) { + listeners.forEach { it.onResultsUpdated() } } } - override fun onQueryStateChange(queryState: QueryState) = - weakJackettService.get().notNull { jackettService -> - jackettService.listeners.forEach { it.onQueryStateChange(queryState) } - } + override fun onQueryStateChange(queryState: QueryState) = listeners.forEach { + it.onQueryStateChange(queryState) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesConfigurationRepository.kt b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesConfigurationRepository.kt new file mode 100644 index 0000000..ab5f0d4 --- /dev/null +++ b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesConfigurationRepository.kt @@ -0,0 +1,37 @@ +package com.masterwok.shrimplesearch.common.data.repositories + +import android.content.Context +import com.masterwok.shrimplesearch.common.data.repositories.contracts.ConfigurationRepository +import com.masterwok.shrimplesearch.di.modules.RepositoryModule +import javax.inject.Inject +import javax.inject.Named + +class SharedPreferencesConfigurationRepository @Inject constructor( + appContext: Context, + @Named(RepositoryModule.NAMED_SHARED_PREFERENCES_NAME) sharedPreferencesName: String +) : ConfigurationRepository { + + private val sharedPreferences = appContext.getSharedPreferences( + sharedPreferencesName, + Context.MODE_PRIVATE + ) + + override suspend fun incrementResultTapCount() { + val resultItemCount = getResultItemTapCount() + + sharedPreferences + .edit() + .putLong(NAME_RESULT_ITEM_TAP_COUNT, resultItemCount + 1L) + .apply() + } + + override suspend fun getResultItemTapCount(): Long = sharedPreferences.getLong( + NAME_RESULT_ITEM_TAP_COUNT, + 0 + ) + + companion object { + private const val NAME_RESULT_ITEM_TAP_COUNT = "configuration.result_item_tap_count" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesUserSettingsRepository.kt b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesUserSettingsRepository.kt index 06ee2a3..5829fef 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesUserSettingsRepository.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/SharedPreferencesUserSettingsRepository.kt @@ -1,21 +1,31 @@ package com.masterwok.shrimplesearch.common.data.repositories import android.content.Context +import android.content.SharedPreferences import com.masterwok.shrimplesearch.R import com.masterwok.shrimplesearch.common.constants.Theme import com.masterwok.shrimplesearch.common.data.models.UserSettings import com.masterwok.shrimplesearch.common.data.models.from import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository +import com.masterwok.shrimplesearch.di.modules.RepositoryModule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Named +import kotlin.system.measureTimeMillis class SharedPreferencesUserSettingsRepository @Inject constructor( appContext: Context, - @Named("shared_preferences_name") sharedPreferencesName: String, - @Named("default_user_settings") private val defaultUserSettings: UserSettings + @Named(RepositoryModule.NAMED_SHARED_PREFERENCES_NAME) sharedPreferencesName: String, + @Named(RepositoryModule.NAMED_DEFAULT_USER_SETTINGS) private val defaultUserSettings: UserSettings ) : UserSettingsRepository { private val sharedPreferences = appContext.getSharedPreferences( @@ -23,6 +33,21 @@ class SharedPreferencesUserSettingsRepository @Inject constructor( Context.MODE_PRIVATE ) + @ExperimentalCoroutinesApi + override fun getUserSettingsAsFlow() = callbackFlow { + send(read()) + + val callback = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == NAME_USER_SETTINGS) { + sendBlocking(read()) + } + } + + sharedPreferences.registerOnSharedPreferenceChangeListener(callback) + + awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback) } + } + override fun read(): UserSettings { val serialized = sharedPreferences .getString(NAME_USER_SETTINGS, null) @@ -38,7 +63,7 @@ class SharedPreferencesUserSettingsRepository @Inject constructor( } } - override fun update(userSettings: UserSettings) { + override suspend fun update(userSettings: UserSettings) = withContext(Dispatchers.IO) { sharedPreferences .edit() .putString(NAME_USER_SETTINGS, Json.encodeToString(userSettings)) diff --git a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/ConfigurationRepository.kt b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/ConfigurationRepository.kt new file mode 100644 index 0000000..e25ddf5 --- /dev/null +++ b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/ConfigurationRepository.kt @@ -0,0 +1,6 @@ +package com.masterwok.shrimplesearch.common.data.repositories.contracts + +interface ConfigurationRepository { + suspend fun incrementResultTapCount() + suspend fun getResultItemTapCount(): Long +} \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/UserSettingsRepository.kt b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/UserSettingsRepository.kt index 6904d52..45e738e 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/UserSettingsRepository.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/common/data/repositories/contracts/UserSettingsRepository.kt @@ -1,15 +1,20 @@ package com.masterwok.shrimplesearch.common.data.repositories.contracts import com.masterwok.shrimplesearch.common.data.models.UserSettings +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow interface UserSettingsRepository { fun read(): UserSettings - fun update(userSettings: UserSettings) + suspend fun update(userSettings: UserSettings) fun getThemeId(): Int fun getSplashThemeId(): Int + @ExperimentalCoroutinesApi + fun getUserSettingsAsFlow(): Flow + } \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/di/modules/RepositoryModule.kt b/app/src/main/java/com/masterwok/shrimplesearch/di/modules/RepositoryModule.kt index 94421db..76aaa45 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/di/modules/RepositoryModule.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/di/modules/RepositoryModule.kt @@ -1,9 +1,12 @@ package com.masterwok.shrimplesearch.di.modules import com.masterwok.shrimplesearch.common.DEFAULT_USER_SETTINGS +import com.masterwok.shrimplesearch.common.IN_APP_REVIEW_RESULT_ITEM_TAP_COUNT import com.masterwok.shrimplesearch.common.SHARED_PREFERENCES_NAME import com.masterwok.shrimplesearch.common.data.models.UserSettings +import com.masterwok.shrimplesearch.common.data.repositories.SharedPreferencesConfigurationRepository import com.masterwok.shrimplesearch.common.data.repositories.SharedPreferencesUserSettingsRepository +import com.masterwok.shrimplesearch.common.data.repositories.contracts.ConfigurationRepository import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository import dagger.Binds import dagger.Module @@ -26,13 +29,29 @@ class RepositoryModule { abstract fun bindSharedPreferencesUserSettingsRepository( sharedPreferencesUserSettingsRepository: SharedPreferencesUserSettingsRepository ): UserSettingsRepository + + @Singleton + @Binds + abstract fun bindConfigurationRepository( + sharedPreferencesUserConfigurationRepository: SharedPreferencesConfigurationRepository + ): ConfigurationRepository } @Provides - @Named("shared_preferences_name") + @Named(NAMED_SHARED_PREFERENCES_NAME) fun provideSharedPreferencesName(): String = SHARED_PREFERENCES_NAME @Provides - @Named("default_user_settings") + @Named(NAMED_DEFAULT_USER_SETTINGS) fun provideDefaultUserSettings(): UserSettings = DEFAULT_USER_SETTINGS + + @Provides + @Named(NAMED_IN_APP_REVIEW_RESULT_ITEM_TAP_COUNT) + fun provideInAppReviewResultItemTapCount(): Int = IN_APP_REVIEW_RESULT_ITEM_TAP_COUNT + + companion object { + const val NAMED_SHARED_PREFERENCES_NAME = "shared_preferences_name" + const val NAMED_DEFAULT_USER_SETTINGS = "default_user_settings" + const val NAMED_IN_APP_REVIEW_RESULT_ITEM_TAP_COUNT = "in_app_review_result_item_tap_count" + } } \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/di/modules/ServiceModule.kt b/app/src/main/java/com/masterwok/shrimplesearch/di/modules/ServiceModule.kt index 80b831f..b3e74f6 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/di/modules/ServiceModule.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/di/modules/ServiceModule.kt @@ -1,5 +1,8 @@ package com.masterwok.shrimplesearch.di.modules +import android.content.Context +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory import com.masterwok.shrimplesearch.common.INDEXER_BLOCK_LIST import com.masterwok.shrimplesearch.common.data.repositories.JackettServiceImpl import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService @@ -44,4 +47,10 @@ class ServiceModule { INDEXER_BLOCK_LIST ) + @Suppress("unused") + @Singleton + @Provides + fun provideReviewManager( + appContext: Context + ): ReviewManager = ReviewManagerFactory.create(appContext) } \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/features/about/fragments/AboutFragment.kt b/app/src/main/java/com/masterwok/shrimplesearch/features/about/fragments/AboutFragment.kt index ea4580c..fd48c75 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/features/about/fragments/AboutFragment.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/features/about/fragments/AboutFragment.kt @@ -60,27 +60,6 @@ class AboutFragment : Fragment() { private fun subscribeToViewComponents() { buttonViewOnGitHub.setOnClickListener { openGitHubProjectUri() } - buttonViewReview.setOnClickListener { openReviewPlayStore() } - buttonViewShare.setOnClickListener { onShareButtonTapped() } - } - - private fun onShareButtonTapped() = activity.notNull { activity -> - analyticService.logEvent(AnalyticEvent.ShareManeki) - - val chooserTitle = activity.getString(R.string.share_chooser_title) - val shareText = activity.getString( - R.string.share_text, - activity.getPlayStoreUri().toString() - ) - - val intent = ShareCompat - .IntentBuilder - .from(activity) - .setType("text/plain") - .setText(shareText) - .intent - - activity.startActivity(Intent.createChooser(intent, chooserTitle)) } private fun openGitHubProjectUri() = context.notNull { context -> @@ -96,15 +75,6 @@ class AboutFragment : Fragment() { } } - private fun openReviewPlayStore() = context.notNull { context -> - try { - context.startPlayStoreActivity() - } catch (exception: ActivityNotFoundException) { - analyticService.logException(exception, "No activity found to handle open GitHub Uri") - presentUnableToOpenPlayStoreDialog() - } - } - private fun presentUnableToOpenPlayStoreDialog() = context.notNull { context -> MaterialDialog(context).show { title(res = R.string.dialog_header_whoops) diff --git a/app/src/main/java/com/masterwok/shrimplesearch/features/query/fragments/IndexerQueryResultsFragment.kt b/app/src/main/java/com/masterwok/shrimplesearch/features/query/fragments/IndexerQueryResultsFragment.kt index 4919a92..bbc1724 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/features/query/fragments/IndexerQueryResultsFragment.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/features/query/fragments/IndexerQueryResultsFragment.kt @@ -6,10 +6,11 @@ import android.content.Intent import android.os.Bundle import android.view.* import androidx.core.app.ShareCompat -import androidx.fragment.app.Fragment import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.observe import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -19,6 +20,7 @@ import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.afollestad.materialdialogs.list.customListAdapter import com.google.android.material.snackbar.Snackbar +import com.google.android.play.core.review.ReviewManager import com.masterwok.shrimplesearch.R import com.masterwok.shrimplesearch.common.constants.AnalyticEvent import com.masterwok.shrimplesearch.common.data.models.UserSettings @@ -34,12 +36,11 @@ import com.masterwok.shrimplesearch.features.query.adapters.MaterialDialogIconLi import com.masterwok.shrimplesearch.features.query.components.SortComponent import com.masterwok.shrimplesearch.features.query.constants.IndexerQueryResultSortBy import com.masterwok.shrimplesearch.features.query.constants.OrderBy -import com.masterwok.xamarininterface.enums.QueryState import com.masterwok.shrimplesearch.features.query.viewmodels.QueryViewModel +import com.masterwok.xamarininterface.enums.QueryState import com.masterwok.xamarininterface.models.QueryResultItem import kotlinx.android.synthetic.main.fragment_indexer_query_results.* -import kotlinx.android.synthetic.main.fragment_indexer_query_results.progressBar -import kotlinx.android.synthetic.main.fragment_indexer_query_results.recyclerView +import kotlinx.coroutines.Job import javax.inject.Inject @@ -51,16 +52,17 @@ class IndexerQueryResultsFragment : Fragment() { @Inject lateinit var analyticService: AnalyticService - private lateinit var linearLayoutManager: LinearLayoutManager + @Inject + lateinit var reviewManager: ReviewManager private val viewModel: QueryViewModel by viewModels(this::requireActivity) { viewModelFactory } - private val queryResultsAdapter = IndexerQueryResultsAdapter { queryResultItem -> - presentBottomSheet(queryResultItem) - } + private val queryResultsAdapter = IndexerQueryResultsAdapter { presentBottomSheet(it) } private val userSettings: UserSettings get() = viewModel.getUserSettings() + private lateinit var linearLayoutManager: LinearLayoutManager + private var snackbarNewResults: Snackbar? = null private fun openQueryResultItem(queryResultItem: QueryResultItem) = activity.notNull { @@ -121,7 +123,6 @@ class IndexerQueryResultsFragment : Fragment() { analyticService.logScreen(IndexerQueryResultsFragment::class.java) } - private fun subscribeToViewComponents() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -157,12 +158,13 @@ class IndexerQueryResultsFragment : Fragment() { adapter = queryResultsAdapter } - viewModel.liveDataQueryState.observe(viewLifecycleOwner, ::onQueryStateChange) + viewModel.liveDataQueryState.observe(owner = viewLifecycleOwner, ::onQueryStateChange) } private fun subscribeToLiveData() { viewModel.liveDataSelectedIndexerQueryResultItem.observe( - viewLifecycleOwner, ::configure + owner = viewLifecycleOwner, + ::configure ) } @@ -243,7 +245,6 @@ class IndexerQueryResultsFragment : Fragment() { .setType("text/plain") .setText(uri.toString()) .startChooser() - } private fun copyQueryResultItem(queryResultItem: QueryResultItem) = @@ -254,6 +255,21 @@ class IndexerQueryResultsFragment : Fragment() { context.copyToClipboard(CLIPBOARD_LABEL, uri.toString()) } + private fun attemptToPresentInAppReview( + deferredAction: () -> Unit + ): Job = lifecycleScope.launchWhenResumed { + viewModel.incrementResultItemTapCount() + + if (viewModel.reviewInfo == null || !viewModel.shouldAttemptToPresentInAppReview()) { + deferredAction() + return@launchWhenResumed + } + + reviewManager + .launchReviewFlow(requireActivity(), checkNotNull(viewModel.reviewInfo)) + .addOnCompleteListener { deferredAction() } + } + private fun presentBottomSheet(queryResultItem: QueryResultItem) = context.notNull { context -> val hasMagnetUri = queryResultItem .linkInfo @@ -266,25 +282,31 @@ class IndexerQueryResultsFragment : Fragment() { R.drawable.ic_baseline_share_24, if (hasMagnetUri) R.string.share_magnet else R.string.share_link ) { - analyticService.logEvent(AnalyticEvent.ShareResult) - shareQueryResultItem(queryResultItem) dismiss() + + analyticService.logEvent(AnalyticEvent.ShareResult) + + attemptToPresentInAppReview { shareQueryResultItem(queryResultItem) } }, MaterialDialogIconListItemAdapter.Item( R.drawable.ic_content_copy_black_24dp, if (hasMagnetUri) R.string.copy_magnet else R.string.copy_torrent ) { - analyticService.logEvent(AnalyticEvent.CopyResult) - copyQueryResultItem(queryResultItem) dismiss() + + analyticService.logEvent(AnalyticEvent.CopyResult) + + attemptToPresentInAppReview { copyQueryResultItem(queryResultItem) } }, MaterialDialogIconListItemAdapter.Item( R.drawable.ic_baseline_open_in_new_24, if (hasMagnetUri) R.string.open_magnet else R.string.open_link ) { - analyticService.logEvent(AnalyticEvent.OpenResult) - openQueryResultItem(queryResultItem) dismiss() + + analyticService.logEvent(AnalyticEvent.OpenResult) + + attemptToPresentInAppReview { openQueryResultItem(queryResultItem) } } ) diff --git a/app/src/main/java/com/masterwok/shrimplesearch/features/query/viewmodels/QueryViewModel.kt b/app/src/main/java/com/masterwok/shrimplesearch/features/query/viewmodels/QueryViewModel.kt index 7506ef5..c993ef9 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/features/query/viewmodels/QueryViewModel.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/features/query/viewmodels/QueryViewModel.kt @@ -1,11 +1,16 @@ package com.masterwok.shrimplesearch.features.query.viewmodels import androidx.lifecycle.* +import com.google.android.play.core.ktx.requestReview +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager import com.masterwok.shrimplesearch.common.constants.AnalyticEvent import com.masterwok.shrimplesearch.common.data.models.UserSettings +import com.masterwok.shrimplesearch.common.data.repositories.contracts.ConfigurationRepository import com.masterwok.shrimplesearch.common.data.repositories.contracts.JackettService import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository import com.masterwok.shrimplesearch.common.data.services.contracts.AnalyticService +import com.masterwok.shrimplesearch.di.modules.RepositoryModule import com.masterwok.shrimplesearch.features.query.constants.IndexerQueryResultSortBy import com.masterwok.shrimplesearch.features.query.constants.OrderBy import com.masterwok.shrimplesearch.features.query.constants.QueryResultSortBy @@ -16,12 +21,16 @@ import com.masterwok.xamarininterface.models.Query import com.masterwok.xamarininterface.models.QueryResultItem import kotlinx.coroutines.launch import javax.inject.Inject +import javax.inject.Named class QueryViewModel @Inject constructor( private val jackettService: JackettService, private val userSettingsRepository: UserSettingsRepository, - private val analyticService: AnalyticService + private val analyticService: AnalyticService, + private val configurationRepository: ConfigurationRepository, + private val reviewManager: ReviewManager, + @Named(RepositoryModule.NAMED_IN_APP_REVIEW_RESULT_ITEM_TAP_COUNT) private val inAppReviewResultItemTapCount: Int ) : ViewModel(), JackettService.Listener { private val _liveDataIndexerQueryResults = MutableLiveData( @@ -56,10 +65,32 @@ class QueryViewModel @Inject constructor( addSource(_liveDataIndexerQueryResults) { value = getIndexerQueryResults() } } + var reviewInfo: ReviewInfo? = null + private set + init { jackettService.addListener(this) + + viewModelScope.launch { + reviewInfo = requestReviewInfo() + } + } + + private suspend fun requestReviewInfo(): ReviewInfo? = try { + reviewManager.requestReview() + } catch (exception: Exception) { + analyticService.logException(exception, "Failed to request review information.") + null } + suspend fun shouldAttemptToPresentInAppReview(): Boolean { + val count = configurationRepository.getResultItemTapCount() + + return if (count == 0L) false else count % inAppReviewResultItemTapCount == 0L + } + + suspend fun incrementResultItemTapCount() = configurationRepository.incrementResultTapCount() + override fun onCleared() { jackettService.removeListener(this@QueryViewModel) diff --git a/app/src/main/java/com/masterwok/shrimplesearch/features/settings/fragments/SettingsFragment.kt b/app/src/main/java/com/masterwok/shrimplesearch/features/settings/fragments/SettingsFragment.kt index d225a2f..4897c32 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/features/settings/fragments/SettingsFragment.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/features/settings/fragments/SettingsFragment.kt @@ -27,19 +27,25 @@ class SettingsFragment : Fragment() { private val viewModel: SettingsViewModel by viewModels { viewModelFactory } + private val currentSettings get() = checkNotNull(viewModel.liveDataUserSettings.value) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate( - R.layout.fragment_settings, container, false + R.layout.fragment_settings, + container, + false ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - configure(viewModel.readUserSettings()) + subscribeToLiveData() + } - subscribeToViewComponents() + private fun subscribeToLiveData() { + viewModel.liveDataUserSettings.observe(viewLifecycleOwner, this::configure) } override fun onResume() { @@ -55,10 +61,15 @@ class SettingsFragment : Fragment() { } private fun configure(userSettings: UserSettings) { + unsubscribeFromViewComponents() configureThemeSelection(userSettings.theme) + configureSwitches(userSettings) + subscribeToViewComponents() + } - switchScrollToTop.isChecked = checkNotNull(userSettings.isScrollToTopNotificationsEnabled) - switchMagnet.isChecked = checkNotNull(userSettings.isOnlyMagnetQueryResultItemsEnabled) + private fun configureSwitches(userSettings: UserSettings) { + switchScrollToTop. isChecked = checkNotNull(userSettings.isScrollToTopNotificationsEnabled) + switchMagnet. isChecked = checkNotNull(userSettings.isOnlyMagnetQueryResultItemsEnabled) } private fun configureThemeSelection(theme: Theme): Unit = when (theme) { @@ -66,6 +77,11 @@ class SettingsFragment : Fragment() { Theme.Oled -> radioButtonThemeOled.isChecked = true } + private fun unsubscribeFromViewComponents() { + switchScrollToTop.setOnCheckedChangeListener(null) + switchMagnet.setOnCheckedChangeListener(null) + } + private fun subscribeToViewComponents() { subscribeToThemeRadioGroup() subscribeToScrollToTopNotificationsSwitch() @@ -74,38 +90,33 @@ class SettingsFragment : Fragment() { private fun subscribeToScrollToTopNotificationsSwitch() { switchScrollToTop.setOnCheckedChangeListener { _, isChecked -> viewModel.updateUserSettings( - viewModel.readUserSettings().copy( - isScrollToTopNotificationsEnabled = isChecked - ) + currentSettings.copy(isScrollToTopNotificationsEnabled = isChecked) ) } switchMagnet.setOnCheckedChangeListener { _, isChecked -> viewModel.updateUserSettings( - viewModel.readUserSettings().copy( - isOnlyMagnetQueryResultItemsEnabled = isChecked - ) + currentSettings.copy(isOnlyMagnetQueryResultItemsEnabled = isChecked) ) } } private fun subscribeToThemeRadioGroup() = radioGroupTheme.setOnCheckedChangeListener { _, _ -> + val oldSettings = checkNotNull(viewModel.liveDataUserSettings.value) val selectedThemeId: Int - val userSettings = viewModel - .readUserSettings() - .copy( - theme = when (radioGroupTheme.checkedRadioButtonId) { - R.id.radioButtonThemeLight -> { - selectedThemeId = R.style.AppTheme - Theme.Light - } - R.id.radioButtonThemeOled -> { - selectedThemeId = R.style.AppTheme_Oled - Theme.Oled - } - else -> error("Theme not registered on settings.") + val userSettings = oldSettings.copy( + theme = when (radioGroupTheme.checkedRadioButtonId) { + R.id.radioButtonThemeLight -> { + selectedThemeId = R.style.AppTheme + Theme.Light } - ) + R.id.radioButtonThemeOled -> { + selectedThemeId = R.style.AppTheme_Oled + Theme.Oled + } + else -> error("Theme not registered on settings.") + } + ) viewModel.updateUserSettings(userSettings.copy()) diff --git a/app/src/main/java/com/masterwok/shrimplesearch/features/settings/viewmodels/SettingsViewModel.kt b/app/src/main/java/com/masterwok/shrimplesearch/features/settings/viewmodels/SettingsViewModel.kt index 6f04fe6..8cbc098 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/features/settings/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/features/settings/viewmodels/SettingsViewModel.kt @@ -1,16 +1,25 @@ package com.masterwok.shrimplesearch.features.settings.viewmodels import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import com.masterwok.shrimplesearch.common.data.models.UserSettings import com.masterwok.shrimplesearch.common.data.repositories.contracts.UserSettingsRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import javax.inject.Inject +@ExperimentalCoroutinesApi class SettingsViewModel @Inject constructor( private val userSettingsRepository: UserSettingsRepository ) : ViewModel() { - fun readUserSettings(): UserSettings = userSettingsRepository.read() + val liveDataUserSettings = userSettingsRepository + .getUserSettingsAsFlow() + .asLiveData(viewModelScope.coroutineContext) - fun updateUserSettings(userSettings: UserSettings) = userSettingsRepository.update(userSettings) + fun updateUserSettings(userSettings: UserSettings) = viewModelScope.launch { + userSettingsRepository.update(userSettings) + } } \ No newline at end of file diff --git a/app/src/main/java/com/masterwok/shrimplesearch/main/MainActivityViewModel.kt b/app/src/main/java/com/masterwok/shrimplesearch/main/MainActivityViewModel.kt index d4b3c54..ea96843 100644 --- a/app/src/main/java/com/masterwok/shrimplesearch/main/MainActivityViewModel.kt +++ b/app/src/main/java/com/masterwok/shrimplesearch/main/MainActivityViewModel.kt @@ -19,11 +19,13 @@ class MainActivityViewModel @Inject constructor( .read() .isExitDialogEnabled - fun disableExitDialog() = userSettingsRepository.update( - userSettingsRepository - .read() - .copy(isExitDialogEnabled = false) - ) + fun disableExitDialog() = viewModelScope.launch { + userSettingsRepository.update( + userSettingsRepository + .read() + .copy(isExitDialogEnabled = false) + ) + } fun cancelQuery() = viewModelScope.launch { jackettService.cancelQuery() diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index a065635..5673583 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -70,68 +70,4 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textViewAboutVersion" /> - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 960dc44..a13448e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,14 +7,14 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files - classpath 'com.google.gms:google-services:4.3.3' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.1' + classpath 'com.google.gms:google-services:4.3.5' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" }