Merge pull request #192 from lucasnlm/move-instant-app-data

Move instant app data
This commit is contained in:
Lucas Nunes 2020-10-11 20:28:02 -03:00 committed by GitHub
commit 425c8b494b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 645 additions and 82 deletions

View file

@ -12,8 +12,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 802031
versionName '8.2.3'
versionCode 803001
versionName '8.3.0'
minSdkVersion 21
targetSdkVersion 30
multiDexEnabled true

View file

@ -38,7 +38,6 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:targetSandboxVersion="2"
tools:targetApi="lollipop">
<activity

View file

@ -5,6 +5,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.format.DateUtils
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.WindowManager
@ -23,6 +24,7 @@ import androidx.lifecycle.Transformations
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.cloud.CloudSaveManager
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.models.Score
@ -58,10 +60,12 @@ import kotlinx.android.synthetic.main.activity_tv_game.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.lang.Exception
class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.OnDismissListener {
private val billingManager: IBillingManager by inject()
@ -82,6 +86,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
val gameViewModel by viewModel<GameViewModel>()
private val cloudSaveManager by inject<CloudSaveManager>()
override val noActionBar: Boolean = true
private var status: Status = Status.PreGame
@ -91,7 +97,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private var currentTime: Long = 0
private var currentSaveId: Long = 0
private val areaSizeMultiplier by lazy { preferencesRepository.areaSizeMultiplier() }
private val areaSizeMultiplier by lazy { preferencesRepository.squareSizeMultiplier() }
private val currentRadius by lazy { preferencesRepository.squareRadius() }
private val useHelp by lazy { preferencesRepository.useHelp() }
@ -102,6 +108,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
bindViewModel()
bindPlayGames()
bindToolbar()
bindDrawer()
bindNavigationMenu()
@ -121,6 +128,22 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
onOpenAppActions()
}
private fun bindPlayGames() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
silentGooglePlayLogin()
}
withContext(Dispatchers.Main) {
if (!isFinishing) {
invalidateOptionsMenu()
}
}
playGamesManager.showPlayPopUp(this@GameActivity)
}
}
private fun bindViewModel() = gameViewModel.apply {
Transformations
.distinctUntilChanged(eventObserver)
@ -222,7 +245,6 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
analyticsManager.sentEvent(Analytics.Resume)
}
silentGooglePlayLogin()
refreshAds()
}
}
@ -711,6 +733,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
analyticsManager.sentEvent(Analytics.TutorialCompleted)
preferencesRepository.completeTutorial()
showCompletedTutorialDialog()
cloudSaveManager.uploadSave()
}
Event.Victory -> {
val isResuming = (status == Status.PreGame)
@ -727,6 +750,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
keepScreenOn(false)
if (!isResuming) {
cloudSaveManager.uploadSave()
gameViewModel.addNewTip()
waitAndShowEndGameAlert(
@ -748,6 +772,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
gameViewModel.stopClock()
if (!isResuming) {
cloudSaveManager.uploadSave()
GlobalScope.launch(context = Dispatchers.Main) {
gameViewModel.gameOver(isResuming)
waitAndShowEndGameAlert(
@ -767,7 +792,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
*/
private fun restartIfNeed(): Boolean {
return (
areaSizeMultiplier != preferencesRepository.areaSizeMultiplier() ||
areaSizeMultiplier != preferencesRepository.squareSizeMultiplier() ||
currentRadius != preferencesRepository.squareRadius() ||
useHelp != preferencesRepository.useHelp()
).also {
@ -853,8 +878,11 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private fun silentGooglePlayLogin() {
if (playGamesManager.hasGooglePlayGames()) {
playGamesManager.silentLogin(this)
invalidateOptionsMenu()
try {
playGamesManager.silentLogin()
} catch (e: Exception) {
Log.e(TAG, "User not logged in Play Games")
}
}
}
@ -871,9 +899,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
private fun openCrowdIn() {
Intent(Intent.ACTION_VIEW, Uri.parse("https://crowdin.com/project/antimine-android")).let {
startActivity(it)
}
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://crowdin.com/project/antimine-android")))
}
private fun showSupportAppDialog() {
@ -900,6 +926,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
companion object {
val TAG = GameActivity::class.simpleName
const val GOOGLE_PLAY_REQUEST_CODE = 6
const val MIN_USAGES_TO_IAP = 5

View file

@ -0,0 +1,52 @@
package dev.lucasnlm.antimine.cloud
import dev.lucasnlm.antimine.common.level.database.models.toHashMap
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.external.ICloudStorageManager
import dev.lucasnlm.external.IPlayGamesManager
import dev.lucasnlm.external.model.CloudSave
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CloudSaveManager(
private val playGamesManager: IPlayGamesManager,
private val preferencesRepository: IPreferencesRepository,
private val statsRepository: IStatsRepository,
private val cloudStorageManager: ICloudStorageManager,
) {
fun uploadSave() {
GlobalScope.launch {
withContext(Dispatchers.IO) {
getCloudSave()?.let {
cloudStorageManager.uploadSave(it)
}
}
}
}
private suspend fun getCloudSave(): CloudSave? {
val minId = preferencesRepository.getStatsBase()
return playGamesManager.playerId()?.let { playerId ->
CloudSave(
playId = playerId,
completeTutorial = if (preferencesRepository.isTutorialCompleted()) 1 else 0,
selectedTheme = preferencesRepository.themeId().toInt(),
squareRadius = preferencesRepository.squareRadius(),
squareSize = preferencesRepository.squareSizeMultiplier(),
touchTiming = preferencesRepository.customLongPressTimeout().toInt(),
questionMark = if (preferencesRepository.useQuestionMark()) 1 else 0,
gameAssistance = if (preferencesRepository.useFlagAssistant()) 1 else 0,
help = if (preferencesRepository.useHelp()) 1 else 0,
hapticFeedback = if (preferencesRepository.useHapticFeedback()) 1 else 0,
soundEffects = if (preferencesRepository.isSoundEffectsEnabled()) 1 else 0,
stats = statsRepository.getAllStats(minId).map { it.toHashMap() },
premiumFeatures = if (preferencesRepository.isPremiumEnabled()) 1 else 0,
controlStyle = preferencesRepository.controlStyle().ordinal,
)
}
}
}

View file

@ -9,14 +9,17 @@ import android.widget.BaseAdapter
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.cloud.CloudSaveManager
import dev.lucasnlm.antimine.control.view.ControlItemView
import dev.lucasnlm.antimine.control.view.SimpleControlItemView
import dev.lucasnlm.antimine.control.viewmodel.ControlEvent
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import dev.lucasnlm.antimine.core.control.ControlStyle
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class ControlDialogFragment : AppCompatDialogFragment() {
private val cloudSaveManager by inject<CloudSaveManager>()
private val controlViewModel by viewModel<ControlViewModel>()
private val adapter by lazy { ControlListAdapter(controlViewModel) }
@ -33,6 +36,7 @@ class ControlDialogFragment : AppCompatDialogFragment() {
if (activity is DialogInterface.OnDismissListener) {
(activity as DialogInterface.OnDismissListener).onDismiss(dialog)
}
cloudSaveManager.uploadSave()
super.onDismiss(dialog)
}

View file

@ -1,5 +1,6 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.cloud.CloudSaveManager
import dev.lucasnlm.antimine.support.IapHandler
import dev.lucasnlm.antimine.common.BuildConfig
import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager
@ -8,9 +9,11 @@ import dev.lucasnlm.antimine.core.analytics.ProdAnalyticsManager
import dev.lucasnlm.antimine.share.ShareManager
import dev.lucasnlm.external.AdsManager
import dev.lucasnlm.external.BillingManager
import dev.lucasnlm.external.CloudStorageManager
import dev.lucasnlm.external.ExternalAnalyticsWrapper
import dev.lucasnlm.external.IAdsManager
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.ICloudStorageManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import dev.lucasnlm.external.IReviewWrapper
@ -31,10 +34,14 @@ val AppModule = module {
single { ReviewWrapper() } bind IReviewWrapper::class
single { CloudStorageManager() } bind ICloudStorageManager::class
single { ShareManager(get(), get()) }
single { IapHandler(get(), get(), get()) }
single { CloudSaveManager(get(), get(), get(), get()) }
single {
if (BuildConfig.DEBUG) {
DebugAnalyticsManager()

View file

@ -7,6 +7,7 @@ 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.splash.viewmodel.SplashViewModel
import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel
import dev.lucasnlm.antimine.text.viewmodel.TextViewModel
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
@ -24,6 +25,7 @@ val ViewModelModule = module {
viewModel { StatsViewModel(get(), get(), get(), get()) }
viewModel { TextViewModel(get()) }
viewModel { ThemeViewModel(get(), get(), get(), get()) }
viewModel { SplashViewModel(get(), get(), get(), get(), get(), get(), get()) }
viewModel {
GameViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}

View file

@ -28,8 +28,10 @@ class HistoryFragment : Fragment(R.layout.fragment_history) {
historyViewModel.sendEvent(HistoryEvent.LoadAllSaves)
historyViewModel.observeState().collect {
if (it.saveList.isEmpty()) {
empty.visibility = View.GONE
empty.visibility = if (it.saveList.isEmpty()) {
View.VISIBLE
} else {
View.GONE
}
saveHistory.apply {

View file

@ -8,6 +8,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.cloud.CloudSaveManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import org.koin.android.ext.android.inject
@ -17,6 +18,8 @@ class PreferencesActivity :
private val preferenceRepository: IPreferencesRepository by inject()
private val cloudSaveManager by inject<CloudSaveManager>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
@ -30,6 +33,8 @@ class PreferencesActivity :
override fun onDestroy() {
super.onDestroy()
cloudSaveManager.uploadSave()
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(this)
}

View file

@ -2,20 +2,75 @@ package dev.lucasnlm.antimine.splash
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import dev.lucasnlm.antimine.GameActivity
import dev.lucasnlm.antimine.support.IapHandler
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.splash.viewmodel.SplashViewModel
import dev.lucasnlm.external.IPlayGamesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class SplashActivity : AppCompatActivity() {
private val iapHandler: IapHandler by inject()
private val playGamesManager: IPlayGamesManager by inject()
private val splashViewMode: SplashViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
iapHandler.start()
val layout = LinearLayout(this).apply {
fitsSystemWindows = true
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
orientation = LinearLayout.VERTICAL
Intent(this, GameActivity::class.java).run { startActivity(this) }
finish()
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { setMargins(0, 0, 0, 200) }
val loadingMessage = TextView(context).apply {
text = context.getString(R.string.loading)
setTextColor(ContextCompat.getColor(context, R.color.splash_text_color))
}
addView(loadingMessage, params)
}
setContentView(layout)
splashViewMode.startIap()
lifecycleScope.launchWhenCreated {
if (playGamesManager.hasGooglePlayGames()) {
withContext(Dispatchers.IO) {
try {
playGamesManager.silentLogin()
splashViewMode.migrateCloudSave()
} catch (e: Exception) {
Log.e(TAG, "User not logged in Play Games")
}
}
}
withContext(Dispatchers.Main) {
if (!isFinishing) {
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
Intent(this@SplashActivity, GameActivity::class.java)
.run { startActivity(this) }
finish()
}
}
}
}
companion object {
val TAG = SplashActivity::class.simpleName
}
}

View file

@ -0,0 +1,82 @@
package dev.lucasnlm.antimine.splash.viewmodel
import android.content.Context
import androidx.lifecycle.ViewModel
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.support.IapHandler
import dev.lucasnlm.external.ICloudStorageManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import dev.lucasnlm.external.model.CloudSave
class SplashViewModel(
private val context: Context,
private val preferencesRepository: IPreferencesRepository,
private val statsRepository: IStatsRepository,
private val saveCloudStorageManager: ICloudStorageManager,
private val playGamesManager: IPlayGamesManager,
private val instantAppManager: IInstantAppManager,
private val iapHandler: IapHandler,
) : ViewModel() {
fun startIap() {
iapHandler.start()
}
suspend fun migrateCloudSave() {
if (instantAppManager.isEnabled(context) || preferencesRepository.shouldMigrateFromCloud()) {
val userId = playGamesManager.playerId()
userId?.let {
saveCloudStorageManager.getSave(it)?.let { cloudSave ->
loadCloudSave(cloudSave)
}
}
}
if (instantAppManager.isEnabled(context)) {
preferencesRepository.setMigrateFromCloud(true)
}
}
private suspend fun loadCloudSave(cloudSave: CloudSave) = with(cloudSave) {
if (cloudSave.completeTutorial == 1) {
preferencesRepository.completeTutorial()
}
preferencesRepository.completeFirstUse()
preferencesRepository.useTheme(cloudSave.selectedTheme.toLong())
preferencesRepository.setSquareRadius(cloudSave.squareRadius)
preferencesRepository.setSquareMultiplier(cloudSave.squareSize)
preferencesRepository.setCustomLongPressTimeout(cloudSave.touchTiming.toLong())
preferencesRepository.setQuestionMark(cloudSave.questionMark != 0)
preferencesRepository.setFlagAssistant(gameAssistance != 0)
preferencesRepository.setHapticFeedback(hapticFeedback != 0)
preferencesRepository.setHelp(help != 0)
preferencesRepository.setSoundEffectsEnabled(soundEffects != 0)
preferencesRepository.setPremiumFeatures(cloudSave.premiumFeatures != 0)
preferencesRepository.useControlStyle(ControlStyle.values()[cloudSave.controlStyle])
cloudSave.stats.mapNotNull {
try {
Stats(
uid = it["uid"]!!.toInt(),
duration = it["duration"]!!.toLong(),
mines = it["mines"]!!.toInt(),
victory = it["victory"]!!.toInt(),
width = it["width"]!!.toInt(),
height = it["height"]!!.toInt(),
openArea = it["openArea"]!!.toInt(),
)
} catch (e: Exception) {
null
}
}.forEach {
statsRepository.addStats(it)
}
preferencesRepository.setMigrateFromCloud(false)
}
}

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.cloud.CloudSaveManager
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
@ -25,6 +26,8 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
private val themeViewModel by viewModel<ThemeViewModel>()
private val cloudSaveManager by inject<CloudSaveManager>()
private val preferencesRepository: IPreferencesRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) {
@ -62,6 +65,7 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
themeViewModel.observeState().collect {
if (usingTheme.id != it.current.id) {
recreate()
cloudSaveManager.uploadSave()
}
}
}

View file

@ -4,10 +4,8 @@ import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.animation.AnimationUtils
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
@ -87,7 +85,7 @@ class TutorialLevelFragment : Fragment(R.layout.fragment_tutorial_level) {
.forEach {
when (it) {
flagAction, openAction -> {
bold { color(ContextCompat.getColor(context, R.color.accent)) { append(it) } }
bold { append(it) }
}
else -> append(it)
}
@ -109,7 +107,7 @@ class TutorialLevelFragment : Fragment(R.layout.fragment_tutorial_level) {
.forEach {
when (it) {
flagAction, openAction -> {
bold { color(ContextCompat.getColor(context, R.color.accent)) { append(it) } }
bold { append(it) }
}
else -> append(it)
}

View file

@ -19,7 +19,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"

View file

@ -38,9 +38,13 @@ val AppModule = module {
single {
object : IPlayGamesManager {
override fun playerId(): String? = null
override fun hasGooglePlayGames(): Boolean = false
override fun silentLogin(activity: Activity) { }
override fun silentLogin() { }
override fun showPlayPopUp(activity: Activity) { }
override fun getLoginIntent(): Intent? = null

View file

@ -31,6 +31,8 @@ class MockPreferencesRepository : IPreferencesRepository {
override fun customLongPressTimeout(): Long = 400L
override fun setCustomLongPressTimeout(value: Long) { }
override fun themeId(): Long = 1L
override fun useTheme(themeId: Long) { }
@ -63,8 +65,12 @@ class MockPreferencesRepository : IPreferencesRepository {
override fun useHelp(): Boolean = false
override fun setHelp(value: Boolean) { }
override fun squareRadius(): Int = 2
override fun setSquareRadius(value: Int) { }
override fun getTips(): Int = 0
override fun setTips(tips: Int) { }
@ -79,15 +85,29 @@ class MockPreferencesRepository : IPreferencesRepository {
override fun useFlagAssistant(): Boolean = false
override fun setFlagAssistant(value: Boolean) { }
override fun useHapticFeedback(): Boolean = true
override fun areaSizeMultiplier(): Int = 50
override fun setHapticFeedback(value: Boolean) { }
override fun squareSizeMultiplier(): Int = 50
override fun setSquareMultiplier(value: Int) { }
override fun useAnimations(): Boolean = false
override fun useQuestionMark(): Boolean = false
override fun setQuestionMark(value: Boolean) { }
override fun isSoundEffectsEnabled(): Boolean = false
override fun setSoundEffectsEnabled(value: Boolean) { }
override fun shouldMigrateFromCloud(): Boolean = false
override fun setMigrateFromCloud(value: Boolean) { }
override fun showWindowsWhenFinishGame(): Boolean = true
}

View file

@ -8,8 +8,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 802031
versionName '8.2.3'
versionCode 803001
versionName '8.3.0'
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -27,3 +27,13 @@ data class Stats(
@ColumnInfo(name = "openArea")
val openArea: Int,
)
fun Stats.toHashMap(): HashMap<String, String> = hashMapOf(
"uid" to uid.toString(),
"duration" to duration.toString(),
"mines" to mines.toString(),
"victory" to victory.toString(),
"width" to width.toString(),
"height" to height.toString(),
"openArea" to openArea.toString(),
)

View file

@ -27,9 +27,9 @@ class DimensionRepository(
) : IDimensionRepository {
override fun areaSize(): Float {
val multiplier = preferencesRepository.areaSizeMultiplier() / 100.0f
val regularSize = context.resources.getDimension(R.dimen.field_size)
return regularSize * multiplier
val multiplier = preferencesRepository.squareSizeMultiplier() / 100.0f
val maxArea = context.resources.getDimension(R.dimen.field_size)
return maxArea * multiplier
}
override fun areaSeparator(): Float {

View file

@ -313,6 +313,9 @@ open class GameViewModel(
ActionResponse.OpenNeighbors -> {
analyticsManager.sentEvent(Analytics.OpenNeighbors(index))
}
ActionResponse.OpenOrMark -> {
analyticsManager.sentEvent(Analytics.OpenOrFlagTile(index))
}
}
}
@ -535,7 +538,7 @@ open class GameViewModel(
fun getAppTheme(): AppTheme = themeRepository.getTheme()
private fun getAreaSizeMultiplier() = preferencesRepository.areaSizeMultiplier()
private fun getAreaSizeMultiplier() = preferencesRepository.squareSizeMultiplier()
private fun refreshField() {
field.postValue(gameController.field())

View file

@ -50,6 +50,8 @@ sealed class Analytics(
class OpenTile(index: Int) : Analytics("Open Tile", mapOf("Index" to index.toString()))
class OpenOrFlagTile(index: Int) : Analytics("Open or Flag Tile", mapOf("Index" to index.toString()))
class SwitchMark(index: Int) : Analytics("Switch Mark", mapOf("Index" to index.toString()))
class HighlightNeighbors(index: Int) : Analytics("Highlight Neighbors", mapOf("Index" to index.toString()))

View file

@ -20,6 +20,7 @@ interface IPreferencesRepository {
fun completeTutorial()
fun customLongPressTimeout(): Long
fun setCustomLongPressTimeout(value: Long)
fun themeId(): Long
fun useTheme(themeId: Long)
@ -44,7 +45,10 @@ interface IPreferencesRepository {
fun showSupport(): Boolean
fun useHelp(): Boolean
fun setHelp(value: Boolean)
fun squareRadius(): Int
fun setSquareRadius(value: Int)
fun getTips(): Int
fun setTips(tips: Int)
@ -55,10 +59,24 @@ interface IPreferencesRepository {
fun setSwitchControl(useOpen: Boolean)
fun useFlagAssistant(): Boolean
fun setFlagAssistant(value: Boolean)
fun useHapticFeedback(): Boolean
fun areaSizeMultiplier(): Int
fun setHapticFeedback(value: Boolean)
fun squareSizeMultiplier(): Int
fun setSquareMultiplier(value: Int)
fun useAnimations(): Boolean
fun useQuestionMark(): Boolean
fun setQuestionMark(value: Boolean)
fun isSoundEffectsEnabled(): Boolean
fun setSoundEffectsEnabled(value: Boolean)
fun shouldMigrateFromCloud(): Boolean
fun setMigrateFromCloud(value: Boolean)
fun showWindowsWhenFinishGame(): Boolean
}

View file

@ -48,21 +48,48 @@ class PreferencesRepository(
override fun useFlagAssistant(): Boolean =
preferencesManager.getBoolean(PREFERENCE_ASSISTANT, true)
override fun setFlagAssistant(value: Boolean) {
preferencesManager.putBoolean(PREFERENCE_ASSISTANT, value)
}
override fun useHapticFeedback(): Boolean =
preferencesManager.getBoolean(PREFERENCE_VIBRATION, true)
override fun areaSizeMultiplier(): Int =
override fun setHapticFeedback(value: Boolean) {
preferencesManager.putBoolean(PREFERENCE_VIBRATION, value)
}
override fun squareSizeMultiplier(): Int =
preferencesManager.getInt(PREFERENCE_AREA_SIZE, 50)
override fun setSquareMultiplier(value: Int) {
preferencesManager.putInt(PREFERENCE_AREA_SIZE, value)
}
override fun useAnimations(): Boolean =
preferencesManager.getBoolean(PREFERENCE_ANIMATION, true)
override fun useQuestionMark(): Boolean =
preferencesManager.getBoolean(PREFERENCE_QUESTION_MARK, false)
override fun setQuestionMark(value: Boolean) {
preferencesManager.putBoolean(PREFERENCE_QUESTION_MARK, value)
}
override fun isSoundEffectsEnabled(): Boolean =
preferencesManager.getBoolean(PREFERENCE_SOUND_EFFECTS, false)
override fun setSoundEffectsEnabled(value: Boolean) {
preferencesManager.putBoolean(PREFERENCE_SOUND_EFFECTS, value)
}
override fun shouldMigrateFromCloud(): Boolean =
preferencesManager.getBoolean(PREFERENCE_SHOULD_MIGRATE_FROM_CLOUD, true)
override fun setMigrateFromCloud(value: Boolean) {
preferencesManager.putBoolean(PREFERENCE_SHOULD_MIGRATE_FROM_CLOUD, value)
}
override fun showWindowsWhenFinishGame(): Boolean =
preferencesManager.getBoolean(PREFERENCE_SHOW_WINDOWS, true)
@ -93,6 +120,10 @@ class PreferencesRepository(
override fun customLongPressTimeout(): Long =
preferencesManager.getInt(PREFERENCE_LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()).toLong()
override fun setCustomLongPressTimeout(value: Long) {
preferencesManager.putInt(PREFERENCE_LONG_PRESS_TIMEOUT, value.toInt())
}
override fun themeId(): Long =
preferencesManager.getInt(PREFERENCE_CUSTOM_THEME, 0).toLong()
@ -195,10 +226,18 @@ class PreferencesRepository(
return preferencesManager.getBoolean(PREFERENCE_USE_HELP, false)
}
override fun setHelp(value: Boolean) {
preferencesManager.putBoolean(PREFERENCE_USE_HELP, value)
}
override fun squareRadius(): Int {
return preferencesManager.getInt(PREFERENCE_SQUARE_RADIUS, 2)
}
override fun setSquareRadius(value: Int) {
preferencesManager.putInt(PREFERENCE_SQUARE_RADIUS, value)
}
override fun getTips(): Int {
return preferencesManager.getInt(PREFERENCE_TIPS, 5)
}
@ -252,5 +291,6 @@ class PreferencesRepository(
private const val PREFERENCE_EXTRA_TIPS = "preference_extra_tips"
private const val PREFERENCE_SHOW_WINDOWS = "preference_show_windows"
private const val PREFERENCE_USE_OPEN_SWITCH_CONTROL = "preference_use_open_switch_control"
private const val PREFERENCE_SHOULD_MIGRATE_FROM_CLOUD = "preference_should_migrate_from_cloud"
}
}

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="187.5"
android:viewportHeight="187.5">
<path
android:pathData="m88.569,20.255a73.602,73.602 0,0 0,-17.187 3.4l-2.311,27.252 -26.94,-9.324a73.602,73.602 90,0 0,-10.276 12.877L48.552,78.169 20.719,88.803a73.602,73.602 0,0 0,-0.159 4.84,73.602 73.602,90 0,0 0.814,10.917l28.983,7.683 -14.584,26.212a73.602,73.602 90,0 0,10.879 11.404l26.989,-12.674 5.495,28.51a73.602,73.602 90,0 0,15.024 1.55,73.602 73.602,90 0,0 1.354,-0.013L107.511,141.325 128.774,158.598a73.602,73.602 90,0 0,14.35 -10.002l-7.007,-25.871 25.831,-0.41a73.602,73.602 90,0 0,4.992 -17.699l-20.865,-14.527 18.93,-16.403a73.602,73.602 0,0 0,-7.339 -17.248l-24.941,2.249 4.013,-25.08a73.602,73.602 90,0 0,-16.228 -8.687l-18.195,18.294z"
android:strokeAlpha="0.698615"
android:strokeLineJoin="round"
android:strokeWidth="95.1572"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M93.75,93.75m-60.612,0a60.612,60.612 45,1 1,121.224 0a60.612,60.612 135,1 1,-121.224 0"
android:strokeAlpha="0.698615"
android:strokeLineJoin="round"
android:strokeWidth="42.2765"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -2,12 +2,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#ff212121" />
<solid android:color="@color/launcher_background_night" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_image" />
</item>
<item android:gravity="center" android:drawable="@drawable/new_splash"/>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="187.5"
android:viewportHeight="187.5">
<path
android:pathData="m88.569,20.255a73.602,73.602 0,0 0,-17.187 3.4l-2.311,27.252 -26.94,-9.324a73.602,73.602 90,0 0,-10.276 12.877L48.552,78.169 20.719,88.803a73.602,73.602 0,0 0,-0.159 4.84,73.602 73.602,90 0,0 0.814,10.917l28.983,7.683 -14.584,26.212a73.602,73.602 90,0 0,10.879 11.404l26.989,-12.674 5.495,28.51a73.602,73.602 90,0 0,15.024 1.55,73.602 73.602,90 0,0 1.354,-0.013L107.511,141.325 128.774,158.598a73.602,73.602 90,0 0,14.35 -10.002l-7.007,-25.871 25.831,-0.41a73.602,73.602 90,0 0,4.992 -17.699l-20.865,-14.527 18.93,-16.403a73.602,73.602 0,0 0,-7.339 -17.248l-24.941,2.249 4.013,-25.08a73.602,73.602 90,0 0,-16.228 -8.687l-18.195,18.294z"
android:strokeAlpha="0.698615"
android:strokeLineJoin="round"
android:strokeWidth="95.1572"
android:fillColor="#1a1a1a"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
<path
android:pathData="M93.75,93.75m-60.612,0a60.612,60.612 45,1 1,121.224 0a60.612,60.612 135,1 1,-121.224 0"
android:strokeAlpha="0.698615"
android:strokeLineJoin="round"
android:strokeWidth="42.2765"
android:fillColor="#1a1a1a"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -2,12 +2,8 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<solid android:color="@color/launcher_background_start" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_image" />
</item>
<item android:gravity="center" android:drawable="@drawable/new_splash"/>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -95,6 +95,7 @@
<string name="translation">Vertalings</string>
<string name="licenses">Licenses</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Laai…</string>
<string name="connect">Konnekteer</string>
<string name="connecting">Besig om aanlyn te koppel…</string>
<string name="disconnect">Ontkoppel</string>

View file

@ -95,6 +95,7 @@
<string name="translation">الترجمة</string>
<string name="licenses">التراخيص</string>
<string name="google_play_games">ألعاب Google Play</string>
<string name="loading">جاري التحميل…</string>
<string name="connect">الاتصال</string>
<string name="connecting">جاري الاتصال…</string>
<string name="disconnect">قطع الاتصال</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Превод</string>
<string name="licenses">Лицензи</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Зареждане…</string>
<string name="connect">Свързване</string>
<string name="connecting">Свързване…</string>
<string name="disconnect">Прекъсване</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Traduccions</string>
<string name="licenses">Llicències</string>
<string name="google_play_games">Google Play Jocs</string>
<string name="loading">Carregant…</string>
<string name="connect">Connectar</string>
<string name="connecting">Connectant…</string>
<string name="disconnect">Desconnectar</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Překlad</string>
<string name="licenses">Licence</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Načítání…</string>
<string name="connect">Připojit</string>
<string name="connecting">Připojování…</string>
<string name="disconnect">Odpojit</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Oversættelser</string>
<string name="licenses">Licenser</string>
<string name="google_play_games">Google Play Spil</string>
<string name="loading">Indlæser…</string>
<string name="connect">Tilslut</string>
<string name="connecting">Tilslutter…</string>
<string name="disconnect">Afbryd</string>

View file

@ -46,7 +46,7 @@
<string name="system">System</string>
<string name="rating">Rückmeldung</string>
<string name="support_title">Unterstütze uns!</string>
<string name="support_action">Hilfestellung</string>
<string name="support_action">Unterstützung</string>
<string name="rating_message">Wenn dir dieses Spiel gefällt, gib uns bitte eine Rückmeldung. Es wird uns sehr helfen.</string>
<string name="used_software_text">Dieses Spiel benutzt folgende Software von Drittanbietern:</string>
<string name="translators_text">Dieses Spiel wurde von den folgenden Personen übersetzt:</string>
@ -95,6 +95,7 @@
<string name="translation">Übersetzung</string>
<string name="licenses">Lizenzen</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Wird geladen...</string>
<string name="connect">Verbinden</string>
<string name="connecting">Verbindung wird hergestellt…</string>
<string name="disconnect">Trennen</string>
@ -121,9 +122,9 @@
<string name="flag_removed">Markierung entfernt!</string>
<string name="remove_ad">Werbung entfernen</string>
<string name="help">Hilfe</string>
<string name="show_windows">Show windows</string>
<string name="switch_control">Switch: Flag and Open</string>
<string name="switch_control_desc">Use button to switch between Flag and Open</string>
<string name="show_windows">Fenster anzeigen</string>
<string name="switch_control">Umschalter: Markieren und Öffnen</string>
<string name="switch_control_desc">Benutze den Knopf, um zwischen Markierung und Öffnen zu wechseln</string>
<string name="app_description">Du musst ein rechteckiges Spielfeld, das versteckte Minen enthält, räumen, ohne irgendeine davon zur Explosion zu bringen.</string>
<string name="app_name">Antimine</string>
</resources>

View file

@ -95,6 +95,7 @@
<string name="translation">Μετάφραση</string>
<string name="licenses">Άδειες</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Φόρτωση…</string>
<string name="connect">Σύνδεση</string>
<string name="connecting">Σύνδεση…</string>
<string name="disconnect">Αποσύνδεση</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Translation</string>
<string name="licenses">Licenses</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Loading…</string>
<string name="connect">Connect</string>
<string name="connecting">Connecting…</string>
<string name="disconnect">Disconnect</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Traducción</string>
<string name="licenses">Licencias</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Cargando…</string>
<string name="connect">Conectar</string>
<string name="connecting">Conectando…</string>
<string name="disconnect">Desconectar</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Käännökset</string>
<string name="licenses">Lisenssit</string>
<string name="google_play_games">Google Play Pelit</string>
<string name="loading">Ladataan…</string>
<string name="connect">Yhdistä</string>
<string name="connecting">Yhdistetään…</string>
<string name="disconnect">Katkaise yhteys</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Traduction</string>
<string name="licenses">Licences</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Chargement…</string>
<string name="connect">Connexion</string>
<string name="connecting">Connexion en cours…</string>
<string name="disconnect">Déconnexion</string>

View file

@ -95,6 +95,7 @@
<string name="translation">अनुवाद</string>
<string name="licenses">लाइसेंस</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">लोड हो रहा है…</string>
<string name="connect">कनेक्ट करें</string>
<string name="connecting">संपर्क बना रहा है...</string>
<string name="disconnect">संपर्क तोड़ें</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Fordítás</string>
<string name="licenses">Licencek</string>
<string name="google_play_games">Google Play Játékok</string>
<string name="loading">Betöltés…</string>
<string name="connect">Csatlakozás</string>
<string name="connecting">Csatlakozás…</string>
<string name="disconnect">Lecsatlakozás</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Terjemahan</string>
<string name="licenses">Lisensi</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Memuat…</string>
<string name="connect">Menyambungkan</string>
<string name="connecting">Menghubungkan…</string>
<string name="disconnect">Terputus</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Traduzione</string>
<string name="licenses">Licenze</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Caricamento…</string>
<string name="connect">Connetti</string>
<string name="connecting">Connessione in corso…</string>
<string name="disconnect">Disconnetti</string>

View file

@ -95,6 +95,7 @@
<string name="translation">תרגומים</string>
<string name="licenses">רשיונות</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">טוען…</string>
<string name="connect">התחבר</string>
<string name="connecting">מתחבר…</string>
<string name="disconnect">נתק</string>

View file

@ -95,6 +95,7 @@
<string name="translation">翻訳</string>
<string name="licenses">ライセンス</string>
<string name="google_play_games">Google Play ゲーム</string>
<string name="loading">読み込み中…</string>
<string name="connect">接続</string>
<string name="connecting">接続しています...</string>
<string name="disconnect">切断</string>

View file

@ -95,6 +95,7 @@
<string name="translation">번역</string>
<string name="licenses">라이선스</string>
<string name="google_play_games">Google Play 게임</string>
<string name="loading">시작중…</string>
<string name="connect">연결하기</string>
<string name="connecting">연결중…</string>
<string name="disconnect">연결 해제</string>

View file

@ -21,4 +21,6 @@
<color name="view_cover">#171717</color>
<color name="view_clean">#ff424242</color>
<color name="splash_text_color">#FFFFFF</color>
</resources>

View file

@ -95,6 +95,7 @@
<string name="translation">Vertaling</string>
<string name="licenses">Licenties</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Bezig met laden…</string>
<string name="connect">Verbinden</string>
<string name="connecting">Verbinden…</string>
<string name="disconnect">Verbinding verbreken</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Oversettelse</string>
<string name="licenses">Lisenser</string>
<string name="google_play_games">Google Play Spill</string>
<string name="loading">Laster…</string>
<string name="connect">Koble til</string>
<string name="connecting">Kobler til…</string>
<string name="disconnect">Koble fra</string>

View file

@ -65,7 +65,7 @@
<string name="open_areas">Odkryte pola</string>
<string name="total_time">Całkowity czas gry</string>
<string name="average_time">Średni czas gry</string>
<string name="shortest_time">Najkrótszy czas gry</string>
<string name="shortest_time">Najlepszy czas gry</string>
<string name="performance">Skuteczność</string>
<string name="ok">OK</string>
<string name="use_question_mark">Używaj Znaku zapytania</string>
@ -95,6 +95,7 @@
<string name="translation">Tłumaczenie</string>
<string name="licenses">Licencje</string>
<string name="google_play_games">Gry Google Play</string>
<string name="loading">Wczytywanie…</string>
<string name="connect">Połącz</string>
<string name="connecting">Łączenie…</string>
<string name="disconnect">Rozłącz</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Tradução</string>
<string name="licenses">Licenças</string>
<string name="google_play_games">Google Play Jogos</string>
<string name="loading">Carregando…</string>
<string name="connect">Conectar</string>
<string name="connecting">Conectando…</string>
<string name="disconnect">Desconectar</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Tradução</string>
<string name="licenses">Licenças</string>
<string name="google_play_games">Google Play Jogos</string>
<string name="loading">A carregar…</string>
<string name="connect">Conectar</string>
<string name="connecting">Conectando…</string>
<string name="disconnect">Desconectar</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Translare</string>
<string name="licenses">Licenţe</string>
<string name="google_play_games">Jocurile Google Play</string>
<string name="loading">Încărcare…</string>
<string name="connect">Conectează-te</string>
<string name="connecting">Se conectează…</string>
<string name="disconnect">Deconectează</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Перевод</string>
<string name="licenses">Лицензии</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Загрузка…</string>
<string name="connect">Подключиться</string>
<string name="connecting">Подключение…</string>
<string name="disconnect">Отключиться</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Translation</string>
<string name="licenses">Licenses</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Loading…</string>
<string name="connect">Connect</string>
<string name="connecting">Connecting…</string>
<string name="disconnect">Disconnect</string>

View file

@ -95,6 +95,7 @@
<string name="translation">แปลภาษา</string>
<string name="licenses">ใบอนุญาต</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">กำลังโหลด...</string>
<string name="connect">เชื่อมต่อ</string>
<string name="connecting">กำลังเชื่อมต่อ…</string>
<string name="disconnect">ตัดการเชื่อมต่อ</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Çeviri</string>
<string name="licenses">Lisanslar</string>
<string name="google_play_games">Google Play Oyunlar</string>
<string name="loading">Yükleniyor…</string>
<string name="connect">Bağlan</string>
<string name="connecting">Bağlanıyor…</string>
<string name="disconnect">Bağlantıyı kes</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Переклад</string>
<string name="licenses">Ліцензії</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Завантаження…</string>
<string name="connect">Під’єднатись</string>
<string name="connecting">Підключення…</string>
<string name="disconnect">Відключитись</string>

View file

@ -95,6 +95,7 @@
<string name="translation">Dịch thuật</string>
<string name="licenses">Giấy phép</string>
<string name="google_play_games">Google Play Trò chơi</string>
<string name="loading">Đang tải…</string>
<string name="connect">Kết nối</string>
<string name="connecting">Đang kết nối…</string>
<string name="disconnect">Ngừng kết nối</string>

View file

@ -95,6 +95,7 @@
<string name="translation">翻译</string>
<string name="licenses">许可证</string>
<string name="google_play_games">Google Play 游戏</string>
<string name="loading">启动中…</string>
<string name="connect">连接</string>
<string name="connecting">正在连接…</string>
<string name="disconnect">断开</string>

View file

@ -22,6 +22,7 @@
<color name="launcher_background_start">#FCC216</color>
<color name="launcher_background_end">#FF8C3A</color>
<color name="launcher_background_night">#212121</color>
<color name="accent">#FFD32F2F</color>
<color name="install_button">#00C853</color>
@ -29,4 +30,6 @@
<color name="view_cover">#424242</color>
<color name="view_clean">#d5d2cc</color>
<color name="splash_text_color">#000000</color>
</resources>

View file

@ -95,6 +95,7 @@
<string name="translation">Translation</string>
<string name="licenses">Licenses</string>
<string name="google_play_games">Google Play Games</string>
<string name="loading">Loading…</string>
<string name="connect">Connect</string>
<string name="connecting">Connecting…</string>
<string name="disconnect">Disconnect</string>

View file

@ -19,10 +19,8 @@
<item name="android:windowTranslucentNavigation">true</item>
</style>
<style name="Theme.Splash" parent="Theme.AppCompat.NoActionBar">
<style name="Theme.Splash" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@drawable/splash</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">true</item>

View file

@ -0,0 +1,9 @@
package dev.lucasnlm.external
import dev.lucasnlm.external.model.CloudSave
interface ICloudStorageManager {
fun uploadSave(cloudSave: CloudSave)
suspend fun getSave(playId: String): CloudSave?
}

View file

@ -25,8 +25,10 @@ enum class Leaderboard(
}
interface IPlayGamesManager {
fun playerId(): String?
fun hasGooglePlayGames(): Boolean
fun silentLogin(activity: Activity)
fun silentLogin()
fun showPlayPopUp(activity: Activity)
fun getLoginIntent(): Intent?
fun handleLoginResult(data: Intent?)
fun isLogged(): Boolean

View file

@ -0,0 +1,53 @@
package dev.lucasnlm.external.model
data class CloudSave(
val playId: String,
val completeTutorial: Int,
val selectedTheme: Int,
val squareRadius: Int,
val squareSize: Int,
val touchTiming: Int,
val questionMark: Int,
val gameAssistance: Int,
val help: Int,
val hapticFeedback: Int,
val soundEffects: Int,
val stats: List<HashMap<String, String>>,
val premiumFeatures: Int,
val controlStyle: Int,
)
fun CloudSave.toHashMap(): HashMap<String, Any> = hashMapOf(
"completeTutorial" to completeTutorial,
"selectedTheme" to selectedTheme,
"squareRadius" to squareRadius,
"squareSize" to squareSize,
"touchTiming" to touchTiming,
"questionMark" to questionMark,
"gameAssistance" to gameAssistance,
"help" to help,
"hapticFeedback" to hapticFeedback,
"soundEffects" to soundEffects,
"stats" to stats,
"premiumFeatures" to premiumFeatures,
"controlStyle" to controlStyle,
)
@Suppress("UNCHECKED_CAST")
fun cloudSaveOf(id: String, data: Map<String, Any>) =
CloudSave(
id,
data["completeTutorial"].toString().toInt(),
data["selectedTheme"].toString().toInt(),
data["squareRadius"].toString().toInt(),
data["squareSize"].toString().toInt(),
data["touchTiming"].toString().toInt(),
data["questionMark"].toString().toInt(),
data["gameAssistance"].toString().toInt(),
data["help"].toString().toInt(),
data["hapticFeedback"].toString().toInt(),
data["soundEffects"].toString().toInt(),
data["stats"] as List<HashMap<String, String>>,
data["premiumFeatures"].toString().toInt(),
data["controlStyle"].toString().toInt(),
)

View file

@ -6,8 +6,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 802031
versionName '8.2.3'
versionCode 803001
versionName '8.3.0'
minSdkVersion 21
targetSdkVersion 30
}

View file

@ -0,0 +1,14 @@
package dev.lucasnlm.external
import dev.lucasnlm.external.model.CloudSave
class CloudStorageManager : ICloudStorageManager {
override fun uploadSave(cloudSave: CloudSave) {
// FOSS build doesn't support cloud save.
}
override suspend fun getSave(playId: String): CloudSave? {
// FOSS build doesn't support cloud save.
return null
}
}

View file

@ -7,9 +7,15 @@ import android.content.Intent
class PlayGamesManager(
context: Context,
) : IPlayGamesManager {
override fun playerId(): String? = null
override fun hasGooglePlayGames(): Boolean = false
override fun silentLogin(activity: Activity) {
override fun silentLogin() {
// F-droid build doesn't have Google Play Games
}
override fun showPlayPopUp(activity: Activity) {
// F-droid build doesn't have Google Play Games
}

View file

@ -10,8 +10,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 802031
versionName '8.2.3'
versionCode 803001
versionName '8.3.0'
minSdkVersion 21
targetSdkVersion 30
}
@ -38,16 +38,17 @@ dependencies {
implementation project(':external')
// Google
implementation 'com.android.billingclient:billing-ktx:3.0.0'
implementation 'com.android.billingclient:billing-ktx:3.0.1'
implementation 'com.google.android.gms:play-services-instantapps:17.0.0'
implementation 'com.google.android.gms:play-services-games:20.0.0'
implementation 'com.google.android.gms:play-services-games:20.0.1'
implementation 'com.google.android.gms:play-services-auth:18.1.0'
implementation 'com.google.android.gms:play-services-ads:19.3.0'
implementation 'com.google.android.gms:play-services-ads:19.4.0'
implementation 'com.google.android.play:core-ktx:1.8.1'
// Firebase
implementation 'com.google.firebase:firebase-analytics-ktx:17.5.0'
implementation 'com.google.firebase:firebase-crashlytics:17.2.1'
implementation 'com.google.firebase:firebase-analytics-ktx:17.6.0'
implementation 'com.google.firebase:firebase-crashlytics:17.2.2'
implementation 'com.google.firebase:firebase-firestore-ktx:21.7.1'
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

View file

@ -0,0 +1,66 @@
package dev.lucasnlm.external
import android.util.Log
import com.google.android.gms.tasks.Tasks
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import dev.lucasnlm.external.model.CloudSave
import dev.lucasnlm.external.model.cloudSaveOf
import dev.lucasnlm.external.model.toHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class CloudStorageManager : ICloudStorageManager {
private val db by lazy { Firebase.firestore }
override fun uploadSave(cloudSave: CloudSave) {
FirebaseFirestore.setLoggingEnabled(true)
val data = cloudSave.toHashMap()
Tasks.await(
db.collection(SAVES)
.document(cloudSave.playId)
.set(data)
.addOnCompleteListener {
Log.v(TAG, "Cloud storage complete")
}
.addOnCanceledListener {
Log.v(TAG, "Cloud storage canceled")
}
.addOnFailureListener {
Log.e(TAG, "Cloud storage error", it)
}
.addOnSuccessListener {
Log.v(TAG, "Cloud storage success")
}
)
}
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getSave(playId: String): CloudSave? {
return runBlocking {
try {
withContext(Dispatchers.IO) {
val result = Tasks.await(
db.collection(SAVES)
.document(playId)
.get()
)
result.data?.let {
cloudSaveOf(playId, it.toMap())
}
}
} catch (e: Exception) {
Log.e(TAG, "Fail to load save on cloud", e)
null
}
}
}
companion object {
val TAG = CloudStorageManager::class.simpleName
const val SAVES = "saves"
}
}

View file

@ -10,6 +10,7 @@ import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.games.Games
import com.google.android.gms.tasks.Tasks
class PlayGamesManager(
private val context: Context,
@ -17,28 +18,33 @@ class PlayGamesManager(
private var account: GoogleSignInAccount? = null
private fun setupPopUp(activity: Activity, account: GoogleSignInAccount) {
Games.getGamesClient(context, account).setViewForPopups(activity.findViewById(android.R.id.content))
Games.getGamesClient(context, account).setGravityForPopups(Gravity.TOP or Gravity.END)
Games.getGamesClient(context, account)
.setViewForPopups(activity.findViewById(android.R.id.content))
Games.getGamesClient(context, account)
.setGravityForPopups(Gravity.TOP or Gravity.END)
}
override fun playerId(): String? {
return account?.let {
Tasks.await(Games.getPlayersClient(context, it).currentPlayerId)
}
}
override fun showPlayPopUp(activity: Activity) {
if (!activity.isFinishing) {
account?.let {
setupPopUp(activity, it)
}
}
}
override fun hasGooglePlayGames(): Boolean = true
override fun silentLogin(activity: Activity) {
override fun silentLogin() {
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build()
val lastAccount: GoogleSignInAccount? = GoogleSignIn.getLastSignedInAccount(context)
if (lastAccount != null) {
account = lastAccount.also { setupPopUp(activity, it) }
} else {
GoogleSignIn
.getClient(context, signInOptions)
.silentSignIn()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
account = task.result?.also { setupPopUp(activity, it) }
}
}
}
account = lastAccount ?: Tasks.await(GoogleSignIn.getClient(context, signInOptions).silentSignIn())
}
override fun getLoginIntent(): Intent? {

View file

@ -8,8 +8,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 802031
versionName '8.2.3'
versionCode 803001
versionName '8.3.0'
applicationId 'dev.lucasnlm.antimine'
minSdkVersion 23
targetSdkVersion 30