Add support to Google stuff and small optimizations

This commit is contained in:
Lucas Lima 2020-08-16 20:28:16 -03:00
parent 90041d9bc5
commit ac8868d56a
No known key found for this signature in database
GPG key ID: 049CCC5A365B00D2
64 changed files with 1262 additions and 146 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ app/local.properties
app/release/ app/release/
app/standalone/ app/standalone/
app/google/ app/google/
app/google-services.json
proprietary/google-services.json

View file

@ -9,8 +9,8 @@ android {
defaultConfig { defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid // versionCode and versionName must be hardcoded to support F-droid
versionCode 800041 versionCode 800051
versionName '8.0.4' versionName '8.0.5'
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
multiDexEnabled true multiDexEnabled true
@ -93,6 +93,7 @@ dependencies {
// Dependencies must be hardcoded to support F-droid // Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
implementation project(':common') implementation project(':common')
googleImplementation project(':proprietary') googleImplementation project(':proprietary')

View file

@ -12,6 +12,4 @@ object DeepLink {
const val EXPERT_PATH = "expert" const val EXPERT_PATH = "expert"
const val STANDARD_PATH = "standard" const val STANDARD_PATH = "standard"
const val CUSTOM_PATH = "custom" const val CUSTOM_PATH = "custom"
const val CUSTOM_NEW_GAME = "antimine://new-game/custom"
} }

View file

@ -9,6 +9,7 @@ import android.os.Handler
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.FrameLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -16,6 +17,7 @@ import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.HandlerCompat.postDelayed import androidx.core.os.HandlerCompat.postDelayed
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.doOnLayout
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentTransaction 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.instant.InstantAppManager
import dev.lucasnlm.antimine.level.view.EndGameDialogFragment import dev.lucasnlm.antimine.level.view.EndGameDialogFragment
import dev.lucasnlm.antimine.level.view.LevelFragment import dev.lucasnlm.antimine.level.view.LevelFragment
import dev.lucasnlm.antimine.playgames.PlayGamesDialogFragment
import dev.lucasnlm.antimine.preferences.PreferencesActivity import dev.lucasnlm.antimine.preferences.PreferencesActivity
import dev.lucasnlm.antimine.share.viewmodel.ShareViewModel import dev.lucasnlm.antimine.share.viewmodel.ShareViewModel
import dev.lucasnlm.antimine.stats.StatsActivity import dev.lucasnlm.antimine.stats.StatsActivity
import dev.lucasnlm.antimine.theme.ThemeActivity 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.*
import kotlinx.android.synthetic.main.activity_game.minesCount import kotlinx.android.synthetic.main.activity_game.minesCount
import kotlinx.android.synthetic.main.activity_game.timer import kotlinx.android.synthetic.main.activity_game.timer
@ -66,6 +70,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
@Inject @Inject
lateinit var savesRepository: ISavesRepository lateinit var savesRepository: ISavesRepository
@Inject
lateinit var playGamesManager: IPlayGamesManager
val viewModel: GameViewModel by viewModels() val viewModel: GameViewModel by viewModels()
private val shareViewModel: ShareViewModel by viewModels() private val shareViewModel: ShareViewModel by viewModels()
@ -90,7 +97,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
bindToolbar() bindToolbar()
bindDrawer() bindDrawer()
bindNavigationMenu() bindNavigationMenu()
loadGameFragment()
findViewById<FrameLayout>(R.id.levelContainer).doOnLayout {
loadGameFragment()
}
if (instantAppManager.isEnabled()) { if (instantAppManager.isEnabled()) {
bindInstantApp() bindInstantApp()
@ -201,6 +211,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
analyticsManager.sentEvent(Analytics.Resume) 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.previous_games -> openSaveHistory()
R.id.stats -> openStats() R.id.stats -> openStats()
R.id.install_new -> installFromInstantApp() R.id.install_new -> installFromInstantApp()
R.id.play_games -> googlePlay()
else -> handled = false 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() navigationView.menu.findItem(R.id.share_now).isVisible = instantAppManager.isNotEnabled()
if (!playGamesManager.hasGooglePlayGames()) {
navigationView.menu.removeGroup(R.id.play_games_group)
}
} }
private fun checkUseCount() { private fun checkUseCount() {
@ -369,22 +386,22 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
private fun loadGameFragment() { 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 -> beginTransaction().apply {
fragmentManager.beginTransaction().apply { replace(R.id.levelContainer, LevelFragment())
remove(it) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss() commitAllowingStateLoss()
} }
} }
fragmentManager.beginTransaction().apply {
replace(R.id.levelContainer, LevelFragment())
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss()
}
} }
private fun showRequestRating() { 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 { companion object {
const val PREFERENCE_FIRST_USE = "preference_first_use" const val PREFERENCE_FIRST_USE = "preference_first_use"
const val PREFERENCE_USE_COUNT = "preference_use_count" 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_REFERRER = "InstallApiActivity"
const val IA_REQUEST_CODE = 5 const val IA_REQUEST_CODE = 5
const val GOOGLE_PLAY_REQUEST_CODE = 6
const val MIN_USAGES_TO_RATING = 4 const val MIN_USAGES_TO_RATING = 4
} }

View file

@ -21,10 +21,10 @@ class ControlDialogFragment : AppCompatDialogFragment() {
private val adapter by lazy { ControlListAdapter(controlViewModel) } private val adapter by lazy { ControlListAdapter(controlViewModel) }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val currentControl = controlViewModel.singleState().selectedId val state = controlViewModel.singleState()
return AlertDialog.Builder(requireContext()).apply { return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.control) setTitle(R.string.control)
setSingleChoiceItems(adapter, currentControl, null) setSingleChoiceItems(adapter, state.selectedIndex, null)
setPositiveButton(R.string.ok, null) setPositiveButton(R.string.ok, null)
}.create() }.create()
} }
@ -41,8 +41,6 @@ class ControlDialogFragment : AppCompatDialogFragment() {
) : BaseAdapter() { ) : BaseAdapter() {
private val controlList = controlViewModel.singleState().gameControls private val controlList = controlViewModel.singleState().gameControls
fun getSelectedId() = controlViewModel.singleState().selectedId
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = if (convertView == null) { val view = if (convertView == null) {
ControlItemView(parent!!.context) ControlItemView(parent!!.context)
@ -50,12 +48,12 @@ class ControlDialogFragment : AppCompatDialogFragment() {
(convertView as ControlItemView) (convertView as ControlItemView)
} }
val selectedId = getSelectedId() val selected = controlViewModel.singleState().selected
return view.apply { return view.apply {
val controlModel = controlList[position] val controlModel = controlList[position]
bind(controlModel) bind(controlModel)
setRadio(selectedId == controlModel.controlStyle.ordinal) setRadio(selected == controlModel.controlStyle)
setOnClickListener { setOnClickListener {
controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle)) controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle))
notifyDataSetChanged() notifyDataSetChanged()

View file

@ -6,7 +6,6 @@ import dev.lucasnlm.antimine.core.control.ControlStyle
data class ControlDetails( data class ControlDetails(
val id: Long, val id: Long,
val controlStyle: ControlStyle, val controlStyle: ControlStyle,
@StringRes val titleId: Int,
@StringRes val firstActionId: Int, @StringRes val firstActionId: Int,
@StringRes val firstActionResponseId: Int, @StringRes val firstActionResponseId: Int,
@StringRes val secondActionId: Int, @StringRes val secondActionId: Int,

View file

@ -17,7 +17,6 @@ class ControlItemView : FrameLayout {
private val radio: AppCompatRadioButton private val radio: AppCompatRadioButton
private val root: View private val root: View
private val title: TextView
private val firstAction: TextView private val firstAction: TextView
private val firstActionResponse: TextView private val firstActionResponse: TextView
private val secondAction: TextView private val secondAction: TextView
@ -30,7 +29,6 @@ class ControlItemView : FrameLayout {
radio = findViewById(R.id.radio) radio = findViewById(R.id.radio)
root = findViewById(R.id.root) root = findViewById(R.id.root)
title = findViewById(R.id.title)
firstAction = findViewById(R.id.firstAction) firstAction = findViewById(R.id.firstAction)
firstActionResponse = findViewById(R.id.firstActionResponse) firstActionResponse = findViewById(R.id.firstActionResponse)
secondAction = findViewById(R.id.secondAction) secondAction = findViewById(R.id.secondAction)
@ -38,7 +36,6 @@ class ControlItemView : FrameLayout {
} }
fun bind(controlDetails: ControlDetails) { fun bind(controlDetails: ControlDetails) {
title.text = context.getString(controlDetails.titleId)
firstAction.text = context.getString(controlDetails.firstActionId) firstAction.text = context.getString(controlDetails.firstActionId)
firstActionResponse.text = context.getString(controlDetails.firstActionResponseId) firstActionResponse.text = context.getString(controlDetails.firstActionResponseId)
secondAction.text = context.getString(controlDetails.secondActionId) secondAction.text = context.getString(controlDetails.secondActionId)

View file

@ -1,8 +1,10 @@
package dev.lucasnlm.antimine.control.viewmodel package dev.lucasnlm.antimine.control.viewmodel
import dev.lucasnlm.antimine.control.models.ControlDetails import dev.lucasnlm.antimine.control.models.ControlDetails
import dev.lucasnlm.antimine.core.control.ControlStyle
data class ControlState( data class ControlState(
val selectedId: Int, val selectedIndex: Int,
val selected: ControlStyle,
val gameControls: List<ControlDetails> val gameControls: List<ControlDetails>
) )

View file

@ -16,7 +16,6 @@ class ControlViewModel @ViewModelInject constructor(
ControlDetails( ControlDetails(
id = 0L, id = 0L,
controlStyle = ControlStyle.Standard, controlStyle = ControlStyle.Standard,
titleId = R.string.standard,
firstActionId = R.string.single_click, firstActionId = R.string.single_click,
firstActionResponseId = R.string.open_tile, firstActionResponseId = R.string.open_tile,
secondActionId = R.string.long_press, secondActionId = R.string.long_press,
@ -25,7 +24,6 @@ class ControlViewModel @ViewModelInject constructor(
ControlDetails( ControlDetails(
id = 1L, id = 1L,
controlStyle = ControlStyle.FastFlag, controlStyle = ControlStyle.FastFlag,
titleId = R.string.flag_first,
firstActionId = R.string.single_click, firstActionId = R.string.single_click,
firstActionResponseId = R.string.flag_tile, firstActionResponseId = R.string.flag_tile,
secondActionId = R.string.long_press, secondActionId = R.string.long_press,
@ -34,29 +32,43 @@ class ControlViewModel @ViewModelInject constructor(
ControlDetails( ControlDetails(
id = 2L, id = 2L,
controlStyle = ControlStyle.DoubleClick, controlStyle = ControlStyle.DoubleClick,
titleId = R.string.double_click,
firstActionId = R.string.single_click, firstActionId = R.string.single_click,
firstActionResponseId = R.string.flag_tile, firstActionResponseId = R.string.flag_tile,
secondActionId = R.string.double_click, secondActionId = R.string.double_click,
secondActionResponseId = R.string.open_tile 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 = override fun initialState(): ControlState {
ControlState( val controlDetails = gameControlOptions.firstOrNull {
selectedId = gameControlOptions.firstOrNull { it.controlStyle == preferencesRepository.controlStyle()
it.controlStyle == preferencesRepository.controlStyle() }
}?.id?.toInt() ?: 0, return ControlState(
selectedIndex = controlDetails?.id?.toInt() ?: 0,
selected = controlDetails?.controlStyle ?: ControlStyle.Standard,
gameControls = gameControlOptions gameControls = gameControlOptions
) )
}
override suspend fun mapEventToState(event: ControlEvent) = flow { override suspend fun mapEventToState(event: ControlEvent) = flow {
if (event is ControlEvent.SelectControlStyle) { if (event is ControlEvent.SelectControlStyle) {
val controlStyle = event.controlStyle val controlStyle = event.controlStyle
preferencesRepository.useControlStyle(controlStyle) preferencesRepository.useControlStyle(controlStyle)
val selected = state.gameControls.first { it.controlStyle == event.controlStyle }
val newState = state.copy( val newState = state.copy(
selectedId = state.gameControls.first { it.controlStyle == event.controlStyle }.id.toInt() selectedIndex = selected.id.toInt(),
selected = selected.controlStyle
) )
emit(newState) emit(newState)

View file

@ -6,13 +6,48 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent import dagger.hilt.android.components.ApplicationComponent
import dagger.hilt.android.qualifiers.ApplicationContext 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.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 @Module
@InstallIn(ApplicationComponent::class) @InstallIn(ApplicationComponent::class)
class AppModule { class AppModule {
@Singleton
@Provides @Provides
fun provideInstantAppManager( fun provideInstantAppManager(
@ApplicationContext context: Context @ApplicationContext context: Context
): InstantAppManager = InstantAppManager(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))
}
}
} }

View file

@ -3,15 +3,15 @@ package dev.lucasnlm.antimine.instant
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import dev.lucasnlm.external.InstantAppWrapper import dev.lucasnlm.external.InstantAppManager
class InstantAppManager( class InstantAppManager(
private val context: Context private val context: Context
) { ) {
fun isEnabled(): Boolean = InstantAppWrapper().isEnabled(context) fun isEnabled(): Boolean = InstantAppManager().isInstantAppSupported(context)
fun isNotEnabled(): Boolean = isEnabled().not() fun isNotEnabled(): Boolean = isEnabled().not()
fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) = fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) =
InstantAppWrapper().showInstallPrompt(activity, intent, requestCode, referrer) InstantAppManager().showInstallPrompt(activity, intent, requestCode, referrer)
} }

View file

@ -1,9 +1,9 @@
package dev.lucasnlm.antimine.level.view package dev.lucasnlm.antimine.level.view
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.core.view.doOnLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint 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.R
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event 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.CommonLevelFragment
import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@AndroidEntryPoint @AndroidEntryPoint
open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) { open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
GlobalScope.launch { lifecycleScope.launch {
viewModel.saveGame() viewModel.saveGame()
} }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerGrid = view.findViewById(R.id.recyclerGrid) 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 { viewModel.run {
field.observe( field.observe(
@ -44,13 +64,7 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
levelSetup.observe( levelSetup.observe(
viewLifecycleOwner, viewLifecycleOwner,
Observer { Observer {
recyclerGrid.apply { setupRecyclerViewSize(it)
val horizontalPadding = calcHorizontalPadding(it.width)
val verticalPadding = calcVerticalPadding(it.height)
layoutManager = makeNewLayoutManager(it.width)
setHasFixedSize(true)
setPadding(horizontalPadding, verticalPadding, 0, 0)
}
} }
) )
@ -81,38 +95,19 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
} }
} }
override fun onAttach(context: Context) { private fun setupRecyclerViewSize(levelSetup: Minefield) {
super.onAttach(context) 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 { animate().apply {
val loadGameUid = checkLoadGameDeepLink() alpha(1.0f)
val newGameDeepLink = checkNewGameDeepLink() duration = DateUtils.SECOND_IN_MILLIS
val retryDeepLink = checkRetryGameDeepLink() }.start()
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()
}
}
} }
} }

View file

@ -28,6 +28,7 @@ class EndGameDialogViewModel @ViewModelInject constructor(
R.drawable.emoji_grinning_squinting_face, R.drawable.emoji_grinning_squinting_face,
R.drawable.emoji_smiling_face_with_sunglasses, R.drawable.emoji_smiling_face_with_sunglasses,
R.drawable.emoji_squinting_face_with_tongue, R.drawable.emoji_squinting_face_with_tongue,
R.drawable.emoji_hugging_face,
R.drawable.emoji_partying_face, R.drawable.emoji_partying_face,
R.drawable.emoji_clapping_hands, R.drawable.emoji_clapping_hands,
R.drawable.emoji_triangular_flag R.drawable.emoji_triangular_flag

View file

@ -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<PlayGamesViewModel>()
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
}

View file

@ -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
)

View file

@ -0,0 +1,6 @@
package dev.lucasnlm.antimine.playgames.viewmodel
sealed class PlayGamesEvent {
object OpenAchievements : PlayGamesEvent()
object OpenLeaderboards : PlayGamesEvent()
}

View file

@ -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<PlayGamesEvent>() {
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)
}
}

View file

@ -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
}
}

View file

@ -1,5 +1,6 @@
package dev.lucasnlm.antimine.theme package dev.lucasnlm.antimine.theme
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -9,6 +10,8 @@ import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration 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.view.ThemeAdapter
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import kotlinx.android.synthetic.main.activity_theme.* 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)
}
}
} }
} }

View file

@ -0,0 +1,359 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M63.6,118.8c-27.9,0 -58,-17.5 -58,-55.9S35.7,7 63.6,7c15.5,0 29.8,5.1 40.4,14.4c11.5,10.2 17.6,24.6 17.6,41.5s-6.1,31.2 -17.6,41.4c-10.6,9.3 -25,14.5 -40.4,14.5z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="56.96"
android:centerX="63.6"
android:centerY="62.9"
android:type="radial">
<item android:offset="0.5" android:color="#FFFDE030"/>
<item android:offset="0.919" android:color="#FFF7C02B"/>
<item android:offset="1" android:color="#FFF4A223"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M63.6,118.8c-27.9,0 -58,-17.5 -58,-55.9S35.7,7 63.6,7c15.5,0 29.8,5.1 40.4,14.4c11.5,10.2 17.6,24.6 17.6,41.5s-6.1,31.2 -17.6,41.4c-10.6,9.3 -25,14.5 -40.4,14.5z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="118.8"
android:startX="63.6"
android:endY="7"
android:endX="63.6"
android:type="linear">
<item android:offset="0.158" android:color="#FFF4A223"/>
<item android:offset="0.333" android:color="#FFF7C02B"/>
<item android:offset="0.807" android:color="#00FDE030"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M111.49,29.67c5.33,8.6 8.11,18.84 8.11,30.23c0,16.9 -6.1,31.2 -17.6,41.4c-10.6,9.3 -25,14.5 -40.4,14.5c-18.06,0 -37.04,-7.35 -48.18,-22.94c10.76,17.66 30.99,25.94 50.18,25.94c15.4,0 29.8,-5.2 40.4,-14.5c11.5,-10.2 17.6,-24.5 17.6,-41.4c0,-12.74 -3.47,-24.06 -10.11,-33.23z"
android:fillColor="#eb8f00"/>
<path
android:pathData="M29.7,64.1m-16.3,0a16.3,16.3 0,1 1,32.6 0a16.3,16.3 0,1 1,-32.6 0"
android:strokeAlpha="0.8"
android:fillAlpha="0.8">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="17.73736"
android:centerX="29.6654"
android:centerY="64.10675"
android:type="radial">
<item android:offset="0" android:color="#FFED7770"/>
<item android:offset="0.9" android:color="#00ED7770"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M96.9,51.5m-16.3,0a16.3,16.3 0,1 1,32.6 0a16.3,16.3 0,1 1,-32.6 0"
android:strokeAlpha="0.8"
android:fillAlpha="0.8">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="17.735403"
android:centerX="96.90706"
android:centerY="51.534607"
android:type="radial">
<item android:offset="0" android:color="#FFED7770"/>
<item android:offset="0.9" android:color="#00ED7770"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M96.9,49.4C89.2,56.5 79,62.1 67.2,64.8c-11.8,2.7 -23.4,2.2 -33.4,-0.9c-1.8,-0.6 -3.1,1.8 -1.5,3c10.1,7.5 23.6,10.7 37.1,7.5c13.5,-3.1 24.3,-11.8 30.1,-23c1,-1.7 -1.2,-3.3 -2.6,-2z"
android:fillColor="#422b0d"/>
<path
android:pathData="M48.5,47.2l-0.2,-0.2c-0.1,-0.1 -0.3,-0.2 -0.5,-0.4c-0.2,-0.1 -0.4,-0.3 -0.6,-0.4c-0.2,-0.2 -0.5,-0.4 -0.8,-0.6c-0.3,-0.2 -0.6,-0.4 -0.9,-0.5c-0.3,-0.2 -0.6,-0.3 -0.9,-0.3c-0.3,-0.1 -0.5,-0.1 -0.6,-0.1h-0.2h-0.1h0.2l-0.5,0.1c-0.1,0 0,0 0,0h0.1c0.1,0 0,0 0,0h-0.1c-0.1,0.1 -0.3,0.2 -0.5,0.3c-0.2,0.2 -0.5,0.4 -0.7,0.6c-0.2,0.2 -0.4,0.5 -0.6,0.8c-0.4,0.6 -0.7,1.1 -0.9,1.5s-0.4,0.6 -0.4,0.6l-0.2,0.4c-1,1.7 -3.2,2.4 -5,1.5c-1.2,-0.6 -1.9,-1.7 -2,-2.9v-1.1c0.1,-0.7 0.2,-1.7 0.6,-2.9s1.1,-2.7 2.4,-4.1c0.7,-0.7 1.5,-1.5 2.5,-2c0.2,-0.2 0.5,-0.3 0.8,-0.4c0.3,-0.1 0.5,-0.3 0.9,-0.4l0.5,-0.2c0.2,-0.1 0.4,-0.1 0.5,-0.1l0.5,-0.1l0.3,-0.1h0.4l0.5,-0.1h0.9c0.6,0 1.2,0 1.8,0.1c1.2,0.2 2.2,0.6 3.1,1c1.8,0.9 2.9,2 3.8,2.9c0.4,0.5 0.8,0.9 1,1.3c0.3,0.4 0.5,0.8 0.7,1.1s0.2,0.5 0.3,0.6c0,0.1 0.1,0.2 0.1,0.2c0.7,1.8 -0.3,3.8 -2.3,4.5c-1.4,0.5 -2.9,0.2 -3.9,-0.6z"
android:fillColor="#422b0d"/>
<path
android:pathData="M82.8,40.5l-0.2,-0.2c-0.1,-0.1 -0.3,-0.2 -0.5,-0.4c-0.2,-0.1 -0.4,-0.3 -0.6,-0.4c-0.2,-0.2 -0.5,-0.4 -0.8,-0.6c-0.3,-0.2 -0.6,-0.4 -0.9,-0.5c-0.3,-0.2 -0.6,-0.3 -0.9,-0.3c-0.2,-0.1 -0.4,-0.1 -0.5,-0.1H78.1h0.2l-0.5,0.1c-0.1,0 0,0 0,0h0.1c0.1,0 0,0 0,0h-0.1c-0.1,0.1 -0.3,0.2 -0.6,0.3c-0.2,0.2 -0.4,0.4 -0.7,0.6c-0.2,0.2 -0.4,0.5 -0.6,0.8c-0.4,0.6 -0.7,1.1 -0.9,1.5c-0.2,0.4 -0.4,0.6 -0.4,0.6l-0.2,0.3c-1,1.7 -3.2,2.4 -5,1.5c-1.2,-0.6 -1.9,-1.7 -2,-2.9c0,0 0,-0.4 0.1,-1.1c0.1,-0.7 0.2,-1.7 0.6,-2.9s1.1,-2.7 2.4,-4.1c0.7,-0.7 1.5,-1.5 2.5,-2c0.2,-0.1 0.5,-0.3 0.8,-0.4s0.5,-0.3 0.9,-0.4l0.6,-0.2c0.2,-0.1 0.4,-0.1 0.5,-0.1l0.5,-0.1l0.3,-0.1h0.4l0.5,-0.1h0.9c0.6,0 1.2,0 1.8,0.1c1.2,0.2 2.2,0.6 3.1,1c1.8,0.9 2.9,2 3.8,2.9c0.4,0.5 0.8,0.9 1,1.3c0.3,0.4 0.5,0.8 0.7,1.1s0.2,0.5 0.3,0.6c0.1,0.1 0.1,0.2 0.1,0.2c0.7,1.8 -0.3,3.8 -2.3,4.5c-1.6,0.7 -3.1,0.4 -4.1,-0.5z"
android:fillColor="#422b0d"/>
<path
android:pathData="M38.87,84.46C37.78,81.38 37,113.28 37,113.28c2.92,1.23 5.87,2.31 8.93,3.13c1.21,-2.73 2.79,-7.76 1.96,-13.46c-1.34,-9.11 -6.78,-12.14 -9.02,-18.49z"
android:strokeAlpha="0.66"
android:fillAlpha="0.66">
<aapt:attr name="android:fillColor">
<gradient
android:startY="128.189"
android:startX="42.556"
android:endY="97.296"
android:endX="42.556"
android:type="linear">
<item android:offset="0" android:color="#FFBF360C"/>
<item android:offset="1" android:color="#33BF360C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M39.5,81.23c-0.49,-1.33 -1.11,-2.32 -2.68,-2.32c-0.72,0 -1.94,0.18 -2.59,1.17c-1.28,1.92 -0.78,5.52 0.2,8.65c0.55,1.77 0.4,2.42 0.4,2.42c-2.72,-0.17 -5.88,-3.88 -9.03,-7.58c-1.93,-2.27 -3.05,-4.36 -4.93,-6.68c-1.14,-1.41 -2.81,-2.83 -4.77,-2.11c-5.37,1.99 1.27,9.72 2.93,11.84c1.89,2.41 3.76,4.72 3.76,4.72c-1.76,-0.41 -2.87,-0.88 -5.97,-4.06c-2.65,-2.72 -4.62,-6.23 -6.06,-7.54c-3.27,-2.97 -5.27,-2 -6.14,-0.02c-0.81,1.84 0.53,5.09 1.44,6.61c4.51,7.48 10.27,10.41 10.27,10.41c-4.74,-0.21 -8.48,-5.29 -9.99,-6.83c-4.38,-4.45 -6.38,1.06 -3.99,5.05c1.1,1.83 2.2,3.12 4.21,5.19c3.25,3.33 6.31,4.84 6.31,4.84s-1.31,0.2 -2.63,-0.49c-1.78,-0.93 -3.77,-2.33 -4.8,-3.13c-2.95,-2.27 -5.76,-0.32 -4.63,2.7c1.31,3.49 6.09,6.45 8.98,8.6c5.62,4.16 12.63,7.49 19.73,7.26c7.9,-0.25 13.34,-5.68 15.48,-13.31c1.24,-4.44 -1.72,-11.5 -2.47,-13.28c-2.47,-5.76 -2.54,-10.78 -3.03,-12.11z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="29.186"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M23.62,99.08c0.14,8.38 5.4,11.88 9.48,12.65a13.771,13.771 0,0 1,-2.39 -8.2c0.1,-2.9 1.22,-6.51 2.88,-8.47c1.66,-1.96 1.24,-3.89 1.24,-3.89s-1.12,-0.09 -1.54,-0.34c-1.34,-0.82 -2.88,-1.48 -4.04,-2.55c-0.85,-0.79 -5.77,2.42 -5.63,10.8z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="11.727577"
android:centerX="36.794704"
android:centerY="102.449486"
android:type="radial">
<item android:offset="0" android:color="#FFFFC709"/>
<item android:offset="1" android:color="#00FFC709"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M6.4,78.54a2.84,2.84 0,0 0,-1.28 2.35c-0.02,0.91 0.25,1.82 0.58,2.68c0.7,1.73 1.8,3.28 2.93,4.77c1.16,1.48 2.41,2.91 3.71,4.29c1.3,1.39 2.63,2.76 3.98,4.13c-1.67,-0.98 -3.22,-2.16 -4.65,-3.46c-1.42,-1.31 -2.77,-2.73 -3.91,-4.31c-1.17,-1.57 -2.1,-3.32 -2.71,-5.2c-0.28,-0.94 -0.48,-1.95 -0.31,-2.95c0.12,-0.99 0.76,-1.94 1.66,-2.3z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="95.255"
android:startX="18.517"
android:endY="82.775"
android:endX="5.827"
android:type="linear">
<item android:offset="0.127" android:color="#FFB7537A"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M0.55,102.67c0.03,-0.28 0.07,-0.52 0.12,-0.73c-0.07,0.23 -0.11,0.47 -0.12,0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="29.186"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M4.31,106.78c-0.72,-0.69 -1.21,-1.27 -1.58,-1.88c-0.42,-0.7 -0.66,-1.44 -0.68,-2.13c-0.02,-0.77 0.05,-1.34 0.78,-1.9c0.36,-0.28 1.37,-0.18 1.35,-0.19c-0.01,-0.01 -0.03,-0.01 -0.04,-0.02c-0.61,-0.24 -1.18,-0.3 -1.69,-0.22l0.01,-0.01l-0.01,0.01c-0.86,0.14 -1.52,0.69 -1.78,1.5c-0.05,0.21 -0.09,0.45 -0.12,0.73c-0.01,0.43 0.06,0.91 0.25,1.42c0.29,0.77 0.76,1.52 1.33,2.24c0.34,0.52 0.74,1.01 1.11,1.48c0.59,0.7 1.19,1.39 1.86,2.01a26.45,26.45 0,0 0,4.3 3.38c0.75,0.46 8.17,5.01 14.67,6.26c0.02,-0.02 -12.39,-5.14 -19.76,-12.68z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="109.935"
android:startX="0.549"
android:endY="109.935"
android:endX="24.074"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M13.25,105.53c-0.07,0 -0.15,-0.01 -0.21,-0.04c-1.58,-0.66 -3.15,-1.6 -4.81,-2.88a27.08,27.08 0,0 1,-4.01 -3.91c-0.64,-0.74 -1.21,-1.55 -1.67,-2.24l-0.14,-0.22c-0.47,-0.73 -0.95,-1.48 -1.25,-2.36c-0.39,-1.04 -0.49,-2.11 -0.27,-3.1c0.25,-1.19 0.93,-2 1.88,-2.41c1.36,-0.59 2.88,0.91 2.79,0.87c-1.05,-0.47 -1.97,-0.21 -2.21,-0.05c-0.55,0.38 -0.9,1.04 -0.97,1.81c-0.05,0.69 0.11,1.45 0.46,2.19c0.3,0.65 0.72,1.28 1.37,2.04c0.56,0.72 1.14,1.38 1.71,2.03a57.36,57.36 0,0 0,3.68 3.77c1.07,0.99 2.49,2.28 4.02,3.5c0.22,0.17 0.28,0.48 0.13,0.72c-0.12,0.18 -0.3,0.28 -0.5,0.28z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="96.876"
android:startX="0.779"
android:endY="96.876"
android:endX="13.816"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M16.78,96.91c0.19,-0.21 0.15,-0.34 -0.05,-0.54a218.71,218.71 0,0 1,-3.97 -4.12a61.13,61.13 0,0 1,-3.67 -4.24C8,86.58 6.9,85.04 6.22,83.37c-0.26,-0.68 -0.56,-1.58 -0.54,-2.47c0,-0.4 0.11,-0.77 0.29,-1.1c0.17,-0.31 0.42,-0.58 0.73,-0.78c0.01,0 0.01,-0.01 0.01,-0.01c0.51,-0.31 1.34,-0.54 2.67,-0.31c-2.33,-1.57 -4.08,-0.68 -4.82,1.01c-0.81,1.84 0.57,5.13 1.49,6.65c4.51,7.48 10.02,10.47 10.02,10.47c0.27,0.17 0.57,0.22 0.71,0.08z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="87.433"
android:startX="4.074"
android:endY="87.433"
android:endX="16.901"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M23.54,91.61c-1.69,-2.24 -8.47,-9.35 -8.3,-14c0.05,-1.49 2.43,-3.02 4.81,-1.63c0.03,0.02 -1.74,-2 -3.96,-1.21c-1.93,0.68 -2.21,2.46 -2.23,2.78c-0.1,1.01 0.18,1.98 0.45,2.72c0.56,1.4 1.27,2.76 2.29,4.39c0.76,1.24 1.65,2.5 2.79,3.99c0.63,0.81 1.27,1.55 1.92,2.25c0.44,0.48 0.89,0.94 1.35,1.39c0.11,0.1 0.25,0.16 0.39,0.16c0.12,0 0.24,-0.04 0.34,-0.11c0.21,-0.18 0.31,-0.51 0.15,-0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="83.513"
android:startX="13.586"
android:endY="83.513"
android:endX="23.62"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M36.82,78.92c-0.72,0 -1.94,0.18 -2.59,1.17c-1.28,1.92 -0.78,5.52 0.2,8.65c0.16,0.51 0.26,0.92 0.32,1.26a0.794,0.794 0,0 1,-1.06 0.9c-2.45,-0.92 -5.17,-4.13 -7.89,-7.32l-0.41,-0.5c0.03,0.46 1.59,2.96 3.72,5.65c0.57,0.73 4.36,4.86 6.38,3.68c1.25,-0.72 -0.79,-6.32 -0.81,-7.77c-0.01,-0.79 -0.12,-1.81 0.13,-2.75c0.05,-0.24 0.2,-0.49 0.39,-0.74c1.09,-1.46 3.35,-1.52 4.28,0.05l0.03,0.04c-0.5,-1.34 -1.12,-2.32 -2.69,-2.32z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="85.763"
android:startX="25.394"
android:endY="85.763"
android:endX="40.599"
android:type="linear">
<item android:offset="0" android:color="#FFFFC400"/>
<item android:offset="0.873" android:color="#FFDA6727"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M88.31,84.46c1.09,-3.08 1.87,28.82 1.87,28.82c-2.92,1.23 -5.87,2.31 -8.93,3.13c-1.21,-2.73 -2.79,-7.76 -1.96,-13.46c1.34,-9.11 6.77,-12.14 9.02,-18.49z"
android:strokeAlpha="0.66"
android:fillAlpha="0.66">
<aapt:attr name="android:fillColor">
<gradient
android:startY="128.189"
android:startX="84.62198"
android:endY="97.296"
android:endX="84.62198"
android:type="linear">
<item android:offset="0" android:color="#FFBF360C"/>
<item android:offset="1" android:color="#33BF360C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M87.68,81.23c0.49,-1.33 1.11,-2.32 2.68,-2.32c0.72,0 1.94,0.18 2.59,1.17c1.28,1.92 0.78,5.52 -0.2,8.65c-0.55,1.77 -0.4,2.42 -0.4,2.42c2.72,-0.17 5.88,-3.88 9.03,-7.58c1.93,-2.27 3.05,-4.36 4.93,-6.68c1.14,-1.41 2.81,-2.83 4.77,-2.11c5.37,1.99 -1.27,9.72 -2.93,11.84c-1.89,2.41 -3.76,4.72 -3.76,4.72c1.76,-0.41 2.87,-0.88 5.97,-4.06c2.65,-2.72 4.62,-6.23 6.06,-7.54c3.27,-2.97 5.27,-2 6.14,-0.02c0.81,1.84 -0.53,5.09 -1.44,6.61c-4.51,7.48 -10.27,10.41 -10.27,10.41c4.74,-0.21 8.48,-5.29 9.99,-6.83c4.38,-4.45 6.38,1.06 3.99,5.05c-1.1,1.83 -2.2,3.12 -4.21,5.19c-3.25,3.33 -6.31,4.84 -6.31,4.84s1.31,0.2 2.63,-0.49c1.78,-0.93 3.77,-2.33 4.8,-3.13c2.95,-2.27 5.76,-0.32 4.63,2.7c-1.31,3.49 -6.09,6.45 -8.98,8.6c-5.62,4.16 -12.63,7.49 -19.73,7.26c-7.9,-0.25 -13.34,-5.68 -15.48,-13.31c-1.24,-4.44 1.72,-11.5 2.47,-13.28c2.46,-5.76 2.54,-10.78 3.03,-12.11z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="97.99198"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M103.56,99.08c-0.14,8.38 -5.4,11.88 -9.48,12.65c1.64,-2.39 2.49,-5.31 2.39,-8.2c-0.1,-2.9 -1.22,-6.51 -2.88,-8.47c-1.66,-1.96 -1.24,-3.89 -1.24,-3.89s1.12,-0.09 1.54,-0.34c1.34,-0.82 2.88,-1.48 4.04,-2.55c0.84,-0.79 5.77,2.42 5.63,10.8z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="11.727577"
android:centerX="90.41754"
android:centerY="102.50223"
android:type="radial">
<item android:offset="0" android:color="#FFFFC709"/>
<item android:offset="1" android:color="#00FFC709"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M120.77,78.54a2.84,2.84 0,0 1,1.28 2.35c0.03,0.92 -0.24,1.82 -0.57,2.68c-0.7,1.73 -1.8,3.28 -2.93,4.77a62.626,62.626 0,0 1,-3.71 4.29c-1.3,1.39 -2.63,2.76 -3.98,4.13c1.67,-0.98 3.22,-2.16 4.65,-3.46c1.42,-1.31 2.77,-2.73 3.91,-4.31c1.17,-1.57 2.1,-3.32 2.71,-5.2c0.28,-0.94 0.48,-1.95 0.31,-2.95c-0.13,-0.99 -0.76,-1.94 -1.67,-2.3z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="95.255"
android:startX="108.66098"
android:endY="82.775"
android:endX="121.35098"
android:type="linear">
<item android:offset="0.127" android:color="#FFB7537A"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M126.63,102.67c-0.03,-0.28 -0.07,-0.52 -0.12,-0.73c0.06,0.23 0.11,0.47 0.12,0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="97.99198"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M122.86,106.78c0.72,-0.69 1.21,-1.27 1.58,-1.88c0.42,-0.7 0.66,-1.44 0.68,-2.13c0.02,-0.77 -0.05,-1.34 -0.78,-1.9c-0.36,-0.28 -1.37,-0.18 -1.35,-0.19c0.01,-0.01 0.03,-0.01 0.04,-0.02c0.61,-0.24 1.18,-0.3 1.69,-0.22l-0.01,-0.01l0.01,0.01c0.86,0.14 1.52,0.69 1.78,1.5c0.05,0.21 0.09,0.45 0.12,0.73c0.01,0.43 -0.06,0.91 -0.25,1.42c-0.29,0.77 -0.76,1.52 -1.33,2.24c-0.34,0.52 -0.74,1.01 -1.11,1.48c-0.59,0.7 -1.19,1.39 -1.86,2.01a26.45,26.45 0,0 1,-4.3 3.38c-0.75,0.46 -8.17,5.01 -14.67,6.26c-0.01,-0.02 12.4,-5.14 19.76,-12.68z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="109.935"
android:startX="126.62798"
android:endY="109.935"
android:endX="103.10398"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M113.92,105.53c0.07,0 0.15,-0.01 0.21,-0.04c1.58,-0.66 3.15,-1.6 4.81,-2.88a27.08,27.08 0,0 0,4.01 -3.91c0.64,-0.74 1.21,-1.55 1.67,-2.24l0.14,-0.22c0.47,-0.73 0.95,-1.48 1.25,-2.36c0.39,-1.04 0.49,-2.11 0.27,-3.1c-0.25,-1.19 -0.93,-2 -1.88,-2.41c-1.36,-0.59 -2.88,0.91 -2.79,0.87c1.05,-0.47 1.97,-0.21 2.21,-0.05c0.55,0.38 0.91,1.04 0.98,1.81c0.05,0.69 -0.11,1.45 -0.46,2.19c-0.3,0.65 -0.72,1.28 -1.37,2.04c-0.56,0.72 -1.14,1.38 -1.71,2.03a57.36,57.36 0,0 1,-3.68 3.77c-1.07,0.99 -2.49,2.28 -4.02,3.5c-0.22,0.17 -0.28,0.48 -0.13,0.72c0.11,0.18 0.3,0.28 0.49,0.28z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="96.876"
android:startX="126.39898"
android:endY="96.876"
android:endX="113.361984"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M110.4,96.91c-0.19,-0.21 -0.15,-0.34 0.05,-0.54c1.49,-1.51 2.79,-2.86 3.97,-4.12a61.13,61.13 0,0 0,3.67 -4.24c1.08,-1.42 2.18,-2.96 2.86,-4.63c0.26,-0.68 0.56,-1.58 0.54,-2.47c0,-0.4 -0.11,-0.77 -0.29,-1.1c-0.17,-0.31 -0.42,-0.58 -0.73,-0.78c-0.01,0 -0.01,-0.01 -0.01,-0.01c-0.51,-0.31 -1.34,-0.54 -2.67,-0.31c2.33,-1.57 4.08,-0.68 4.82,1.01c0.81,1.84 -0.57,5.13 -1.49,6.65c-4.51,7.48 -10.02,10.47 -10.02,10.47c-0.26,0.16 -0.57,0.21 -0.7,0.07z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="87.433"
android:startX="123.10298"
android:endY="87.433"
android:endX="110.275986"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M103.64,91.61c1.69,-2.24 8.47,-9.35 8.3,-14c-0.05,-1.49 -2.43,-3.02 -4.81,-1.63c-0.03,0.02 1.74,-2 3.96,-1.21c1.93,0.68 2.21,2.46 2.23,2.78c0.1,1.01 -0.18,1.98 -0.45,2.72c-0.56,1.4 -1.27,2.76 -2.29,4.39c-0.76,1.24 -1.65,2.5 -2.79,3.99c-0.63,0.81 -1.27,1.55 -1.92,2.25c-0.44,0.48 -0.89,0.94 -1.35,1.39a0.59,0.59 0,0 1,-0.39 0.16c-0.12,0 -0.24,-0.04 -0.34,-0.11c-0.22,-0.18 -0.32,-0.51 -0.15,-0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="83.513"
android:startX="113.59198"
android:endY="83.513"
android:endX="103.55798"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M90.36,78.92c0.72,0 1.94,0.18 2.59,1.17c1.28,1.92 0.78,5.52 -0.2,8.65c-0.16,0.51 -0.26,0.92 -0.32,1.26a0.794,0.794 0,0 0,1.06 0.9c2.45,-0.92 5.17,-4.13 7.89,-7.32l0.41,-0.5c-0.03,0.46 -1.59,2.96 -3.72,5.65c-0.57,0.73 -4.36,4.86 -6.38,3.68c-1.25,-0.72 0.79,-6.32 0.81,-7.77c0.01,-0.79 0.12,-1.81 -0.13,-2.75c-0.05,-0.24 -0.2,-0.49 -0.39,-0.74c-1.09,-1.46 -3.35,-1.52 -4.28,0.05l-0.03,0.04c0.5,-1.34 1.11,-2.32 2.69,-2.32z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="85.763"
android:startX="101.78398"
android:endY="85.763"
android:endX="86.57798"
android:type="linear">
<item android:offset="0" android:color="#FFFFC400"/>
<item android:offset="0.873" android:color="#FFDA6727"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M0,0h128v128h-128z"
android:fillColor="#00000000"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -5,7 +5,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"> android:padding="16dp">
<LinearLayout <LinearLayout

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/emoji"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/emoji_hugging_face"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:id="@+id/supportText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textSize="18sp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/emoji"/>
<TextView
android:id="@+id/supportDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/support_description"
android:layout_marginTop="12dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:gravity="center"
android:textSize="16sp"
android:paddingBottom="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/supportText"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -13,7 +13,7 @@
android:paddingRight="48dp" android:paddingRight="48dp"
android:paddingStart="32dp" android:paddingStart="32dp"
android:paddingEnd="48dp" android:paddingEnd="48dp"
android:paddingVertical="24dp"> android:paddingVertical="12dp">
<androidx.appcompat.widget.AppCompatRadioButton <androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/radio" android:id="@+id/radio"
@ -35,15 +35,6 @@
tools:text=""> tools:text="">
<TableRow> <TableRow>
<TextView
android:id="@+id/title"
android:textStyle="bold"
tools:text="Title" />
</TableRow>
<TableRow android:paddingVertical="4dp">
<TextView <TextView
android:id="@+id/firstAction" android:id="@+id/firstAction"
tools:text="First Action" /> tools:text="First Action" />
@ -62,7 +53,6 @@
</TableRow> </TableRow>
<TableRow> <TableRow>
<TextView <TextView
android:id="@+id/secondAction" android:id="@+id/secondAction"
tools:text="Long Tap" /> tools:text="Long Tap" />

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?selectableItemBackgroundBorderless"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:drawablePadding="10dp"
app:tint="@android:color/tab_indicator_text"
app:tintMode="src_in"
tools:src="@drawable/games_achievements" />
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:textStyle="bold"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
tools:text="Text" />
</LinearLayout>

View file

@ -71,6 +71,14 @@
android:title="@string/settings" /> android:title="@string/settings" />
</group> </group>
<group android:id="@+id/play_games_group">
<item
android:id="@+id/play_games"
android:checkable="false"
android:icon="@drawable/games_controller"
android:title="@string/google_play_games" />
</group>
<group <group
android:id="@+id/install_group" android:id="@+id/install_group"
android:visible="false" android:visible="false"

View file

@ -16,7 +16,7 @@ class ControlViewModelTest : IntentViewModelTest() {
val rule = InstantTaskExecutorRule() val rule = InstantTaskExecutorRule()
private fun ControlViewModel.selectedControlStyle() = singleState().let { private fun ControlViewModel.selectedControlStyle() = singleState().let {
it.gameControls[it.selectedId].controlStyle it.gameControls[it.selectedIndex].controlStyle
} }
@Test @Test

View file

@ -35,5 +35,5 @@ class TestLevelModule {
fun provideMinefieldRepository(): IMinefieldRepository = FixedMinefieldRepository() fun provideMinefieldRepository(): IMinefieldRepository = FixedMinefieldRepository()
@Provides @Provides
fun provideHapticFeedbackInteractor(): IHapticFeedbackManager = DisabledHapticFeedbackManager() fun provideHapticFeedbackManager(): IHapticFeedbackManager = DisabledHapticFeedbackManager()
} }

View file

@ -6,7 +6,8 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.0' classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28.1-alpha' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28.1-alpha'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72'
} }

View file

@ -9,8 +9,8 @@ android {
defaultConfig { defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid // versionCode and versionName must be hardcoded to support F-droid
versionCode 800041 versionCode 800051
versionName '8.0.4' versionName '8.0.5'
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@ -41,6 +41,7 @@ dependencies {
// Dependencies must be hardcoded to support F-droid // Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
// AndroidX // AndroidX
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
@ -61,8 +62,8 @@ dependencies {
kapt 'com.google.dagger:hilt-android-compiler:2.28.1-alpha' kapt 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
testImplementation 'com.google.dagger:hilt-android-testing: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' kaptTest 'com.google.dagger:hilt-android-compiler: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 "androidx.hilt:hilt-compiler:1.0.0-alpha01" kapt "androidx.hilt:hilt-compiler:1.0.0-alpha02"
// Room // Room
api 'androidx.room:room-runtime:2.2.5' api 'androidx.room:room-runtime:2.2.5'

View file

@ -61,10 +61,6 @@ class AreaAdapter(
return AreaViewHolder(view).apply { return AreaViewHolder(view).apply {
view.setOnDoubleClickListener(object : GestureDetector.OnDoubleTapListener { view.setOnDoubleClickListener(object : GestureDetector.OnDoubleTapListener {
override fun onDoubleTap(e: MotionEvent?): Boolean { override fun onDoubleTap(e: MotionEvent?): Boolean {
return false
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
val position = adapterPosition val position = adapterPosition
return when { return when {
position == RecyclerView.NO_POSITION -> { position == RecyclerView.NO_POSITION -> {
@ -83,8 +79,13 @@ class AreaAdapter(
} }
} }
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
return false
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { 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 val position = adapterPosition
if (position == RecyclerView.NO_POSITION) { if (position == RecyclerView.NO_POSITION) {
Log.d(TAG, "Item no longer exists.") Log.d(TAG, "Item no longer exists.")
@ -115,7 +116,8 @@ class AreaAdapter(
} }
itemView.setOnClickListener { itemView.setOnClickListener {
if (preferencesRepository.controlStyle() != ControlStyle.DoubleClick) { val style = preferencesRepository.controlStyle()
if (style != ControlStyle.DoubleClick && style != ControlStyle.DoubleClickInverted) {
val position = adapterPosition val position = adapterPosition
if (position == RecyclerView.NO_POSITION) { if (position == RecyclerView.NO_POSITION) {
Log.d(TAG, "Item no longer exists.") Log.d(TAG, "Item no longer exists.")

View file

@ -1,5 +1,8 @@
package dev.lucasnlm.antimine.common.level.view 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.annotation.LayoutRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -39,9 +42,7 @@ abstract class CommonLevelFragment(@LayoutRes val contentLayoutId: Int) : Fragme
val height = requireView().measuredHeight val height = requireView().measuredHeight
val recyclerViewHeight = (dimensionRepository.areaSize() * boardHeight) val recyclerViewHeight = (dimensionRepository.areaSize() * boardHeight)
val separatorsHeight = (2 * dimensionRepository.areaSeparator() * (boardHeight - 1)) val separatorsHeight = (2 * dimensionRepository.areaSeparator() * (boardHeight - 1))
val calculatedHeight = (height - recyclerViewHeight - separatorsHeight) val calculatedHeight = (height - recyclerViewHeight - separatorsHeight)
return (calculatedHeight / 2).coerceAtLeast(0.0f).toInt() return (calculatedHeight / 2).coerceAtLeast(0.0f).toInt()
} }
} }

View file

@ -208,7 +208,7 @@ class GameViewModel @ViewModelInject constructor(
} }
suspend fun saveGame() { suspend fun saveGame() {
if (initialized && gameController.hasMines) { if (gameController.hasMines) {
val id = savesRepository.saveGame( val id = savesRepository.saveGame(
gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty) gameController.getSaveState(elapsedTimeSeconds.value ?: 0L, currentDifficulty)
) )

View file

@ -5,12 +5,18 @@ import android.util.Log
import dev.lucasnlm.antimine.core.analytics.models.Analytics import dev.lucasnlm.antimine.core.analytics.models.Analytics
class DebugAnalyticsManager : IAnalyticsManager { class DebugAnalyticsManager : IAnalyticsManager {
override fun setup(context: Context, userProperties: Map<String, String>) { override fun setup(context: Context, properties: Map<String, String>) {
Log.d(TAG, "Setup Analytics using $userProperties") if (properties.isNotEmpty()) {
Log.d(TAG, "Setup Analytics using $properties")
}
} }
override fun sentEvent(event: Analytics) { 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 { companion object {

View file

@ -4,6 +4,6 @@ import android.content.Context
import dev.lucasnlm.antimine.core.analytics.models.Analytics import dev.lucasnlm.antimine.core.analytics.models.Analytics
interface IAnalyticsManager { interface IAnalyticsManager {
fun setup(context: Context, userProperties: Map<String, String>) fun setup(context: Context, properties: Map<String, String>)
fun sentEvent(event: Analytics) fun sentEvent(event: Analytics)
} }

View file

@ -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<String, String>) {
analyticsWrapper.setup(context, properties)
}
override fun sentEvent(event: Analytics) {
analyticsWrapper.sendEvent(event.name, event.extra)
}
}

View file

@ -5,7 +5,7 @@ import dev.lucasnlm.antimine.common.level.models.Score
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
sealed class Analytics( sealed class Analytics(
val title: String, val name: String,
val extra: Map<String, String> = mapOf() val extra: Map<String, String> = mapOf()
) { ) {
object Open : Analytics("Open game") object Open : Analytics("Open game")

View file

@ -26,7 +26,8 @@ data class Actions(
enum class ControlStyle { enum class ControlStyle {
Standard, Standard,
DoubleClick, 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 { companion object {
fun fromControlType(controlStyle: ControlStyle): GameControl { fun fromControlType(controlStyle: ControlStyle): GameControl {
return when (controlStyle) { return when (controlStyle) {
ControlStyle.Standard -> Standard ControlStyle.Standard -> Standard
ControlStyle.DoubleClick -> DoubleClick ControlStyle.DoubleClick -> DoubleClick
ControlStyle.FastFlag -> FastFlag ControlStyle.FastFlag -> FastFlag
ControlStyle.DoubleClickInverted -> DoubleClickInverted
} }
} }
} }

View file

@ -6,10 +6,12 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent import dagger.hilt.android.components.ApplicationComponent
import dagger.hilt.android.qualifiers.ApplicationContext 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.DimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager 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.IPreferencesRepository
import dev.lucasnlm.antimine.core.preferences.PreferencesManager import dev.lucasnlm.antimine.core.preferences.PreferencesManager
import dev.lucasnlm.antimine.core.preferences.PreferencesRepository 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.sound.SoundManager
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.core.themes.repository.ThemeRepository import dev.lucasnlm.antimine.core.themes.repository.ThemeRepository
import javax.inject.Singleton
@Module @Module
@InstallIn(ApplicationComponent::class) @InstallIn(ApplicationComponent::class)
@ -28,6 +31,7 @@ class CommonModule {
): IDimensionRepository = ): IDimensionRepository =
DimensionRepository(context, preferencesRepository) DimensionRepository(context, preferencesRepository)
@Singleton
@Provides @Provides
fun providePreferencesRepository( fun providePreferencesRepository(
preferencesManager: PreferencesManager preferencesManager: PreferencesManager
@ -38,9 +42,6 @@ class CommonModule {
@ApplicationContext context: Context @ApplicationContext context: Context
): PreferencesManager = PreferencesManager(context) ): PreferencesManager = PreferencesManager(context)
@Provides
fun provideAnalyticsManager(): IAnalyticsManager = DebugAnalyticsManager()
@Provides @Provides
fun provideSoundManager( fun provideSoundManager(
@ApplicationContext context: Context @ApplicationContext context: Context

View file

@ -33,6 +33,8 @@
<string name="size">Size</string> <string name="size">Size</string>
<string name="system">System</string> <string name="system">System</string>
<string name="rating">Feedback</string> <string name="rating">Feedback</string>
<string name="support">Help supporting us!</string>
<string name="support_description">It will allow us building new features and to keep our project active.</string>
<string name="rating_message">If you like this game, please give us a feedback. It will help us a lot.</string> <string name="rating_message">If you like this game, please give us a feedback. It will help us a lot.</string>
<string name="used_software_text">This game uses the following third parties software:</string> <string name="used_software_text">This game uses the following third parties software:</string>
<string name="translators_text">This game was translated by the following people:</string> <string name="translators_text">This game was translated by the following people:</string>
@ -58,7 +60,6 @@
<string name="use_question_mark">Use Question Mark</string> <string name="use_question_mark">Use Question Mark</string>
<string name="control">Controls</string> <string name="control">Controls</string>
<string name="arrow"></string> <string name="arrow"></string>
<string name="flag_first">Flag First</string>
<string name="single_click">Single Click</string> <string name="single_click">Single Click</string>
<string name="double_click">Double Click</string> <string name="double_click">Double Click</string>
<string name="long_press">Long Press</string> <string name="long_press">Long Press</string>
@ -71,6 +72,8 @@
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="resume">Resume</string> <string name="resume">Resume</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="unlock">Unlock</string>
<string name="achievements">Achievements</string>
<string name="rating_button_no">No</string> <string name="rating_button_no">No</string>
<string name="general">General</string> <string name="general">General</string>
<string name="source_code">Source Code</string> <string name="source_code">Source Code</string>

View file

@ -91,13 +91,13 @@ class GameControllerTest {
val firstMine = controller.field.first { it.hasMine } val firstMine = controller.field.first { it.hasMine }
assertEquals( 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() controller.takeExplosionRadius(firstMine).map { it.id }.toList()
) )
val midMine = controller.field.filter { it.hasMine }.take(controller.getMinesCount() / 2).last() val midMine = controller.field.filter { it.hasMine }.take(controller.getMinesCount() / 2).last()
assertEquals( 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() 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 @Test
fun testControlFastFlagOpenMultiple() { fun testControlFastFlagOpenMultiple() {
withGameController { controller -> withGameController { controller ->

1
external/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

40
external/build.gradle vendored Normal file
View file

@ -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'
}

5
external/src/main/AndroidManifest.xml vendored Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.lucasnlm.external">
</manifest>

View file

@ -0,0 +1,8 @@
package dev.lucasnlm.external
import android.app.Activity
interface IBillingManager {
fun start()
suspend fun charge(activity: Activity)
}

View file

@ -0,0 +1,8 @@
package dev.lucasnlm.external
import android.content.Context
interface IExternalAnalyticsWrapper {
fun setup(context: Context, properties: Map<String, String>)
fun sendEvent(name: String, content: Map<String, String>)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -6,8 +6,8 @@ android {
compileSdkVersion 30 compileSdkVersion 30
defaultConfig { defaultConfig {
versionCode 800041 // MMmmPPv versionCode 800051 // MMmmPPv
versionName '8.0.4' versionName '8.0.5'
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
} }
@ -24,4 +24,5 @@ dependencies {
// Dependencies must be hardcoded to support F-droid // Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
} }

View file

@ -1,2 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest
package="dev.lucasnlm.foss" /> package="dev.lucasnlm.foss" />

View file

@ -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
}
}

View file

@ -5,9 +5,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
class InstantAppWrapper { class ProprietaryAppWrapper {
// FOSS build doesn't support Instant App // 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?) { fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) {
// Empty // Empty

View file

@ -1,13 +1,14 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.gms.google-services'
android { android {
compileSdkVersion 30 compileSdkVersion 30
defaultConfig { defaultConfig {
versionCode 800041 // MMmmPPv versionCode 800051
versionName '8.0.4' versionName '8.0.5'
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
} }
@ -24,7 +25,17 @@ dependencies {
// Dependencies must be hardcoded to support F-droid // Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
// Google // 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-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'
} }

View file

@ -1,2 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.lucasnlm.proprietary" /> package="dev.lucasnlm.antimine">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.vending.BILLING"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application>
<meta-data android:name="com.google.android.gms.games.APP_ID"
android:value="@string/app_id"/>
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</application>
</manifest>

View file

@ -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"
}
}

View file

@ -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<String, String>) {
properties.forEach { (key, value) ->
firebaseAnalytics.setUserProperty(key, value)
}
}
override fun sendEvent(name: String, content: Map<String, String>) {
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)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_id">0</string>
</resources>

View file

@ -1 +1,2 @@
include ':app', ':wear', ':common', ':proprietary', ':foss' include ':app', ':wear', ':common', ':proprietary', ':foss'
include ':external'

View file

@ -9,8 +9,8 @@ android {
defaultConfig { defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid // versionCode and versionName must be hardcoded to support F-droid
versionCode 800041 versionCode 800051
versionName '8.0.4' versionName '8.0.5'
applicationId 'dev.lucasnlm.antimine' applicationId 'dev.lucasnlm.antimine'
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
@ -67,7 +67,7 @@ dependencies {
// Dagger // Dagger
implementation 'com.google.dagger:hilt-android:2.28.1-alpha' 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' kapt 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
testImplementation 'com.google.dagger:hilt-android-testing: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' kaptTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'