From ac8868d56a66e4266c9dc91cd8627b3c950d8af5 Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Sun, 16 Aug 2020 20:28:16 -0300 Subject: [PATCH] Add support to Google stuff and small optimizations --- .gitignore | 2 + app/build.gradle | 5 +- .../java/dev/lucasnlm/antimine/DeepLink.kt | 2 - .../dev/lucasnlm/antimine/GameActivity.kt | 68 +++- .../antimine/control/ControlDialogFragment.kt | 10 +- .../antimine/control/models/ControlDetails.kt | 1 - .../antimine/control/view/ControlItemView.kt | 3 - .../control/viewmodel/ControlState.kt | 4 +- .../control/viewmodel/ControlViewModel.kt | 30 +- .../dev/lucasnlm/antimine/di/AppModule.kt | 35 ++ .../antimine/instant/InstantAppManager.kt | 6 +- .../antimine/level/view/LevelFragment.kt | 81 ++-- .../level/viewmodel/EndGameDialogViewModel.kt | 1 + .../playgames/PlayGamesDialogFragment.kt | 108 ++++++ .../antimine/playgames/model/PlayGamesItem.kt | 12 + .../playgames/viewmodel/PlayGamesEvent.kt | 6 + .../playgames/viewmodel/PlayGamesViewModel.kt | 29 ++ .../support/SupportAppDialogFragment.kt | 40 ++ .../lucasnlm/antimine/theme/ThemeActivity.kt | 13 + .../main/res/drawable/emoji_hugging_face.xml | 359 ++++++++++++++++++ .../main/res/drawable/games_achievements.png | Bin 0 -> 15636 bytes .../main/res/drawable/games_controller.png | Bin 0 -> 12658 bytes .../main/res/drawable/games_leaderboards.png | Bin 0 -> 11158 bytes .../main/res/layout/dialog_custom_game.xml | 1 - app/src/main/res/layout/dialog_payments.xml | 47 +++ app/src/main/res/layout/view_control_item.xml | 12 +- .../res/layout/view_play_games_button.xml | 34 ++ app/src/main/res/menu/nav_menu.xml | 8 + .../control/viewmodel/ControlViewModelTest.kt | 2 +- .../lucasnlm/antimine/di/TestLevelModule.kt | 2 +- build.gradle | 3 +- common/build.gradle | 9 +- .../antimine/common/level/view/AreaAdapter.kt | 14 +- .../common/level/view/CommonLevelFragment.kt | 5 +- .../common/level/viewmodel/GameViewModel.kt | 2 +- .../core/analytics/DebugAnalyticsManager.kt | 12 +- .../core/analytics/IAnalyticsManager.kt | 2 +- .../core/analytics/ProdAnalyticsManager.kt | 17 + .../core/analytics/models/Analytics.kt | 2 +- .../antimine/core/control/GameControl.kt | 18 +- .../lucasnlm/antimine/core/di/CommonModule.kt | 7 +- common/src/main/res/values/strings.xml | 5 +- .../common/level/logic/GameControllerTest.kt | 32 +- external/.gitignore | 1 + external/build.gradle | 40 ++ external/src/main/AndroidManifest.xml | 5 + .../dev/lucasnlm/external/BillingManager.kt | 8 + .../external/ExternalAnalyticsWrapper.kt | 8 + .../lucasnlm/external/InstantAppManager.kt | 11 + .../dev/lucasnlm/external/PlayGamesManager.kt | 14 + foss/build.gradle | 5 +- foss/src/main/AndroidManifest.xml | 2 +- .../dev/lucasnlm/external/BillingManager.kt | 16 + ...AppWrapper.kt => ProprietaryAppWrapper.kt} | 6 +- proprietary/build.gradle | 15 +- proprietary/src/main/AndroidManifest.xml | 15 +- .../dev/lucasnlm/external/BillingManager.kt | 59 +++ .../external/ExternalAnalyticsWrapper.kt | 30 ++ .../lucasnlm/external/InstantAppManager.kt | 15 + .../lucasnlm/external/InstantAppWrapper.kt | 13 - .../dev/lucasnlm/external/PlayGamesManager.kt | 85 +++++ proprietary/src/main/res/values/values.xml | 4 + settings.gradle | 1 + wear/build.gradle | 6 +- 64 files changed, 1262 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/dev/lucasnlm/antimine/playgames/PlayGamesDialogFragment.kt create mode 100644 app/src/main/java/dev/lucasnlm/antimine/playgames/model/PlayGamesItem.kt create mode 100644 app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesEvent.kt create mode 100644 app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesViewModel.kt create mode 100644 app/src/main/java/dev/lucasnlm/antimine/support/SupportAppDialogFragment.kt create mode 100644 app/src/main/res/drawable/emoji_hugging_face.xml create mode 100644 app/src/main/res/drawable/games_achievements.png create mode 100644 app/src/main/res/drawable/games_controller.png create mode 100644 app/src/main/res/drawable/games_leaderboards.png create mode 100644 app/src/main/res/layout/dialog_payments.xml create mode 100644 app/src/main/res/layout/view_play_games_button.xml create mode 100644 common/src/main/java/dev/lucasnlm/antimine/core/analytics/ProdAnalyticsManager.kt create mode 100644 external/.gitignore create mode 100644 external/build.gradle create mode 100644 external/src/main/AndroidManifest.xml create mode 100644 external/src/main/java/dev/lucasnlm/external/BillingManager.kt create mode 100644 external/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt create mode 100644 external/src/main/java/dev/lucasnlm/external/InstantAppManager.kt create mode 100644 external/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt create mode 100644 foss/src/main/java/dev/lucasnlm/external/BillingManager.kt rename foss/src/main/java/dev/lucasnlm/external/{InstantAppWrapper.kt => ProprietaryAppWrapper.kt} (67%) create mode 100644 proprietary/src/main/java/dev/lucasnlm/external/BillingManager.kt create mode 100644 proprietary/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt create mode 100644 proprietary/src/main/java/dev/lucasnlm/external/InstantAppManager.kt delete mode 100644 proprietary/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt create mode 100644 proprietary/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt create mode 100644 proprietary/src/main/res/values/values.xml diff --git a/.gitignore b/.gitignore index ab2070fa..679c6420 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ app/local.properties app/release/ app/standalone/ app/google/ +app/google-services.json +proprietary/google-services.json \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 4419a658..79d06f6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { // versionCode and versionName must be hardcoded to support F-droid - versionCode 800041 - versionName '8.0.4' + versionCode 800051 + versionName '8.0.5' minSdkVersion 16 targetSdkVersion 30 multiDexEnabled true @@ -93,6 +93,7 @@ dependencies { // Dependencies must be hardcoded to support F-droid implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':external') implementation project(':common') googleImplementation project(':proprietary') diff --git a/app/src/main/java/dev/lucasnlm/antimine/DeepLink.kt b/app/src/main/java/dev/lucasnlm/antimine/DeepLink.kt index 6a3a2a8b..24da9042 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/DeepLink.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/DeepLink.kt @@ -12,6 +12,4 @@ object DeepLink { const val EXPERT_PATH = "expert" const val STANDARD_PATH = "standard" const val CUSTOM_PATH = "custom" - - const val CUSTOM_NEW_GAME = "antimine://new-game/custom" } diff --git a/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt index f83dca0c..d8b7323e 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/GameActivity.kt @@ -9,6 +9,7 @@ import android.os.Handler import android.text.format.DateUtils import android.view.View import android.view.WindowManager +import android.widget.FrameLayout import androidx.activity.viewModels import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog @@ -16,6 +17,7 @@ import androidx.appcompat.widget.TooltipCompat import androidx.core.content.ContextCompat import androidx.core.os.HandlerCompat.postDelayed import androidx.core.view.GravityCompat +import androidx.core.view.doOnLayout import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentTransaction @@ -39,10 +41,12 @@ import dev.lucasnlm.antimine.history.HistoryActivity import dev.lucasnlm.antimine.instant.InstantAppManager import dev.lucasnlm.antimine.level.view.EndGameDialogFragment import dev.lucasnlm.antimine.level.view.LevelFragment +import dev.lucasnlm.antimine.playgames.PlayGamesDialogFragment import dev.lucasnlm.antimine.preferences.PreferencesActivity import dev.lucasnlm.antimine.share.viewmodel.ShareViewModel import dev.lucasnlm.antimine.stats.StatsActivity import dev.lucasnlm.antimine.theme.ThemeActivity +import dev.lucasnlm.external.IPlayGamesManager import kotlinx.android.synthetic.main.activity_game.* import kotlinx.android.synthetic.main.activity_game.minesCount import kotlinx.android.synthetic.main.activity_game.timer @@ -66,6 +70,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O @Inject lateinit var savesRepository: ISavesRepository + @Inject + lateinit var playGamesManager: IPlayGamesManager + val viewModel: GameViewModel by viewModels() private val shareViewModel: ShareViewModel by viewModels() @@ -90,7 +97,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O bindToolbar() bindDrawer() bindNavigationMenu() - loadGameFragment() + + findViewById(R.id.levelContainer).doOnLayout { + loadGameFragment() + } if (instantAppManager.isEnabled()) { bindInstantApp() @@ -201,6 +211,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O analyticsManager.sentEvent(Analytics.Resume) } + + silentGooglePlayLogin() } } @@ -327,6 +339,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O R.id.previous_games -> openSaveHistory() R.id.stats -> openStats() R.id.install_new -> installFromInstantApp() + R.id.play_games -> googlePlay() else -> handled = false } @@ -338,6 +351,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O } navigationView.menu.findItem(R.id.share_now).isVisible = instantAppManager.isNotEnabled() + + if (!playGamesManager.hasGooglePlayGames()) { + navigationView.menu.removeGroup(R.id.play_games_group) + } } private fun checkUseCount() { @@ -369,22 +386,22 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O } private fun loadGameFragment() { - val fragmentManager = supportFragmentManager + supportFragmentManager.apply { + popBackStack() - fragmentManager.popBackStack() + findFragmentById(R.id.levelContainer)?.let { it -> + beginTransaction().apply { + remove(it) + commitAllowingStateLoss() + } + } - fragmentManager.findFragmentById(R.id.levelContainer)?.let { it -> - fragmentManager.beginTransaction().apply { - remove(it) + beginTransaction().apply { + replace(R.id.levelContainer, LevelFragment()) + setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) commitAllowingStateLoss() } } - - fragmentManager.beginTransaction().apply { - replace(R.id.levelContainer, LevelFragment()) - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - commitAllowingStateLoss() - } } private fun showRequestRating() { @@ -649,6 +666,32 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O } } + private fun silentGooglePlayLogin() { + if (playGamesManager.hasGooglePlayGames()) { + playGamesManager.silentLogin(this) + invalidateOptionsMenu() + } + } + + private fun googlePlay() { + if (playGamesManager.isLogged()) { + PlayGamesDialogFragment().show(supportFragmentManager, PlayGamesDialogFragment.TAG) + } else { + playGamesManager.getLoginIntent()?.let { + startActivityForResult(it, GOOGLE_PLAY_REQUEST_CODE) + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == GOOGLE_PLAY_REQUEST_CODE) { + playGamesManager.handleLoginResult(data) + invalidateOptionsMenu() + } + } + companion object { const val PREFERENCE_FIRST_USE = "preference_first_use" const val PREFERENCE_USE_COUNT = "preference_use_count" @@ -656,6 +699,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O const val IA_REFERRER = "InstallApiActivity" const val IA_REQUEST_CODE = 5 + const val GOOGLE_PLAY_REQUEST_CODE = 6 const val MIN_USAGES_TO_RATING = 4 } diff --git a/app/src/main/java/dev/lucasnlm/antimine/control/ControlDialogFragment.kt b/app/src/main/java/dev/lucasnlm/antimine/control/ControlDialogFragment.kt index 26289fea..4d7096d0 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/control/ControlDialogFragment.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/control/ControlDialogFragment.kt @@ -21,10 +21,10 @@ class ControlDialogFragment : AppCompatDialogFragment() { private val adapter by lazy { ControlListAdapter(controlViewModel) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val currentControl = controlViewModel.singleState().selectedId + val state = controlViewModel.singleState() return AlertDialog.Builder(requireContext()).apply { setTitle(R.string.control) - setSingleChoiceItems(adapter, currentControl, null) + setSingleChoiceItems(adapter, state.selectedIndex, null) setPositiveButton(R.string.ok, null) }.create() } @@ -41,8 +41,6 @@ class ControlDialogFragment : AppCompatDialogFragment() { ) : BaseAdapter() { private val controlList = controlViewModel.singleState().gameControls - fun getSelectedId() = controlViewModel.singleState().selectedId - override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { val view = if (convertView == null) { ControlItemView(parent!!.context) @@ -50,12 +48,12 @@ class ControlDialogFragment : AppCompatDialogFragment() { (convertView as ControlItemView) } - val selectedId = getSelectedId() + val selected = controlViewModel.singleState().selected return view.apply { val controlModel = controlList[position] bind(controlModel) - setRadio(selectedId == controlModel.controlStyle.ordinal) + setRadio(selected == controlModel.controlStyle) setOnClickListener { controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle)) notifyDataSetChanged() diff --git a/app/src/main/java/dev/lucasnlm/antimine/control/models/ControlDetails.kt b/app/src/main/java/dev/lucasnlm/antimine/control/models/ControlDetails.kt index f4d9d7ed..ba8798f1 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/control/models/ControlDetails.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/control/models/ControlDetails.kt @@ -6,7 +6,6 @@ import dev.lucasnlm.antimine.core.control.ControlStyle data class ControlDetails( val id: Long, val controlStyle: ControlStyle, - @StringRes val titleId: Int, @StringRes val firstActionId: Int, @StringRes val firstActionResponseId: Int, @StringRes val secondActionId: Int, diff --git a/app/src/main/java/dev/lucasnlm/antimine/control/view/ControlItemView.kt b/app/src/main/java/dev/lucasnlm/antimine/control/view/ControlItemView.kt index 8f2f8dfc..4838bf19 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/control/view/ControlItemView.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/control/view/ControlItemView.kt @@ -17,7 +17,6 @@ class ControlItemView : FrameLayout { private val radio: AppCompatRadioButton private val root: View - private val title: TextView private val firstAction: TextView private val firstActionResponse: TextView private val secondAction: TextView @@ -30,7 +29,6 @@ class ControlItemView : FrameLayout { radio = findViewById(R.id.radio) root = findViewById(R.id.root) - title = findViewById(R.id.title) firstAction = findViewById(R.id.firstAction) firstActionResponse = findViewById(R.id.firstActionResponse) secondAction = findViewById(R.id.secondAction) @@ -38,7 +36,6 @@ class ControlItemView : FrameLayout { } fun bind(controlDetails: ControlDetails) { - title.text = context.getString(controlDetails.titleId) firstAction.text = context.getString(controlDetails.firstActionId) firstActionResponse.text = context.getString(controlDetails.firstActionResponseId) secondAction.text = context.getString(controlDetails.secondActionId) diff --git a/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlState.kt b/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlState.kt index 8b98456e..9a7c7c06 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlState.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlState.kt @@ -1,8 +1,10 @@ package dev.lucasnlm.antimine.control.viewmodel import dev.lucasnlm.antimine.control.models.ControlDetails +import dev.lucasnlm.antimine.core.control.ControlStyle data class ControlState( - val selectedId: Int, + val selectedIndex: Int, + val selected: ControlStyle, val gameControls: List ) diff --git a/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlViewModel.kt b/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlViewModel.kt index c8868293..73b8f397 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlViewModel.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/control/viewmodel/ControlViewModel.kt @@ -16,7 +16,6 @@ class ControlViewModel @ViewModelInject constructor( ControlDetails( id = 0L, controlStyle = ControlStyle.Standard, - titleId = R.string.standard, firstActionId = R.string.single_click, firstActionResponseId = R.string.open_tile, secondActionId = R.string.long_press, @@ -25,7 +24,6 @@ class ControlViewModel @ViewModelInject constructor( ControlDetails( id = 1L, controlStyle = ControlStyle.FastFlag, - titleId = R.string.flag_first, firstActionId = R.string.single_click, firstActionResponseId = R.string.flag_tile, secondActionId = R.string.long_press, @@ -34,29 +32,43 @@ class ControlViewModel @ViewModelInject constructor( ControlDetails( id = 2L, controlStyle = ControlStyle.DoubleClick, - titleId = R.string.double_click, firstActionId = R.string.single_click, firstActionResponseId = R.string.flag_tile, secondActionId = R.string.double_click, secondActionResponseId = R.string.open_tile + ), + ControlDetails( + id = 3L, + controlStyle = ControlStyle.DoubleClickInverted, + firstActionId = R.string.single_click, + firstActionResponseId = R.string.open_tile, + secondActionId = R.string.double_click, + secondActionResponseId = R.string.flag_tile ) ) - override fun initialState(): ControlState = - ControlState( - selectedId = gameControlOptions.firstOrNull { - it.controlStyle == preferencesRepository.controlStyle() - }?.id?.toInt() ?: 0, + override fun initialState(): ControlState { + val controlDetails = gameControlOptions.firstOrNull { + it.controlStyle == preferencesRepository.controlStyle() + } + return ControlState( + selectedIndex = controlDetails?.id?.toInt() ?: 0, + selected = controlDetails?.controlStyle ?: ControlStyle.Standard, gameControls = gameControlOptions ) + } + override suspend fun mapEventToState(event: ControlEvent) = flow { if (event is ControlEvent.SelectControlStyle) { val controlStyle = event.controlStyle preferencesRepository.useControlStyle(controlStyle) + val selected = state.gameControls.first { it.controlStyle == event.controlStyle } + val newState = state.copy( - selectedId = state.gameControls.first { it.controlStyle == event.controlStyle }.id.toInt() + selectedIndex = selected.id.toInt(), + selected = selected.controlStyle ) emit(newState) diff --git a/app/src/main/java/dev/lucasnlm/antimine/di/AppModule.kt b/app/src/main/java/dev/lucasnlm/antimine/di/AppModule.kt index 5cf081a0..fec64651 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/di/AppModule.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/di/AppModule.kt @@ -6,13 +6,48 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ApplicationComponent import dagger.hilt.android.qualifiers.ApplicationContext +import dev.lucasnlm.antimine.common.BuildConfig +import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager +import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager +import dev.lucasnlm.antimine.core.analytics.ProdAnalyticsManager import dev.lucasnlm.antimine.instant.InstantAppManager +import dev.lucasnlm.external.BillingManager +import dev.lucasnlm.external.ExternalAnalyticsWrapper +import dev.lucasnlm.external.IBillingManager +import dev.lucasnlm.external.IPlayGamesManager +import dev.lucasnlm.external.PlayGamesManager +import javax.inject.Singleton @Module @InstallIn(ApplicationComponent::class) class AppModule { + @Singleton @Provides fun provideInstantAppManager( @ApplicationContext context: Context ): InstantAppManager = InstantAppManager(context) + + @Singleton + @Provides + fun provideBillingManager( + @ApplicationContext context: Context + ): IBillingManager = BillingManager(context) + + @Singleton + @Provides + fun providePlayGamesManager( + @ApplicationContext context: Context + ): IPlayGamesManager = PlayGamesManager(context) + + @Singleton + @Provides + fun provideAnalyticsManager( + @ApplicationContext context: Context + ): IAnalyticsManager { + return if (BuildConfig.DEBUG) { + DebugAnalyticsManager() + } else { + ProdAnalyticsManager(ExternalAnalyticsWrapper(context)) + } + } } diff --git a/app/src/main/java/dev/lucasnlm/antimine/instant/InstantAppManager.kt b/app/src/main/java/dev/lucasnlm/antimine/instant/InstantAppManager.kt index af5cb4b6..75182300 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/instant/InstantAppManager.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/instant/InstantAppManager.kt @@ -3,15 +3,15 @@ package dev.lucasnlm.antimine.instant import android.app.Activity import android.content.Context import android.content.Intent -import dev.lucasnlm.external.InstantAppWrapper +import dev.lucasnlm.external.InstantAppManager class InstantAppManager( private val context: Context ) { - fun isEnabled(): Boolean = InstantAppWrapper().isEnabled(context) + fun isEnabled(): Boolean = InstantAppManager().isInstantAppSupported(context) fun isNotEnabled(): Boolean = isEnabled().not() fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) = - InstantAppWrapper().showInstallPrompt(activity, intent, requestCode, referrer) + InstantAppManager().showInstallPrompt(activity, intent, requestCode, referrer) } diff --git a/app/src/main/java/dev/lucasnlm/antimine/level/view/LevelFragment.kt b/app/src/main/java/dev/lucasnlm/antimine/level/view/LevelFragment.kt index d626890c..91a0451e 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/level/view/LevelFragment.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/level/view/LevelFragment.kt @@ -1,9 +1,9 @@ package dev.lucasnlm.antimine.level.view -import android.content.Context import android.os.Bundle import android.text.format.DateUtils import android.view.View +import androidx.core.view.doOnLayout import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint @@ -11,27 +11,47 @@ import dev.lucasnlm.antimine.DeepLink import dev.lucasnlm.antimine.common.R import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Event +import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.view.CommonLevelFragment import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @AndroidEntryPoint open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) { - override fun onPause() { super.onPause() - GlobalScope.launch { + lifecycleScope.launch { viewModel.saveGame() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerGrid = view.findViewById(R.id.recyclerGrid) + recyclerGrid.doOnLayout { + lifecycleScope.launch { + val loadGameUid = checkLoadGameDeepLink() + val newGameDeepLink = checkNewGameDeepLink() + val retryDeepLink = checkRetryGameDeepLink() + + val levelSetup = when { + loadGameUid != null -> viewModel.loadGame(loadGameUid) + newGameDeepLink != null -> viewModel.startNewGame(newGameDeepLink) + retryDeepLink != null -> viewModel.retryGame(retryDeepLink) + else -> viewModel.loadLastGame() + } + + withContext(Dispatchers.Main) { + recyclerGrid.apply { + addItemDecoration(SpaceItemDecoration(R.dimen.field_padding)) + setHasFixedSize(true) + } + setupRecyclerViewSize(levelSetup) + } + } + } viewModel.run { field.observe( @@ -44,13 +64,7 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) { levelSetup.observe( viewLifecycleOwner, Observer { - recyclerGrid.apply { - val horizontalPadding = calcHorizontalPadding(it.width) - val verticalPadding = calcVerticalPadding(it.height) - layoutManager = makeNewLayoutManager(it.width) - setHasFixedSize(true) - setPadding(horizontalPadding, verticalPadding, 0, 0) - } + setupRecyclerViewSize(it) } ) @@ -81,38 +95,19 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) { } } - override fun onAttach(context: Context) { - super.onAttach(context) + private fun setupRecyclerViewSize(levelSetup: Minefield) { + recyclerGrid.apply { + val horizontalPadding = calcHorizontalPadding(levelSetup.width) + val verticalPadding = calcVerticalPadding(levelSetup.height) + setPadding(horizontalPadding, verticalPadding, 0, 0) + layoutManager = makeNewLayoutManager(levelSetup.width) + adapter = areaAdapter + alpha = 0.0f - lifecycleScope.launchWhenCreated { - val loadGameUid = checkLoadGameDeepLink() - val newGameDeepLink = checkNewGameDeepLink() - val retryDeepLink = checkRetryGameDeepLink() - - val levelSetup = when { - loadGameUid != null -> viewModel.loadGame(loadGameUid) - newGameDeepLink != null -> viewModel.startNewGame(newGameDeepLink) - retryDeepLink != null -> viewModel.retryGame(retryDeepLink) - else -> viewModel.loadLastGame() - } - - withContext(Dispatchers.Main) { - recyclerGrid.apply { - val horizontalPadding = calcHorizontalPadding(levelSetup.width) - val verticalPadding = calcVerticalPadding(levelSetup.height) - addItemDecoration(SpaceItemDecoration(R.dimen.field_padding)) - setPadding(horizontalPadding, verticalPadding, 0, 0) - layoutManager = makeNewLayoutManager(levelSetup.width) - setHasFixedSize(true) - adapter = areaAdapter - alpha = 0.0f - - animate().apply { - alpha(1.0f) - duration = DateUtils.SECOND_IN_MILLIS - }.start() - } - } + animate().apply { + alpha(1.0f) + duration = DateUtils.SECOND_IN_MILLIS + }.start() } } diff --git a/app/src/main/java/dev/lucasnlm/antimine/level/viewmodel/EndGameDialogViewModel.kt b/app/src/main/java/dev/lucasnlm/antimine/level/viewmodel/EndGameDialogViewModel.kt index 7cc4ee00..0642fb6d 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/level/viewmodel/EndGameDialogViewModel.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/level/viewmodel/EndGameDialogViewModel.kt @@ -28,6 +28,7 @@ class EndGameDialogViewModel @ViewModelInject constructor( R.drawable.emoji_grinning_squinting_face, R.drawable.emoji_smiling_face_with_sunglasses, R.drawable.emoji_squinting_face_with_tongue, + R.drawable.emoji_hugging_face, R.drawable.emoji_partying_face, R.drawable.emoji_clapping_hands, R.drawable.emoji_triangular_flag diff --git a/app/src/main/java/dev/lucasnlm/antimine/playgames/PlayGamesDialogFragment.kt b/app/src/main/java/dev/lucasnlm/antimine/playgames/PlayGamesDialogFragment.kt new file mode 100644 index 00000000..212e842e --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/playgames/PlayGamesDialogFragment.kt @@ -0,0 +1,108 @@ +package dev.lucasnlm.antimine.playgames + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.FrameLayout +import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import dev.lucasnlm.antimine.R +import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesEvent +import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter + +@AndroidEntryPoint +class PlayGamesDialogFragment : DialogFragment() { + private val playGamesViewModel by viewModels() + private val adapter by lazy { PlayGamesAdapter(playGamesViewModel) } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(requireContext()).apply { + setTitle(R.string.google_play_games) + setAdapter(adapter, null) + setPositiveButton(R.string.ok, null) + }.create() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launchWhenCreated { + playGamesViewModel.observeEvent().collect { + when (it) { + is PlayGamesEvent.OpenAchievements -> { + activity?.let { activity -> + playGamesViewModel.openAchievements(activity) + } + } + is PlayGamesEvent.OpenLeaderboards -> { + activity?.let { activity -> + playGamesViewModel.openLeaderboards(activity) + } + } + } + } + } + } + + private class PlayGamesAdapter( + private val playGamesViewModel: PlayGamesViewModel + ) : BaseAdapter() { + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + val view = if (convertView == null) { + PlayGamesButton(parent!!.context) + } else { + (convertView as PlayGamesButton) + } + + val item = playGamesViewModel.playGamesItems[position] + + return view.apply { + text.text = view.context.getString(item.stringRes) + icon.setImageResource(item.iconRes) + + setOnClickListener { + playGamesViewModel.sendEvent(item.triggerEvent) + } + } + } + + override fun hasStableIds(): Boolean = true + + override fun getItem(position: Int): Any = playGamesViewModel.playGamesItems[position] + + override fun getItemId(position: Int): Long = playGamesViewModel.playGamesItems[position].id.toLong() + + override fun getCount(): Int = playGamesViewModel.playGamesItems.count() + } + + companion object { + val TAG = PlayGamesDialogFragment::class.simpleName + } +} + +class PlayGamesButton : FrameLayout { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + init { + inflate(context, R.layout.view_play_games_button, this) + + icon = findViewById(R.id.icon) + text = findViewById(R.id.text) + } + + val icon: AppCompatImageView + val text: TextView +} diff --git a/app/src/main/java/dev/lucasnlm/antimine/playgames/model/PlayGamesItem.kt b/app/src/main/java/dev/lucasnlm/antimine/playgames/model/PlayGamesItem.kt new file mode 100644 index 00000000..76f84cbd --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/playgames/model/PlayGamesItem.kt @@ -0,0 +1,12 @@ +package dev.lucasnlm.antimine.playgames.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesEvent + +data class PlayGamesItem( + val id: Int, + @DrawableRes val iconRes: Int, + @StringRes val stringRes: Int, + val triggerEvent: PlayGamesEvent +) diff --git a/app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesEvent.kt b/app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesEvent.kt new file mode 100644 index 00000000..7a6580be --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesEvent.kt @@ -0,0 +1,6 @@ +package dev.lucasnlm.antimine.playgames.viewmodel + +sealed class PlayGamesEvent { + object OpenAchievements : PlayGamesEvent() + object OpenLeaderboards : PlayGamesEvent() +} diff --git a/app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesViewModel.kt b/app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesViewModel.kt new file mode 100644 index 00000000..fe4daaee --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/playgames/viewmodel/PlayGamesViewModel.kt @@ -0,0 +1,29 @@ +package dev.lucasnlm.antimine.playgames.viewmodel + +import android.app.Activity +import android.content.Context +import androidx.hilt.lifecycle.ViewModelInject +import dagger.hilt.android.qualifiers.ApplicationContext +import dev.lucasnlm.antimine.R +import dev.lucasnlm.antimine.core.viewmodel.StatelessViewModel +import dev.lucasnlm.antimine.playgames.model.PlayGamesItem +import dev.lucasnlm.external.IPlayGamesManager + +class PlayGamesViewModel @ViewModelInject constructor( + @ApplicationContext private val context: Context, + private val playGamesManager: IPlayGamesManager +) : StatelessViewModel() { + + val playGamesItems = listOf( + PlayGamesItem(0, R.drawable.games_achievements, R.string.achievements, PlayGamesEvent.OpenAchievements), + PlayGamesItem(1, R.drawable.games_leaderboards, R.string.leaderboards, PlayGamesEvent.OpenLeaderboards) + ) + + fun openAchievements(activity: Activity) { + playGamesManager.openAchievements(activity) + } + + fun openLeaderboards(activity: Activity) { + playGamesManager.openLeaderboards(activity) + } +} diff --git a/app/src/main/java/dev/lucasnlm/antimine/support/SupportAppDialogFragment.kt b/app/src/main/java/dev/lucasnlm/antimine/support/SupportAppDialogFragment.kt new file mode 100644 index 00000000..bab2c992 --- /dev/null +++ b/app/src/main/java/dev/lucasnlm/antimine/support/SupportAppDialogFragment.kt @@ -0,0 +1,40 @@ +package dev.lucasnlm.antimine.support + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import dev.lucasnlm.antimine.R +import dev.lucasnlm.external.IBillingManager +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class SupportAppDialogFragment : AppCompatDialogFragment() { + @Inject + lateinit var billingManager: IBillingManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + billingManager.start() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return AlertDialog.Builder(requireContext()).apply { + setView(R.layout.dialog_payments) + setNeutralButton(R.string.rating_button_no, null) + setPositiveButton(R.string.unlock) { _, _ -> + lifecycleScope.launch { + billingManager.charge(requireActivity()) + } + } + }.create() + } + + companion object { + val TAG = SupportAppDialogFragment::class.simpleName + } +} diff --git a/app/src/main/java/dev/lucasnlm/antimine/theme/ThemeActivity.kt b/app/src/main/java/dev/lucasnlm/antimine/theme/ThemeActivity.kt index 313333ca..30461196 100644 --- a/app/src/main/java/dev/lucasnlm/antimine/theme/ThemeActivity.kt +++ b/app/src/main/java/dev/lucasnlm/antimine/theme/ThemeActivity.kt @@ -1,5 +1,6 @@ package dev.lucasnlm.antimine.theme +import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope @@ -9,6 +10,8 @@ import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration +import dev.lucasnlm.antimine.custom.CustomLevelDialogFragment +import dev.lucasnlm.antimine.support.SupportAppDialogFragment import dev.lucasnlm.antimine.theme.view.ThemeAdapter import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel import kotlinx.android.synthetic.main.activity_theme.* @@ -41,5 +44,15 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) { } } } + + showUnlockDialog() + } + + private fun showUnlockDialog() { + if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) { + SupportAppDialogFragment().apply { + show(supportFragmentManager, SupportAppDialogFragment.TAG) + } + } } } diff --git a/app/src/main/res/drawable/emoji_hugging_face.xml b/app/src/main/res/drawable/emoji_hugging_face.xml new file mode 100644 index 00000000..2311da3e --- /dev/null +++ b/app/src/main/res/drawable/emoji_hugging_face.xml @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/games_achievements.png b/app/src/main/res/drawable/games_achievements.png new file mode 100644 index 0000000000000000000000000000000000000000..1a02211241604607fd041a1704b5c0cc030cd5f0 GIT binary patch literal 15636 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%0D;) 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 Y7I97YE$>s%c!MBgeRI7sog26RAC3KJ2LJ#7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/games_controller.png b/app/src/main/res/drawable/games_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..a9424b905ed48c82cfdabeb412572b7974022bb8 GIT binary patch literal 12658 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 Iw``vMAB*q%4gdfE literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/games_leaderboards.png b/app/src/main/res/drawable/games_leaderboards.png new file mode 100644 index 0000000000000000000000000000000000000000..c2aa3bb67a08799c577fb72baf0908be2240b500 GIT binary patch literal 11158 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 + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_control_item.xml b/app/src/main/res/layout/view_control_item.xml index cd8b3a81..d08c9e76 100644 --- a/app/src/main/res/layout/view_control_item.xml +++ b/app/src/main/res/layout/view_control_item.xml @@ -13,7 +13,7 @@ android:paddingRight="48dp" android:paddingStart="32dp" android:paddingEnd="48dp" - android:paddingVertical="24dp"> + android:paddingVertical="12dp"> - - - - - - @@ -62,7 +53,6 @@ - diff --git a/app/src/main/res/layout/view_play_games_button.xml b/app/src/main/res/layout/view_play_games_button.xml new file mode 100644 index 00000000..cd9f0dab --- /dev/null +++ b/app/src/main/res/layout/view_play_games_button.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/app/src/main/res/menu/nav_menu.xml b/app/src/main/res/menu/nav_menu.xml index 8d86ef73..b6031233 100644 --- a/app/src/main/res/menu/nav_menu.xml +++ b/app/src/main/res/menu/nav_menu.xml @@ -71,6 +71,14 @@ android:title="@string/settings" /> + + + + { @@ -83,8 +79,13 @@ class AreaAdapter( } } + override fun onDoubleTapEvent(e: MotionEvent?): Boolean { + return false + } + override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { - if (preferencesRepository.controlStyle() == ControlStyle.DoubleClick) { + val style = preferencesRepository.controlStyle() + if (style == ControlStyle.DoubleClick || style == ControlStyle.DoubleClickInverted) { val position = adapterPosition if (position == RecyclerView.NO_POSITION) { Log.d(TAG, "Item no longer exists.") @@ -115,7 +116,8 @@ class AreaAdapter( } itemView.setOnClickListener { - if (preferencesRepository.controlStyle() != ControlStyle.DoubleClick) { + val style = preferencesRepository.controlStyle() + if (style != ControlStyle.DoubleClick && style != ControlStyle.DoubleClickInverted) { val position = adapterPosition if (position == RecyclerView.NO_POSITION) { Log.d(TAG, "Item no longer exists.") diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/view/CommonLevelFragment.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/view/CommonLevelFragment.kt index dc46dd10..336a5b80 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/view/CommonLevelFragment.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/view/CommonLevelFragment.kt @@ -1,5 +1,8 @@ package dev.lucasnlm.antimine.common.level.view +import android.content.Context +import android.os.Bundle +import android.view.View import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -39,9 +42,7 @@ abstract class CommonLevelFragment(@LayoutRes val contentLayoutId: Int) : Fragme val height = requireView().measuredHeight val recyclerViewHeight = (dimensionRepository.areaSize() * boardHeight) val separatorsHeight = (2 * dimensionRepository.areaSeparator() * (boardHeight - 1)) - val calculatedHeight = (height - recyclerViewHeight - separatorsHeight) - return (calculatedHeight / 2).coerceAtLeast(0.0f).toInt() } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt index afe3b4c5..5b75499b 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/common/level/viewmodel/GameViewModel.kt @@ -208,7 +208,7 @@ class GameViewModel @ViewModelInject constructor( } suspend fun saveGame() { - if (initialized && gameController.hasMines) { + if (gameController.hasMines) { val id = savesRepository.saveGame( gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty) ) diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/DebugAnalyticsManager.kt b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/DebugAnalyticsManager.kt index 3900fb65..b6c8a170 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/DebugAnalyticsManager.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/DebugAnalyticsManager.kt @@ -5,12 +5,18 @@ import android.util.Log import dev.lucasnlm.antimine.core.analytics.models.Analytics class DebugAnalyticsManager : IAnalyticsManager { - override fun setup(context: Context, userProperties: Map) { - Log.d(TAG, "Setup Analytics using $userProperties") + override fun setup(context: Context, properties: Map) { + if (properties.isNotEmpty()) { + Log.d(TAG, "Setup Analytics using $properties") + } } override fun sentEvent(event: Analytics) { - Log.d(TAG, "Sent event: '${event.title}' with ${event.extra}") + if (event.extra.isNotEmpty()) { + Log.d(TAG, "Sent event: '${event.name}' with ${event.extra}") + } else { + Log.d(TAG, "Sent event: '${event.name}'") + } } companion object { diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/IAnalyticsManager.kt b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/IAnalyticsManager.kt index 6b87e16e..b50d19aa 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/IAnalyticsManager.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/IAnalyticsManager.kt @@ -4,6 +4,6 @@ import android.content.Context import dev.lucasnlm.antimine.core.analytics.models.Analytics interface IAnalyticsManager { - fun setup(context: Context, userProperties: Map) + fun setup(context: Context, properties: Map) fun sentEvent(event: Analytics) } diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/ProdAnalyticsManager.kt b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/ProdAnalyticsManager.kt new file mode 100644 index 00000000..24e6b1c4 --- /dev/null +++ b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/ProdAnalyticsManager.kt @@ -0,0 +1,17 @@ +package dev.lucasnlm.antimine.core.analytics + +import android.content.Context +import dev.lucasnlm.antimine.core.analytics.models.Analytics +import dev.lucasnlm.external.IExternalAnalyticsWrapper + +class ProdAnalyticsManager( + private val analyticsWrapper: IExternalAnalyticsWrapper +) : IAnalyticsManager{ + override fun setup(context: Context, properties: Map) { + analyticsWrapper.setup(context, properties) + } + + override fun sentEvent(event: Analytics) { + analyticsWrapper.sendEvent(event.name, event.extra) + } +} diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt index dbbb5274..e8cf0632 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/analytics/models/Analytics.kt @@ -5,7 +5,7 @@ import dev.lucasnlm.antimine.common.level.models.Score import dev.lucasnlm.antimine.common.level.models.Minefield sealed class Analytics( - val title: String, + val name: String, val extra: Map = mapOf() ) { object Open : Analytics("Open game") diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt b/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt index b423bf1b..1d9c4cc0 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/control/GameControl.kt @@ -26,7 +26,8 @@ data class Actions( enum class ControlStyle { Standard, DoubleClick, - FastFlag + FastFlag, + DoubleClickInverted, } /** @@ -80,12 +81,27 @@ sealed class GameControl( ) ) + object DoubleClickInverted : GameControl( + id = ControlStyle.DoubleClickInverted, + onCovered = Actions( + singleClick = ActionResponse.OpenTile, + longPress = null, + doubleClick = ActionResponse.SwitchMark + ), + onOpen = Actions( + singleClick = ActionResponse.HighlightNeighbors, + longPress = null, + doubleClick = ActionResponse.OpenNeighbors + ) + ) + companion object { fun fromControlType(controlStyle: ControlStyle): GameControl { return when (controlStyle) { ControlStyle.Standard -> Standard ControlStyle.DoubleClick -> DoubleClick ControlStyle.FastFlag -> FastFlag + ControlStyle.DoubleClickInverted -> DoubleClickInverted } } } diff --git a/common/src/main/java/dev/lucasnlm/antimine/core/di/CommonModule.kt b/common/src/main/java/dev/lucasnlm/antimine/core/di/CommonModule.kt index aa17ee08..4273e1f7 100644 --- a/common/src/main/java/dev/lucasnlm/antimine/core/di/CommonModule.kt +++ b/common/src/main/java/dev/lucasnlm/antimine/core/di/CommonModule.kt @@ -6,10 +6,12 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ApplicationComponent import dagger.hilt.android.qualifiers.ApplicationContext +import dev.lucasnlm.antimine.common.BuildConfig import dev.lucasnlm.antimine.common.level.repository.DimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager +import dev.lucasnlm.antimine.core.analytics.ProdAnalyticsManager import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.PreferencesManager import dev.lucasnlm.antimine.core.preferences.PreferencesRepository @@ -17,6 +19,7 @@ import dev.lucasnlm.antimine.core.sound.ISoundManager import dev.lucasnlm.antimine.core.sound.SoundManager import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository import dev.lucasnlm.antimine.core.themes.repository.ThemeRepository +import javax.inject.Singleton @Module @InstallIn(ApplicationComponent::class) @@ -28,6 +31,7 @@ class CommonModule { ): IDimensionRepository = DimensionRepository(context, preferencesRepository) + @Singleton @Provides fun providePreferencesRepository( preferencesManager: PreferencesManager @@ -38,9 +42,6 @@ class CommonModule { @ApplicationContext context: Context ): PreferencesManager = PreferencesManager(context) - @Provides - fun provideAnalyticsManager(): IAnalyticsManager = DebugAnalyticsManager() - @Provides fun provideSoundManager( @ApplicationContext context: Context diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index f95c81d5..0b32cd43 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -33,6 +33,8 @@ Size System Feedback + Help supporting us! + It will allow us building new features and to keep our project active. If you like this game, please give us a feedback. It will help us a lot. This game uses the following third parties software: This game was translated by the following people: @@ -58,7 +60,6 @@ Use Question Mark Controls - Flag First Single Click Double Click Long Press @@ -71,6 +72,8 @@ Cancel Resume Yes + Unlock + Achievements No General Source Code diff --git a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt index b18472c0..e2146029 100644 --- a/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt +++ b/common/src/test/java/dev/lucasnlm/antimine/common/level/logic/GameControllerTest.kt @@ -91,13 +91,13 @@ class GameControllerTest { val firstMine = controller.field.first { it.hasMine } assertEquals( - listOf(4, 5, 13, 33, 27, 36, 38, 41, 19, 29, 39, 49, 67, 59, 70, 86, 81, 87, 92, 91), + listOf(3, 4, 32, 42, 36, 45, 52, 28, 55, 47, 65, 39, 73, 74, 59, 85, 91, 95, 88, 90), controller.takeExplosionRadius(firstMine).map { it.id }.toList() ) val midMine = controller.field.filter { it.hasMine }.take(controller.getMinesCount() / 2).last() assertEquals( - listOf(39, 29, 38, 49, 19, 59, 27, 36, 67, 5, 87, 4, 86, 33, 13, 41, 92, 81, 70, 91), + listOf(52, 42, 32, 73, 74, 55, 45, 65, 91, 85, 36, 90, 95, 3, 47, 4, 28, 88, 59, 39), controller.takeExplosionRadius(midMine).map { it.id }.toList() ) } @@ -313,6 +313,34 @@ class GameControllerTest { } } + @Test + fun testControlFirstActionWithInvertedDoubleClick() { + withGameController { controller -> + controller.run { + updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClickInverted)) + assertTrue(at(3).isCovered) + fakeDoubleClick(3) + assertTrue(at(3).isCovered) + assertTrue(at(3).mark.isFlag()) + fakeDoubleClick(3) + assertFalse(at(3).mark.isFlag()) + assertTrue(at(3).isCovered) + } + } + } + + @Test + fun testControlSecondActionWithInvertedDoubleClick() { + withGameController { controller -> + controller.run { + updateGameControl(GameControl.fromControlType(ControlStyle.DoubleClickInverted)) + assertTrue(at(3).isCovered) + fakeSingleClick(3) + assertFalse(at(3).isCovered) + } + } + } + @Test fun testControlFastFlagOpenMultiple() { withGameController { controller -> diff --git a/external/.gitignore b/external/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/external/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/external/build.gradle b/external/build.gradle new file mode 100644 index 00000000..09bdf1d5 --- /dev/null +++ b/external/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 30 + versionCode 1 + versionName '1.0' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + // Dependencies must be hardcoded to support F-droid + + implementation fileTree(dir: 'libs', include: ['*.jar']) + + // Kotlin Lib + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72' +} \ No newline at end of file diff --git a/external/src/main/AndroidManifest.xml b/external/src/main/AndroidManifest.xml new file mode 100644 index 00000000..9c48198c --- /dev/null +++ b/external/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/external/src/main/java/dev/lucasnlm/external/BillingManager.kt b/external/src/main/java/dev/lucasnlm/external/BillingManager.kt new file mode 100644 index 00000000..fa3b178a --- /dev/null +++ b/external/src/main/java/dev/lucasnlm/external/BillingManager.kt @@ -0,0 +1,8 @@ +package dev.lucasnlm.external + +import android.app.Activity + +interface IBillingManager { + fun start() + suspend fun charge(activity: Activity) +} diff --git a/external/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt b/external/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt new file mode 100644 index 00000000..ac16c973 --- /dev/null +++ b/external/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt @@ -0,0 +1,8 @@ +package dev.lucasnlm.external + +import android.content.Context + +interface IExternalAnalyticsWrapper { + fun setup(context: Context, properties: Map) + fun sendEvent(name: String, content: Map) +} diff --git a/external/src/main/java/dev/lucasnlm/external/InstantAppManager.kt b/external/src/main/java/dev/lucasnlm/external/InstantAppManager.kt new file mode 100644 index 00000000..29fd5cf0 --- /dev/null +++ b/external/src/main/java/dev/lucasnlm/external/InstantAppManager.kt @@ -0,0 +1,11 @@ +package dev.lucasnlm.external + +import android.app.Activity +import android.content.Context +import android.content.Intent + +interface IInstantAppManager { + fun isInstantAppSupported(context: Context): Boolean + fun isInAppPaymentsSupported(context: Context): Boolean + fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?): Boolean +} diff --git a/external/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt b/external/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt new file mode 100644 index 00000000..632e8fc7 --- /dev/null +++ b/external/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt @@ -0,0 +1,14 @@ +package dev.lucasnlm.external + +import android.app.Activity +import android.content.Intent + +interface IPlayGamesManager { + fun hasGooglePlayGames(): Boolean + fun silentLogin(activity: Activity) + fun getLoginIntent(): Intent? + fun handleLoginResult(data: Intent?) + fun isLogged(): Boolean + fun openAchievements(activity: Activity) + fun openLeaderboards(activity: Activity) +} diff --git a/foss/build.gradle b/foss/build.gradle index 064e510d..6c972a31 100644 --- a/foss/build.gradle +++ b/foss/build.gradle @@ -6,8 +6,8 @@ android { compileSdkVersion 30 defaultConfig { - versionCode 800041 // MMmmPPv - versionName '8.0.4' + versionCode 800051 // MMmmPPv + versionName '8.0.5' minSdkVersion 16 targetSdkVersion 30 } @@ -24,4 +24,5 @@ dependencies { // Dependencies must be hardcoded to support F-droid implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':external') } diff --git a/foss/src/main/AndroidManifest.xml b/foss/src/main/AndroidManifest.xml index 430136db..aeb3e311 100644 --- a/foss/src/main/AndroidManifest.xml +++ b/foss/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - diff --git a/foss/src/main/java/dev/lucasnlm/external/BillingManager.kt b/foss/src/main/java/dev/lucasnlm/external/BillingManager.kt new file mode 100644 index 00000000..577676c1 --- /dev/null +++ b/foss/src/main/java/dev/lucasnlm/external/BillingManager.kt @@ -0,0 +1,16 @@ +package dev.lucasnlm.external + +import android.app.Activity +import android.content.Context + +class BillingManager( + private val context: Context +) : IBillingManager { + override fun start() { + // Void + } + + override suspend fun charge(activity: Activity) { + // Void + } +} diff --git a/foss/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt b/foss/src/main/java/dev/lucasnlm/external/ProprietaryAppWrapper.kt similarity index 67% rename from foss/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt rename to foss/src/main/java/dev/lucasnlm/external/ProprietaryAppWrapper.kt index 94eec4ba..ec583230 100644 --- a/foss/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt +++ b/foss/src/main/java/dev/lucasnlm/external/ProprietaryAppWrapper.kt @@ -5,9 +5,11 @@ import android.content.Context import android.content.Intent @Suppress("UNUSED_PARAMETER") -class InstantAppWrapper { +class ProprietaryAppWrapper { // FOSS build doesn't support Instant App - fun isEnabled(context: Context): Boolean = false + fun isInstantAppSupported(context: Context): Boolean = false + + fun isInAppPaymentsSupported(context: Context) = false fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) { // Empty diff --git a/proprietary/build.gradle b/proprietary/build.gradle index 97a3837d..1dee3639 100644 --- a/proprietary/build.gradle +++ b/proprietary/build.gradle @@ -1,13 +1,14 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'com.google.gms.google-services' android { compileSdkVersion 30 defaultConfig { - versionCode 800041 // MMmmPPv - versionName '8.0.4' + versionCode 800051 + versionName '8.0.5' minSdkVersion 16 targetSdkVersion 30 } @@ -24,7 +25,17 @@ dependencies { // Dependencies must be hardcoded to support F-droid implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':external') // Google + implementation 'com.android.billingclient:billing-ktx:3.0.0' implementation 'com.google.android.gms:play-services-instantapps:17.0.0' + implementation 'com.google.android.gms:play-services-games:19.0.0' + implementation 'com.google.android.gms:play-services-auth:18.1.0' + + // Firebase + implementation 'com.google.firebase:firebase-analytics:17.5.0' + + // Kotlin Lib + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72' } diff --git a/proprietary/src/main/AndroidManifest.xml b/proprietary/src/main/AndroidManifest.xml index 84ebe750..0594ed19 100644 --- a/proprietary/src/main/AndroidManifest.xml +++ b/proprietary/src/main/AndroidManifest.xml @@ -1,2 +1,15 @@ + package="dev.lucasnlm.antimine"> + + + + + + + + + + + diff --git a/proprietary/src/main/java/dev/lucasnlm/external/BillingManager.kt b/proprietary/src/main/java/dev/lucasnlm/external/BillingManager.kt new file mode 100644 index 00000000..29b3d60f --- /dev/null +++ b/proprietary/src/main/java/dev/lucasnlm/external/BillingManager.kt @@ -0,0 +1,59 @@ +package dev.lucasnlm.external + +import android.app.Activity +import android.content.Context +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.SkuDetailsParams +import com.android.billingclient.api.querySkuDetails + +class BillingManager( + private val context: Context +) : IBillingManager { + private val purchaseUpdateListener = + PurchasesUpdatedListener { billingResult, purchases -> + if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { + // The BillingClient is ready. You can query purchases here. + } + } + + private val billingClient by lazy { + BillingClient.newBuilder(context) + .setListener(purchaseUpdateListener) + .enablePendingPurchases() + .build() + } + + override fun start() { + billingClient.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(billingResult: BillingResult) { + if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { + // The BillingClient is ready. You can query purchases here. + } + } + override fun onBillingServiceDisconnected() { + // Try to restart the connection on the next request to + // Google Play by calling the startConnection() method. + } + }) + } + + override suspend fun charge(activity: Activity) { + val skuDetailsParams = SkuDetailsParams.newBuilder() + .setSkusList(listOf(BASIC_SUPPORT)) + .setType(BillingClient.SkuType.INAPP) + .build() + + val details = billingClient.querySkuDetails(skuDetailsParams) + +print(details.toString()) + + //billingClient.launchBillingFlow(activity, flowParams) + } + + companion object { + private const val BASIC_SUPPORT = "unlock_0" + } +} diff --git a/proprietary/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt b/proprietary/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt new file mode 100644 index 00000000..619d2bf8 --- /dev/null +++ b/proprietary/src/main/java/dev/lucasnlm/external/ExternalAnalyticsWrapper.kt @@ -0,0 +1,30 @@ +package dev.lucasnlm.external + +import android.content.Context +import android.os.Bundle +import com.google.firebase.analytics.FirebaseAnalytics + +class ExternalAnalyticsWrapper( + private val context: Context +) : IExternalAnalyticsWrapper { + private val firebaseAnalytics by lazy { + FirebaseAnalytics.getInstance(context) + } + + override fun setup(context: Context, properties: Map) { + properties.forEach { (key, value) -> + firebaseAnalytics.setUserProperty(key, value) + } + } + + override fun sendEvent(name: String, content: Map) { + val bundle = Bundle().apply { + putString(FirebaseAnalytics.Param.ITEM_NAME, name) + content.forEach { (key, value) -> + this.putString(key, value) + } + } + + firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, bundle) + } +} diff --git a/proprietary/src/main/java/dev/lucasnlm/external/InstantAppManager.kt b/proprietary/src/main/java/dev/lucasnlm/external/InstantAppManager.kt new file mode 100644 index 00000000..728af1ce --- /dev/null +++ b/proprietary/src/main/java/dev/lucasnlm/external/InstantAppManager.kt @@ -0,0 +1,15 @@ +package dev.lucasnlm.external + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.google.android.gms.instantapps.InstantApps + +class InstantAppManager : IInstantAppManager { + override fun isInstantAppSupported(context: Context): Boolean = InstantApps.getPackageManagerCompat(context).isInstantApp + + override fun isInAppPaymentsSupported(context: Context): Boolean = true + + override fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?): Boolean = + InstantApps.showInstallPrompt(activity, intent, requestCode, referrer) +} diff --git a/proprietary/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt b/proprietary/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt deleted file mode 100644 index c5116ca6..00000000 --- a/proprietary/src/main/java/dev/lucasnlm/external/InstantAppWrapper.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.lucasnlm.external - -import android.app.Activity -import android.content.Context -import android.content.Intent -import com.google.android.gms.instantapps.InstantApps - -class InstantAppWrapper { - fun isEnabled(context: Context): Boolean = InstantApps.getPackageManagerCompat(context).isInstantApp - - fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) = - InstantApps.showInstallPrompt(activity, intent, requestCode, referrer) -} diff --git a/proprietary/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt b/proprietary/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt new file mode 100644 index 00000000..952f2ade --- /dev/null +++ b/proprietary/src/main/java/dev/lucasnlm/external/PlayGamesManager.kt @@ -0,0 +1,85 @@ +package dev.lucasnlm.external + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.widget.Toast +import com.google.android.gms.auth.api.Auth +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.games.Games + + +class PlayGamesManager( + private val context: Context +) : IPlayGamesManager { + private var account: GoogleSignInAccount? = null + + private fun setupPopUp(activity: Activity, account: GoogleSignInAccount) { + Games.getGamesClient(context, account).setViewForPopups(activity.findViewById(android.R.id.content)) + } + + override fun hasGooglePlayGames(): Boolean = true + + override fun silentLogin(activity: Activity) { + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build() + val lastAccount: GoogleSignInAccount? = GoogleSignIn.getLastSignedInAccount(context) + + if (lastAccount != null) { + account = lastAccount.also { setupPopUp(activity, it) } + } else { + GoogleSignIn + .getClient(context, signInOptions) + .silentSignIn() + .addOnCompleteListener { task -> + if (task.isSuccessful) { + account = task.result?.also { setupPopUp(activity, it) } + } + } + } + } + + override fun getLoginIntent(): Intent? { + val signInClient = GoogleSignIn.getClient(context, GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN) + return signInClient.signInIntent + } + + override fun handleLoginResult(data: Intent?) { + if (data != null) { + Auth.GoogleSignInApi.getSignInResultFromIntent(data)?.let { result -> + if (result.isSuccess) { + account = result.signInAccount + } else { + result.status.statusMessage?.let { message -> + if (message.isNotBlank()) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + } + } + } + } + + override fun isLogged(): Boolean = account != null + + override fun openAchievements(activity: Activity) { + account?.let { + Games.getAchievementsClient(context, it) + .achievementsIntent + .addOnSuccessListener { intent -> + activity.startActivityForResult(intent, 0) + } + } + } + + override fun openLeaderboards(activity: Activity) { + account?.let { + Games.getLeaderboardsClient(context, it) + .allLeaderboardsIntent + .addOnSuccessListener { intent -> + activity.startActivityForResult(intent, 0) + } + } + } +} diff --git a/proprietary/src/main/res/values/values.xml b/proprietary/src/main/res/values/values.xml new file mode 100644 index 00000000..787df199 --- /dev/null +++ b/proprietary/src/main/res/values/values.xml @@ -0,0 +1,4 @@ + + + 0 + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fbf0d8c0..cc717647 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':app', ':wear', ':common', ':proprietary', ':foss' +include ':external' diff --git a/wear/build.gradle b/wear/build.gradle index 10722fe2..b1489ed7 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { // versionCode and versionName must be hardcoded to support F-droid - versionCode 800041 - versionName '8.0.4' + versionCode 800051 + versionName '8.0.5' applicationId 'dev.lucasnlm.antimine' minSdkVersion 23 targetSdkVersion 30 @@ -67,7 +67,7 @@ dependencies { // Dagger implementation 'com.google.dagger:hilt-android:2.28.1-alpha' - implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01' + implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02' kapt 'com.google.dagger:hilt-android-compiler:2.28.1-alpha' testImplementation 'com.google.dagger:hilt-android-testing:2.28.1-alpha' kaptTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'