Merge pull request #159 from lucasnlm/add-support-screen

Add support screen
This commit is contained in:
Lucas Nunes 2020-08-20 00:35:21 -03:00 committed by GitHub
commit 388bcdcfce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
199 changed files with 3112 additions and 1353 deletions

2
.gitignore vendored
View file

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

View file

@ -66,7 +66,7 @@ Where `ANDROID_JRE` is the Java runtime provided by Android Studio.
- [Android SDK 30](https://developer.android.com/about/versions/11)
- [AndroidX](https://developer.android.com/jetpack/androidx)
- [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle)
- [Dagger Hilt](https://dagger.dev/hilt/)
- [Koin Kotlin](https://github.com/InsertKoinIO/koin)
- [Room](https://developer.android.com/training/data-storage/room)
- [Robolectric](http://robolectric.org/)
- [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)

View file

@ -1,7 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlin-kapt'
android {
@ -9,9 +8,9 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 800041
versionName '8.0.4'
minSdkVersion 16
versionCode 800051
versionName '8.0.5'
minSdkVersion 21
targetSdkVersion 30
multiDexEnabled true
vectorDrawables.useSupportLibrary true
@ -85,14 +84,11 @@ kapt {
correctErrorTypes true
}
hilt {
enableTransformForLocalTests true
}
dependencies {
// Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
implementation project(':common')
googleImplementation project(':proprietary')
@ -119,15 +115,10 @@ dependencies {
// Google
implementation 'com.google.android.material:material:1.2.0'
// Dagger
implementation 'com.google.dagger:hilt-android: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'
kaptTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.28.1-alpha'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
// Koin
implementation 'org.koin:koin-android:2.1.6'
implementation 'org.koin:koin-androidx-viewmodel:2.1.6'
testImplementation 'org.koin:koin-test:2.1.6'
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'

View file

@ -106,7 +106,6 @@ class GameFlowTests {
.perform(NavigationViewActions.navigateTo(R.id.control))
onView(withText(R.string.standard)).perform(click())
onView(withText(R.string.flag_first)).perform(click())
onView(withText(R.string.ok)).perform(click())
}

View file

@ -1,2 +0,0 @@
sdk=28
application=dagger.hilt.android.testing.HiltTestApplication

View file

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

View file

@ -9,20 +9,20 @@ import android.os.Handler
import android.text.format.DateUtils
import android.view.View
import android.view.WindowManager
import androidx.activity.viewModels
import android.widget.FrameLayout
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
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
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event
@ -36,13 +36,15 @@ import dev.lucasnlm.antimine.core.analytics.models.Analytics
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.CustomLevelDialogFragment
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.share.ShareManager
import dev.lucasnlm.antimine.stats.StatsActivity
import dev.lucasnlm.antimine.theme.ThemeActivity
import dev.lucasnlm.external.IInstantAppManager
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
@ -50,24 +52,23 @@ import kotlinx.android.synthetic.main.activity_tv_game.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.OnDismissListener {
@Inject
lateinit var preferencesRepository: IPreferencesRepository
private val preferencesRepository: IPreferencesRepository by inject()
@Inject
lateinit var analyticsManager: IAnalyticsManager
private val analyticsManager: IAnalyticsManager by inject()
@Inject
lateinit var instantAppManager: InstantAppManager
private val instantAppManager: IInstantAppManager by inject()
@Inject
lateinit var savesRepository: ISavesRepository
private val savesRepository: ISavesRepository by inject()
val viewModel: GameViewModel by viewModels()
private val shareViewModel: ShareViewModel by viewModels()
private val playGamesManager: IPlayGamesManager by inject()
private val shareViewModel: ShareManager by inject()
val gameViewModel by viewModel<GameViewModel>()
override val noActionBar: Boolean = true
@ -90,9 +91,12 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
bindToolbar()
bindDrawer()
bindNavigationMenu()
loadGameFragment()
if (instantAppManager.isEnabled()) {
findViewById<FrameLayout>(R.id.levelContainer).doOnLayout {
loadGameFragment()
}
if (instantAppManager.isEnabled(applicationContext)) {
bindInstantApp()
savesRepository.setLimit(1)
} else {
@ -100,7 +104,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun bindViewModel() = viewModel.apply {
private fun bindViewModel() = gameViewModel.apply {
var lastEvent: Event? = null // TODO use distinctUntilChanged when available
eventObserver.observe(
@ -117,7 +121,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
this@GameActivity,
Observer {
lifecycleScope.launch {
viewModel.retryGame(currentSaveId.toInt())
gameViewModel.retryGame(currentSaveId.toInt())
}
}
)
@ -179,9 +183,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
when {
drawer.isDrawerOpen(GravityCompat.START) -> {
drawer.closeDrawer(GravityCompat.START)
viewModel.resumeGame()
gameViewModel.resumeGame()
}
status == Status.Running && instantAppManager.isEnabled() -> showQuitConfirmation {
status == Status.Running && instantAppManager.isEnabled(applicationContext) -> showQuitConfirmation {
super.onBackPressed()
}
else -> super.onBackPressed()
@ -194,13 +198,15 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (!willReset) {
if (status == Status.Running) {
viewModel.run {
gameViewModel.run {
refreshUserPreferences()
resumeGame()
}
analyticsManager.sentEvent(Analytics.Resume)
}
silentGooglePlayLogin()
}
}
@ -208,7 +214,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
super.onPause()
if (status == Status.Running) {
viewModel.pauseGame()
gameViewModel.pauseGame()
}
if (isFinishing) {
@ -242,12 +248,12 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (confirmResign) {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame()
gameViewModel.startNewGame()
}
}
} else {
GlobalScope.launch {
viewModel.startNewGame()
gameViewModel.startNewGame()
}
}
}
@ -284,13 +290,13 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
override fun onDrawerOpened(drawerView: View) {
viewModel.pauseGame()
gameViewModel.pauseGame()
analyticsManager.sentEvent(Analytics.OpenDrawer)
}
override fun onDrawerClosed(drawerView: View) {
if (hasNoOtherFocusedDialog()) {
viewModel.resumeGame()
gameViewModel.resumeGame()
}
analyticsManager.sentEvent(Analytics.CloseDrawer)
@ -301,9 +307,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
})
if (preferencesRepository.getBoolean(PREFERENCE_FIRST_USE, false)) {
if (preferencesRepository.isFirstUse()) {
openDrawer(GravityCompat.START)
preferencesRepository.putBoolean(PREFERENCE_FIRST_USE, true)
preferencesRepository.completeFirstUse()
}
}
}
@ -327,6 +333,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
}
@ -337,19 +344,23 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
handled
}
navigationView.menu.findItem(R.id.share_now).isVisible = instantAppManager.isNotEnabled()
navigationView.menu.findItem(R.id.share_now).isVisible = !instantAppManager.isEnabled(applicationContext)
if (!playGamesManager.hasGooglePlayGames()) {
navigationView.menu.removeGroup(R.id.play_games_group)
}
}
private fun checkUseCount() {
val current = preferencesRepository.getInt(PREFERENCE_USE_COUNT, 0)
val shouldRequestRating = preferencesRepository.getBoolean(PREFERENCE_REQUEST_RATING, true)
val current = preferencesRepository.getUseCount()
val shouldRequestRating = preferencesRepository.isRequestRatingEnabled()
if (current >= MIN_USAGES_TO_RATING && shouldRequestRating) {
analyticsManager.sentEvent(Analytics.ShowRatingRequest(current))
showRequestRating()
}
preferencesRepository.putInt(PREFERENCE_USE_COUNT, current + 1)
preferencesRepository.incrementUseCount()
}
private fun onChangeDifficulty(difficulty: Difficulty) {
@ -369,22 +380,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() {
@ -397,7 +408,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
openRateUsLink("Dialog")
}
.setNegativeButton(R.string.rating_button_no) { _, _ ->
preferencesRepository.putBoolean(PREFERENCE_REQUEST_RATING, false)
preferencesRepository.disableRequestRating()
}
.show()
}
@ -431,7 +442,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
private fun showControlDialog() {
viewModel.pauseGame()
gameViewModel.pauseGame()
if (supportFragmentManager.findFragmentByTag(CustomLevelDialogFragment.TAG) == null) {
ControlDialogFragment().apply {
@ -493,13 +504,13 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
private fun waitAndShowEndGameDialog(victory: Boolean, await: Boolean) {
if (await && viewModel.explosionDelay() != 0L) {
if (await && gameViewModel.explosionDelay() != 0L) {
postDelayed(
Handler(),
{
showEndGameDialog(victory)
},
null, (viewModel.explosionDelay() * 0.3).toLong()
null, (gameViewModel.explosionDelay() * 0.3).toLong()
)
} else {
showEndGameDialog(victory)
@ -509,12 +520,12 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private fun changeDifficulty(newDifficulty: Difficulty) {
if (status == Status.PreGame) {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
gameViewModel.startNewGame(newDifficulty)
}
} else {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
gameViewModel.startNewGame(newDifficulty)
}
}
}
@ -532,7 +543,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
Event.Resume, Event.Running -> {
status = Status.Running
viewModel.runClock()
gameViewModel.runClock()
refreshNewGameButton()
keepScreenOn(true)
}
@ -543,9 +554,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
totalArea
)
status = Status.Over(currentTime, score)
viewModel.stopClock()
viewModel.revealAllEmptyAreas()
viewModel.victory()
gameViewModel.stopClock()
gameViewModel.revealAllEmptyAreas()
gameViewModel.victory()
refreshNewGameButton()
keepScreenOn(false)
waitAndShowEndGameDialog(
@ -563,10 +574,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
status = Status.Over(currentTime, score)
refreshNewGameButton()
keepScreenOn(false)
viewModel.stopClock()
gameViewModel.stopClock()
GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver(isResuming)
gameViewModel.gameOver(isResuming)
waitAndShowEndGameDialog(
victory = false,
await = true
@ -590,10 +601,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
private fun shareCurrentGame() {
val levelSetup = viewModel.levelSetup.value
val field = viewModel.field.value
GlobalScope.launch {
shareViewModel.share(levelSetup, field)
val levelSetup = gameViewModel.levelSetup.value
val field = gameViewModel.field.value
lifecycleScope.launch {
shareViewModel.shareField(levelSetup, field)
}
}
@ -625,7 +636,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
analyticsManager.sentEvent(Analytics.TapRatingRequest(from))
preferencesRepository.putBoolean(PREFERENCE_REQUEST_RATING, false)
preferencesRepository.disableRequestRating()
}
private fun keepScreenOn(enabled: Boolean) {
@ -643,19 +654,42 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
override fun onDismiss(dialog: DialogInterface?) {
viewModel.run {
gameViewModel.run {
refreshUserPreferences()
resumeGame()
}
}
companion object {
const val PREFERENCE_FIRST_USE = "preference_first_use"
const val PREFERENCE_USE_COUNT = "preference_use_count"
const val PREFERENCE_REQUEST_RATING = "preference_request_rating"
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 IA_REFERRER = "InstallApiActivity"
const val IA_REQUEST_CODE = 5
const val GOOGLE_PLAY_REQUEST_CODE = 6
const val MIN_USAGES_TO_RATING = 4
}

View file

@ -1,18 +1,26 @@
package dev.lucasnlm.antimine
import androidx.multidex.MultiDexApplication
import dagger.hilt.android.HiltAndroidApp
import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.models.Analytics
import javax.inject.Inject
import dev.lucasnlm.antimine.core.di.CommonModule
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.ViewModelModule
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
@HiltAndroidApp
open class MainApplication : MultiDexApplication() {
@Inject
lateinit var analyticsManager: IAnalyticsManager
private val analyticsManager: IAnalyticsManager by inject()
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(applicationContext)
modules(AppModule, CommonModule, LevelModule, ViewModelModule)
}
analyticsManager.apply {
setup(applicationContext, mapOf())
sentEvent(Analytics.Open)

View file

@ -5,14 +5,11 @@ import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import javax.inject.Inject
import org.koin.android.ext.android.inject
open class ThematicActivity : AppCompatActivity {
constructor() : super()
constructor(@LayoutRes contentLayoutId: Int) : super(contentLayoutId)
open class ThematicActivity(@LayoutRes contentLayoutId: Int) : AppCompatActivity(contentLayoutId) {
@Inject
lateinit var themeRepository: IThemeRepository
private val themeRepository: IThemeRepository by inject()
protected open val noActionBar: Boolean = false

View file

@ -5,14 +5,12 @@ import android.os.Bundle
import android.os.Handler
import android.text.format.DateUtils
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.HandlerCompat
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event
@ -26,10 +24,10 @@ import kotlinx.android.synthetic.main.activity_tv_game.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class TvGameActivity : AppCompatActivity() {
private val viewModel: GameViewModel by viewModels()
private val gameViewModel by viewModel<GameViewModel>()
private var status: Status = Status.PreGame
private var totalMines: Int = 0
@ -49,7 +47,7 @@ class TvGameActivity : AppCompatActivity() {
loadGameFragment()
}
private fun bindViewModel() = viewModel.apply {
private fun bindViewModel() = gameViewModel.apply {
eventObserver.observe(
this@TvGameActivity,
Observer {
@ -99,7 +97,7 @@ class TvGameActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (status == Status.Running) {
viewModel.resumeGame()
gameViewModel.resumeGame()
}
}
@ -107,7 +105,7 @@ class TvGameActivity : AppCompatActivity() {
super.onPause()
if (status == Status.Running) {
viewModel.pauseGame()
gameViewModel.pauseGame()
}
}
@ -174,7 +172,7 @@ class TvGameActivity : AppCompatActivity() {
setCancelable(false)
setPositiveButton(R.string.new_game) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
gameViewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
@ -193,7 +191,7 @@ class TvGameActivity : AppCompatActivity() {
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
gameViewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
@ -217,7 +215,7 @@ class TvGameActivity : AppCompatActivity() {
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
gameViewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
@ -231,12 +229,12 @@ class TvGameActivity : AppCompatActivity() {
private fun changeDifficulty(newDifficulty: Difficulty) {
if (status == Status.PreGame) {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
gameViewModel.startNewGame(newDifficulty)
}
} else {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
gameViewModel.startNewGame(newDifficulty)
}
}
}
@ -253,7 +251,7 @@ class TvGameActivity : AppCompatActivity() {
}
Event.Resume, Event.Running -> {
status = Status.Running
viewModel.runClock()
gameViewModel.runClock()
invalidateOptionsMenu()
}
Event.Victory -> {
@ -263,8 +261,8 @@ class TvGameActivity : AppCompatActivity() {
totalArea
)
status = Status.Over(currentTime, score)
viewModel.stopClock()
viewModel.revealAllEmptyAreas()
gameViewModel.stopClock()
gameViewModel.revealAllEmptyAreas()
invalidateOptionsMenu()
showVictory()
}
@ -276,10 +274,10 @@ class TvGameActivity : AppCompatActivity() {
)
status = Status.Over(currentTime, score)
invalidateOptionsMenu()
viewModel.stopClock()
gameViewModel.stopClock()
GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver(false)
gameViewModel.gameOver(false)
waitAndShowGameOverConfirmNewGame()
}
}
@ -291,7 +289,7 @@ class TvGameActivity : AppCompatActivity() {
)
status = Status.Over(currentTime, score)
invalidateOptionsMenu()
viewModel.stopClock()
gameViewModel.stopClock()
waitAndShowConfirmNewGame()
}

View file

@ -1,23 +1,21 @@
package dev.lucasnlm.antimine.about
import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.about.viewmodel.AboutEvent
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import dev.lucasnlm.antimine.about.views.AboutInfoFragment
import dev.lucasnlm.antimine.about.views.info.AboutInfoFragment
import dev.lucasnlm.antimine.about.views.licenses.LicensesFragment
import dev.lucasnlm.antimine.about.views.translators.TranslatorsFragment
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class AboutActivity : ThematicActivity(R.layout.activity_empty) {
private val aboutViewModel: AboutViewModel by viewModels()
private val aboutViewModel: AboutViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View file

@ -3,13 +3,11 @@ package dev.lucasnlm.antimine.about.viewmodel
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
class AboutViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context
class AboutViewModel(
private val context: Context
) : IntentViewModel<AboutEvent, AboutState>() {
override fun onEvent(event: AboutEvent) {
@ -47,10 +45,11 @@ class AboutViewModel @ViewModelInject constructor(
private fun getLicensesList() = mapOf(
"Android SDK License" to R.raw.android_sdk,
"Material Design Icons" to R.raw.apache2,
"Dagger Hilt" to R.raw.apache2,
"Material Design" to R.raw.apache2,
"Koin" to R.raw.apache2,
"Moshi" to R.raw.apache2,
"Mockito" to R.raw.mockito,
"Noto Emoji" to R.raw.apache2,
"Sounds" to R.raw.sounds
).map {
License(it.key, it.value)

View file

@ -1,17 +1,17 @@
package dev.lucasnlm.antimine.about.views
package dev.lucasnlm.antimine.about.views.info
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import dev.lucasnlm.antimine.BuildConfig
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.viewmodel.AboutEvent
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import kotlinx.android.synthetic.main.fragment_about_info.*
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class AboutInfoFragment : Fragment(R.layout.fragment_about_info) {
private val aboutViewModel: AboutViewModel by activityViewModels()
private val aboutViewModel: AboutViewModel by sharedViewModel()
override fun onResume() {
super.onResume()

View file

@ -3,7 +3,6 @@ package dev.lucasnlm.antimine.about.views.licenses
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -11,9 +10,10 @@ import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import kotlinx.android.synthetic.main.fragment_licenses.*
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class LicensesFragment : Fragment(R.layout.fragment_licenses) {
private val aboutViewModel: AboutViewModel by activityViewModels()
private val aboutViewModel: AboutViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -14,9 +13,10 @@ import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import kotlinx.android.synthetic.main.fragment_translators.*
import kotlinx.android.synthetic.main.view_translator.view.*
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
internal class TranslatorsFragment : Fragment(R.layout.fragment_translators) {
private val aboutViewModel: AboutViewModel by activityViewModels()
private val aboutViewModel: AboutViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -8,23 +8,21 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.control.view.ControlItemView
import dev.lucasnlm.antimine.control.viewmodel.ControlEvent
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class ControlDialogFragment : AppCompatDialogFragment() {
private val controlViewModel by activityViewModels<ControlViewModel>()
private val controlViewModel by viewModel<ControlViewModel>()
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 +39,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 +46,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()

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package dev.lucasnlm.antimine.control.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.control.models.ControlDetails
import dev.lucasnlm.antimine.core.control.ControlStyle
@ -8,7 +7,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow
class ControlViewModel @ViewModelInject constructor(
class ControlViewModel(
private val preferencesRepository: IPreferencesRepository
) : IntentViewModel<ControlEvent, ControlState>() {
@ -16,7 +15,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 +23,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 +31,42 @@ 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)

View file

@ -1,60 +1,67 @@
package dev.lucasnlm.antimine.custom
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel
import dev.lucasnlm.antimine.custom.viewmodel.CustomEvent
import javax.inject.Inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class CustomLevelDialogFragment : AppCompatDialogFragment() {
@Inject
lateinit var preferencesRepository: IPreferencesRepository
private val gameViewModel by sharedViewModel<GameViewModel>()
private val createGameViewModel by viewModel<CreateGameViewModel>()
private val viewModel by activityViewModels<GameViewModel>()
private val createGameViewModel by activityViewModels<CreateGameViewModel>()
private lateinit var mapWidth: TextView
private lateinit var mapHeight: TextView
private lateinit var mapMines: TextView
private fun getSelectedMinefield(): Minefield {
val mapWidth: TextView? = dialog?.findViewById(R.id.map_width)
val mapHeight: TextView? = dialog?.findViewById(R.id.map_height)
val mapMines: TextView? = dialog?.findViewById(R.id.map_mines)
val width = filterInput(
mapWidth?.text.toString(),
MIN_WIDTH
).coerceAtMost(MAX_WIDTH)
val height = filterInput(
mapHeight?.text.toString(),
MIN_HEIGHT
).coerceAtMost(MAX_HEIGHT)
val mines = filterInput(
mapMines?.text.toString(),
MIN_MINES
).coerceAtMost(width * height - 1)
val width = filterInput(mapWidth.text.toString(), MIN_WIDTH).coerceAtMost(MAX_WIDTH)
val height = filterInput(mapHeight.text.toString(), MIN_HEIGHT).coerceAtMost(MAX_HEIGHT)
val mines = filterInput(mapMines.text.toString(), MIN_MINES).coerceAtMost(width * height - 1)
return Minefield(width, height, mines)
}
@SuppressLint("InflateParams")
private fun createView(): View {
val view = LayoutInflater
.from(context)
.inflate(R.layout.dialog_custom_game, null, false)
mapWidth = view.findViewById(R.id.map_width)
mapHeight = view.findViewById(R.id.map_height)
mapMines = view.findViewById(R.id.map_mines)
createGameViewModel.singleState().let {
mapWidth.text = it.width.toString()
mapHeight.text = it.height.toString()
mapMines.text = it.mines.toString()
}
return view
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.new_game)
setView(R.layout.dialog_custom_game)
setView(createView())
setNegativeButton(R.string.cancel, null)
setPositiveButton(R.string.start) { _, _ ->
val minefield = getSelectedMinefield()
createGameViewModel.sendEvent(CustomEvent.UpdateCustomGameEvent(minefield))
viewModel.startNewGame(Difficulty.Custom)
gameViewModel.startNewGame(Difficulty.Custom)
}
}.create()
}

View file

@ -1,17 +1,22 @@
package dev.lucasnlm.antimine.custom.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.StatelessViewModel
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow
class CreateGameViewModel @ViewModelInject constructor(
class CreateGameViewModel(
private val preferencesRepository: IPreferencesRepository
) : StatelessViewModel<CustomEvent>() {
override fun onEvent(event: CustomEvent) {
when (event) {
is CustomEvent.UpdateCustomGameEvent -> {
preferencesRepository.updateCustomGameMode(event.minefield)
}
) : IntentViewModel<CustomEvent, CustomState>() {
override suspend fun mapEventToState(event: CustomEvent) = flow {
if (event is CustomEvent.UpdateCustomGameEvent) {
val minefield = event.minefield
preferencesRepository.updateCustomGameMode(minefield)
emit(CustomState(minefield.width, minefield.height, minefield.mines))
}
}
override fun initialState() = with(preferencesRepository.customGameMode()) {
CustomState(width, height, mines)
}
}

View file

@ -0,0 +1,7 @@
package dev.lucasnlm.antimine.custom.viewmodel
data class CustomState(
val width: Int,
val height: Int,
val mines: Int
)

View file

@ -1,18 +1,34 @@
package dev.lucasnlm.antimine.di
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.instant.InstantAppManager
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.share.ShareManager
import dev.lucasnlm.external.BillingManager
import dev.lucasnlm.external.ExternalAnalyticsWrapper
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import dev.lucasnlm.external.InstantAppManager
import dev.lucasnlm.external.PlayGamesManager
import org.koin.dsl.bind
import org.koin.dsl.module
@Module
@InstallIn(ApplicationComponent::class)
class AppModule {
@Provides
fun provideInstantAppManager(
@ApplicationContext context: Context
): InstantAppManager = InstantAppManager(context)
val AppModule = module {
single { InstantAppManager() } bind IInstantAppManager::class
single { BillingManager(get()) } bind IBillingManager::class
single { PlayGamesManager(get()) } bind IPlayGamesManager::class
single { ShareManager(get()) }
single {
if (BuildConfig.DEBUG) {
DebugAnalyticsManager()
} else {
ProdAnalyticsManager(ExternalAnalyticsWrapper(get()))
}
} bind IAnalyticsManager::class
}

View file

@ -0,0 +1,29 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel
import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogViewModel
import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesViewModel
import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel
import dev.lucasnlm.antimine.text.viewmodel.TextViewModel
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val ViewModelModule = module {
viewModel { AboutViewModel(get()) }
viewModel { ControlViewModel(get()) }
viewModel { CreateGameViewModel(get()) }
viewModel { HistoryViewModel(get(), get()) }
viewModel { EndGameDialogViewModel(get()) }
viewModel { PlayGamesViewModel(get(), get()) }
viewModel { StatsViewModel(get(), get()) }
viewModel { TextViewModel(get()) }
viewModel { ThemeViewModel(get()) }
viewModel {
GameViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
}

View file

@ -1,12 +1,10 @@
package dev.lucasnlm.antimine.history
import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.history.views.HistoryFragment
@AndroidEntryPoint
class HistoryActivity : ThematicActivity(R.layout.activity_empty) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View file

@ -3,15 +3,13 @@ package dev.lucasnlm.antimine.history.viewmodel
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.DeepLink
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow
class HistoryViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context,
class HistoryViewModel(
private val context: Context,
private val savesRepository: ISavesRepository
) : IntentViewModel<HistoryEvent, HistoryState>() {
@ -31,7 +29,7 @@ class HistoryViewModel @ViewModelInject constructor(
}
}
override suspend fun mapEventToState(event: HistoryEvent) = flow<HistoryState> {
override suspend fun mapEventToState(event: HistoryEvent) = flow {
when (event) {
is HistoryEvent.LoadAllSaves -> {
val newSaveList = savesRepository.getAllSaves().sortedByDescending { it.uid }

View file

@ -3,20 +3,18 @@ package dev.lucasnlm.antimine.history.views
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.history.viewmodel.HistoryEvent
import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel
import kotlinx.android.synthetic.main.fragment_history.*
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val historyViewModel: HistoryViewModel by activityViewModels()
private val historyViewModel by viewModel<HistoryViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

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

View file

@ -9,24 +9,22 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.instant.InstantAppManager
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogEvent
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogViewModel
import dev.lucasnlm.external.IInstantAppManager
import kotlinx.coroutines.flow.collect
import javax.inject.Inject
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class EndGameDialogFragment : AppCompatDialogFragment() {
@Inject
lateinit var instantAppManager: InstantAppManager
private val instantAppManager: IInstantAppManager by inject()
private val endGameViewModel by activityViewModels<EndGameDialogViewModel>()
private val viewModel by activityViewModels<GameViewModel>()
private val endGameViewModel by viewModel<EndGameDialogViewModel>()
private val gameViewModel by sharedViewModel<GameViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -70,7 +68,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}
when {
instantAppManager.isEnabled() -> {
instantAppManager.isEnabled(context) -> {
setNeutralButton(R.string.install) { _, _ ->
activity?.run {
instantAppManager.showInstallPrompt(this, null, 0, null)
@ -79,12 +77,12 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}
state.isVictory == true -> {
setNeutralButton(R.string.share) { _, _ ->
viewModel.shareObserver.postValue(Unit)
gameViewModel.shareObserver.postValue(Unit)
}
}
else -> {
setNeutralButton(R.string.retry) { _, _ ->
viewModel.retryObserver.postValue(Unit)
gameViewModel.retryObserver.postValue(Unit)
}
}
}
@ -95,7 +93,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
setView(view)
setPositiveButton(R.string.new_game) { _, _ ->
viewModel.startNewGame()
gameViewModel.startNewGame()
}
}.create()

View file

@ -1,39 +1,57 @@
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
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 {
viewModel.saveGame()
lifecycleScope.launch {
gameViewModel.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()
viewModel.run {
val levelSetup = when {
loadGameUid != null -> gameViewModel.loadGame(loadGameUid)
newGameDeepLink != null -> gameViewModel.startNewGame(newGameDeepLink)
retryDeepLink != null -> gameViewModel.retryGame(retryDeepLink)
else -> gameViewModel.loadLastGame()
}
withContext(Dispatchers.Main) {
recyclerGrid.apply {
addItemDecoration(SpaceItemDecoration(R.dimen.field_padding))
setHasFixedSize(true)
}
setupRecyclerViewSize(levelSetup)
}
}
}
gameViewModel.run {
field.observe(
viewLifecycleOwner,
Observer {
@ -44,20 +62,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)
}
}
)
fieldRefresh.observe(
viewLifecycleOwner,
Observer {
areaAdapter.notifyItemChanged(it)
setupRecyclerViewSize(it)
}
)
@ -81,38 +86,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()
}
}

View file

@ -1,14 +1,12 @@
package dev.lucasnlm.antimine.level.viewmodel
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.IntentViewModel
import kotlinx.coroutines.flow.flow
class EndGameDialogViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context
class EndGameDialogViewModel(
private val context: Context
) : IntentViewModel<EndGameDialogEvent, EndGameDialogState>() {
private fun List<Int>.safeRandomEmoji(
@ -28,6 +26,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
@ -74,7 +73,7 @@ class EndGameDialogViewModel @ViewModelInject constructor(
false
)
override suspend fun mapEventToState(event: EndGameDialogEvent) = flow<EndGameDialogState> {
override suspend fun mapEventToState(event: EndGameDialogEvent) = flow {
if (event is EndGameDialogEvent.BuildCustomEndGame) {
val state = when (event.isVictory) {
true -> {

View file

@ -0,0 +1,104 @@
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.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.lifecycle.lifecycleScope
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 org.koin.androidx.viewmodel.ext.android.viewModel
class PlayGamesDialogFragment : DialogFragment() {
private val playGamesViewModel by viewModel<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,27 @@
package dev.lucasnlm.antimine.playgames.viewmodel
import android.app.Activity
import android.content.Context
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(
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

@ -3,11 +3,9 @@ package dev.lucasnlm.antimine.preferences
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
@AndroidEntryPoint
class PreferencesActivity : ThematicActivity(R.layout.activity_empty) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View file

@ -9,6 +9,7 @@ import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.widget.Toast
import androidx.core.content.FileProvider
import dev.lucasnlm.antimine.BuildConfig
import dev.lucasnlm.antimine.R
@ -22,12 +23,10 @@ import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
class ShareBuilder(
context: Context
class ShareManager(
private val context: Context
) {
private val context: Context = context.applicationContext
suspend fun share(minefield: Minefield, field: List<Area>): Boolean {
private suspend fun share(minefield: Minefield, field: List<Area>): Boolean {
val file = createImage(minefield, field)
return if (file != null) {
@ -129,4 +128,16 @@ class ShareBuilder(
false
}
}
suspend fun shareField(minefield: Minefield?, field: List<Area>?) {
val result = if (minefield != null && field != null && field.count() != 0) {
share(minefield, field)
} else {
false
}
if (!result) {
Toast.makeText(context, context.getString(R.string.fail_to_share), Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -1,27 +0,0 @@
package dev.lucasnlm.antimine.share.viewmodel
import android.app.Application
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.share.ShareBuilder
import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Minefield
class ShareViewModel(
application: Application
) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
suspend fun share(minefield: Minefield?, field: List<Area>?) {
val result = if (minefield != null && field != null && field.count() != 0) {
ShareBuilder(context).share(minefield, field)
} else {
false
}
if (!result) {
Toast.makeText(context, context.getString(R.string.fail_to_share), Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -3,10 +3,8 @@ package dev.lucasnlm.antimine.stats
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.stats.model.StatsModel
@ -16,19 +14,19 @@ import kotlinx.android.synthetic.main.activity_stats.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class StatsActivity : ThematicActivity(R.layout.activity_stats) {
private val viewModel: StatsViewModel by viewModels()
private val statsViewModel by viewModel<StatsViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
refreshStats(StatsViewModel.emptyStats)
lifecycleScope.launchWhenResumed {
viewModel.sendEvent(StatsEvent.LoadStats)
statsViewModel.sendEvent(StatsEvent.LoadStats)
viewModel.observeState().collect {
statsViewModel.observeState().collect {
refreshStats(it)
}
}
@ -61,7 +59,7 @@ class StatsActivity : ThematicActivity(R.layout.activity_stats) {
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
viewModel.singleState().let {
statsViewModel.singleState().let {
if (it.totalGames > 0) {
menuInflater.inflate(R.menu.stats_menu, menu)
}
@ -85,7 +83,7 @@ class StatsActivity : ThematicActivity(R.layout.activity_stats) {
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_all) { _, _ ->
GlobalScope.launch {
viewModel.sendEvent(StatsEvent.DeleteStats)
statsViewModel.sendEvent(StatsEvent.DeleteStats)
}
}
.show()

View file

@ -1,13 +1,12 @@
package dev.lucasnlm.antimine.stats.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import dev.lucasnlm.antimine.stats.model.StatsModel
import kotlinx.coroutines.flow.flow
class StatsViewModel @ViewModelInject constructor(
class StatsViewModel(
private val statsRepository: IStatsRepository,
private val preferenceRepository: IPreferencesRepository
) : IntentViewModel<StatsEvent, StatsModel>() {

View file

@ -0,0 +1,37 @@
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 dev.lucasnlm.antimine.R
import dev.lucasnlm.external.IBillingManager
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class SupportAppDialogFragment : AppCompatDialogFragment() {
private val billingManager: IBillingManager by inject()
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

@ -4,10 +4,8 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.annotation.RawRes
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.text.viewmodel.TextEvent
@ -16,10 +14,10 @@ import kotlinx.android.synthetic.main.activity_text.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class TextActivity : ThematicActivity(R.layout.activity_text) {
private val viewModel: TextViewModel by viewModels()
private val textViewModel by viewModel<TextViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -28,7 +26,7 @@ class TextActivity : ThematicActivity(R.layout.activity_text) {
title = bundle.getString(TEXT_TITLE)
lifecycleScope.launchWhenCreated {
viewModel.sendEvent(
textViewModel.sendEvent(
TextEvent.LoadText(
title = bundle.getString(TEXT_TITLE, ""),
rawFileRes = bundle.getInt(TEXT_PATH, -1)
@ -39,7 +37,7 @@ class TextActivity : ThematicActivity(R.layout.activity_text) {
progressBar.visibility = View.VISIBLE
}
viewModel.observeState().collect {
textViewModel.observeState().collect {
textView.text = it.body
progressBar.visibility = View.GONE
}

View file

@ -2,16 +2,14 @@ package dev.lucasnlm.antimine.text.viewmodel
import android.content.Context
import androidx.annotation.RawRes
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import dev.lucasnlm.antimine.text.models.TextState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
class TextViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context
class TextViewModel(
private val context: Context
) : IntentViewModel<TextEvent, TextState>() {
private suspend fun loadText(@RawRes rawFile: Int): String {

View file

@ -1,26 +1,24 @@
package dev.lucasnlm.antimine.theme
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint
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.support.SupportAppDialogFragment
import dev.lucasnlm.antimine.theme.view.ThemeAdapter
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import kotlinx.android.synthetic.main.activity_theme.*
import kotlinx.coroutines.flow.collect
import javax.inject.Inject
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
@Inject
lateinit var dimensionRepository: IDimensionRepository
private val dimensionRepository: IDimensionRepository by inject()
private val viewModel by viewModels<ThemeViewModel>()
private val themeViewModel by viewModel<ThemeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -32,14 +30,22 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
addItemDecoration(SpaceItemDecoration(R.dimen.theme_divider))
setHasFixedSize(true)
layoutManager = GridLayoutManager(context, 3)
adapter = ThemeAdapter(viewModel, areaSize)
adapter = ThemeAdapter(themeViewModel, areaSize)
}
viewModel.observeState().collect {
themeViewModel.observeState().collect {
if (usingTheme.id != it.current.id) {
recreate()
}
}
}
}
private fun showUnlockDialog() {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment().apply {
show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}
}
}

View file

@ -58,14 +58,27 @@ class ThemeAdapter(
val theme = themes[position]
val paintSettings = createAreaPaintSettings(holder.itemView.context, areaSize)
holder.itemView.run {
val selected = (theme.id == themeViewModel.singleState().current.id)
val areas = listOf(area0, area1, area2, area3, area4, area5, area6, area7, area8)
if (selected) {
areas.forEach { it.alpha = 0.25f }
} else {
areas.forEach { it.alpha = 1.0f }
}
areas.forEachIndexed { index, areaView -> areaView.bindTheme(minefield[index], theme, paintSettings) }
if (position == 0) {
if (position < 2 && !selected) {
areas.forEach { it.alpha = 0.35f }
label.apply {
text = if (position == 0) {
label.context.getString(R.string.system)
} else {
label.context.getString(R.string.amoled)
}
setTextColor(
with(theme.palette.background) {
Color.rgb(255 - Color.red(this), 255 - Color.green(this), 255 - Color.blue(this))

View file

@ -1,19 +1,18 @@
package dev.lucasnlm.antimine.theme.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow
class ThemeViewModel @ViewModelInject constructor(
class ThemeViewModel(
private val themeRepository: IThemeRepository
) : IntentViewModel<ThemeEvent, ThemeState>() {
private fun setTheme(theme: AppTheme) {
themeRepository.setTheme(theme)
}
override suspend fun mapEventToState(event: ThemeEvent) = flow<ThemeState> {
override suspend fun mapEventToState(event: ThemeEvent) = flow {
if (event is ThemeEvent.ChangeTheme) {
setTheme(event.newTheme)
emit(state.copy(current = event.newTheme))

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

@ -31,7 +31,6 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:includeFontPadding="false"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="match_parent">
@ -10,7 +11,7 @@
android:layout_height="wrap_content"
android:elevation="0dp"
android:minHeight="30dp"
android:theme="@style/AppTheme.AppBarOverlay"
android:theme="@style/AppTheme"
tools:targetApi="lollipop" />
<LinearLayout
@ -29,9 +30,6 @@
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:drawableStart="@drawable/timer"
android:drawableLeft="@drawable/timer"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
@ -41,6 +39,8 @@
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:visibility="gone"
app:drawableLeftCompat="@drawable/timer"
app:drawableStartCompat="@drawable/timer"
tools:text="10:00"
tools:visibility="visible" />
@ -49,8 +49,6 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:drawableStart="@drawable/mine"
android:drawableLeft="@drawable/mine"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
@ -59,6 +57,7 @@
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/mine"
tools:text="99"
tools:visibility="visible" />

View file

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

View file

@ -13,13 +13,14 @@
android:layout_height="64dp"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:focusable="false"
android:importantForAccessibility="no"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/emoji_smiling_face_with_sunglasses" />
tools:src="@drawable/emoji_smiling_face_with_sunglasses"
tools:ignore="KeyboardInaccessibleWidget" />
<TextView
android:id="@+id/title"

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

@ -9,11 +9,9 @@
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingLeft="32dp"
android:paddingRight="48dp"
android:paddingStart="32dp"
android:paddingEnd="48dp"
android:paddingVertical="24dp">
android:paddingVertical="12dp">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/radio"
@ -35,15 +33,6 @@
tools:text="">
<TableRow>
<TextView
android:id="@+id/title"
android:textStyle="bold"
tools:text="Title" />
</TableRow>
<TableRow android:paddingVertical="4dp">
<TextView
android:id="@+id/firstAction"
tools:text="First Action" />
@ -62,7 +51,6 @@
</TableRow>
<TableRow>
<TextView
android:id="@+id/secondAction"
tools:text="Long Tap" />

View file

@ -24,7 +24,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/badge"
app:layout_constraintTop_toTopOf="parent"
@ -35,7 +34,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/badge"
app:layout_constraintTop_toBottomOf="@id/difficulty"
@ -46,7 +44,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:text="-"
app:layout_constraintStart_toEndOf="@id/minefieldSize"
@ -58,7 +55,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
app:layout_constraintStart_toEndOf="@id/dash"
app:layout_constraintTop_toBottomOf="@id/difficulty"

View file

@ -0,0 +1,33 @@
<?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_marginStart="8dp"
tools:text="Text" />
</LinearLayout>

View file

@ -11,7 +11,6 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:columnCount="3"
android:background="#FFFF0000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

View file

@ -71,6 +71,14 @@
android:title="@string/settings" />
</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
android:id="@+id/install_group"
android:visible="false"

View file

@ -3,11 +3,10 @@ package dev.lucasnlm.antimine
import android.os.Build
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.launchActivity
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import dagger.hilt.android.testing.UninstallModules
import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.TestCommonModule
import dev.lucasnlm.antimine.di.TestLevelModule
import dev.lucasnlm.antimine.di.ViewModelModule
import dev.lucasnlm.antimine.level.view.EndGameDialogFragment
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -15,23 +14,24 @@ import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.test.KoinTestRule
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper
import java.util.concurrent.TimeUnit
@HiltAndroidTest
@UninstallModules(LevelModule::class)
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P], application = HiltTestApplication::class)
@Config(sdk = [Build.VERSION_CODES.P], application = TestApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class GameActivityTest {
@get:Rule
var rule = HiltAndroidRule(this)
val koinTestRule = KoinTestRule.create {
modules(AppModule, TestLevelModule, TestCommonModule, ViewModelModule)
}
@Test
@Ignore("Dagger hilt issue")
@Ignore("Disabled until fix touch on tests")
fun testShowGameOverWhenTapAMine() {
launchActivity<GameActivity>().onActivity { activity ->
ShadowLooper.runUiThreadTasks()
@ -57,7 +57,7 @@ class GameActivityTest {
}
@Test
@Ignore("Dagger hilt issue")
@Ignore("Disabled until fix touch on tests")
fun testShowVictoryWhenTapAllSafeAreas() {
launchActivity<GameActivity>().onActivity { activity ->
ShadowLooper.runUiThreadTasks()
@ -70,7 +70,7 @@ class GameActivityTest {
ShadowLooper.runUiThreadTasks()
// Tap on safe places
activity.viewModel.field
activity.gameViewModel.field
.value!!
.filter { !it.hasMine && it.isCovered }
.forEach {

View file

@ -0,0 +1,5 @@
package dev.lucasnlm.antimine
import android.app.Application
class TestApplication : Application()

View file

@ -0,0 +1,17 @@
package dev.lucasnlm.antimine.about
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import io.mockk.mockk
import org.junit.Assert.assertTrue
import org.junit.Test
class AboutViewModelTest : IntentViewModelTest() {
@Test
fun testLoad() {
val viewModel = AboutViewModel(mockk())
val state = viewModel.singleState()
assertTrue(state.licenses.isNotEmpty())
assertTrue(state.translators.isNotEmpty())
}
}

View file

@ -1,22 +1,19 @@
package dev.lucasnlm.antimine.control.viewmodel
package dev.lucasnlm.antimine.control
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.control.viewmodel.ControlEvent
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class ControlViewModelTest : IntentViewModelTest() {
@get:Rule
val rule = InstantTaskExecutorRule()
private fun ControlViewModel.selectedControlStyle() = singleState().let {
it.gameControls[it.selectedId].controlStyle
it.gameControls[it.selectedIndex].controlStyle
}
@Test
@ -36,7 +33,11 @@ class ControlViewModelTest : IntentViewModelTest() {
}
val viewModel = ControlViewModel(preferenceRepository)
viewModel.sendEvent(ControlEvent.SelectControlStyle(ControlStyle.FastFlag))
viewModel.sendEvent(
ControlEvent.SelectControlStyle(
ControlStyle.FastFlag
)
)
assertEquals(ControlStyle.FastFlag, viewModel.selectedControlStyle())
verify { preferenceRepository.useControlStyle(ControlStyle.FastFlag) }
}

View file

@ -0,0 +1,43 @@
package dev.lucasnlm.antimine.custom
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel
import dev.lucasnlm.antimine.custom.viewmodel.CustomEvent
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Test
class CreateGameViewModelTest : IntentViewModelTest() {
@Test
fun testInitialValue() {
val preferenceRepository: IPreferencesRepository = mockk {
every { customGameMode() } returns Minefield(10, 12, 9)
}
val result = CreateGameViewModel(preferenceRepository).singleState()
assertEquals(10, result.width)
assertEquals(12, result.height)
assertEquals(9, result.mines)
}
@Test
fun testSetNewCustomValues() {
val preferenceRepository: IPreferencesRepository = mockk {
every { customGameMode() } returns Minefield(10, 12, 9)
every { updateCustomGameMode(any()) } returns Unit
}
val result = CreateGameViewModel(preferenceRepository).run {
sendEvent(CustomEvent.UpdateCustomGameEvent(Minefield(9, 8, 5)))
singleState()
}
assertEquals(9, result.width)
assertEquals(8, result.height)
assertEquals(5, result.mines)
}
}

View file

@ -0,0 +1,58 @@
package dev.lucasnlm.antimine.di
import android.app.Activity
import android.content.Context
import android.content.Intent
import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.share.ShareManager
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import org.koin.dsl.bind
import org.koin.dsl.module
val AppModule = module {
single {
object : IInstantAppManager {
override fun isEnabled(context: Context): Boolean = false
override fun showInstallPrompt(
activity: Activity,
intent: Intent?,
requestCode: Int,
referrer: String?
): Boolean = false
}
} bind IInstantAppManager::class
single {
object : IBillingManager {
override fun start() { }
override suspend fun charge(activity: Activity) { }
}
} bind IBillingManager::class
single { object : IPlayGamesManager {
override fun hasGooglePlayGames(): Boolean = false
override fun silentLogin(activity: Activity) { }
override fun getLoginIntent(): Intent? = null
override fun handleLoginResult(data: Intent?) { }
override fun isLogged(): Boolean = false
override fun openAchievements(activity: Activity) { }
override fun openLeaderboards(activity: Activity) { }
} } bind IPlayGamesManager::class
single { ShareManager(get()) }
single {
DebugAnalyticsManager()
} bind IAnalyticsManager::class
}

View file

@ -0,0 +1,32 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.sound.ISoundManager
import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.core.themes.repository.Themes.LightTheme
import dev.lucasnlm.antimine.mocks.FixedDimensionRepository
import dev.lucasnlm.antimine.mocks.MockPreferencesRepository
import org.koin.dsl.bind
import org.koin.dsl.module
val TestCommonModule = module {
single { FixedDimensionRepository() } bind IDimensionRepository::class
single { MockPreferencesRepository() } bind IPreferencesRepository::class
single { object : ISoundManager {
override fun play(soundId: Int) { }
} } bind ISoundManager::class
single { object : IThemeRepository {
override fun getCustomTheme(): AppTheme? = null
override fun getTheme(): AppTheme = LightTheme
override fun getAllThemes(): List<AppTheme> = listOf(LightTheme)
override fun setTheme(theme: AppTheme) { }
} } bind IThemeRepository::class
}

View file

@ -1,11 +1,5 @@
package dev.lucasnlm.antimine.di
import androidx.lifecycle.MutableLiveData
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
@ -15,25 +9,27 @@ import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackManager
import dev.lucasnlm.antimine.mocks.DisabledHapticFeedbackManager
import dev.lucasnlm.antimine.mocks.FixedMinefieldRepository
import org.koin.dsl.bind
import org.koin.dsl.module
@Module
@InstallIn(ActivityComponent::class)
class TestLevelModule {
@Provides
fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData()
val TestLevelModule = module {
single {
Clock()
}
@Provides
fun provideClock(): Clock = Clock()
single {
MemorySavesRepository()
} bind ISavesRepository::class
@Provides
fun provideSavesRepository(): ISavesRepository = MemorySavesRepository()
single {
MemoryStatsRepository()
} bind IStatsRepository::class
@Provides
fun provideStatsRepository(): IStatsRepository = MemoryStatsRepository()
single {
FixedMinefieldRepository()
} bind IMinefieldRepository::class
@Provides
fun provideMinefieldRepository(): IMinefieldRepository = FixedMinefieldRepository()
@Provides
fun provideHapticFeedbackInteractor(): IHapticFeedbackManager = DisabledHapticFeedbackManager()
single {
DisabledHapticFeedbackManager()
} bind IHapticFeedbackManager::class
}

View file

@ -0,0 +1,53 @@
package dev.lucasnlm.antimine.history
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.level.database.models.FirstOpen
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.database.models.SaveStatus
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.history.viewmodel.HistoryEvent
import dev.lucasnlm.antimine.history.viewmodel.HistoryState
import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel
import io.mockk.coEvery
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Test
class HistoryViewModelTest : IntentViewModelTest() {
private val fakeMinefield = Minefield(9, 9, 9)
private val allSaves = listOf(
Save(
0, 1, 0L, 100L, fakeMinefield,
Difficulty.Beginner, FirstOpen.Unknown, SaveStatus.ON_GOING, listOf()
),
Save(
1, 2, 0L, 100L, fakeMinefield,
Difficulty.Beginner, FirstOpen.Unknown, SaveStatus.ON_GOING, listOf()
),
Save(
2, 3, 0L, 100L, fakeMinefield,
Difficulty.Beginner, FirstOpen.Unknown, SaveStatus.ON_GOING, listOf()
)
)
@Test
fun testInitialValue() {
val viewModel = HistoryViewModel(mockk(), mockk())
assertEquals(HistoryState(listOf()), viewModel.singleState())
}
@Test
fun testLoadHistory() {
val savesRepository = mockk<ISavesRepository> {
coEvery { getAllSaves() } returns allSaves
}
val state = HistoryViewModel(mockk(), savesRepository).run {
sendEvent(HistoryEvent.LoadAllSaves)
singleState()
}
assertEquals(HistoryState(allSaves.sortedByDescending { it.uid }), state)
}
}

View file

@ -0,0 +1,59 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
class MockPreferencesRepository : IPreferencesRepository {
var customMinefield = Minefield(9, 9, 9)
override fun customGameMode(): Minefield = customMinefield
override fun updateCustomGameMode(minefield: Minefield) {
customMinefield = minefield
}
override fun controlStyle(): ControlStyle = ControlStyle.Standard
override fun useControlStyle(controlStyle: ControlStyle) { }
override fun isFirstUse(): Boolean = false
override fun completeFirstUse() { }
override fun customLongPressTimeout(): Long = 400L
override fun themeId(): Long = 1L
override fun useTheme(themeId: Long) { }
override fun updateStatsBase(statsBase: Int) { }
override fun getStatsBase(): Int = 0
override fun getUseCount(): Int = 10
override fun incrementUseCount() { }
override fun incrementProgressiveValue() { }
override fun decrementProgressiveValue() { }
override fun getProgressiveValue(): Int = 0
override fun isRequestRatingEnabled(): Boolean = false
override fun disableRequestRating() { }
override fun useFlagAssistant(): Boolean = false
override fun useHapticFeedback(): Boolean = true
override fun areaSizeMultiplier(): Int = 50
override fun useAnimations(): Boolean = false
override fun useQuestionMark(): Boolean = false
override fun isSoundEffectsEnabled(): Boolean = false
}

View file

@ -0,0 +1,149 @@
package dev.lucasnlm.antimine.theme
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.model.AreaPalette
import dev.lucasnlm.antimine.core.themes.model.Assets
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.theme.viewmodel.ThemeEvent
import dev.lucasnlm.antimine.theme.viewmodel.ThemeState
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Test
class ThemeViewModelTest : IntentViewModelTest() {
private val lightTheme = AppTheme(
id = 1L,
theme = R.style.CustomLightTheme,
themeNoActionBar = R.style.CustomLightTheme_NoActionBar,
palette = AreaPalette(
border = 0x424242,
background = 0xFFFFFF,
covered = 0x424242,
coveredOdd = 0x424242,
uncovered = 0xd5d2cc,
uncoveredOdd = 0xd5d2cc,
minesAround1 = 0x527F8D,
minesAround2 = 0x2B8D43,
minesAround3 = 0xE65100,
minesAround4 = 0x20A5f7,
minesAround5 = 0xED1C24,
minesAround6 = 0xFFC107,
minesAround7 = 0x66126B,
minesAround8 = 0x000000,
highlight = 0x212121,
focus = 0xD32F2F
),
assets = Assets(
wrongFlag = R.drawable.red_flag,
flag = R.drawable.flag,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low
)
)
private val darkTheme = AppTheme(
id = 2L,
theme = R.style.CustomDarkTheme,
themeNoActionBar = R.style.CustomDarkTheme_NoActionBar,
palette = AreaPalette(
border = 0x171717,
background = 0x212121,
covered = 0x171717,
coveredOdd = 0x171717,
uncovered = 0x424242,
uncoveredOdd = 0x424242,
minesAround1 = 0xd5d2cc,
minesAround2 = 0xd5d2cc,
minesAround3 = 0xd5d2cc,
minesAround4 = 0xd5d2cc,
minesAround5 = 0xd5d2cc,
minesAround6 = 0xd5d2cc,
minesAround7 = 0xd5d2cc,
minesAround8 = 0xd5d2cc,
highlight = 0xFFFFFF,
focus = 0xFFFFFF
),
assets = Assets(
wrongFlag = R.drawable.flag,
flag = R.drawable.flag,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low
)
)
private val gardenTheme = AppTheme(
id = 3L,
theme = R.style.CustomGardenTheme,
themeNoActionBar = R.style.CustomGardenTheme_NoActionBar,
palette = AreaPalette(
border = 0x171717,
background = 0xefebe9,
covered = 0x689f38,
coveredOdd = 0x558b2f,
uncovered = 0xefebe9,
uncoveredOdd = 0xd7ccc8,
minesAround1 = 0x527F8D,
minesAround2 = 0x2B8D43,
minesAround3 = 0xE65100,
minesAround4 = 0x20A5f7,
minesAround5 = 0xED1C24,
minesAround6 = 0xFFC107,
minesAround7 = 0x66126B,
minesAround8 = 0x000000,
highlight = 0xFFFFFF,
focus = 0xFFFFFF
),
assets = Assets(
wrongFlag = R.drawable.flag,
flag = R.drawable.flag,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low
)
)
private val allThemes = listOf(
lightTheme, darkTheme, gardenTheme
)
@Test
fun testInitialValue() {
val themeRepository = mockk<IThemeRepository> {
every { getAllThemes() } returns allThemes
every { getTheme() } returns gardenTheme
}
val viewModel = ThemeViewModel(themeRepository)
assertEquals(ThemeState(gardenTheme, allThemes), viewModel.singleState())
}
@Test
fun testChangeValue() {
val themeRepository = mockk<IThemeRepository> {
every { getAllThemes() } returns allThemes
every { getTheme() } returns gardenTheme
every { setTheme(any()) } returns Unit
}
val state = ThemeViewModel(themeRepository).run {
sendEvent(ThemeEvent.ChangeTheme(darkTheme))
singleState()
}
assertEquals(ThemeState(darkTheme, allThemes), state)
verify { themeRepository.setTheme(darkTheme) }
}
}

View file

@ -1,2 +0,0 @@
sdk=28
application=dagger.hilt.android.testing.HiltTestApplication

View file

@ -6,9 +6,12 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28.1-alpha'
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72'
if (System.getenv('IS_GOOGLE_BUILD')) {
classpath 'com.google.gms:google-services:4.3.3'
}
}
}

View file

@ -2,16 +2,15 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
compileSdkVersion 30
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 800041
versionName '8.0.4'
minSdkVersion 16
versionCode 800051
versionName '8.0.5'
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@ -33,14 +32,11 @@ kapt {
correctErrorTypes true
}
hilt {
enableTransformForLocalTests true
}
dependencies {
// Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
// AndroidX
implementation 'androidx.appcompat:appcompat:1.2.0'
@ -56,13 +52,10 @@ dependencies {
api 'android.arch.lifecycle:extensions:1.1.1'
implementation 'android.arch.lifecycle:viewmodel:1.1.1'
// Dagger
implementation 'com.google.dagger:hilt-android: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'
kaptTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01"
kapt "androidx.hilt:hilt-compiler:1.0.0-alpha01"
// Koin
implementation 'org.koin:koin-android:2.1.6'
implementation 'org.koin:koin-androidx-viewmodel:2.1.6'
testImplementation 'org.koin:koin-test:2.1.6'
// Room
api 'androidx.room:room-runtime:2.2.5'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 B

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,001 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show more