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 00000000..1a022112 Binary files /dev/null and b/app/src/main/res/drawable/games_achievements.png differ 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 00000000..a9424b90 Binary files /dev/null and b/app/src/main/res/drawable/games_controller.png differ 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 00000000..c2aa3bb6 Binary files /dev/null and b/app/src/main/res/drawable/games_leaderboards.png differ diff --git a/app/src/main/res/layout/dialog_custom_game.xml b/app/src/main/res/layout/dialog_custom_game.xml index 806e72ef..2961c8be 100644 --- a/app/src/main/res/layout/dialog_custom_game.xml +++ b/app/src/main/res/layout/dialog_custom_game.xml @@ -5,7 +5,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" android:padding="16dp"> + + + + + + + + + \ 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'