First open source release

This commit is contained in:
Lucas Lima 2020-02-06 22:50:47 -03:00
parent 87083e1607
commit fa79d3eac2
No known key found for this signature in database
GPG key ID: C828A958035D9C34
181 changed files with 7521 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.idea/
app/local.properties
*.apk
app/release/
app/standalone/

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

62
app/build.gradle Normal file
View file

@ -0,0 +1,62 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.fabric'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
defaultConfig {
applicationId 'dev.lucasnlm.antimine'
minSdkVersion 16
targetSdkVersion 29
versionCode 40
versionName '6.0'
resConfigs 'en', 'pt', 'es', 'zh'
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
kapt {
generateStubs true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':common')
// Android
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.android.play:core:1.6.4'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// Dagger
def dagger_version='2.21'
api "com.google.dagger:dagger-android:$dagger_version"
api "com.google.dagger:dagger-android-support:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50'
}

1
app/fabric.properties Normal file
View file

@ -0,0 +1 @@
apiKey=18aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

BIN
app/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Mon May 21 22:33:16 BRT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

172
app/gradlew vendored Normal file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
app/gradlew.bat vendored Normal file
View file

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

43
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,43 @@
# Project specific ProGuard rules
# Crashlytics 2.+
-keep class com.crashlytics.** { *; }
-keep class com.crashlytics.android.**
-keepattributes SourceFile, LineNumberTable, *Annotation*
# For Fabric to properly de-obfuscate your crash reports, you need to remove this line from your ProGuard config:
-printmapping mapping.txt
# support design
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }
# support v7
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
# support v4
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.** { *; }
# build config
-keep class com.example.BuildConfig { *; }
# firebase
-keep class com.firebase.** { *; }
# google gms
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
# recycler view
-keep public class * extends android.support.v7.widget.RecyclerView$LayoutManager {
public <init>(...);
}

View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.lucasnlm.antimine"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
xmlns:dist="http://schemas.android.com/apk/distribution">
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.faketouch" android:required="false" />
<dist:module dist:instant="true" />
<application
android:allowBackup="true"
android:allowClearUserData="true"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:name="dev.lucasnlm.antimine.MainApplication"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:isGame="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="lollipop">
<activity
android:name="dev.lucasnlm.antimine.splash.SplashActivity"
android:theme="@style/Theme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="dev.lucasnlm.antimine.GameActivity"
android:configChanges="screenSize|orientation"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="default-url"
android:value="https://www.lucasnlm.dev/antimine" />
<intent-filter
android:order="1"
android:autoVerify="true"
tools:targetApi="m">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="lucasnlm.dev"
android:scheme="https" />
<data android:scheme="http" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="antimine" android:host="new-game" />
</intent-filter>
</activity>
<activity android:name="dev.lucasnlm.antimine.TvGameActivity"
android:configChanges="screenSize|orientation"
android:theme="@style/AppTheme.NoActionBar">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN"/>-->
<!-- <category android:name="android.intent.category.LAUNCHER"/>-->
<!-- </intent-filter>-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LEANBACK_LAUNCHER" />-->
<!-- </intent-filter>-->
</activity>
<activity
android:name="dev.lucasnlm.antimine.about.thirds.ThirdPartiesActivity"
android:theme="@style/AppTheme"/>
<activity
android:name="dev.lucasnlm.antimine.about.TextActivity"
android:theme="@style/AppTheme"/>
<activity
android:name="dev.lucasnlm.antimine.about.AboutActivity"
android:theme="@style/AppTheme"/>
<activity
android:name="dev.lucasnlm.antimine.preferences.PreferencesActivity"
android:label="@string/settings"
android:parentActivityName="dev.lucasnlm.antimine.GameActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.lucasnlm.antimine.GameActivity"/>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,504 @@
package dev.lucasnlm.antimine
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.IntentSender
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.text.format.DateUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.os.HandlerCompat.postDelayed
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.preferences.PreferencesActivity
import dagger.android.support.DaggerAppCompatActivity
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.data.GameEvent
import dev.lucasnlm.antimine.common.level.data.GameStatus
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.analytics.Event
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.utils.isDarkModeEnabled
import dev.lucasnlm.antimine.level.view.CustomLevelDialogFragment
import dev.lucasnlm.antimine.level.view.LevelFragment
import kotlinx.android.synthetic.main.activity_game.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class GameActivity : DaggerAppCompatActivity() {
@Inject
lateinit var viewModelFactory: GameViewModelFactory
@Inject
lateinit var preferencesRepository: IPreferencesRepository
@Inject
lateinit var analyticsManager: AnalyticsManager
private lateinit var viewModel: GameViewModel
private var gameStatus: GameStatus = GameStatus.PreGame
private var keepConfirmingNewGame = true
private val usingLargeArea by lazy { preferencesRepository.useLargeAreas() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_game)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(GameViewModel::class.java)
bindViewModel()
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
bindToolbarAndDrawer()
loadGameFragment()
if (Build.VERSION.SDK_INT >= 21) {
checkUpdate()
}
checkUseCount()
}
private fun bindViewModel() = viewModel.apply {
eventObserver.observe(this@GameActivity, Observer {
onGameEvent(it)
})
elapsedTimeSeconds.observe(this@GameActivity, Observer {
timer.apply {
visibility = if (it == 0L) View.GONE else View.VISIBLE
text = DateUtils.formatElapsedTime(it)
}
})
mineCount.observe(this@GameActivity, Observer {
minesCount.apply {
visibility = View.VISIBLE
text = it.toString()
}
})
difficulty.observe(this@GameActivity, Observer {
onChangeDifficulty(it)
})
}
override fun onBackPressed() {
when {
drawer.isDrawerOpen(GravityCompat.START) -> {
drawer.closeDrawer(GravityCompat.START)
viewModel.resumeGame()
}
gameStatus == GameStatus.Running -> showQuitConfirmation {
super.onBackPressed()
}
else -> super.onBackPressed()
}
}
override fun onResume() {
super.onResume()
if (gameStatus == GameStatus.Running) {
viewModel.resumeGame()
analyticsManager.sentEvent(Event.Resume())
}
restartIfNeed()
}
override fun onPause() {
super.onPause()
if (gameStatus == GameStatus.Running) {
viewModel.pauseGame()
}
analyticsManager.sentEvent(Event.Quit())
}
override fun onCreateOptionsMenu(menu: Menu): Boolean =
when (gameStatus) {
GameStatus.Over, GameStatus.Running -> {
menuInflater.inflate(R.menu.top_menu_over, menu)
true
}
else -> true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.reset) {
val confirmResign = gameStatus == GameStatus.Running
analyticsManager.sentEvent(Event.TapGameReset(confirmResign))
if (confirmResign) {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame()
}
}
} else {
GlobalScope.launch {
viewModel.startNewGame()
}
}
true
} else {
super.onOptionsItemSelected(item)
}
}
private fun bindToolbarAndDrawer() {
setSupportActionBar(toolbar)
toolbar.title = ""
supportActionBar?.apply {
title = ""
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true)
}
drawer.apply {
addDrawerListener(
ActionBarDrawerToggle(this@GameActivity, drawer, toolbar, R.string.open_menu, R.string.close_menu).apply {
if (!isDarkModeEnabled(applicationContext)) {
drawerArrowDrawable.color = ContextCompat.getColor(applicationContext, R.color.primary)
}
syncState()
}
)
addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
// Empty
}
override fun onDrawerOpened(drawerView: View) {
if (gameStatus != GameStatus.Over) {
viewModel.pauseGame()
}
analyticsManager.sentEvent(Event.OpenDrawer())
}
override fun onDrawerClosed(drawerView: View) {
if (gameStatus != GameStatus.Over) {
viewModel.resumeGame()
}
analyticsManager.sentEvent(Event.CloseDrawer())
}
override fun onDrawerStateChanged(newState: Int) {
// Empty
}
})
}
navigationView.setNavigationItemSelectedListener { item ->
var handled = true
when (item.itemId) {
R.id.standard -> changeDifficulty(DifficultyPreset.Standard)
R.id.beginner -> changeDifficulty(DifficultyPreset.Beginner)
R.id.intermediate -> changeDifficulty(DifficultyPreset.Intermediate)
R.id.expert -> changeDifficulty(DifficultyPreset.Expert)
R.id.custom -> showCustomLevelDialog()
R.id.about -> showAbout()
R.id.settings -> showSettings()
R.id.rate -> openRateUsLink("Drawer")
else -> handled = false
}
if (handled) {
drawer.closeDrawer(GravityCompat.START)
}
handled
}
if (preferencesRepository.getBoolean(PREFERENCE_FIRST_USE, false)) {
drawer.openDrawer(GravityCompat.START)
preferencesRepository.putBoolean(PREFERENCE_FIRST_USE, true)
}
}
private fun checkUseCount() {
val current = preferencesRepository.getInt(PREFERENCE_USE_COUNT, 0)
val shouldRequestRating = preferencesRepository.getBoolean(PREFERENCE_REQUEST_RATING, true)
if (current >= 4 && shouldRequestRating) {
analyticsManager.sentEvent(Event.ShowRatingRequest(current))
showRequestRating()
}
preferencesRepository.putInt(PREFERENCE_USE_COUNT, current + 1)
}
private fun onChangeDifficulty(difficulty: DifficultyPreset) {
navigationView.menu.apply {
arrayOf(
DifficultyPreset.Standard to findItem(R.id.standard),
DifficultyPreset.Beginner to findItem(R.id.beginner),
DifficultyPreset.Intermediate to findItem(R.id.intermediate),
DifficultyPreset.Expert to findItem(R.id.expert),
DifficultyPreset.Custom to findItem(R.id.custom)
).map {
it.second to (if (it.first == difficulty) R.drawable.checked else R.drawable.unchecked)
}.forEach { (menuItem, icon) ->
menuItem.setIcon(icon)
}
}
}
private fun loadGameFragment() {
val fragmentManager = supportFragmentManager
fragmentManager.popBackStack()
fragmentManager.findFragmentById(R.id.levelContainer)?.let { it ->
fragmentManager.beginTransaction().apply {
remove(it)
commitAllowingStateLoss()
}
}
fragmentManager.beginTransaction().apply {
replace(R.id.levelContainer, LevelFragment())
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss()
}
}
private fun showRequestRating() {
if (getString(R.string.rating_message).isNotEmpty()) {
AlertDialog.Builder(this)
.setMessage(R.string.rating_message)
.setPositiveButton(R.string.rating_button) { _, _ ->
openRateUsLink("Dialog")
}
.setNegativeButton(R.string.rating_button_no) { _, _ ->
preferencesRepository.putBoolean(PREFERENCE_REQUEST_RATING, false)
}
.show()
}
}
private fun newGameConfirmation(action: () -> Unit) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.start_over)
setMessage(R.string.retry_sure)
setPositiveButton(R.string.resume) { _, _ -> action() }
setNegativeButton(R.string.cancel, null)
show()
}
}
private fun showQuitConfirmation(action: () -> Unit) {
AlertDialog.Builder(this, R.style.MyDialog)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.sure_quit_desc)
.setPositiveButton(R.string.quit) { _, _ -> action() }
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun showCustomLevelDialog() {
CustomLevelDialogFragment().apply {
show(supportFragmentManager, "custom_level_fragment")
}
}
private fun showAbout() {
analyticsManager.sentEvent(Event.OpenAbout())
Intent(this, AboutActivity::class.java).apply {
startActivity(this)
}
}
private fun showSettings() {
analyticsManager.sentEvent(Event.OpenSettings())
Intent(this, PreferencesActivity::class.java).apply {
startActivity(this)
}
}
private fun showVictory() {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.you_won)
setMessage(R.string.all_mines_disabled)
setCancelable(false)
setPositiveButton(R.string.new_game) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
show()
}
}
private fun waitAndShowConfirmNewGame() {
if (keepConfirmingNewGame) {
postDelayed(Handler(), {
if (this.gameStatus == GameStatus.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.new_game)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
}.show()
keepConfirmingNewGame = false
}
}, null, DateUtils.SECOND_IN_MILLIS)
}
}
private fun waitAndShowGameOverConfirmNewGame() {
postDelayed(Handler(), {
if (this.gameStatus == GameStatus.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.you_lost)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
}.show()
}
}, null, DateUtils.SECOND_IN_MILLIS)
}
private fun changeDifficulty(newDifficulty: DifficultyPreset) {
if (gameStatus == GameStatus.PreGame) {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
}
} else {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
}
}
}
}
private fun onGameEvent(event: GameEvent) {
when (event) {
GameEvent.ResumeGame -> {
invalidateOptionsMenu()
}
GameEvent.StartNewGame -> {
gameStatus = GameStatus.PreGame
invalidateOptionsMenu()
}
GameEvent.Resume, GameEvent.Running -> {
gameStatus = GameStatus.Running
viewModel.runClock()
invalidateOptionsMenu()
}
GameEvent.Victory -> {
gameStatus = GameStatus.Over
viewModel.stopClock()
viewModel.revealAllEmptyAreas()
viewModel.victory()
invalidateOptionsMenu()
showVictory()
}
GameEvent.GameOver -> {
gameStatus = GameStatus.Over
invalidateOptionsMenu()
viewModel.stopClock()
viewModel.gameOver()
waitAndShowGameOverConfirmNewGame()
}
GameEvent.ResumeVictory, GameEvent.ResumeGameOver -> {
gameStatus = GameStatus.Over
invalidateOptionsMenu()
viewModel.stopClock()
waitAndShowConfirmNewGame()
}
else -> {
}
}
}
/**
* Call Google API to request update.
*/
private fun checkUpdate() {
val appUpdateManager = AppUpdateManagerFactory.create(this)
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
try {
appUpdateManager.startUpdateFlowForResult(
info, AppUpdateType.FLEXIBLE, this, 1)
} catch (e: IntentSender.SendIntentException) {
Log.e(TAG, "Fail to request update.")
}
}
}
}
/**
* If user change any accessibility preference, the game will restart the activity to
* apply these changes.
*/
private fun restartIfNeed() {
if (usingLargeArea != preferencesRepository.useLargeAreas()) {
finish()
Intent(this, GameActivity::class.java).run { startActivity(this) }
}
}
private fun openRateUsLink(from: String) {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")))
} catch (e: ActivityNotFoundException) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName")))
}
analyticsManager.sentEvent(Event.TapRatingRequest(from))
preferencesRepository.putBoolean(PREFERENCE_REQUEST_RATING, false)
}
companion object {
const val TAG = "GameActivity"
const val PREFERENCE_FIRST_USE = "preference_first_use"
const val PREFERENCE_USE_COUNT = "preference_use_count"
const val PREFERENCE_REQUEST_RATING = "preference_request_rating"
}
}

View file

@ -0,0 +1,27 @@
package dev.lucasnlm.antimine
import dagger.android.AndroidInjector
import dagger.android.support.DaggerApplication
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.analytics.Event
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.DaggerAppComponent
import javax.inject.Inject
class MainApplication : DaggerApplication() {
@Inject
lateinit var analyticsManager: AnalyticsManager
override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
DaggerAppComponent.builder()
.application(this)
.appModule(AppModule(this))
.build()
override fun onCreate() {
super.onCreate()
analyticsManager.setup(applicationContext, mapOf())
analyticsManager.sentEvent(Event.Open())
}
}

View file

@ -0,0 +1,318 @@
package dev.lucasnlm.antimine
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.text.format.DateUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.os.HandlerCompat
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import dagger.android.support.DaggerAppCompatActivity
import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.data.GameEvent
import dev.lucasnlm.antimine.common.level.data.GameStatus
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.level.view.CustomLevelDialogFragment
import dev.lucasnlm.antimine.level.view.LevelFragment
import dev.lucasnlm.antimine.preferences.PreferencesActivity
import kotlinx.android.synthetic.main.activity_tv_game.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class TvGameActivity : DaggerAppCompatActivity() {
@Inject
lateinit var viewModelFactory: GameViewModelFactory
@Inject
lateinit var preferencesRepository: IPreferencesRepository
private lateinit var viewModel: GameViewModel
private var gameStatus: GameStatus = GameStatus.PreGame
private var keepConfirmingNewGame = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tv_game)
viewModel = ViewModelProviders.of(this, viewModelFactory).get(GameViewModel::class.java)
bindViewModel()
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
loadGameFragment()
if (Build.VERSION.SDK_INT >= 21) {
checkUpdate()
}
}
private fun bindViewModel() = viewModel.apply {
eventObserver.observe(this@TvGameActivity, Observer {
onGameEvent(it)
})
elapsedTimeSeconds.observe(this@TvGameActivity, Observer {
timer.apply {
visibility = if (it == 0L) View.GONE else View.VISIBLE
text = DateUtils.formatElapsedTime(it)
}
})
mineCount.observe(this@TvGameActivity, Observer {
minesCount.apply {
visibility = View.VISIBLE
text = it.toString()
}
})
difficulty.observe(this@TvGameActivity, Observer {
//onChangeDifficulty(it)
})
}
override fun onResume() {
super.onResume()
if (gameStatus == GameStatus.Running) {
viewModel.resumeGame()
}
}
override fun onPause() {
super.onPause()
if (gameStatus == GameStatus.Running) {
viewModel.pauseGame()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean =
when (gameStatus) {
GameStatus.Over, GameStatus.Running -> {
menuInflater.inflate(R.menu.top_menu_over, menu)
true
}
else -> true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.reset) {
if (gameStatus == GameStatus.Running) {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame()
}
}
} else {
GlobalScope.launch {
viewModel.startNewGame()
}
}
true
} else {
super.onOptionsItemSelected(item)
}
}
private fun loadGameFragment() {
val fragmentManager = supportFragmentManager
fragmentManager.popBackStack()
fragmentManager.findFragmentById(R.id.levelContainer)?.let { it ->
fragmentManager.beginTransaction().apply {
remove(it)
commitAllowingStateLoss()
}
}
fragmentManager.beginTransaction().apply {
replace(R.id.levelContainer, LevelFragment())
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss()
}
}
private fun newGameConfirmation(action: () -> Unit) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.start_over)
setMessage(R.string.retry_sure)
setPositiveButton(R.string.resume) { _, _ -> action() }
setNegativeButton(R.string.cancel, null)
show()
}
}
private fun showQuitConfirmation(action: () -> Unit) {
AlertDialog.Builder(this, R.style.MyDialog)
.setTitle(R.string.are_you_sure)
.setMessage(R.string.sure_quit_desc)
.setPositiveButton(R.string.quit) { _, _ -> action() }
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun showCustomLevelDialog() {
CustomLevelDialogFragment().apply {
show(supportFragmentManager, "custom_level_fragment")
}
}
private fun showAbout() {
Intent(this, AboutActivity::class.java).apply {
startActivity(this)
}
}
private fun showSettings() {
Intent(this, PreferencesActivity::class.java).apply {
startActivity(this)
}
}
private fun showVictory() {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.you_won)
setMessage(R.string.all_mines_disabled)
setCancelable(false)
setPositiveButton(R.string.new_game) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
show()
}
}
private fun waitAndShowConfirmNewGame() {
if (keepConfirmingNewGame) {
HandlerCompat.postDelayed(Handler(), {
if (this.gameStatus == GameStatus.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.new_game)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
}.show()
keepConfirmingNewGame = false
}
}, null, DateUtils.SECOND_IN_MILLIS)
}
}
private fun waitAndShowGameOverConfirmNewGame() {
HandlerCompat.postDelayed(Handler(), {
if (this.gameStatus == GameStatus.Over && !isFinishing) {
AlertDialog.Builder(this, R.style.MyDialog).apply {
setTitle(R.string.you_lost)
setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch {
viewModel.startNewGame()
}
}
setNegativeButton(R.string.cancel, null)
}.show()
}
}, null, DateUtils.SECOND_IN_MILLIS)
}
private fun changeDifficulty(newDifficulty: DifficultyPreset) {
if (gameStatus == GameStatus.PreGame) {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
}
} else {
newGameConfirmation {
GlobalScope.launch {
viewModel.startNewGame(newDifficulty)
}
}
}
}
private fun onGameEvent(event: GameEvent) {
when (event) {
GameEvent.ResumeGame -> {
invalidateOptionsMenu()
}
GameEvent.StartNewGame -> {
gameStatus = GameStatus.PreGame
invalidateOptionsMenu()
}
GameEvent.Resume, GameEvent.Running -> {
gameStatus = GameStatus.Running
viewModel.runClock()
invalidateOptionsMenu()
}
GameEvent.Victory -> {
gameStatus = GameStatus.Over
viewModel.stopClock()
viewModel.revealAllEmptyAreas()
invalidateOptionsMenu()
showVictory()
}
GameEvent.GameOver -> {
gameStatus = GameStatus.Over
invalidateOptionsMenu()
viewModel.stopClock()
viewModel.gameOver()
waitAndShowGameOverConfirmNewGame()
}
GameEvent.ResumeVictory, GameEvent.ResumeGameOver -> {
gameStatus = GameStatus.Over
invalidateOptionsMenu()
viewModel.stopClock()
waitAndShowConfirmNewGame()
}
else -> {
}
}
}
private fun checkUpdate() {
val appUpdateManager = AppUpdateManagerFactory.create(this)
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
try {
appUpdateManager.startUpdateFlowForResult(
info, AppUpdateType.FLEXIBLE, this, 1)
} catch (e: IntentSender.SendIntentException) {
Log.e(TAG, "Fail to request update.")
}
}
}
}
companion object {
const val TAG = "GameActivity"
const val PREFERENCE_FIRST_USE = "preference_first_use"
}
}

View file

@ -0,0 +1,47 @@
package dev.lucasnlm.antimine.about
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import dev.lucasnlm.antimine.BuildConfig
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.thirds.ThirdPartiesActivity
import kotlinx.android.synthetic.main.activity_about.*
class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
bindToolbar()
version.text = getString(
R.string.version_s,
getString(R.string.app_name), BuildConfig.VERSION_NAME
)
thirdsParties.setOnClickListener { openThirdParties() }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun bindToolbar() {
supportActionBar?.let { actionBar ->
actionBar.setTitle(R.string.about)
actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setHomeButtonEnabled(true)
}
}
private fun openThirdParties() {
startActivity(Intent(this, ThirdPartiesActivity::class.java))
}
}

View file

@ -0,0 +1,6 @@
package dev.lucasnlm.antimine.about
object Constants {
const val TEXT_TITLE = "third_title"
const val TEXT_PATH = "third_path"
}

View file

@ -0,0 +1,97 @@
package dev.lucasnlm.antimine.about
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import android.view.MenuItem
import android.view.View
import dev.lucasnlm.antimine.R
import kotlinx.android.synthetic.main.activity_text.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
class TextActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_text)
bindToolbar()
progressBar.isIndeterminate = true
GlobalScope.launch {
withContext(Dispatchers.Main) {
progressBar.visibility = View.VISIBLE
}
withContext(Dispatchers.IO) {
val rawPath = intent.getIntExtra(Constants.TEXT_PATH, -1)
var result: String? = null
if (rawPath > 0) {
resources.openRawResource(rawPath).use { inputStream ->
result = readTextFile(inputStream)
}
}
withContext(Dispatchers.Main) {
textView.text = result
progressBar.visibility = View.GONE
}
}
}
}
private fun readTextFile(inputStream: InputStream): String {
var result = ""
ByteArrayOutputStream().use { outputStream ->
val buf = ByteArray(4096)
var len: Int
try {
while (true) {
len = inputStream.read(buf)
if (len != -1) {
outputStream.write(buf, 0, len)
} else {
break
}
}
} catch (e: IOException) {
Log.e(TAG, "Fail to read file.", e)
}
result = outputStream.toString()
}
return result
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
var handled = false
if (item.itemId == android.R.id.home) {
onBackPressed()
handled = true
}
return handled || super.onOptionsItemSelected(item)
}
private fun bindToolbar() {
supportActionBar?.apply {
title = intent.getStringExtra(Constants.TEXT_TITLE)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true)
}
}
companion object {
private const val TAG = "TextActivity"
}
}

View file

@ -0,0 +1,78 @@
package dev.lucasnlm.antimine.about.thirds
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.thirds.data.ThirdParty
import dev.lucasnlm.antimine.about.thirds.view.ThirdPartyAdapter
import kotlinx.android.synthetic.main.activity_third_party.*
class ThirdPartiesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third_party)
bindToolbar()
licenses.apply {
setHasFixedSize(true)
addItemDecoration(
DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
)
layoutManager = LinearLayoutManager(context)
}
loadLicenses()
}
private fun loadLicenses() {
licenses.adapter = ThirdPartyAdapter(
listOf(
ThirdParty(
"Android SDK License",
R.raw.android_sdk
),
ThirdParty(
"Material Design Icons",
R.raw.apache2
),
ThirdParty(
"Dagger",
R.raw.apache2
),
ThirdParty(
"Moshi",
R.raw.apache2
),
ThirdParty(
"Mockito",
R.raw.mockito
),
ThirdParty(
"Sounds",
R.raw.sounds
)
)
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun bindToolbar() {
supportActionBar?.apply {
setTitle(R.string.licenses)
setDisplayHomeAsUpEnabled(true)
setHomeButtonEnabled(true)
}
}
}

View file

@ -0,0 +1,8 @@
package dev.lucasnlm.antimine.about.thirds.data
import androidx.annotation.RawRes
internal data class ThirdParty(
val name: String,
@RawRes val license: Int
)

View file

@ -0,0 +1,37 @@
package dev.lucasnlm.antimine.about.thirds.view
import android.content.Intent
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.Constants
import dev.lucasnlm.antimine.about.TextActivity
import dev.lucasnlm.antimine.about.thirds.data.ThirdParty
internal class ThirdPartyAdapter(
private val thirdParties: List<ThirdParty>
) : RecyclerView.Adapter<ThirdPartyItemHolder>() {
override fun getItemCount(): Int = thirdParties.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThirdPartyItemHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.view_third_party, parent, false)
return ThirdPartyItemHolder(view)
}
override fun onBindViewHolder(holder: ThirdPartyItemHolder, position: Int) {
val thirdParty = thirdParties[position]
holder.title.text = thirdParty.name
holder.itemView.setOnClickListener { view ->
val intent = Intent(view.context, TextActivity::class.java).apply {
putExtra(Constants.TEXT_TITLE, thirdParty.name)
putExtra(Constants.TEXT_PATH, thirdParty.license)
}
view.context.startActivity(intent)
}
}
}

View file

@ -0,0 +1,11 @@
package dev.lucasnlm.antimine.about.thirds.view
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import android.widget.TextView
import dev.lucasnlm.antimine.R
internal class ThirdPartyItemHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.third_name)
}

View file

@ -0,0 +1,18 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.GameActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dev.lucasnlm.antimine.TvGameActivity
import dev.lucasnlm.antimine.core.scope.ActivityScope
@Module
interface ActivityModule {
@ActivityScope
@ContributesAndroidInjector
fun contributeGameActivityInjector(): GameActivity
@ActivityScope
@ContributesAndroidInjector
fun contributeTvGameActivityInjector(): TvGameActivity
}

View file

@ -0,0 +1,35 @@
package dev.lucasnlm.antimine.di
import android.app.Application
import dagger.BindsInstance
import dagger.Component
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import dev.lucasnlm.antimine.MainApplication
import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.core.di.CommonModule
import javax.inject.Singleton
@Component(
modules = [
AndroidSupportInjectionModule::class,
AppModule::class,
LevelModule::class,
ActivityModule::class,
FragmentModule::class,
CommonModule::class
]
)
@Singleton
interface AppComponent : AndroidInjector<MainApplication> {
@Component.Builder
interface Builder {
@BindsInstance
fun application(application: Application): Builder
fun appModule(module: AppModule): Builder
fun build(): AppComponent
}
}

View file

@ -0,0 +1,14 @@
package dev.lucasnlm.antimine.di
import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
@Module
class AppModule(
private val application: Application
) {
@Provides
fun provideContext(): Context = application.applicationContext
}

View file

@ -0,0 +1,18 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.level.view.CustomLevelDialogFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dev.lucasnlm.antimine.core.scope.ActivityScope
import dev.lucasnlm.antimine.level.view.LevelFragment
@Module
interface FragmentModule {
@ActivityScope
@ContributesAndroidInjector
fun contributeLevelFragmentInjector(): LevelFragment
@ActivityScope
@ContributesAndroidInjector
fun contributeCustomLevelDialogFragmentInjector(): CustomLevelDialogFragment
}

View file

@ -0,0 +1,88 @@
package dev.lucasnlm.antimine.level.view
import android.app.Dialog
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import android.widget.TextView
import androidx.lifecycle.ViewModelProviders
import dagger.android.support.DaggerAppCompatDialogFragment
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.data.LevelSetup
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class CustomLevelDialogFragment : DaggerAppCompatDialogFragment() {
@Inject
lateinit var viewModelFactory: GameViewModelFactory
@Inject
lateinit var preferencesRepository: IPreferencesRepository
private lateinit var viewModel: GameViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let {
viewModel = ViewModelProviders.of(it, viewModelFactory).get(GameViewModel::class.java)
}
}
private fun filterInput(target: String, min: Int): Int {
var result = min
try {
result = Integer.valueOf(target)
} catch (e: NumberFormatException) {
result = min
} finally {
result = result.coerceAtLeast(min)
}
return result
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(context!!, R.style.MyDialog).apply {
setTitle(R.string.new_game)
setView(R.layout.dialog_custom_game)
setNegativeButton(R.string.cancel, null)
setPositiveButton(R.string.start) { _, _ ->
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)
var width = filterInput(mapWidth?.text.toString(), MIN_WIDTH)
var height = filterInput(mapHeight?.text.toString(), MIN_HEIGHT)
var mines = filterInput(mapMines?.text.toString(), MIN_MINES)
if (width * height - 1 < mines) {
mines = width * height - 1
}
width = width.coerceAtMost(50)
height = height.coerceAtMost(50)
mines = mines.coerceAtLeast(1)
preferencesRepository.updateCustomGameMode(LevelSetup(width, height, mines))
GlobalScope.launch(Dispatchers.IO) {
viewModel.startNewGame(DifficultyPreset.Custom)
}
}
}.create()
}
companion object {
const val MIN_WIDTH = 5
const val MIN_HEIGHT = 5
const val MIN_MINES = 3
}
}

View file

@ -0,0 +1,126 @@
package dev.lucasnlm.antimine.level.view
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.common.level.view.UnlockedHorizontalScrollView
import dagger.android.support.DaggerFragment
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.view.AreaAdapter
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
open class LevelFragment : DaggerFragment() {
@Inject
lateinit var viewModelFactory: GameViewModelFactory
private lateinit var viewModel: GameViewModel
private lateinit var recyclerGrid: RecyclerView
private lateinit var bidirectionalScroll: UnlockedHorizontalScrollView
private lateinit var areaAdapter: AreaAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? =
inflater.inflate(R.layout.fragment_level, container, false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.let {
viewModel = ViewModelProviders.of(it, viewModelFactory).get(GameViewModel::class.java)
areaAdapter = AreaAdapter(it.applicationContext, viewModel)
}
}
override fun onPause() {
super.onPause()
GlobalScope.launch {
viewModel.saveGame()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerGrid = view.findViewById(R.id.recyclerGrid)
recyclerGrid.apply {
setHasFixedSize(true)
isNestedScrollingEnabled = false
adapter = areaAdapter
alpha = 0.0f
}
bidirectionalScroll = view.findViewById(R.id.bidirectionalScroll)
bidirectionalScroll.setTarget(recyclerGrid)
GlobalScope.launch {
val levelSetup = viewModel.onCreate(handleNewGameDeeplink())
val width = levelSetup.width
withContext(Dispatchers.Main) {
recyclerGrid.layoutManager =
GridLayoutManager(activity, width, RecyclerView.VERTICAL, false)
view.post {
recyclerGrid.scrollBy(0, recyclerGrid.height / 2)
bidirectionalScroll.scrollBy(recyclerGrid.width / 4, 0)
recyclerGrid.animate().apply {
alpha(1.0f)
duration = 1000
}.start()
}
}
}
viewModel.run {
field.observe(viewLifecycleOwner, Observer {
areaAdapter.bindField(it)
})
levelSetup.observe(viewLifecycleOwner, Observer {
recyclerGrid.layoutManager =
GridLayoutManager(activity, it.width, RecyclerView.VERTICAL, false)
})
fieldRefresh.observe(viewLifecycleOwner, Observer {
areaAdapter.notifyItemChanged(it)
})
}
}
private fun handleNewGameDeeplink(): DifficultyPreset? {
var result: DifficultyPreset? = null
activity?.intent?.data?.let { uri ->
if (uri.scheme == DEFAULT_SCHEME) {
result = when (uri.schemeSpecificPart.removePrefix("//new-game/")) {
"beginner" -> DifficultyPreset.Beginner
"intermediate" -> DifficultyPreset.Intermediate
"expert" -> DifficultyPreset.Expert
"standard" -> DifficultyPreset.Standard
else -> null
}
}
}
return result
}
companion object {
const val DEFAULT_SCHEME = "antimine"
}
}

View file

@ -0,0 +1,29 @@
package dev.lucasnlm.antimine.preferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import dev.lucasnlm.antimine.R
class PreferencesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
// Load the preferences from an XML resource
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, PrefsFragment())
.commitAllowingStateLoss()
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences)
}
}
}

View file

@ -0,0 +1,24 @@
package dev.lucasnlm.antimine.splash
import android.app.UiModeManager
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dev.lucasnlm.antimine.GameActivity
import dev.lucasnlm.antimine.TvGameActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uiModeManager: UiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
Intent(this, TvGameActivity::class.java).run { startActivity(this) }
} else {
Intent(this, GameActivity::class.java).run { startActivity(this) }
}
finish()
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.9148,18.653l4.1235,-0.001l1.9616,-3.3549l-2.0939,-3.2966l2.0939,-3.4216l-1.9656,-3.2037l-4.1195,0l-1.7445,-3.3752l-4.3377,0l-1.7495,3.2785l-4.1474,0l-1.9358,3.3003l1.9358,3.3975l-1.9358,3.3207l1.9346,3.4024l4.1486,-0l2.0287,3.3005l3.8818,-0z"
android:strokeWidth="0.02435089"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,59 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="dev.lucasnlm.antimine.about.AboutActivity">
<ImageView
android:id="@+id/logo"
android:layout_width="115dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/app_name"
android:src="@drawable/title"
app:layout_constraintBottom_toTopOf="@+id/version"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="16dp"
android:text="@string/version_s"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/thirdsParties"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/logo" />
<TextView
android:id="@+id/thirdsParties"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_horizontal"
android:padding="16dp"
android:text="@string/show_licenses"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/version"
tools:targetApi="m" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ads="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
android:minHeight="?attr/actionBarSize"
android:theme="@style/AppTheme.AppBarOverlay"
tools:targetApi="lollipop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/toolbar"
android:layout_alignParentTop="true"
android:layout_marginTop="0dp"
android:animateLayoutChanges="true"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/timer"
android:layout_width="wrap_content"
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"
android:visibility="gone"
android:minEms="2"
android:drawableTint="?android:attr/textColorPrimary"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:text="@string/default_time_value"
tools:visibility="visible"
tools:text="10:00" />
<TextView
android:id="@+id/minesCount"
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"
android:minEms="3"
android:drawableTint="?android:attr/textColorPrimary"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible"
tools:text="99" />
</LinearLayout>
<FrameLayout
android:id="@+id/levelContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar" />
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clipToPadding="true"
android:fitsSystemWindows="true"
ads:menu="@xml/nav_menu"
app:itemTextColor="?android:attr/textColorPrimary" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,39 @@
<?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="match_parent">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/textView"
android:textColor="?android:attr/textColorPrimary"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</ScrollView>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,31 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
tools:context="dev.lucasnlm.antimine.about.thirds.ThirdPartiesActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="?android:attr/textColorPrimary"
android:text="@string/used_software_text"
android:id="@+id/summary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/licenses"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:overScrollMode="never"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/summary" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
android:minHeight="30dp"
android:theme="@style/AppTheme.AppBarOverlay"
tools:targetApi="lollipop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/toolbar"
android:layout_alignParentTop="true"
android:layout_marginTop="0dp"
android:animateLayoutChanges="true"
android:gravity="center_horizontal|bottom"
android:orientation="horizontal">
<TextView
android:id="@+id/timer"
android:layout_width="wrap_content"
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"
android:minEms="2"
android:text="@string/default_time_value"
android:textColor="@color/primary"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:visibility="gone"
tools:text="10:00"
tools:visibility="visible" />
<TextView
android:id="@+id/minesCount"
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"
android:minEms="3"
android:textColor="@color/primary"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:visibility="gone"
tools:text="99"
tools:visibility="visible" />
</LinearLayout>
<FrameLayout
android:id="@+id/levelContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar" />
</RelativeLayout>

View file

@ -0,0 +1,102 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:id="@+id/areaLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:gravity="center_horizontal"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/minesLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/map_width"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="@string/width"
android:importantForAutofill="no"
android:inputType="numberDecimal"
android:minEms="3"
android:text="9"
tools:ignore="HardcodedText" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:gravity="center"
android:includeFontPadding="false"
android:text="x"
tools:ignore="HardcodedText" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/map_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="@string/height"
android:importantForAutofill="no"
android:inputType="numberDecimal"
android:minEms="3"
android:text="9"
tools:ignore="HardcodedText" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/minesLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:gravity="center_horizontal"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/areaLayout">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/map_mines"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="@string/mines"
android:importantForAutofill="no"
android:inputType="numberDecimal"
android:minEms="3"
android:text="9"
tools:ignore="HardcodedText" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,21 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/third_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textStyle="bold"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Third Name" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
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"
tools:context=".level.LevelActivity">
<item
android:id="@+id/reset"
android:icon="@drawable/replay"
android:title="@string/start_over"
app:showAsAction="ifRoom" />
</menu>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="default_time_value" translatable="false">00:00</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="."/>
<include domain="database" path="."/>
</full-backup-content>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutId="play_beginner"
android:shortcutShortLabel="@string/beginner"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:data="antimine://new-game/beginner"/>
<categories android:name="android.shortcut.game"/>
</shortcut>
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutId="play_intermediate"
android:shortcutShortLabel="@string/intermediate"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:data="antimine://new-game/intermediate"/>
<categories android:name="android.shortcut.game"/>
</shortcut>
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutId="play_expert"
android:shortcutShortLabel="@string/expert"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:data="antimine://new-game/expert"/>
<categories android:name="android.shortcut.game"/>
</shortcut>
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:shortcutId="play_standard"
android:shortcutShortLabel="@string/standard"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:data="antimine://new-game/standard"/>
<categories android:name="android.shortcut.game"/>
</shortcut>
</shortcuts>

40
build.gradle Normal file
View file

@ -0,0 +1,40 @@
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
mavenCentral()
jcenter()
google()
maven {
url 'https://maven.fabric.io/public'
name 'Fabric'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
// noinspection GradleDynamicVersion
classpath 'io.fabric.tools:gradle:1.+'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }
}
allprojects {
repositories {
google()
jcenter()
maven {
url 'https://jitpack.io'
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

1
common/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

68
common/build.gradle Normal file
View file

@ -0,0 +1,68 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 14
targetSdkVersion 29
versionCode 19
versionName '3.0'
}
kapt {
generateStubs true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// AndroidX
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// Lifecycle
api 'android.arch.lifecycle:extensions:1.1.1'
implementation "android.arch.lifecycle:viewmodel:1.1.1"
// Amplitude
implementation 'com.amplitude:android-sdk:2.23.2'
// Dagger
def dagger_version='2.21'
api "com.google.dagger:dagger-android:$dagger_version"
api "com.google.dagger:dagger-android-support:$dagger_version"
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
// Room
def room_version = '2.2.3'
api "androidx.room:room-runtime:$room_version"
api "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
// Moshi - Json
def moshi_version = '1.9.1'
api "com.squareup.moshi:moshi:$moshi_version"
api "com.squareup.moshi:moshi-kotlin:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
// Coroutines
def coroutines_version = '1.3.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.24.0'
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0'
}

BIN
common/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Mon May 21 23:30:42 BRT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

172
common/gradlew vendored Normal file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
common/gradlew.bat vendored Normal file
View file

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

47
common/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,47 @@
# Project specific ProGuard rules
# Crashlytics 2.+
-keep class com.crashlytics.** { *; }
-keep class com.crashlytics.android.**
-keepattributes SourceFile, LineNumberTable, *Annotation*
# For Fabric to properly de-obfuscate your crash reports, you need to remove this line from your ProGuard config:
-printmapping mapping.txt
# support design
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }
# support v7
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
# support v4
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.** { *; }
# build config
-keep class com.example.BuildConfig { *; }
# firebase
-keep class com.firebase.** { *; }
# google gms
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
# amplitude
-keep class com.google.android.gms.ads.** { *; }
-dontwarn okio.**
# recycler view
-keep public class * extends android.support.v7.widget.RecyclerView$LayoutManager {
public <init>(...);
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.lucasnlm.antimine.common">
<uses-permission android:name="android.permission.VIBRATE" />
<application android:supportsRtl="true" />
</manifest>

View file

@ -0,0 +1,41 @@
package dev.lucasnlm.antimine.common.level
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.data.LevelSetup
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
object GameModeFactory {
fun fromDifficultyPreset(
difficulty: DifficultyPreset,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository
): LevelSetup =
when (difficulty) {
DifficultyPreset.Standard -> calculateStandardMode(dimensionRepository)
DifficultyPreset.Beginner -> LevelSetup(9, 9, 10, difficulty)
DifficultyPreset.Intermediate -> LevelSetup(16, 16, 40, difficulty)
DifficultyPreset.Expert -> LevelSetup(24, 24, 99, difficulty)
DifficultyPreset.Custom -> preferencesRepository.customGameMode()
}
private fun calculateStandardMode(
dimensionRepository: IDimensionRepository
): LevelSetup {
val fieldSize = dimensionRepository.areaSize()
val display = dimensionRepository.displaySize()
val width = display.widthPixels
val height = display.heightPixels
val finalWidth = ((width / fieldSize).toInt() - 1).coerceAtLeast(6)
val finalHeight = ((height / fieldSize).toInt() - 3).coerceAtLeast(9)
return LevelSetup(
finalWidth,
finalHeight,
(finalWidth * finalHeight * 0.2).toInt(),
DifficultyPreset.Standard
)
}
}

View file

@ -0,0 +1,264 @@
package dev.lucasnlm.antimine.common.level
import dev.lucasnlm.antimine.common.level.data.LevelSetup
import dev.lucasnlm.antimine.common.level.data.Area
import dev.lucasnlm.antimine.common.level.data.GameStats
import dev.lucasnlm.antimine.common.level.data.Mark
import dev.lucasnlm.antimine.common.level.database.data.Save
import dev.lucasnlm.antimine.common.level.database.data.SaveStatus
import java.util.*
import kotlin.math.floor
class LevelFacade {
private val levelSetup: LevelSetup
private val randomGenerator: Random
private var saveId = 0
private val startTime = System.currentTimeMillis()
var hasMines = false
private set
var seed = 0L
private set
lateinit var field: Sequence<Area>
private set
private var mines: Sequence<Area> = sequenceOf()
constructor(gameId: Int, levelSetup: LevelSetup, seed: Long = randomSeed()) {
this.saveId = gameId
this.levelSetup = levelSetup
this.randomGenerator = Random().apply { setSeed(seed) }
this.seed = seed
createEmptyField()
}
constructor(save: Save) {
this.saveId = save.uid
this.levelSetup = save.levelSetup
this.randomGenerator = Random().apply { setSeed(save.seed) }
this.field = save.field.asSequence()
this.hasMines = this.field.firstOrNull { it.hasMine }?.hasMine ?: false
this.mines = this.field.filter { it.hasMine }.asSequence()
}
private fun createEmptyField() {
val width = levelSetup.width
val height = levelSetup.height
val fieldSize = width * height
this.field = (0 until fieldSize).map { index ->
val yPosition = floor((index / width).toDouble()).toInt()
val xPosition = (index % width)
Area(index, xPosition, yPosition)
}.asSequence()
}
private fun getArea(id: Int) = field.first { it.id == id }
fun switchMarkAt(index: Int): Boolean {
val changed: Boolean
getArea(index).apply {
changed = isCovered
if (isCovered) {
mark = when (mark) {
Mark.None -> Mark.Flag
Mark.Flag -> Mark.Question
Mark.Question -> Mark.None
}
}
}
return changed
}
fun removeMark(index: Int) {
getArea(index).apply {
mark = Mark.None
}
}
fun hasCoverOn(index: Int): Boolean = getArea(index).isCovered
fun hasMarkOn(index: Int): Boolean = getArea(index).mark != Mark.None
fun hasMineOn(index: Int): Boolean = getArea(index).hasMine
fun plantMinesExcept(index: Int, includeSafeArea: Boolean = false) {
plantRandomMines(index, includeSafeArea)
putMinesTips()
hasMines = true
}
private fun plantRandomMines(ignoreIndex: Int, includeSafeArea: Boolean) {
getArea(ignoreIndex).run {
safeZone = true
if (includeSafeArea) {
findNeighbors().forEach {
it.safeZone = true
}
}
}
field.filterNot { it.safeZone }
.toSet()
.shuffled(randomGenerator)
.take(levelSetup.mines)
.forEach { it.hasMine = true }
mines = field.filter { it.hasMine }.asSequence()
}
private fun putMinesTips() {
field.forEach {
it.minesAround = if (it.hasMine) 0 else it.findNeighbors().filter { neighbor ->
neighbor.hasMine
}.count()
}
}
/**
* Run "Flood Fill algorithm" to open all empty neighbors of a target area.
*/
fun openField(target: Area): Boolean {
val result: Boolean = target.isCovered
if (target.isCovered) {
target.isCovered = false
target.mark = Mark.None
if (target.minesAround == 0 && !target.hasMine) {
target.findNeighbors().forEach { openField(it) }
}
if (target.hasMine) {
target.mistake = true
}
}
return result
}
fun turnOffAllHighlighted() {
field.forEach {
it.highlighted = false
}
}
private fun toggleHighlight(target: Area) {
target.highlighted = !target.highlighted
target.findNeighbors()
.filter { it.mark == Mark.None && it.isCovered }
.forEach {
it.highlighted = !it.highlighted
}
}
fun clickArea(index: Int): Boolean = getArea(index).let {
when {
it.isCovered -> {
openField(getArea(index))
}
it.minesAround != 0 -> {
toggleHighlight(it)
true
}
else -> {
false
}
}
}
fun openNeighbors(index: Int): List<Int> =
getArea(index)
.findNeighbors()
.filter {
it.mark == Mark.None
}
.map{
openField(it)
it.id
}
fun runFlagAssistant() {
mines.filter { it.mark == Mark.None }.forEach { field ->
val neighbors = field.findNeighbors()
val neighborsCount = neighbors.count()
val revealedNeighborsCount = neighbors.filter { neighbor ->
!neighbor.isCovered || (neighbor.hasMine && neighbor.mark == Mark.Flag)
}.count()
field.mark = if (revealedNeighborsCount == neighborsCount) Mark.Flag else Mark.None
}
}
fun getStats() = GameStats(
mines.filter { !it.mistake && it.mark == Mark.Flag }.count(),
mines.count(),
field.count()
)
fun showAllMines() {
mines.filter { it.mark != Mark.Flag }.forEach { it.isCovered = false }
}
fun showWrongFlags() {
field.filter { it.mark != Mark.None && !it.hasMine }.forEach { it.mistake = true }
}
fun revealAllEmptyAreas() {
field.filter { !it.hasMine }.forEach { it.isCovered = false }
}
fun hasAnyMineExploded(): Boolean = mines.firstOrNull { it.mistake } != null
fun hasFlaggedAllMines(): Boolean = rightFlags() == levelSetup.mines
fun hasIsolatedAllMines() =
mines.map {
val neighbors = it.findNeighbors()
val neighborsCount = neighbors.count()
val isolatedNeighborsCount = neighbors.filter { neighbor ->
!neighbor.isCovered || neighbor.hasMine
}.count()
neighborsCount == isolatedNeighborsCount
}.filterNot { it }.count() == 0
private fun rightFlags() = mines.count { it.mark == Mark.Flag }
fun checkVictory(): Boolean =
hasFlaggedAllMines() && hasIsolatedAllMines() && !hasAnyMineExploded()
fun remainingMines(): Int {
val flagsCount = field.count { it.mark == Mark.Flag }
val minesCount = mines.count()
return (minesCount - flagsCount).coerceAtLeast(0)
}
private fun Area.findNeighbors() = arrayOf(
getNeighbor(1, 0),
getNeighbor(1, 1),
getNeighbor(0, 1),
getNeighbor(-1, 1),
getNeighbor(-1, 0),
getNeighbor(-1, -1),
getNeighbor(0, -1),
getNeighbor(1, -1)
).filterNotNull()
private fun Area.getNeighbor(x: Int, y: Int) = field.firstOrNull {
(it.posX == this.posX + x) && (it.posY == this.posY + y)
}
fun getSaveState(): Save {
val saveStatus: SaveStatus = when {
checkVictory() -> SaveStatus.VICTORY
hasAnyMineExploded() -> SaveStatus.DEFEAT
else -> SaveStatus.ON_GOING
}
return Save(saveId, seed, startTime, 0L, levelSetup, saveStatus, field.toList())
}
companion object {
fun randomSeed(): Long = Random().nextLong()
}
}

View file

@ -0,0 +1,6 @@
package dev.lucasnlm.antimine.common.level.data
data class AmbientSettings(
val isAmbientMode: Boolean = false,
val isLowBitAmbient: Boolean = false
)

View file

@ -0,0 +1,14 @@
package dev.lucasnlm.antimine.common.level.data
data class Area(
val id: Int,
val posX: Int,
val posY: Int,
var minesAround: Int = 0,
var safeZone: Boolean = false,
var hasMine: Boolean = false,
var mistake: Boolean = false,
var isCovered: Boolean = true,
var mark: Mark = Mark.None,
var highlighted: Boolean = false
)

View file

@ -0,0 +1,9 @@
package dev.lucasnlm.antimine.common.level.data
enum class DifficultyPreset(val text: String) {
Standard("STANDARD"),
Beginner("BEGINNER"),
Intermediate("INTERMEDIATE"),
Expert("EXPERT"),
Custom("CUSTOM")
}

View file

@ -0,0 +1,13 @@
package dev.lucasnlm.antimine.common.level.data
enum class GameEvent {
StartNewGame,
ResumeGame,
ResumeGameOver,
ResumeVictory,
Pause,
Resume,
Running,
Victory,
GameOver
}

View file

@ -0,0 +1,7 @@
package dev.lucasnlm.antimine.common.level.data
data class GameStats(
val rightMines: Int,
val totalMines: Int,
val totalArea: Int
)

View file

@ -0,0 +1,7 @@
package dev.lucasnlm.antimine.common.level.data
enum class GameStatus {
PreGame,
Running,
Over
}

View file

@ -0,0 +1,8 @@
package dev.lucasnlm.antimine.common.level.data
data class LevelSetup(
val width: Int,
val height: Int,
val mines: Int,
val preset: DifficultyPreset = DifficultyPreset.Custom
)

View file

@ -0,0 +1,11 @@
package dev.lucasnlm.antimine.common.level.data
enum class Mark {
None,
Flag,
Question
}
fun Mark.isFlag(): Boolean = this == Mark.Flag
fun Mark.isQuestion(): Boolean = this == Mark.Question

View file

@ -0,0 +1,24 @@
package dev.lucasnlm.antimine.common.level.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import dev.lucasnlm.antimine.common.level.database.converters.FieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.LevelSetupConverter
import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter
import dev.lucasnlm.antimine.common.level.database.dao.SaveDao
import dev.lucasnlm.antimine.common.level.database.data.Save
@Database(
entities = [
Save::class
], version = 1, exportSchema = false
)
@TypeConverters(
FieldConverter::class,
SaveStatusConverter::class,
LevelSetupConverter::class
)
abstract class AppDataBase : RoomDatabase() {
abstract fun userDao(): SaveDao
}

View file

@ -0,0 +1,25 @@
package dev.lucasnlm.antimine.common.level.database.converters
import androidx.room.TypeConverter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.lucasnlm.antimine.common.level.data.Area
import java.lang.reflect.Type
class FieldConverter {
private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val jsonAdapter: JsonAdapter<List<Area>>
init {
val type: Type = Types.newParameterizedType(List::class.java, Area::class.java)
this.jsonAdapter = moshi.adapter(type)
}
@TypeConverter
fun toAreaList(jsonInput: String): List<Area> = jsonAdapter.fromJson(jsonInput) ?: listOf()
@TypeConverter
fun toJsonString(field: List<Area>): String = jsonAdapter.toJson(field)
}

View file

@ -0,0 +1,24 @@
package dev.lucasnlm.antimine.common.level.database.converters
import androidx.room.TypeConverter
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.data.LevelSetup
class LevelSetupConverter {
private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val jsonAdapter: JsonAdapter<LevelSetup>
init {
this.jsonAdapter = moshi.adapter(LevelSetup::class.java)
}
@TypeConverter
fun toLevelSetup(jsonInput: String): LevelSetup =
jsonAdapter.fromJson(jsonInput) ?: LevelSetup(9, 9, 10, DifficultyPreset.Beginner)
@TypeConverter
fun toJsonString(field: LevelSetup): String = jsonAdapter.toJson(field)
}

View file

@ -0,0 +1,19 @@
package dev.lucasnlm.antimine.common.level.database.converters
import androidx.room.TypeConverter
import dev.lucasnlm.antimine.common.level.database.data.SaveStatus
class SaveStatusConverter {
@TypeConverter
fun toSaveStatus(status: Int): SaveStatus =
when (status) {
0 -> SaveStatus.ON_GOING
1 -> SaveStatus.VICTORY
2 -> SaveStatus.DEFEAT
else -> throw IllegalArgumentException("Could not recognize SaveStatus")
}
@TypeConverter
fun toInteger(status: SaveStatus): Int = status.code
}

View file

@ -0,0 +1,28 @@
package dev.lucasnlm.antimine.common.level.database.dao
import androidx.room.*
import dev.lucasnlm.antimine.common.level.database.data.Save
@Dao
interface SaveDao {
@Query("SELECT * FROM save")
fun getAll(): List<Save>
@Query("SELECT * FROM save WHERE uid IN (:gameIds)")
fun loadAllByIds(gameIds: IntArray): List<Save>
@Query("SELECT * FROM save WHERE uid = :gameId LIMIT 1")
fun loadById(gameId: Int): Save
@Query("SELECT * FROM save ORDER BY uid DESC LIMIT 1")
fun loadCurrent(): Save?
@Query("SELECT count(uid) FROM save")
fun getSaveCounts(): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg saves: Save): Array<Long>
@Delete
fun delete(save: Save)
}

View file

@ -0,0 +1,36 @@
package dev.lucasnlm.antimine.common.level.database.data
import androidx.room.*
import dev.lucasnlm.antimine.common.level.data.Area
import dev.lucasnlm.antimine.common.level.data.LevelSetup
import dev.lucasnlm.antimine.common.level.database.converters.FieldConverter
import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter
@Entity
data class Save(
@PrimaryKey(autoGenerate = true)
val uid: Int,
@ColumnInfo(name = "seed")
val seed: Long,
@ColumnInfo(name = "date")
val startDate: Long,
@ColumnInfo(name = "duration")
val duration: Long,
@ColumnInfo(name = "width")
val levelSetup: LevelSetup,
@TypeConverters(SaveStatusConverter::class)
@ColumnInfo(name = "status")
val status: SaveStatus,
@TypeConverters(FieldConverter::class)
@ColumnInfo(name = "field")
val field: List<Area>
)

View file

@ -0,0 +1,11 @@
package dev.lucasnlm.antimine.common.level.database.data
import androidx.room.TypeConverters
import dev.lucasnlm.antimine.common.level.database.converters.SaveStatusConverter
@TypeConverters(SaveStatusConverter::class)
enum class SaveStatus constructor(val code: Int) {
ON_GOING(0),
VICTORY(1),
DEFEAT(2)
}

View file

@ -0,0 +1,18 @@
package dev.lucasnlm.antimine.common.level.di
import dagger.Component
import dagger.android.support.AndroidSupportInjectionModule
@Component(
modules = [
AndroidSupportInjectionModule::class,
LevelModule::class
]
)
abstract class LevelComponent {
@Component.Builder
interface Builder {
fun levelModule(levelModule: LevelModule): Builder
fun build(): LevelComponent
}
}

View file

@ -0,0 +1,73 @@
package dev.lucasnlm.antimine.common.level.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dev.lucasnlm.antimine.common.level.data.GameEvent
import dev.lucasnlm.antimine.common.level.database.AppDataBase
import dev.lucasnlm.antimine.common.level.database.dao.SaveDao
import dev.lucasnlm.antimine.common.level.repository.DimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.SavesRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.HapticFeedbackInteractor
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModelFactory
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
@Module
class LevelModule {
@Provides
fun provideGameEventObserver(): MutableLiveData<GameEvent> = MutableLiveData()
@Provides
fun provideClock(): Clock = Clock()
@Provides
fun provideGameViewModelFactory(
application: Application,
gameEventObserver: MutableLiveData<GameEvent>,
savesRepository: ISavesRepository,
dimensionRepository: IDimensionRepository,
preferencesRepository: IPreferencesRepository,
hapticFeedbackInteractor: IHapticFeedbackInteractor,
analyticsManager: AnalyticsManager,
clock: Clock
) = GameViewModelFactory(
application, gameEventObserver, savesRepository,
dimensionRepository, preferencesRepository, hapticFeedbackInteractor, analyticsManager, clock
)
@Provides
fun provideDimensionRepository(
context: Context,
preferencesRepository: IPreferencesRepository
): IDimensionRepository =
DimensionRepository(context, preferencesRepository)
@Provides
fun provideDataBase(application: Application): AppDataBase =
Room.databaseBuilder(application, AppDataBase::class.java, DATA_BASE_NAME).build()
@Provides
fun provideSaveDao(appDataBase: AppDataBase): SaveDao = appDataBase.userDao()
@Provides
fun provideSavesRepository(saveDao: SaveDao): ISavesRepository = SavesRepository(saveDao)
@Provides
fun provideHapticFeedbackInteractor(
application: Application,
preferencesRepository: IPreferencesRepository
): IHapticFeedbackInteractor =
HapticFeedbackInteractor(application, preferencesRepository)
companion object {
const val DATA_BASE_NAME = "saves-db"
}
}

View file

@ -0,0 +1,38 @@
package dev.lucasnlm.antimine.common.level.repository
import android.content.Context
import android.content.res.Resources
import android.content.res.TypedArray
import android.util.DisplayMetrics
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
interface IDimensionRepository {
fun areaSize(): Float
fun displaySize(): DisplayMetrics
fun actionBarSize(): Int
}
class DimensionRepository(
private val context: Context,
private val preferencesRepository: IPreferencesRepository
) : IDimensionRepository {
override fun areaSize(): Float = if (preferencesRepository.useLargeAreas()) {
context.resources.getDimension(R.dimen.accessible_field_size)
} else {
context.resources.getDimension(R.dimen.field_size)
}
override fun displaySize(): DisplayMetrics = Resources.getSystem().displayMetrics
override fun actionBarSize(): Int {
val styledAttributes: TypedArray =
context.theme.obtainStyledAttributes(
IntArray(1) { android.R.attr.actionBarSize }
)
val actionBarSize: Int = styledAttributes.getDimension(0, 0.0f).toInt()
styledAttributes.recycle()
return actionBarSize
}
}

View file

@ -0,0 +1,44 @@
package dev.lucasnlm.antimine.common.level.repository
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import dev.lucasnlm.antimine.common.R
class DrawableRepository {
private var flag: Drawable? = null
private var redFlag: Drawable? = null
private var mineExploded: Drawable? = null
private var mine: Drawable? = null
private var mineLow: Drawable? = null
private var question: Drawable? = null
fun provideFlagDrawable(context: Context) =
flag ?: ContextCompat.getDrawable(context, R.drawable.flag).also { flag = it }
fun provideRedFlagDrawable(context: Context) =
redFlag ?: ContextCompat.getDrawable(context, R.drawable.red_flag).also { redFlag = it }
fun provideQuestionDrawable(context: Context) =
question ?: ContextCompat.getDrawable(context, R.drawable.question).also { question = it }
fun provideMineExploded(context: Context) =
mineExploded ?: ContextCompat.getDrawable(
context,
R.drawable.mine_exploded
).also { mineExploded = it }
fun provideMine(context: Context) =
mine ?: ContextCompat.getDrawable(context, R.drawable.mine).also { mine = it }
fun provideMineLow(context: Context) =
mineLow ?: ContextCompat.getDrawable(context, R.drawable.mine_low).also { mineLow = it }
fun free() {
flag = null
mineExploded = null
mine = null
mineLow = null
question = null
}
}

View file

@ -0,0 +1,22 @@
package dev.lucasnlm.antimine.common.level.repository
import dev.lucasnlm.antimine.common.level.database.dao.SaveDao
import dev.lucasnlm.antimine.common.level.database.data.Save
import javax.inject.Inject
interface ISavesRepository {
suspend fun getNewSaveId(): Int
suspend fun fetchCurrentSave(): Save?
suspend fun saveGame(save: Save): Long?
}
class SavesRepository @Inject constructor(
private val savesDao: SaveDao
) : ISavesRepository {
override suspend fun getNewSaveId(): Int = savesDao.getSaveCounts() + 1
override suspend fun fetchCurrentSave(): Save? = savesDao.loadCurrent()
override suspend fun saveGame(save: Save) = savesDao.insertAll(save).firstOrNull()
}

View file

@ -0,0 +1,39 @@
package dev.lucasnlm.antimine.common.level.utils
import java.util.Timer
import java.util.TimerTask
class Clock {
private var elapsedTimeSeconds: Long = 0
private var timer: Timer? = null
val isStopped: Boolean
get() = (timer == null)
fun reset(initialValue: Long = 0L) {
stop()
this.elapsedTimeSeconds = initialValue
}
fun time() = elapsedTimeSeconds
fun stop() {
timer?.apply {
cancel()
purge()
}
timer = null
}
fun start(onTick: (seconds: Long) -> Unit) {
stop()
timer = Timer().apply {
scheduleAtFixedRate(object : TimerTask() {
override fun run() {
elapsedTimeSeconds++
onTick(elapsedTimeSeconds)
}
}, 1000L, 1000L)
}
}
}

View file

@ -0,0 +1,44 @@
package dev.lucasnlm.antimine.common.level.utils
import android.app.Application
import android.content.Context.VIBRATOR_SERVICE
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
interface IHapticFeedbackInteractor {
fun toggleFlagFeedback()
fun explosionFeedback()
}
class HapticFeedbackInteractor(
application: Application,
private val preferencesRepository: IPreferencesRepository
) : IHapticFeedbackInteractor {
private val vibrator: Vibrator = application.getSystemService(VIBRATOR_SERVICE) as Vibrator
override fun toggleFlagFeedback() {
if (preferencesRepository.useHapticFeedback()) {
vibrateTo(70, 240)
vibrateTo(10, 100)
}
}
override fun explosionFeedback() {
if (preferencesRepository.useHapticFeedback()) {
vibrateTo(400, -1)
}
}
private fun vibrateTo(time: Long, amplitude: Int) {
if (Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(
VibrationEffect.createOneShot(time, amplitude)
)
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(time)
}
}
}

View file

@ -0,0 +1,109 @@
package dev.lucasnlm.antimine.common.level.view
import android.content.Context
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.common.level.data.Area
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
class AreaAdapter(
context: Context,
private val viewModel: GameViewModel
) : RecyclerView.Adapter<FieldViewHolder>() {
private var field = listOf<Area>()
private var isLowBitAmbient = false
private var isAmbientMode = false
private val paintSettings: AreaPaintSettings
private val clickEnabled: Boolean
get() = viewModel.isGameActive()
init {
setHasStableIds(true)
paintSettings = createAreaPaintSettings(context, viewModel.useAccessibilityMode())
}
fun setAmbientMode(isAmbientMode: Boolean, isLowBitAmbient: Boolean) {
this.isLowBitAmbient = isLowBitAmbient
this.isAmbientMode = isAmbientMode
}
fun bindField(area: List<Area>) {
this.field = area
notifyDataSetChanged()
}
override fun getItemCount() = field.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
val layout = if (viewModel.useAccessibilityMode()) { R.layout.view_accessibility_field } else { R.layout.view_field }
val view = LayoutInflater.from(parent.context).inflate(layout, parent, false)
val holder = FieldViewHolder(view)
holder.itemView.setOnLongClickListener { target ->
target.requestFocus()
val position = holder.adapterPosition
if (position == RecyclerView.NO_POSITION) {
Log.d(TAG, "Item no longer exists.")
} else if (clickEnabled) {
viewModel.onLongClick(position)
}
true
}
holder.itemView.setOnClickListener {
val position = holder.adapterPosition
if (position == RecyclerView.NO_POSITION) {
Log.d(TAG, "Item no longer exists.")
} else if (clickEnabled) {
viewModel.onClickArea(position)
}
}
return holder
}
private fun getItem(position: Int) = field[position]
override fun getItemId(position: Int): Long = getItem(position).id.toLong()
override fun onBindViewHolder(holder: FieldViewHolder, position: Int) {
val field = getItem(position)
holder.areaView.bindField(field, isAmbientMode, isLowBitAmbient, paintSettings)
}
companion object {
private const val TAG = "AreaAdapter"
private fun createAreaPaintSettings(context: Context, useLargeArea: Boolean): AreaPaintSettings {
val resources = context.resources
val padding = resources.getDimension(R.dimen.field_padding)
val size = if (useLargeArea) {
resources.getDimension(R.dimen.accessible_field_size).toInt()
} else {
resources.getDimension(R.dimen.field_size).toInt()
}
return AreaPaintSettings(
Paint().apply {
isAntiAlias = true
isDither = true
style = Paint.Style.FILL
textSize = 18.0f * context.resources.displayMetrics.density
typeface = Typeface.DEFAULT_BOLD
textAlign = Paint.Align.CENTER
},
RectF(padding, padding, size - padding, size - padding),
resources.getDimension(R.dimen.field_radius)
)
}
}
}

View file

@ -0,0 +1,11 @@
package dev.lucasnlm.antimine.common.level.view
import android.graphics.Paint
import android.graphics.RectF
data class AreaPaintSettings(
val painter: Paint,
val rectF: RectF,
val radius: Float
)

View file

@ -0,0 +1,273 @@
package dev.lucasnlm.antimine.common.level.view
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import androidx.core.content.ContextCompat
import android.util.AttributeSet
import android.view.View
import dev.lucasnlm.antimine.common.level.data.Area
import android.graphics.RectF
import dev.lucasnlm.antimine.common.level.repository.DrawableRepository
import android.util.TypedValue
import android.graphics.drawable.Drawable
import android.os.Build
import androidx.core.view.ViewCompat
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.common.level.data.Mark
class AreaView : View {
private var covered = true
private var minesAround: Int = -1
private var mark = Mark.None
private var hasMine = false
private var mistake = false
private var isAmbientMode = false
private var isLowBitAmbient = false
private var highlighted = false
private lateinit var paintSettings: AreaPaintSettings
private val drawableRepository = DrawableRepository()
constructor(context: Context)
: super(context)
constructor(context: Context, attrs: AttributeSet?)
: super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
: super(context, attrs, defStyleAttr)
fun bindField(area: Area, isAmbientMode: Boolean, isLowBitAmbient: Boolean, paintSettings: AreaPaintSettings) {
this.paintSettings = paintSettings
minesAround = if (covered) -1 else area.minesAround
bindContentDescription(area)
val changed = arrayOf(
this.isAmbientMode != isAmbientMode,
covered != area.isCovered,
minesAround != area.minesAround,
mark != area.mark,
hasMine != area.hasMine,
mistake != area.mistake,
minesAround != area.minesAround,
highlighted != area.highlighted
).find { it } ?: false
// Used on Wear OS
this.isAmbientMode = isAmbientMode
this.isLowBitAmbient = isLowBitAmbient
paintSettings.painter.isAntiAlias = !isAmbientMode || isAmbientMode && !isLowBitAmbient
covered = area.isCovered
mark = area.mark
hasMine = area.hasMine
mistake = area.mistake
minesAround = area.minesAround
highlighted = area.highlighted
if (Build.VERSION.SDK_INT >= 23) {
this.foreground = when {
!isAmbientMode && covered -> getRippleEffect(context)
else -> null
}
}
if (changed) {
invalidate()
}
}
@SuppressLint("InlinedApi")
private fun bindContentDescription(area: Area) {
contentDescription = when {
area.mark == Mark.Flag -> {
context.getString(if (area.mistake) R.string.desc_wrongly_marked_area else R.string.desc_marked_area)
}
area.mark == Mark.Question -> context.getString(R.string.desc_marked_area)
area.isCovered -> context.getString(R.string.desc_convered_area)
!area.isCovered && area.minesAround > 0 -> area.minesAround.toString()
!area.isCovered && area.hasMine -> context.getString(R.string.exploded_mine)
else -> ""
}
ViewCompat.setImportantForAccessibility(
this,
when {
area.minesAround != 0 -> IMPORTANT_FOR_ACCESSIBILITY_YES
area.hasMine -> IMPORTANT_FOR_ACCESSIBILITY_YES
area.mistake -> IMPORTANT_FOR_ACCESSIBILITY_YES
area.mark != Mark.None -> IMPORTANT_FOR_ACCESSIBILITY_YES
!area.isCovered -> IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
else -> IMPORTANT_FOR_ACCESSIBILITY_YES
}
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
paintSettings.run {
if (isAmbientMode) {
setBackgroundResource(android.R.color.black)
} else {
setBackgroundResource(android.R.color.transparent)
}
if (covered) {
if (isAmbientMode) {
painter.apply {
style = Paint.Style.STROKE
strokeWidth = 2.0f
isAntiAlias = !isLowBitAmbient
color = ContextCompat.getColor(context, android.R.color.white)
}
} else {
painter.apply {
style = Paint.Style.FILL
isAntiAlias = !isLowBitAmbient
color = ContextCompat.getColor(context, R.color.view_cover)
alpha = if(highlighted) 155 else 255
}
}
painter.run {
canvas.drawRoundRect(rectF, radius, radius, this)
}
when (mark) {
Mark.Flag -> {
val padding = resources.getDimension(R.dimen.mark_padding).toInt()
val flag = if (mistake) {
drawableRepository.provideRedFlagDrawable(context)
} else {
drawableRepository.provideFlagDrawable(context)
}
flag?.setBounds(
rectF.left.toInt() + padding,
rectF.top.toInt() + padding,
rectF.right.toInt() - padding,
rectF.bottom.toInt() - padding
)
flag?.draw(canvas)
}
Mark.Question -> {
val question = drawableRepository.provideQuestionDrawable(context)
question?.setBounds(
rectF.left.toInt(),
rectF.top.toInt(),
rectF.right.toInt(),
rectF.bottom.toInt()
)
question?.draw(canvas)
}
else -> {}
}
} else {
if (isAmbientMode) {
painter.apply {
style = Paint.Style.STROKE
strokeWidth = 0.5f
isAntiAlias = !isLowBitAmbient
color = ContextCompat.getColor(context, android.R.color.white)
}
} else {
painter.apply {
style = Paint.Style.FILL
isAntiAlias = !isLowBitAmbient
color = ContextCompat.getColor(context, R.color.view_clean)
}
}
painter.run {
canvas.drawRoundRect(rectF, radius, radius, this)
}
if (hasMine) {
val mine = when {
isAmbientMode -> drawableRepository.provideMineLow(context)
mistake -> drawableRepository.provideMineExploded(context)
else -> drawableRepository.provideMine(context)
}
mine?.setBounds(
rectF.left.toInt(),
rectF.top.toInt(),
rectF.right.toInt(),
rectF.bottom.toInt()
)
mine?.draw(canvas)
} else if (minesAround > 0) {
val color = if (isAmbientMode) {
R.color.ambient_color_white
} else {
when (minesAround) {
1 -> R.color.mines_arround_1
2 -> R.color.mines_arround_2
3 -> R.color.mines_arround_3
4 -> R.color.mines_arround_4
5 -> R.color.mines_arround_5
6 -> R.color.mines_arround_6
7 -> R.color.mines_arround_7
else -> R.color.mines_arround_8
}
}
painter.color = ContextCompat.getColor(context, color)
drawText(canvas, painter, minesAround.toString())
}
if (highlighted) {
painter.apply {
style = Paint.Style.STROKE
strokeWidth = 2.0f
isAntiAlias = !isLowBitAmbient
color = if (isAmbientMode) {
ContextCompat.getColor(context, R.color.white)
} else {
ContextCompat.getColor(context, R.color.highlight)
}
canvas.drawRoundRect(rectF, radius, radius, this)
}
}
}
if (isFocused) {
painter.apply {
style = Paint.Style.STROKE
strokeWidth = 4f
isAntiAlias = !isLowBitAmbient
color = ContextCompat.getColor(context, android.R.color.holo_orange_dark)
}
canvas.drawRoundRect(rectF, radius, radius, painter)
}
}
}
private fun getRippleEffect(context: Context): Drawable? {
val outValue = TypedValue()
context.theme.resolveAttribute(
android.R.attr.selectableItemBackground, outValue, true
)
return ContextCompat.getDrawable(context, outValue.resourceId)
}
private fun drawText(canvas: Canvas, paint: Paint, text: String) {
paintSettings.run {
val bounds = RectF(rectF).apply {
right = paint.measureText(text, 0, text.length)
bottom = paint.descent() - paint.ascent()
left += (rectF.width() - right) / 2.0f
top += (rectF.height() - bottom) / 2.0f
}
canvas.drawText(text, rectF.width() * 0.5f, bounds.top - paint.ascent(), paint)
}
}
}

View file

@ -0,0 +1,46 @@
package dev.lucasnlm.antimine.common.level.view
import android.os.Build
import android.view.KeyEvent
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import dev.lucasnlm.antimine.common.R
class FieldViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val areaView: AreaView = view.findViewById(R.id.area)
init {
view.isFocusable = false
areaView.isFocusable = true
areaView.setOnKeyListener { _, keyCode, keyEvent ->
var handled = false
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
when (keyEvent.action) {
KeyEvent.ACTION_DOWN -> {
longPressAt = System.currentTimeMillis()
handled = true
}
KeyEvent.ACTION_UP -> {
if (System.currentTimeMillis() - longPressAt > 300L) {
view.performLongClick()
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
view.callOnClick()
} else {
view.performClick()
}
}
handled = true
}
}
}
handled
}
}
companion object {
var longPressAt: Long = 0L
}
}

View file

@ -0,0 +1,31 @@
package dev.lucasnlm.antimine.common.level.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.HorizontalScrollView
import androidx.recyclerview.widget.RecyclerView
class UnlockedHorizontalScrollView : HorizontalScrollView {
private var recyclerView: RecyclerView? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet)
: super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int)
: super(context, attrs, defStyleAttr)
fun setTarget(recyclerView: RecyclerView) {
this.recyclerView = recyclerView
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean =
super.onTouchEvent(event) or recyclerView!!.onTouchEvent(event)
override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
super.onInterceptTouchEvent(event) or recyclerView!!.onInterceptTouchEvent(event)
}

View file

@ -0,0 +1,257 @@
package dev.lucasnlm.antimine.common.level.viewmodel
import android.app.Application
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import dev.lucasnlm.antimine.common.level.GameModeFactory
import dev.lucasnlm.antimine.common.level.LevelFacade
import dev.lucasnlm.antimine.common.level.data.*
import dev.lucasnlm.antimine.common.level.database.data.Save
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.analytics.Event
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class GameViewModel(
val application: Application,
val eventObserver: MutableLiveData<GameEvent>,
private val savesRepository: ISavesRepository,
private val dimensionRepository: IDimensionRepository,
private val preferencesRepository: IPreferencesRepository,
private val hapticFeedbackInteractor: IHapticFeedbackInteractor,
private val analyticsManager: AnalyticsManager,
private val clock: Clock
) : ViewModel() {
private lateinit var levelFacade: LevelFacade
private var currentDifficulty: DifficultyPreset = DifficultyPreset.Standard
private var initialized = false
val field = MutableLiveData<List<Area>>()
val fieldRefresh = MutableLiveData<Int>()
val elapsedTimeSeconds = MutableLiveData<Long>()
val mineCount = MutableLiveData<Int>()
val difficulty = MutableLiveData<DifficultyPreset>()
val levelSetup = MutableLiveData<LevelSetup>()
private fun startNewGame(gameId: Int, difficultyPreset: DifficultyPreset): LevelSetup {
clock.reset()
elapsedTimeSeconds.postValue(0L)
currentDifficulty = difficultyPreset
val setup = GameModeFactory.fromDifficultyPreset(
difficultyPreset, dimensionRepository, preferencesRepository
)
levelFacade = LevelFacade(gameId, setup)
mineCount.postValue(setup.mines)
difficulty.postValue(difficultyPreset)
levelSetup.postValue(setup)
field.postValue(levelFacade.field.toList())
eventObserver.postValue(GameEvent.StartNewGame)
analyticsManager.sentEvent(Event.NewGame(setup, levelFacade.seed, useAccessibilityMode()))
return setup
}
private fun resumeGameFromSave(save: Save): LevelSetup {
clock.reset(save.duration)
elapsedTimeSeconds.postValue(save.duration)
val setup = save.levelSetup
levelFacade = LevelFacade(save)
mineCount.postValue(setup.mines)
difficulty.postValue(save.levelSetup.preset)
levelSetup.postValue(setup)
field.postValue(levelFacade.field.toList())
when {
levelFacade.hasAnyMineExploded() -> eventObserver.postValue(GameEvent.ResumeGameOver)
levelFacade.checkVictory() -> eventObserver.postValue(GameEvent.ResumeVictory)
else -> eventObserver.postValue(GameEvent.ResumeGame)
}
analyticsManager.sentEvent(Event.ResumePreviousGame())
return setup
}
suspend fun startNewGame(difficultyPreset: DifficultyPreset = currentDifficulty): LevelSetup =
withContext(Dispatchers.IO) {
val newGameId = savesRepository.getNewSaveId()
startNewGame(newGameId, difficultyPreset)
}
suspend fun onCreate(newGame: DifficultyPreset? = null): LevelSetup = withContext(Dispatchers.IO) {
val lastGame = if (newGame == null) savesRepository.fetchCurrentSave() else null
if (lastGame != null) {
currentDifficulty = lastGame.levelSetup.preset
} else if (newGame != null) {
currentDifficulty = newGame
}
if (lastGame == null) {
val newGameId = savesRepository.getNewSaveId()
startNewGame(newGameId, currentDifficulty)
} else {
resumeGameFromSave(lastGame)
}.also {
initialized = true
}
}
fun pauseGame() {
if (initialized) {
if (levelFacade.hasMines) {
eventObserver.postValue(GameEvent.Pause)
}
clock.stop()
}
}
suspend fun saveGame() {
if (initialized) {
if (levelFacade.hasMines) {
savesRepository.saveGame(
levelFacade.getSaveState().copy(duration = elapsedTimeSeconds.value ?: 0L)
)
}
}
}
fun resumeGame() {
if (initialized) {
if (levelFacade.hasMines) {
eventObserver.postValue(GameEvent.Resume)
}
}
}
fun onLongClick(index: Int) {
levelFacade.turnOffAllHighlighted()
if (levelFacade.hasCoverOn(index)) {
if (levelFacade.switchMarkAt(index)) {
refreshField(index)
hapticFeedbackInteractor.toggleFlagFeedback()
}
analyticsManager.sentEvent(Event.LongPressArea(index))
} else {
levelFacade.openNeighbors(index)
refreshGameStatus()
analyticsManager.sentEvent(Event.LongPressMultipleArea(index))
}
field.postValue(levelFacade.field.toList())
if (levelFacade.hasMines) {
mineCount.postValue(levelFacade.remainingMines())
}
}
fun onClickArea(index: Int) {
levelFacade.turnOffAllHighlighted()
if (levelFacade.hasMarkOn(index)) {
levelFacade.removeMark(index)
hapticFeedbackInteractor.toggleFlagFeedback()
refreshField(index)
} else {
if (!levelFacade.hasMines) {
levelFacade.plantMinesExcept(index, true)
}
levelFacade.clickArea(index)
field.postValue(levelFacade.field.toList())
}
refreshGameStatus()
analyticsManager.sentEvent(Event.PressArea(index))
}
private fun refreshMineCount() = mineCount.postValue(levelFacade.remainingMines())
private fun refreshGameStatus() {
refreshMineCount()
when {
levelFacade.hasAnyMineExploded() -> {
hapticFeedbackInteractor.explosionFeedback()
eventObserver.postValue(GameEvent.GameOver)
}
levelFacade.checkVictory() -> eventObserver.postValue(GameEvent.Victory)
else -> {
if (preferencesRepository.useFlagAssistant()){
levelFacade.runFlagAssistant()
}
eventObserver.postValue(GameEvent.Running)
}
}
}
fun runClock() {
if (isGameActive()) {
clock.run {
if (isStopped) start {
elapsedTimeSeconds.postValue(it)
}
}
}
}
fun stopClock() {
clock.stop()
}
fun revealAllEmptyAreas() {
levelFacade.revealAllEmptyAreas()
}
fun gameOver() {
levelFacade.run {
analyticsManager.sentEvent(Event.GameOver(clock.time(), getStats()))
showAllMines()
showWrongFlags()
}
GlobalScope.launch {
saveGame()
}
}
fun victory() {
levelFacade.run {
analyticsManager.sentEvent(Event.Victory(clock.time(), getStats(), currentDifficulty))
showAllMines()
showWrongFlags()
}
GlobalScope.launch {
saveGame()
}
}
fun isGameActive() = (levelFacade.checkVictory() || levelFacade.hasAnyMineExploded()).not()
fun useAccessibilityMode() = preferencesRepository.useLargeAreas()
private fun refreshField(index: Int) {
fieldRefresh.postValue(index)
}
}

View file

@ -0,0 +1,38 @@
package dev.lucasnlm.antimine.common.level.viewmodel
import android.app.Application
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dev.lucasnlm.antimine.common.level.data.GameEvent
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackInteractor
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import javax.inject.Inject
class GameViewModelFactory @Inject constructor(
private val application: Application,
private val gameEventObserver: MutableLiveData<GameEvent>,
private val savesRepository: ISavesRepository,
private val dimensionRepository: IDimensionRepository,
private val preferencesRepository: IPreferencesRepository,
private val hapticFeedbackInteractor: IHapticFeedbackInteractor,
private val analyticsManager: AnalyticsManager,
private val clock: Clock
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
if (modelClass.isAssignableFrom(GameViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
GameViewModel(
application, gameEventObserver, savesRepository,
dimensionRepository, preferencesRepository, hapticFeedbackInteractor,
analyticsManager, clock
) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}

View file

@ -0,0 +1,27 @@
package dev.lucasnlm.antimine.core.analytics
import android.app.Application
import android.content.Context
import com.amplitude.api.Amplitude
import com.amplitude.api.AmplitudeClient
import dev.lucasnlm.antimine.common.R
import org.json.JSONObject
class AmplitudeAnalyticsManager(
private val application: Application
) : AnalyticsManager {
private var amplitudeClient: AmplitudeClient? = null
override fun setup(context: Context, userProperties: Map<String, String>) {
val key = context.getString(R.string.amplitude_key)
amplitudeClient = Amplitude.getInstance().initialize(application, key).apply {
setUserProperties(JSONObject(userProperties))
enableForegroundTracking(application)
}
}
override fun sentEvent(event: Event) {
amplitudeClient?.logEvent(event.title, JSONObject(event.extra))
}
}

View file

@ -0,0 +1,8 @@
package dev.lucasnlm.antimine.core.analytics
import android.content.Context
interface AnalyticsManager {
fun setup(context: Context, userProperties: Map<String, String>)
fun sentEvent(event: Event)
}

View file

@ -0,0 +1,18 @@
package dev.lucasnlm.antimine.core.analytics
import android.content.Context
import android.util.Log
class DebugAnalyticsManager : AnalyticsManager {
override fun setup(context: Context, userProperties: Map<String, String>) {
Log.d(TAG, "Setup Analytics using $userProperties")
}
override fun sentEvent(event: Event) {
Log.d(TAG, "Sent event: '${event.title}' with ${event.extra}")
}
companion object {
const val TAG = "DebugAnalyticsManager"
}
}

View file

@ -0,0 +1,82 @@
package dev.lucasnlm.antimine.core.analytics
import dev.lucasnlm.antimine.common.level.data.DifficultyPreset
import dev.lucasnlm.antimine.common.level.data.GameStats
import dev.lucasnlm.antimine.common.level.data.LevelSetup
sealed class Event(
val title: String, val extra: Map<String, String> = mapOf()
) {
class Open : Event("Open game")
class NewGame(levelSetup: LevelSetup, seed: Long, useAccessibilityMode: Boolean) :
Event("New Game", mapOf(
"Seed" to seed.toString(),
"Difficulty Preset" to levelSetup.preset.text,
"Width" to levelSetup.width.toString(),
"Height" to levelSetup.height.toString(),
"Mines" to levelSetup.mines.toString(),
"Accessibility" to useAccessibilityMode.toString()
)
)
class ResumePreviousGame : Event("Resume previous game")
class LongPressArea(index: Int) : Event("Long press area",
mapOf("Index" to index.toString())
)
class LongPressMultipleArea(index: Int) : Event("Long press to open multiple",
mapOf("Index" to index.toString())
)
class PressArea(index: Int) : Event("Press area",
mapOf("Index" to index.toString())
)
class GameOver(time: Long, gameStats: GameStats) : Event("Game Over",
mapOf(
"Time" to time.toString(),
"Right Mines" to gameStats.rightMines.toString(),
"Total Mines" to gameStats.totalMines.toString(),
"Total Area" to gameStats.totalArea.toString()
)
)
class Victory(time: Long, gameStats: GameStats, difficultyPreset: DifficultyPreset) : Event(
"Victory",
mapOf(
"Time" to time.toString(),
"Difficulty" to difficultyPreset.text,
"Right Mines" to gameStats.rightMines.toString(),
"Total Mines" to gameStats.totalMines.toString(),
"Total Area" to gameStats.totalArea.toString()
)
)
class Resume : Event("Back to the game")
class Quit : Event("Quit game")
class OpenDrawer : Event("Opened Drawer")
class CloseDrawer : Event("Closed Drawer")
class OpenAbout : Event("Open About")
class OpenSettings : Event("Open Settings")
class ShowRatingRequest(usages: Int) : Event("Shown Rating Request",
mapOf(
"Usages" to usages.toString()
))
class TapRatingRequest(from: String) : Event("Rating Request",
mapOf(
"From" to from
))
class TapGameReset(resign: Boolean) : Event("Game reset",
mapOf("Resign" to resign.toString())
)
}

View file

@ -0,0 +1,18 @@
package dev.lucasnlm.antimine.core.di
import dagger.Component
import dagger.android.support.AndroidSupportInjectionModule
@Component(
modules = [
AndroidSupportInjectionModule::class,
CommonModule::class
]
)
abstract class LevelComponent {
@Component.Builder
interface Builder {
fun levelModule(levelModule: CommonModule): Builder
fun build(): LevelComponent
}
}

View file

@ -0,0 +1,38 @@
package dev.lucasnlm.antimine.core.di
import android.app.Application
import dagger.Module
import dagger.Provides
import dev.lucasnlm.antimine.common.BuildConfig
import dev.lucasnlm.antimine.core.analytics.AmplitudeAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.AnalyticsManager
import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.preferences.PreferencesInteractor
import dev.lucasnlm.antimine.core.preferences.PreferencesRepository
import javax.inject.Singleton
@Module
class CommonModule {
@Singleton
@Provides
fun providePreferencesRepository(
preferencesInteractor: PreferencesInteractor
): IPreferencesRepository = PreferencesRepository(preferencesInteractor)
@Singleton
@Provides
fun providePreferencesInteractor(
application: Application
): PreferencesInteractor = PreferencesInteractor(application)
@Singleton
@Provides
fun provideAnalyticsManager(
application: Application
): AnalyticsManager = if (BuildConfig.DEBUG) {
DebugAnalyticsManager()
} else {
AmplitudeAnalyticsManager(application)
}
}

View file

@ -0,0 +1,42 @@
package dev.lucasnlm.antimine.core.preferences
import android.app.Application
import androidx.preference.PreferenceManager
import dev.lucasnlm.antimine.common.level.data.LevelSetup
import javax.inject.Inject
class PreferencesInteractor @Inject constructor(
private val application: Application
) {
private val preferences by lazy {
PreferenceManager.getDefaultSharedPreferences(application)
}
fun getCustomMode() = LevelSetup(
preferences.getInt(PREFERENCE_CUSTOM_GAME_WIDTH, 9),
preferences.getInt(PREFERENCE_CUSTOM_GAME_HEIGHT, 9),
preferences.getInt(PREFERENCE_CUSTOM_GAME_MINES, 9)
)
fun updateCustomMode(customLevelSetup: LevelSetup) {
preferences.edit().apply {
putInt(PREFERENCE_CUSTOM_GAME_WIDTH, customLevelSetup.width)
putInt(PREFERENCE_CUSTOM_GAME_HEIGHT, customLevelSetup.height)
putInt(PREFERENCE_CUSTOM_GAME_MINES, customLevelSetup.mines)
}.apply()
}
fun getBoolean(key: String, defaultValue: Boolean) = preferences.getBoolean(key, defaultValue)
fun putBoolean(key: String, value: Boolean) = preferences.edit().putBoolean(key, value).apply()
fun getInt(key: String, defaultValue: Int) = preferences.getInt(key, defaultValue)
fun putInt(key: String, value: Int) = preferences.edit().putInt(key, value).apply()
companion object {
private const val PREFERENCE_CUSTOM_GAME_WIDTH = "preference_custom_game_width"
private const val PREFERENCE_CUSTOM_GAME_HEIGHT = "preference_custom_game_height"
private const val PREFERENCE_CUSTOM_GAME_MINES = "preference_custom_game_mines"
}
}

View file

@ -0,0 +1,49 @@
package dev.lucasnlm.antimine.core.preferences
import dev.lucasnlm.antimine.common.level.data.LevelSetup
interface IPreferencesRepository {
fun customGameMode(): LevelSetup
fun updateCustomGameMode(levelSetup: LevelSetup)
fun getBoolean(key: String, defaultValue: Boolean): Boolean
fun getInt(key: String, defaultValue: Int): Int
fun putBoolean(key: String, value: Boolean)
fun putInt(key: String, value: Int)
fun useFlagAssistant(): Boolean
fun useHapticFeedback(): Boolean
fun useLargeAreas(): Boolean
}
class PreferencesRepository(
private val preferencesInteractor: PreferencesInteractor
) : IPreferencesRepository {
override fun customGameMode(): LevelSetup =
preferencesInteractor.getCustomMode()
override fun updateCustomGameMode(levelSetup: LevelSetup) =
preferencesInteractor.updateCustomMode(levelSetup)
override fun getBoolean(key: String, defaultValue: Boolean): Boolean =
preferencesInteractor.getBoolean(key, defaultValue)
override fun putBoolean(key: String, value: Boolean) =
preferencesInteractor.putBoolean(key, value)
override fun getInt(key: String, defaultValue: Int): Int =
preferencesInteractor.getInt(key, defaultValue)
override fun putInt(key: String, value: Int) =
preferencesInteractor.putInt(key, value)
override fun useFlagAssistant(): Boolean =
getBoolean("preference_assistant", true)
override fun useHapticFeedback(): Boolean =
getBoolean("preference_vibration", true)
override fun useLargeAreas(): Boolean =
getBoolean("preference_large_area", false)
}

View file

@ -0,0 +1,7 @@
package dev.lucasnlm.antimine.core.scope
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

View file

@ -0,0 +1,7 @@
package dev.lucasnlm.antimine.core.scope
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope

View file

@ -0,0 +1,12 @@
package dev.lucasnlm.antimine.core.utils
import android.content.Context
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
fun isDarkModeEnabled(context: Context): Boolean {
return when (context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) {
UI_MODE_NIGHT_YES -> true
else -> false
}
}

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#1f100d"
android:pathData="M331.722,124.091l-33.743,-67.204l-83.901,0l-33.84,65.28l-80.22,0l-37.443,65.714l37.443,67.649l-37.443,66.12l37.42,67.746l80.243,0l39.239,65.717l75.084,0l37.161,-66.643l79.759,-0.019l37.942,-66.801l-40.501,-65.64l40.501,-68.129l-38.02,-63.79z"
android:strokeWidth="0.02435089" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M360.853,255.692l-52.193,91.05l-104.8,0.177l-52.713,-90.616l52.081,-90.835l104.896,-0.386z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#666666"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M14.4,6L14,4H5v17h2v-7h5.6l0.4,2h7V6z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z" />
</vector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#ff212121" />
</shape>
</item>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/splash_image" />
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M14.4,6L14,4H5v17h2v-7h5.6l0.4,2h7V6z" />
</vector>

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