Add Try Button to Theme and Instant flavor

This commit is contained in:
Lucas Lima 2020-08-30 20:29:02 -03:00
parent 4ff04bea5c
commit 6528708b51
28 changed files with 253 additions and 27 deletions

View file

@ -76,6 +76,11 @@ android {
applicationId 'dev.lucasnlm.antimine'
}
googleInstant {
dimension 'version'
applicationId 'dev.lucasnlm.antimine'
}
foss {
dimension 'version'
// There's a typo on F-Droid release :(
@ -96,6 +101,7 @@ dependencies {
implementation project(':common')
googleImplementation project(':proprietary')
googleInstantImplementation project(':proprietary')
fossImplementation project(':foss')
// AndroidX
@ -115,7 +121,7 @@ dependencies {
implementation 'androidx.room:room-ktx:2.2.5'
// Constraint
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
// Google
implementation 'com.google.android.material:material:1.2.0'
@ -142,7 +148,7 @@ dependencies {
testImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
testImplementation 'androidx.fragment:fragment-testing:1.2.5'
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'io.mockk:mockk:1.10.0'
// Core library

View file

@ -574,7 +574,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private fun restartIfNeed(): Boolean {
return (areaSizeMultiplier != preferencesRepository.areaSizeMultiplier()).also {
if (it) {
recreate()
finish()
startActivity(intent)
overridePendingTransition(0, 0)
}
}
}
@ -635,7 +637,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private fun showSupportAppDialog() {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newInstance(false)
SupportAppDialogFragment.newRequestSupportDialog()
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}

View file

@ -7,6 +7,7 @@ import dev.lucasnlm.antimine.core.analytics.models.Analytics
import dev.lucasnlm.antimine.core.di.CommonModule
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.ViewModelModule
import dev.lucasnlm.external.IAdsManager
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
@ -14,6 +15,8 @@ import org.koin.core.context.startKoin
open class MainApplication : MultiDexApplication() {
private val analyticsManager: IAnalyticsManager by inject()
private val adsManager: IAdsManager by inject()
override fun onCreate() {
super.onCreate()
startKoin {
@ -25,5 +28,7 @@ open class MainApplication : MultiDexApplication() {
setup(applicationContext, mapOf())
sentEvent(Analytics.Open)
}
adsManager.start(applicationContext)
}
}

View file

@ -34,7 +34,9 @@ open class ThematicActivity(@LayoutRes contentLayoutId: Int) : AppCompatActivity
super.onResume()
if (usingTheme.id != currentTheme().id) {
recreate()
finish()
startActivity(intent)
overridePendingTransition(0, 0)
}
}
}

View file

@ -53,7 +53,7 @@ class AboutActivity : ThematicActivity(R.layout.activity_empty) {
private fun showSupportAppDialog() {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newInstance(false)
SupportAppDialogFragment.newRequestSupportDialog()
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}

View file

@ -6,8 +6,10 @@ import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.ProdAnalyticsManager
import dev.lucasnlm.antimine.share.ShareManager
import dev.lucasnlm.external.AdsManager
import dev.lucasnlm.external.BillingManager
import dev.lucasnlm.external.ExternalAnalyticsWrapper
import dev.lucasnlm.external.IAdsManager
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
@ -23,6 +25,8 @@ val AppModule = module {
single { BillingManager(get()) } bind IBillingManager::class
single { AdsManager() } bind IAdsManager::class
single { PlayGamesManager(get()) } bind IPlayGamesManager::class
single { ReviewWrapper() } bind IReviewWrapper::class

View file

@ -8,16 +8,21 @@ import androidx.lifecycle.lifecycleScope
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.models.Analytics
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.external.IAdsManager
import dev.lucasnlm.external.IBillingManager
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class SupportAppDialogFragment : AppCompatDialogFragment() {
private val billingManager: IBillingManager by inject()
private val themeRepository: IThemeRepository by inject()
private val analyticsManager: IAnalyticsManager by inject()
private val adsManager: IAdsManager by inject()
private val iapHandler: IapHandler by inject()
private var unlockMessage: Boolean = false
private var showUnlockMessage: Boolean = false
private var targetThemeId: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -25,16 +30,31 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
billingManager.start(iapHandler)
analyticsManager.sentEvent(Analytics.ShowIapDialog)
unlockMessage = (arguments?.getBoolean(UNLOCK_LABEL) ?: savedInstanceState?.getBoolean(UNLOCK_LABEL)) ?: false
showUnlockMessage = (arguments?.getBoolean(UNLOCK_LABEL) ?: savedInstanceState?.getBoolean(UNLOCK_LABEL)) ?: false
targetThemeId = (arguments?.getLong(TARGET_THEME_ID, -1L)) ?: -1L
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
setView(R.layout.dialog_payments)
setNeutralButton(R.string.rating_button_no) { _, _ ->
analyticsManager.sentEvent(Analytics.DenyIapDialog)
if (showUnlockMessage) {
setNeutralButton(R.string.try_it) { _, _ ->
analyticsManager.sentEvent(Analytics.UnlockRewardedDialog)
adsManager.requestRewarded(requireActivity(), "ca-app-pub-3940256099942544/5224354917") {
if (targetThemeId > 0) {
themeRepository.setTheme(targetThemeId)
requireActivity().recreate()
}
}
}
} else {
setNeutralButton(R.string.rating_button_no) { _, _ ->
analyticsManager.sentEvent(Analytics.DenyIapDialog)
}
}
setPositiveButton(if (unlockMessage) R.string.unlock else R.string.support_action) { _, _ ->
setPositiveButton(if (showUnlockMessage) R.string.unlock else R.string.support_action) { _, _ ->
lifecycleScope.launch {
analyticsManager.sentEvent(Analytics.UnlockIapDialog)
billingManager.charge(requireActivity())
@ -47,11 +67,21 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
val TAG = SupportAppDialogFragment::class.simpleName
private const val UNLOCK_LABEL = "support_unlock_label"
private const val TARGET_THEME_ID = "target_theme_id"
fun newInstance(unlockMessage: Boolean): SupportAppDialogFragment {
fun newRequestSupportDialog(): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(UNLOCK_LABEL, unlockMessage)
putBoolean(UNLOCK_LABEL, false)
}
}
}
fun newChangeThemeDialog(targetThemeId: Long): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(UNLOCK_LABEL, true)
putLong(TARGET_THEME_ID, targetThemeId)
}
}
}

View file

@ -50,7 +50,7 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
launch {
themeViewModel.observeEvent().collect {
if (it is ThemeEvent.Unlock) {
showUnlockDialog()
showUnlockDialog(it.themeId)
}
}
}
@ -58,7 +58,9 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
launch {
themeViewModel.observeState().collect {
if (usingTheme.id != it.current.id) {
recreate()
finish()
startActivity(intent)
overridePendingTransition(0, 0)
}
}
}
@ -81,9 +83,9 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
}
}
private fun showUnlockDialog() {
private fun showUnlockDialog(themeId: Long) {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newInstance(true).apply {
SupportAppDialogFragment.newChangeThemeDialog(themeId).apply {
show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}

View file

@ -3,7 +3,9 @@ package dev.lucasnlm.antimine.theme.viewmodel
import dev.lucasnlm.antimine.core.themes.model.AppTheme
sealed class ThemeEvent {
object Unlock : ThemeEvent()
data class Unlock(
val themeId: Long
) : ThemeEvent()
data class ChangeTheme(
val newTheme: AppTheme,

View file

@ -20,7 +20,7 @@ class ThemeViewModel(
private val analyticsManager: IAnalyticsManager,
) : IntentViewModel<ThemeEvent, ThemeState>() {
private fun setTheme(theme: AppTheme) {
themeRepository.setTheme(theme)
themeRepository.setTheme(theme.id)
}
override fun observeEvent(): Flow<ThemeEvent> {
@ -35,7 +35,7 @@ class ThemeViewModel(
billingManager.isEnabled() &&
!preferencesRepository.isPremiumEnabled()
) {
ThemeEvent.Unlock
ThemeEvent.Unlock(it.newTheme.id)
} else {
it
}

View file

@ -30,7 +30,7 @@ val TestCommonModule = module {
override fun getAllThemes(): List<AppTheme> = listOf(LightTheme)
override fun setTheme(theme: AppTheme) { }
override fun setTheme(themeId: Long) { }
override fun reset(): AppTheme = LightTheme
}

View file

@ -167,7 +167,7 @@ class ThemeViewModelTest : IntentViewModelTest() {
}
assertEquals(ThemeState(darkTheme, allThemes), state)
verify { themeRepository.setTheme(darkTheme) }
verify { themeRepository.setTheme(darkTheme.id) }
}
@Test

View file

@ -46,7 +46,7 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:1.2.5"
// Constraint
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
// Lifecycle
api 'android.arch.lifecycle:extensions:1.1.1'

View file

@ -109,6 +109,8 @@ sealed class Analytics(
object UnlockIapDialog : Analytics("IAP Dialog Unlock")
object UnlockRewardedDialog : Analytics("Rewarded Dialog Unlock")
class ShowRatingRequest(usages: Int) : Analytics("Shown Rating Request", mapOf("Usages" to usages.toString()))
object TapRatingRequest : Analytics("Rating Request")

View file

@ -10,6 +10,7 @@ class PreferencesRepository(
) : IPreferencesRepository {
init {
migrateOldPreferences()
revertTestTheme()
}
override fun hasCustomizations(): Boolean {
@ -121,6 +122,12 @@ class PreferencesRepository(
preferencesManager.putBoolean(PREFERENCE_REQUEST_RATING, false)
}
private fun revertTestTheme() {
if (!isPremiumEnabled()) {
useTheme(0L)
}
}
private fun migrateOldPreferences() {
// Migrate Double Click to the new Control settings
if (preferencesManager.contains(PREFERENCE_OLD_DOUBLE_CLICK)) {

View file

@ -12,7 +12,7 @@ interface IThemeRepository {
fun getCustomTheme(): AppTheme?
fun getTheme(): AppTheme
fun getAllThemes(): List<AppTheme>
fun setTheme(theme: AppTheme)
fun setTheme(themeId: Long)
fun reset(): AppTheme
}
@ -31,8 +31,8 @@ class ThemeRepository(
override fun getAllThemes(): List<AppTheme> =
listOf(buildSystemTheme()) + Themes.getAllCustom()
override fun setTheme(theme: AppTheme) {
preferenceRepository.useTheme(theme.id)
override fun setTheme(themeId: Long) {
preferenceRepository.useTheme(themeId)
}
override fun reset(): AppTheme {

View file

@ -90,6 +90,7 @@
<string name="close_menu">Close Menu</string>
<string name="delete_all">Delete all</string>
<string name="themes">Themes</string>
<string name="try_it">Try It</string>
<string name="delete_all_message">Delete all events permanently.</string>
<string name="all_mines_disabled">All mines were disabled.</string>
<string name="desc_convered_area">Covered area</string>

View file

@ -0,0 +1,10 @@
package dev.lucasnlm.external
import android.app.Activity
import android.content.Context
interface IAdsManager {
fun start(context: Context)
fun isReady(): Boolean
fun requestRewarded(activity: Activity, adUnitId: String, onRewarded: () -> Unit)
}

1
instant/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

37
instant/build.gradle Normal file
View file

@ -0,0 +1,37 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
}
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName '1.0'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
// Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar'])
}

View file

21
instant/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.lucasnlm.antimine.instant"
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data android:name="com.google.android.gms.instant.flavor" android:value="1337"/>
</application>
</manifest>

View file

@ -22,6 +22,13 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [
'-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xuse-experimental=kotlinx.coroutines.FlowPreview']
}
}
dependencies {
@ -35,6 +42,7 @@ dependencies {
implementation 'com.google.android.gms:play-services-instantapps:17.0.0'
implementation 'com.google.android.gms:play-services-games:20.0.0'
implementation 'com.google.android.gms:play-services-auth:18.1.0'
implementation 'com.google.android.gms:play-services-ads:19.3.0'
implementation 'com.google.android.play:core-ktx:1.8.1'
// Firebase

View file

@ -11,5 +11,8 @@
android:value="@string/app_id"/>
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
</application>
</manifest>

View file

@ -0,0 +1,75 @@
package dev.lucasnlm.external
import android.app.Activity
import android.content.Context
import androidx.annotation.NonNull
import com.google.android.gms.ads.AdError
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.LoadAdError
import com.google.android.gms.ads.MobileAds
import com.google.android.gms.ads.rewarded.RewardItem
import com.google.android.gms.ads.rewarded.RewardedAd
import com.google.android.gms.ads.rewarded.RewardedAdCallback
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback
class AdsManager : IAdsManager {
private var unlockTheme: RewardedAd? = null
override fun start(context: Context) {
MobileAds.initialize(context) {
unlockTheme = loadRewardedAd(context)
}
}
private fun loadRewardedAd(context: Context): RewardedAd {
return RewardedAd(context, "ca-app-pub-3940256099942544/5224354917").apply {
val adLoadCallback = object: RewardedAdLoadCallback() {
override fun onRewardedAdLoaded() {
// Loaded
}
override fun onRewardedAdFailedToLoad(adError: LoadAdError) {
// Ad failed to load.
}
}
loadAd(AdRequest.Builder().build(), adLoadCallback)
}
}
override fun isReady(): Boolean {
return unlockTheme != null
}
override fun requestRewarded(activity: Activity, adUnitId: String, onRewarded: () -> Unit) {
if (isReady()) {
val context = activity.applicationContext
unlockTheme?.let {
val adCallback = object : RewardedAdCallback() {
override fun onRewardedAdOpened() {
// Ad opened
}
override fun onRewardedAdClosed() {
// Ad closed
}
override fun onUserEarnedReward(@NonNull reward: RewardItem) {
onRewarded()
}
override fun onRewardedAdFailedToShow(adError: AdError) {
// Ad failed
}
}
unlockTheme = loadRewardedAd(context)
if (!activity.isFinishing) {
it.show(activity, adCallback)
}
}
}
}
}

View file

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

View file

@ -49,7 +49,7 @@ dependencies {
// Constraint
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.activity:activity-ktx:1.1.0'