diff --git a/app/build.gradle b/app/build.gradle index d32cfc1..2861c61 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -146,12 +146,20 @@ repositories { maven { url 'https://maven.fabric.io/public' } } +kotlin { + experimental { + coroutines 'enable' + } +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(":monetization") // Language implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" // Android - Support implementation "androidx.annotation:annotation:1.0.0-rc02" @@ -162,6 +170,8 @@ dependencies { // Google implementation 'com.google.code.gson:gson:2.8.2' + implementation 'com.google.firebase:firebase-core:16.0.3' + implementation "com.google.firebase:firebase-config:16.0.0" // Third party - Monitoring implementation('com.crashlytics.sdk.android:crashlytics:2.9.4@aar') { @@ -191,7 +201,7 @@ dependencies { } testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.21.0' + testImplementation 'org.mockito:mockito-core:2.22.0' } // Spoon @@ -232,3 +242,5 @@ if (rootProject.ext.spoonEnable) { adbTimeout = 30 } } + +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..d8855d0 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,42 @@ +{ + "project_info": { + "project_number": "966913976495", + "firebase_url": "https://browser-e3b83.firebaseio.com", + "project_id": "browser-e3b83", + "storage_bucket": "browser-e3b83.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:966913976495:android:7f07c0d7905eb43b", + "android_client_info": { + "package_name": "com.mercandalli.android.browser" + } + }, + "oauth_client": [ + { + "client_id": "966913976495-sfk9eeao7js481r57ijv0adfp4pcc0ve.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDy9zFmaVwBHij08mOuoKLBuI8e461EswA" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBloackerModule.kt b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBloackerModule.kt deleted file mode 100644 index aae7e57..0000000 --- a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBloackerModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.mercandalli.android.browser.ad_blocker - -import android.content.Context - -class AdBloackerModule { - - fun createAdBlockerManager(context: Context): AdBlockerManager { - val sharedPreferences = context.getSharedPreferences( - AdBlockerManagerImpl.PREFERENCE_NAME, - Context.MODE_PRIVATE - ) - return AdBlockerManagerImpl( - sharedPreferences - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManager.kt b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManager.kt index 7ec5247..bb5ba90 100644 --- a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManager.kt +++ b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManager.kt @@ -2,6 +2,8 @@ package com.mercandalli.android.browser.ad_blocker interface AdBlockerManager { + fun isFeatureAvailable(): Boolean + fun isEnabled(): Boolean fun setEnabled(enabled: Boolean) diff --git a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManagerImpl.kt b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManagerImpl.kt index a9beb95..9ee24a7 100644 --- a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManagerImpl.kt +++ b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerManagerImpl.kt @@ -1,35 +1,42 @@ package com.mercandalli.android.browser.ad_blocker import android.content.SharedPreferences +import com.mercandalli.android.browser.product.ProductManager class AdBlockerManagerImpl( - private val sharedPreferences: SharedPreferences + private val sharedPreferences: SharedPreferences, + private val productManager: ProductManager ) : AdBlockerManager { private var enabled = false - private var enabledLoaded = false + private var loaded = false + + override fun isFeatureAvailable() = productManager.isFullVersionAvailable() override fun isEnabled(): Boolean { + if (!isFeatureAvailable()) { + return false + } load() return enabled } override fun setEnabled(enabled: Boolean) { this.enabled = enabled - sharedPreferences.edit().putBoolean(KEY, enabled).apply() + sharedPreferences.edit().putBoolean(KEY_ENABLED, this.enabled).apply() } private fun load() { - if (enabledLoaded) { + if (loaded) { return } - enabledLoaded = true - enabled = sharedPreferences.getBoolean(KEY, enabled) + loaded = true + enabled = sharedPreferences.getBoolean(KEY_ENABLED, enabled) } companion object { @JvmStatic - val PREFERENCE_NAME = "AdBlockerManager" - private const val KEY = "ad-blocker-enabled" + val PREFERENCE_NAME = "ad-blocker" + private const val KEY_ENABLED = "enabled" } } \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerModule.kt b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerModule.kt new file mode 100644 index 0000000..fd5f3d8 --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/ad_blocker/AdBlockerModule.kt @@ -0,0 +1,21 @@ +package com.mercandalli.android.browser.ad_blocker + +import android.content.Context +import com.mercandalli.android.browser.main.ApplicationGraph + +class AdBlockerModule( + private val context: Context +) { + + fun createAdBlockerManager(): AdBlockerManager { + val sharedPreferences = context.getSharedPreferences( + AdBlockerManagerImpl.PREFERENCE_NAME, + Context.MODE_PRIVATE + ) + val productManager = ApplicationGraph.getProductManager() + return AdBlockerManagerImpl( + sharedPreferences, + productManager + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/browser/BrowserView.kt b/app/src/main/java/com/mercandalli/android/browser/browser/BrowserView.kt index 08de9ba..c48e95f 100644 --- a/app/src/main/java/com/mercandalli/android/browser/browser/BrowserView.kt +++ b/app/src/main/java/com/mercandalli/android/browser/browser/BrowserView.kt @@ -11,8 +11,6 @@ import android.view.View import android.webkit.* import com.mercandalli.android.browser.main.ApplicationGraph import com.mercandalli.android.browser.ad_blocker.AdBlocker -import com.mercandalli.android.browser.main.MainApplication -import com.mercandalli.android.libs.monetization.MonetizationGraph class BrowserView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 diff --git a/app/src/main/java/com/mercandalli/android/browser/main/ApplicationGraph.kt b/app/src/main/java/com/mercandalli/android/browser/main/ApplicationGraph.kt index 028b338..62e8bf0 100644 --- a/app/src/main/java/com/mercandalli/android/browser/main/ApplicationGraph.kt +++ b/app/src/main/java/com/mercandalli/android/browser/main/ApplicationGraph.kt @@ -2,20 +2,26 @@ package com.mercandalli.android.browser.main import android.annotation.SuppressLint import android.content.Context -import com.mercandalli.android.browser.ad_blocker.AdBloackerModule +import com.mercandalli.android.browser.ad_blocker.AdBlockerModule +import com.mercandalli.android.browser.product.ProductModule +import com.mercandalli.android.browser.remote_config.RemoteConfigModule import com.mercandalli.android.browser.theme.ThemeModule import com.mercandalli.android.browser.thread.MainThreadModule import com.mercandalli.android.browser.toast.ToastModule +import com.mercandalli.android.browser.update.UpdateModule import com.mercandalli.android.browser.version.VersionModule class ApplicationGraph( private val context: Context ) { - private val adBlockerManagerInternal by lazy { AdBloackerModule().createAdBlockerManager(context) } + private val adBlockerManagerInternal by lazy { AdBlockerModule(context).createAdBlockerManager() } + private val productManagerInternal by lazy { ProductModule().createProductManager() } private val mainThreadPostInternal by lazy { MainThreadModule().createMainThreadPost() } + private val remoteConfigInternal by lazy { RemoteConfigModule().createRemoteConfig(mainThreadPostInternal) } private val themeManagerInternal by lazy { ThemeModule(context).createThemeManager() } private val toastManagerInternal by lazy { ToastModule().createToastManager(context, mainThreadPostInternal) } + private val updateManagerInternal by lazy { UpdateModule().createUpdateManager(context, versionManagerInternal) } private val versionManagerInternal by lazy { VersionModule().createVersionManager(context) } companion object { @@ -34,12 +40,21 @@ class ApplicationGraph( @JvmStatic fun getAdBlockerManager() = graph!!.adBlockerManagerInternal + @JvmStatic + fun getProductManager() = graph!!.productManagerInternal + + @JvmStatic + fun getRemoteConfig() = graph!!.remoteConfigInternal + @JvmStatic fun getThemeManager() = graph!!.themeManagerInternal @JvmStatic fun getToastManager() = graph!!.toastManagerInternal + @JvmStatic + fun getUpdateManager() = graph!!.updateManagerInternal + @JvmStatic fun getVersionManager() = graph!!.versionManagerInternal } diff --git a/app/src/main/java/com/mercandalli/android/browser/main/MainActivity.kt b/app/src/main/java/com/mercandalli/android/browser/main/MainActivity.kt index f651786..84f18af 100644 --- a/app/src/main/java/com/mercandalli/android/browser/main/MainActivity.kt +++ b/app/src/main/java/com/mercandalli/android/browser/main/MainActivity.kt @@ -28,6 +28,7 @@ import com.mercandalli.android.browser.R import com.mercandalli.android.browser.browser.BrowserView import com.mercandalli.android.browser.keyboard.KeyboardUtils import com.mercandalli.android.browser.settings.SettingsActivity +import com.mercandalli.android.libs.monetization.MonetizationGraph class MainActivity : AppCompatActivity(), MainActivityContract.Screen { @@ -44,9 +45,16 @@ class MainActivity : AppCompatActivity(), MainActivityContract.Screen { private val browserWebViewListener = createBrowserWebViewListener() private val userAction = createUserAction() + private var forceDestroy = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (MonetizationGraph.startOnBoardingIfNeeded(this)) { + forceDestroy = true + finish() + MainApplication.onOnBoardingStarted() + return + } setContentView(R.layout.activity_main) setSupportActionBar(toolbar) more.setOnClickListener { showOverflowPopupMenu(more) } @@ -67,13 +75,19 @@ class MainActivity : AppCompatActivity(), MainActivityContract.Screen { } override fun onDestroy() { + super.onDestroy() + if (forceDestroy) { + return + } webView.browserWebViewListener = null userAction.onDestroy() - super.onDestroy() } override fun onSaveInstanceState(outState: Bundle?) { super.onSaveInstanceState(outState) + if (forceDestroy) { + return + } webView.saveState(outState) } diff --git a/app/src/main/java/com/mercandalli/android/browser/main/MainApplication.kt b/app/src/main/java/com/mercandalli/android/browser/main/MainApplication.kt index 4ba0b5b..ebb9206 100644 --- a/app/src/main/java/com/mercandalli/android/browser/main/MainApplication.kt +++ b/app/src/main/java/com/mercandalli/android/browser/main/MainApplication.kt @@ -3,11 +3,14 @@ package com.mercandalli.android.browser.main import android.app.Application import android.content.pm.ApplicationInfo import android.os.Build +import android.util.Log import android.webkit.WebView import com.crashlytics.android.Crashlytics import com.crashlytics.android.core.CrashlyticsCore import com.mercandalli.android.browser.BuildConfig import com.mercandalli.android.browser.ad_blocker.AdBlocker +import com.mercandalli.android.browser.remote_config.RemoteConfig +import com.mercandalli.android.libs.monetization.Monetization import com.mercandalli.android.libs.monetization.MonetizationGraph import com.mercandalli.android.libs.monetization.log.MonetizationLog import io.fabric.sdk.android.Fabric @@ -40,12 +43,21 @@ class MainApplication : Application() { private fun setupMonetizationGraph() { val monetizationLog = object : MonetizationLog { override fun d(tag: String, message: String) { - + Log.d(tag, message) + } + } + val activityAction = object : MonetizationGraph.ActivityAction { + override fun startFirstActivity() { + MainActivity.start(this@MainApplication) } } MonetizationGraph.init( this, - monetizationLog + Monetization.create( + SKU_SUBSCRIPTION_FULL_VERSION + ), + monetizationLog, + activityAction ) MonetizationGraph.getInAppManager().initialize() } @@ -66,6 +78,21 @@ class MainApplication : Application() { } companion object { - const val SKU_SUBSCRIPTION_ADS_BLOCKER = "googleplay.com.mercandalli.android.browser.subscription.1" + const val SKU_SUBSCRIPTION_FULL_VERSION = "googleplay.com.mercandalli.android.browser.subscription.1" + + @JvmStatic + fun onOnBoardingStarted() { + val remoteConfig = ApplicationGraph.getRemoteConfig() + updateOnBoardingStorePageAvailable(remoteConfig) + remoteConfig.registerListener(object : RemoteConfig.RemoteConfigListener { + override fun onInitialized() { + updateOnBoardingStorePageAvailable(remoteConfig) + } + }) + } + + private fun updateOnBoardingStorePageAvailable(remoteConfig: RemoteConfig) { + MonetizationGraph.setOnBoardingStorePageAvailable(remoteConfig.isOnBoardingStoreAvailable) + } } } diff --git a/app/src/main/java/com/mercandalli/android/browser/product/ProductManager.kt b/app/src/main/java/com/mercandalli/android/browser/product/ProductManager.kt new file mode 100644 index 0000000..3881181 --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/product/ProductManager.kt @@ -0,0 +1,8 @@ +package com.mercandalli.android.browser.product + +interface ProductManager { + + fun isFullVersionAvailable(): Boolean + + fun isSubscribeToFullVersion(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/product/ProductManagerImpl.kt b/app/src/main/java/com/mercandalli/android/browser/product/ProductManagerImpl.kt new file mode 100644 index 0000000..6b46f64 --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/product/ProductManagerImpl.kt @@ -0,0 +1,15 @@ +package com.mercandalli.android.browser.product + +import com.mercandalli.android.browser.main.MainApplication +import com.mercandalli.android.browser.remote_config.RemoteConfig +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +class ProductManagerImpl( + private val remoteConfig: RemoteConfig, + private val inAppManager: InAppManager +) : ProductManager { + + override fun isFullVersionAvailable() = remoteConfig.isFullVersionAvailable + + override fun isSubscribeToFullVersion() = inAppManager.isPurchased(MainApplication.SKU_SUBSCRIPTION_FULL_VERSION) +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/product/ProductModule.kt b/app/src/main/java/com/mercandalli/android/browser/product/ProductModule.kt new file mode 100644 index 0000000..c4191aa --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/product/ProductModule.kt @@ -0,0 +1,16 @@ +package com.mercandalli.android.browser.product + +import com.mercandalli.android.browser.main.ApplicationGraph +import com.mercandalli.android.libs.monetization.MonetizationGraph + +class ProductModule { + + fun createProductManager(): ProductManager { + val remoteConfig = ApplicationGraph.getRemoteConfig() + val inAppManager = MonetizationGraph.getInAppManager() + return ProductManagerImpl( + remoteConfig, + inAppManager + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfig.kt b/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfig.kt new file mode 100644 index 0000000..73bbe6c --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfig.kt @@ -0,0 +1,25 @@ +package com.mercandalli.android.browser.remote_config + +interface RemoteConfig { + + /** + * @return true if the [RemoteConfig] is initialized, false otherwise + */ + val isInitialized: Boolean + + val isFullVersionAvailable: Boolean + + val isOnBoardingStoreAvailable: Boolean + + fun registerListener(listener: RemoteConfigListener) + + fun unregisterListener(listener: RemoteConfigListener) + + interface RemoteConfigListener { + + /*** + * is triggered when the [RemoteConfig] is initialized + */ + fun onInitialized() + } +} diff --git a/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfigImpl.java b/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfigImpl.java new file mode 100644 index 0000000..8f92f5b --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfigImpl.java @@ -0,0 +1,103 @@ +package com.mercandalli.android.browser.remote_config; + +import com.google.firebase.remoteconfig.FirebaseRemoteConfig; +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; +import com.mercandalli.android.browser.BuildConfig; +import com.mercandalli.android.browser.thread.MainThreadPost; +import com.mercandalli.android.browser.update.UpdateManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import androidx.annotation.FloatRange; +import androidx.annotation.NonNull; + +/** + * This class is used to get the remote configuration from firebase + */ +class RemoteConfigImpl implements RemoteConfig { + + private static final HashMap defaultMap = new HashMap<>(); + private static final String FIREBASE_KEY_FULL_VERSION_AVAILABLE_PERCENT = "full_version_available_percent"; + private static final String FIREBASE_KEY_ON_BOARDING_STORE_PAGE_AVAILABLE_PERCENT = "on_boarding_store_page_available_percent"; + + static { + defaultMap.put(FIREBASE_KEY_FULL_VERSION_AVAILABLE_PERCENT, 0); + defaultMap.put(FIREBASE_KEY_ON_BOARDING_STORE_PAGE_AVAILABLE_PERCENT, 0); + } + + private final MainThreadPost mainThreadPost; + private final FirebaseRemoteConfig firebaseRemoteConfig; + @FloatRange(from = 0F, to = 1F) + private final float randomFullVersionAvailablePercent; + @FloatRange(from = 0F, to = 1F) + private final float randomOnBoardingStorePageAvailablePercent; + private final List listeners = new ArrayList<>(); + private boolean initialized = false; + + /* package */ RemoteConfigImpl( + UpdateManager updateManager, + MainThreadPost mainThreadPost, + @FloatRange(from = 0F, to = 1F) float randomFullVersionAvailablePercent, + @FloatRange(from = 0F, to = 1F) float randomOnBoardingStorePageAvailablePercent + ) { + firebaseRemoteConfig = FirebaseRemoteConfig.getInstance(); + this.mainThreadPost = mainThreadPost; + this.randomFullVersionAvailablePercent = randomFullVersionAvailablePercent; + this.randomOnBoardingStorePageAvailablePercent = randomOnBoardingStorePageAvailablePercent; + FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder() + .setDeveloperModeEnabled(BuildConfig.DEBUG) + .build(); + firebaseRemoteConfig.setConfigSettings(configSettings); + firebaseRemoteConfig.setDefaults(defaultMap); + boolean firstLaunchAfterUpdate = updateManager.isFirstLaunchAfterUpdate(); + boolean bypassCache = BuildConfig.DEBUG || firstLaunchAfterUpdate; + (bypassCache ? firebaseRemoteConfig.fetch(0) : firebaseRemoteConfig.fetch()) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + firebaseRemoteConfig.activateFetched(); + initialized = true; + notifyInitialized(); + } + } + ); + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public boolean isFullVersionAvailable() { + return randomFullVersionAvailablePercent <= firebaseRemoteConfig.getDouble(FIREBASE_KEY_FULL_VERSION_AVAILABLE_PERCENT); + } + + @Override + public boolean isOnBoardingStoreAvailable() { + return randomOnBoardingStorePageAvailablePercent <= firebaseRemoteConfig.getDouble(FIREBASE_KEY_ON_BOARDING_STORE_PAGE_AVAILABLE_PERCENT); + } + + @Override + public void registerListener(@NonNull RemoteConfigListener listener) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + @Override + public void unregisterListener(@NonNull RemoteConfigListener listener) { + listeners.remove(listener); + } + + private void notifyInitialized() { + if (!mainThreadPost.isOnMainThread()) { + mainThreadPost.post(this::notifyInitialized); + return; + } + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onInitialized(); + } + } +} diff --git a/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfigModule.kt b/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfigModule.kt new file mode 100644 index 0000000..72ed4eb --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/remote_config/RemoteConfigModule.kt @@ -0,0 +1,22 @@ +package com.mercandalli.android.browser.remote_config + +import com.mercandalli.android.browser.main.ApplicationGraph +import com.mercandalli.android.browser.thread.MainThreadPost +import java.util.* + +/** + * A [Module] for the remote config . + */ +class RemoteConfigModule { + + fun createRemoteConfig(mainThreadPost: MainThreadPost): RemoteConfig { + val updateManager = ApplicationGraph.getUpdateManager() + val random = Random() + return RemoteConfigImpl( + updateManager, + mainThreadPost, + random.nextFloat(), + random.nextFloat() + ) + } +} diff --git a/app/src/main/java/com/mercandalli/android/browser/settings/SettingsContract.kt b/app/src/main/java/com/mercandalli/android/browser/settings/SettingsContract.kt index 4dafce7..4f206d9 100644 --- a/app/src/main/java/com/mercandalli/android/browser/settings/SettingsContract.kt +++ b/app/src/main/java/com/mercandalli/android/browser/settings/SettingsContract.kt @@ -29,6 +29,14 @@ interface SettingsContract { fun hideAdBlockerRow() + fun showAdBlockSection() + + fun hideAdBlockSection() + + fun showAdBlockSectionLabel() + + fun hideAdBlockSectionLabel() + fun setAdBlockerEnabled(enabled: Boolean) } diff --git a/app/src/main/java/com/mercandalli/android/browser/settings/SettingsPresenter.kt b/app/src/main/java/com/mercandalli/android/browser/settings/SettingsPresenter.kt index 60e984c..0bd55a8 100644 --- a/app/src/main/java/com/mercandalli/android/browser/settings/SettingsPresenter.kt +++ b/app/src/main/java/com/mercandalli/android/browser/settings/SettingsPresenter.kt @@ -5,6 +5,7 @@ import com.android.billingclient.api.BillingClient import com.android.billingclient.api.SkuDetails import com.mercandalli.android.browser.ad_blocker.AdBlockerManager import com.mercandalli.android.browser.main.MainApplication +import com.mercandalli.android.browser.product.ProductManager import com.mercandalli.android.browser.theme.Theme import com.mercandalli.android.browser.theme.ThemeManager import com.mercandalli.android.browser.version.VersionManager @@ -15,7 +16,8 @@ class SettingsPresenter( private val themeManager: ThemeManager, private val versionManager: VersionManager, private val inAppManager: InAppManager, - private val adBlockerManager: AdBlockerManager + private val adBlockerManager: AdBlockerManager, + private val productManager: ProductManager ) : SettingsContract.UserAction { private val themeListener = createThemeListener() @@ -46,7 +48,7 @@ class SettingsPresenter( override fun onUnlockAdsBlocker(activityContainer: InAppManager.ActivityContainer) { inAppManager.purchase( activityContainer, - MainApplication.SKU_SUBSCRIPTION_ADS_BLOCKER, + MainApplication.SKU_SUBSCRIPTION_FULL_VERSION, BillingClient.SkuType.SUBS ) } @@ -74,17 +76,29 @@ class SettingsPresenter( } private fun syncAdBlockerRows( - isPurchased: Boolean = inAppManager.isPurchased(MainApplication.SKU_SUBSCRIPTION_ADS_BLOCKER), + isAdBlockAvailable: Boolean = adBlockerManager.isFeatureAvailable(), + isSubscribeToFullVersion: Boolean = productManager.isSubscribeToFullVersion(), isEnabled: Boolean = adBlockerManager.isEnabled() ) { - if (isPurchased) { + if (isSubscribeToFullVersion) { + screen.showAdBlockSection() + screen.showAdBlockSectionLabel() screen.hideAdBlockerUnlockRow() screen.showAdBlockerRow() - } else { - screen.showAdBlockerUnlockRow() + screen.setAdBlockerEnabled(isEnabled) + return + } + if (!isAdBlockAvailable) { + screen.hideAdBlockerUnlockRow() screen.hideAdBlockerRow() + screen.hideAdBlockSection() + screen.hideAdBlockSectionLabel() + return } - screen.setAdBlockerEnabled(isEnabled) + screen.showAdBlockSection() + screen.showAdBlockSectionLabel() + screen.showAdBlockerUnlockRow() + screen.hideAdBlockerRow() } private fun createThemeListener() = object : ThemeManager.ThemeListener { diff --git a/app/src/main/java/com/mercandalli/android/browser/settings/SettingsView.kt b/app/src/main/java/com/mercandalli/android/browser/settings/SettingsView.kt index 636f839..f9b5dd5 100644 --- a/app/src/main/java/com/mercandalli/android/browser/settings/SettingsView.kt +++ b/app/src/main/java/com/mercandalli/android/browser/settings/SettingsView.kt @@ -1,7 +1,6 @@ package com.mercandalli.android.browser.settings import android.content.Context -import android.os.Build import androidx.annotation.ColorRes import androidx.core.content.ContextCompat import android.util.AttributeSet @@ -10,7 +9,6 @@ import android.view.View import android.widget.CheckBox import android.widget.ScrollView import android.widget.TextView -import androidx.annotation.RequiresApi import androidx.cardview.widget.CardView import com.mercandalli.android.browser.R import com.mercandalli.android.browser.main.ApplicationGraph @@ -148,6 +146,22 @@ class SettingsView @JvmOverloads constructor( adBlockerCheckBox.isChecked = enabled } + override fun showAdBlockSection() { + adBlockerSection.visibility = VISIBLE + } + + override fun hideAdBlockSection() { + adBlockerSection.visibility = GONE + } + + override fun showAdBlockSectionLabel() { + adBlockerSectionLabel.visibility = VISIBLE + } + + override fun hideAdBlockSectionLabel() { + adBlockerSectionLabel.visibility = GONE + } + fun setActivityContainer(activityContainer: InAppManager.ActivityContainer) { this.activityContainer = activityContainer } @@ -165,12 +179,14 @@ class SettingsView @JvmOverloads constructor( val versionManager = ApplicationGraph.getVersionManager() val inAppManager = MonetizationGraph.getInAppManager() val adBlockerManager = ApplicationGraph.getAdBlockerManager() + val productManager = ApplicationGraph.getProductManager() SettingsPresenter( this, themeManager, versionManager, inAppManager, - adBlockerManager + adBlockerManager, + productManager ) } } \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/theme/ThemeModule.kt b/app/src/main/java/com/mercandalli/android/browser/theme/ThemeModule.kt index 3360775..658812e 100644 --- a/app/src/main/java/com/mercandalli/android/browser/theme/ThemeModule.kt +++ b/app/src/main/java/com/mercandalli/android/browser/theme/ThemeModule.kt @@ -11,6 +11,8 @@ class ThemeModule( ThemeManagerImpl.PREFERENCE_NAME, Context.MODE_PRIVATE ) - return ThemeManagerImpl(sharedPreferences) + return ThemeManagerImpl( + sharedPreferences + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/update/UpdateManager.kt b/app/src/main/java/com/mercandalli/android/browser/update/UpdateManager.kt new file mode 100644 index 0000000..0366c56 --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/update/UpdateManager.kt @@ -0,0 +1,6 @@ +package com.mercandalli.android.browser.update + +interface UpdateManager { + + fun isFirstLaunchAfterUpdate() : Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/update/UpdateManagerImpl.kt b/app/src/main/java/com/mercandalli/android/browser/update/UpdateManagerImpl.kt new file mode 100644 index 0000000..d9f11c2 --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/update/UpdateManagerImpl.kt @@ -0,0 +1,33 @@ +package com.mercandalli.android.browser.update + +import android.content.SharedPreferences +import com.mercandalli.android.browser.version.VersionManager +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.launch + +@Suppress("EXPERIMENTAL_FEATURE_WARNING") +class UpdateManagerImpl( + private val sharedPreferences: SharedPreferences, + private val versionManager: VersionManager +) : UpdateManager { + + private val lastVersionName: String by lazy { sharedPreferences.getString(KEY_LAST_VERSION_NAME, "1.00.00") } + private var firstLaunchAfterUpdate: Boolean? = null + + override fun isFirstLaunchAfterUpdate(): Boolean { + if (firstLaunchAfterUpdate == null) { + val versionName = versionManager.getVersionName() + firstLaunchAfterUpdate = versionName != lastVersionName + launch(CommonPool) { + sharedPreferences.edit().putString(KEY_LAST_VERSION_NAME, versionName).apply() + } + } + return firstLaunchAfterUpdate!! + } + + companion object { + @JvmStatic + val PREFERENCE_NAME = "UpdateManager" + private const val KEY_LAST_VERSION_NAME = "UpdateManager.KEY_LAST_VERSION_NAME" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/update/UpdateModule.kt b/app/src/main/java/com/mercandalli/android/browser/update/UpdateModule.kt new file mode 100644 index 0000000..cdcf1b3 --- /dev/null +++ b/app/src/main/java/com/mercandalli/android/browser/update/UpdateModule.kt @@ -0,0 +1,19 @@ +package com.mercandalli.android.browser.update + +import android.content.Context +import com.mercandalli.android.browser.version.VersionManager + +class UpdateModule { + + fun createUpdateManager( + context: Context, + versionManager: VersionManager + ): UpdateManager { + val sharedPreferences = context.getSharedPreferences( + UpdateManagerImpl.PREFERENCE_NAME, Context.MODE_PRIVATE) + return UpdateManagerImpl( + sharedPreferences, + versionManager + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mercandalli/android/browser/version/VersionManager.kt b/app/src/main/java/com/mercandalli/android/browser/version/VersionManager.kt index c3f8778..f84755d 100644 --- a/app/src/main/java/com/mercandalli/android/browser/version/VersionManager.kt +++ b/app/src/main/java/com/mercandalli/android/browser/version/VersionManager.kt @@ -4,6 +4,8 @@ import androidx.annotation.RequiresApi interface VersionManager { + fun getVersionName(): String + fun getBuildConfigVersionName(): String fun getBuildConfigVersionCode(): Int diff --git a/app/src/main/java/com/mercandalli/android/browser/version/VersionManagerImpl.kt b/app/src/main/java/com/mercandalli/android/browser/version/VersionManagerImpl.kt index ba0e604..f354b38 100644 --- a/app/src/main/java/com/mercandalli/android/browser/version/VersionManagerImpl.kt +++ b/app/src/main/java/com/mercandalli/android/browser/version/VersionManagerImpl.kt @@ -10,6 +10,8 @@ class VersionManagerImpl( private lateinit var packageInfo: PackageInfo + override fun getVersionName() = getBuildConfigVersionName() + override fun getBuildConfigVersionName() = BuildConfig.VERSION_NAME override fun getBuildConfigVersionCode() = BuildConfig.VERSION_CODE diff --git a/app/src/main/res/settings/layout/view_settings.xml b/app/src/main/res/settings/layout/view_settings.xml index 2168388..77b11a0 100644 --- a/app/src/main/res/settings/layout/view_settings.xml +++ b/app/src/main/res/settings/layout/view_settings.xml @@ -12,10 +12,10 @@ android:paddingTop="@dimen/default_space" android:paddingBottom="@dimen/default_space_half"> - - + + diff --git a/app/src/main/res/settings/layout/view_settings_ad_blocker.xml b/app/src/main/res/settings/layout/view_settings_ad_blocker.xml index 9ddb83a..72f7278 100644 --- a/app/src/main/res/settings/layout/view_settings_ad_blocker.xml +++ b/app/src/main/res/settings/layout/view_settings_ad_blocker.xml @@ -3,7 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical"> + package="com.mercandalli.android.libs.monetization" > + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/Monetization.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/Monetization.kt new file mode 100644 index 0000000..a21178c --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/Monetization.kt @@ -0,0 +1,27 @@ +package com.mercandalli.android.libs.monetization + +data class Monetization private constructor( + val subscriptionSku: String +) { + + companion object { + + fun create( + subscriptionSku: String + ): Monetization { + return Monetization( + subscriptionSku + ) + } + + fun toJson(monetization: Monetization): String { + return "${monetization.subscriptionSku}" + } + + fun fromJson(json: String): Monetization { + return create( + json + ) + } + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationGraph.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationGraph.kt index 8c506b6..15ac1dd 100644 --- a/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationGraph.kt +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationGraph.kt @@ -4,10 +4,15 @@ import android.annotation.SuppressLint import android.content.Context import com.mercandalli.android.libs.monetization.in_app.* import com.mercandalli.android.libs.monetization.log.MonetizationLog +import com.mercandalli.android.libs.monetization.on_boarding.OnBoardingActivity +import com.mercandalli.android.libs.monetization.on_boarding.OnBoardingModule +import com.mercandalli.android.libs.monetization.store.StoreActivity class MonetizationGraph( private val context: Context, - private val monetizationLog: MonetizationLog + private val monetization: Monetization, + private val monetizationLog: MonetizationLog, + private val activityAction: ActivityAction ) { private val inAppModule by lazy { @@ -18,6 +23,18 @@ class MonetizationGraph( inAppModule.createInAppManager(monetizationLog) } + private val monetizationManagerInternal by lazy { + MonetizationManagerImpl() + } + + private val onBoardingRepositoryInternal by lazy { + OnBoardingModule(context).createOnBoardingRepository() + } + + interface ActivityAction { + fun startFirstActivity() + } + companion object { @JvmStatic @@ -27,17 +44,60 @@ class MonetizationGraph( @JvmStatic fun init( context: Context, - monetizationLog: MonetizationLog + monetization: Monetization, + monetizationLog: MonetizationLog, + activityAction: ActivityAction ) { if (graph == null) { graph = MonetizationGraph( context.applicationContext, - monetizationLog + monetization, + monetizationLog, + activityAction ) } } + @JvmStatic + fun setOnBoardingStorePageAvailable(available: Boolean) { + val monetizationManager = getMonetizationManager() + monetizationManager.setOnBoardingStorePageAvailable(available) + } + + @JvmStatic + fun startOnBoardingIfNeeded(context: Context): Boolean { + val onBoardingRepository = getOnBoardingRepository() + if (onBoardingRepository.isOnBoardingEnded()) { + return false + } + OnBoardingActivity.start(context) + return true + } + + @JvmStatic + fun startStore(context: Context) { + val monetization = graph!!.monetization + StoreActivity.start(context, monetization) + } + + @JvmStatic + fun isOnBoardingStorePageSkipped() = getOnBoardingRepository().isOnBoardingStorePageSkipped() + @JvmStatic fun getInAppManager(): InAppManager = graph!!.inAppManagerInternal + + @JvmStatic + internal fun getMonetizationManager(): MonetizationManager = graph!!.monetizationManagerInternal + + @JvmStatic + internal fun getMonetization(): Monetization = graph!!.monetization + + @JvmStatic + internal fun getOnBoardingRepository() = graph!!.onBoardingRepositoryInternal + + @JvmStatic + internal fun startFirstActivity() { + graph!!.activityAction.startFirstActivity() + } } } \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationManager.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationManager.kt new file mode 100644 index 0000000..c536af4 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationManager.kt @@ -0,0 +1,17 @@ +package com.mercandalli.android.libs.monetization + +internal interface MonetizationManager { + + fun isOnBoardingStorePageAvailable(): Boolean + + fun setOnBoardingStorePageAvailable(enable: Boolean) + + fun registerMonetizationListener(listener: MonetizationListener) + + fun unregisterMonetizationListener(listener: MonetizationListener) + + interface MonetizationListener { + + fun onOnBoardingStorePageAvailableChanged() + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationManagerImpl.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationManagerImpl.kt new file mode 100644 index 0000000..bf15803 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/MonetizationManagerImpl.kt @@ -0,0 +1,31 @@ +package com.mercandalli.android.libs.monetization + +internal class MonetizationManagerImpl : MonetizationManager { + + private var monetizationEnabled = false + private val monetizationEnabledListeners = ArrayList() + + override fun isOnBoardingStorePageAvailable() = monetizationEnabled + + override fun setOnBoardingStorePageAvailable(enable: Boolean) { + monetizationEnabled = enable + for (listener in monetizationEnabledListeners) { + listener.onOnBoardingStorePageAvailableChanged() + } + } + + override fun registerMonetizationListener( + listener: MonetizationManager.MonetizationListener + ) { + if (monetizationEnabledListeners.contains(listener)) { + return + } + monetizationEnabledListeners.add(listener) + } + + override fun unregisterMonetizationListener( + listener: MonetizationManager.MonetizationListener + ) { + monetizationEnabledListeners.remove(listener) + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/in_app/InAppManagerImpl.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/in_app/InAppManagerImpl.kt index 36299f5..9539570 100644 --- a/monetization/src/main/java/com/mercandalli/android/libs/monetization/in_app/InAppManagerImpl.kt +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/in_app/InAppManagerImpl.kt @@ -71,7 +71,7 @@ internal class InAppManagerImpl( } } - override fun isPurchased(sku: String) = true//inAppRepository.isPurchased(sku) + override fun isPurchased(sku: String) = inAppRepository.isPurchased(sku) override fun registerListener(listener: InAppManager.Listener) { if (listeners.contains(listener)) { diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingActivity.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingActivity.kt new file mode 100644 index 0000000..6fed570 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingActivity.kt @@ -0,0 +1,39 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.mercandalli.android.libs.monetization.R +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +class OnBoardingActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_on_boarding) + + val onBoardingView = findViewById(R.id.activity_on_boarding_view) + onBoardingView.setCloseOnBoardingAction(object : OnBoardingView.CloseOnBoardingAction { + override fun closeOnBoarding() { + finish() + } + }) + onBoardingView.setCloseOnBoardingAction(object : InAppManager.ActivityContainer { + override fun get() = this@OnBoardingActivity + }) + } + + companion object { + + @JvmStatic + fun start(context: Context) { + val intent = Intent(context, OnBoardingActivity::class.java) + if (context !is Activity) { + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingContract.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingContract.kt new file mode 100644 index 0000000..f7ea008 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingContract.kt @@ -0,0 +1,49 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import androidx.annotation.IntRange +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +internal interface OnBoardingContract { + + interface UserAction { + + fun onAttached() + + fun onDetached() + + fun onPageChanged() + + fun onNextClicked() + + fun onStoreBuyClicked(activityContainer: InAppManager.ActivityContainer) + + fun onStoreSkipClicked() + } + + interface Screen { + + @IntRange(from = 0) + fun getPage(): Int + + fun setPage(@IntRange(from = 0) page: Int) + + @IntRange(from = 0) + fun getPageCount(): Int + + fun enableStorePage() + + fun disableStorePage() + + fun showNextButton() + + fun hideNextButton() + + fun showStoreButtons() + + fun hideStoreButtons() + + fun closeOnBoarding() + + fun startFistActivity() + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingModule.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingModule.kt new file mode 100644 index 0000000..3698939 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingModule.kt @@ -0,0 +1,17 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import android.content.Context + +class OnBoardingModule( + private val context: Context +) { + + fun createOnBoardingRepository(): OnBoardingRepository { + val sharedPreferences = context.getSharedPreferences( + OnBoardingRepositoryImpl.PREFERENCE_NAME, Context.MODE_PRIVATE) + return OnBoardingRepositoryImpl( + sharedPreferences + ) + } + +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPageContract.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPageContract.kt new file mode 100644 index 0000000..0f04438 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPageContract.kt @@ -0,0 +1,13 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +internal interface OnBoardingPageContract { + + interface UserAction { + + } + + interface Screen { + + } + +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPageView.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPageView.kt new file mode 100644 index 0000000..4f04e97 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPageView.kt @@ -0,0 +1,31 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.FloatRange +import androidx.annotation.LayoutRes +import com.mercandalli.android.libs.monetization.R + +class OnBoardingPageView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), + OnBoardingPageContract.Screen { + + private var card: View? = null + + fun initialize(@LayoutRes layout: Int) { + val view = LayoutInflater.from(context).inflate(layout, this) + card = view.findViewById(R.id.view_on_boarding_page_card) + } + + fun applyAlphaToChildren(@FloatRange(from = 0.0, to = 1.0) securedAlpha: Float) { + card?.let { + it.scaleX = securedAlpha * 0.06F + 0.94F + it.scaleY = securedAlpha * 0.15F + 0.85F + it.alpha = securedAlpha * 0.02F + 0.98F + } + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPresenter.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPresenter.kt new file mode 100644 index 0000000..ba7bdb0 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingPresenter.kt @@ -0,0 +1,98 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.SkuDetails +import com.mercandalli.android.libs.monetization.MonetizationManager +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +internal class OnBoardingPresenter( + private val screen: OnBoardingContract.Screen, + private val onBoardingRepository: OnBoardingRepository, + private val monetizationManager: MonetizationManager, + private val inAppManager: InAppManager, + private val subscriptionSku: String +) : OnBoardingContract.UserAction { + + private val monetizationEnabledListener = createMonetizationEnabledListener() + private val inAppManagerListener = createInAppManagerListener() + + override fun onAttached() { + monetizationManager.registerMonetizationListener(monetizationEnabledListener) + syncScreen() + } + + override fun onDetached() { + monetizationManager.unregisterMonetizationListener(monetizationEnabledListener) + inAppManager.unregisterListener(inAppManagerListener) + } + + override fun onPageChanged() { + syncScreen() + } + + override fun onNextClicked() { + val lastPage = isLastPage() + if (lastPage) { + onBoardingRepository.markOnBoardingEnded() + screen.closeOnBoarding() + screen.startFistActivity() + return + } + val page = screen.getPage() + screen.setPage(page + 1) + } + + override fun onStoreBuyClicked(activityContainer: InAppManager.ActivityContainer) { + inAppManager.registerListener(inAppManagerListener) + inAppManager.purchase(activityContainer, subscriptionSku, BillingClient.SkuType.SUBS) + } + + override fun onStoreSkipClicked() { + onBoardingRepository.markOnBoardingStorePageSkipped() + onBoardingRepository.markOnBoardingEnded() + screen.closeOnBoarding() + screen.startFistActivity() + } + + private fun syncScreen( + onBoardingStorePageAvailable: Boolean = monetizationManager.isOnBoardingStorePageAvailable() + ) { + if (onBoardingStorePageAvailable) { + screen.enableStorePage() + } else { + screen.disableStorePage() + } + val lastPage = isLastPage() + if (lastPage && onBoardingStorePageAvailable) { + inAppManager.initialize() + screen.hideNextButton() + screen.showStoreButtons() + } else { + screen.showNextButton() + screen.hideStoreButtons() + } + } + + private fun isLastPage(): Boolean { + val page = screen.getPage() + val pageCount = screen.getPageCount() + return page == pageCount - 1 + } + + private fun createMonetizationEnabledListener() = object : MonetizationManager.MonetizationListener { + override fun onOnBoardingStorePageAvailableChanged() { + syncScreen() + } + } + + private fun createInAppManagerListener() = object : InAppManager.Listener { + override fun onSkuDetailsChanged(skuDetails: SkuDetails) {} + override fun onPurchasedChanged() { + if (inAppManager.isPurchased(subscriptionSku)) { + onBoardingRepository.markOnBoardingEnded() + screen.closeOnBoarding() + screen.startFistActivity() + } + } + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingRepository.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingRepository.kt new file mode 100644 index 0000000..5f43301 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingRepository.kt @@ -0,0 +1,12 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +interface OnBoardingRepository { + + fun isOnBoardingEnded(): Boolean + + fun markOnBoardingEnded() + + fun isOnBoardingStorePageSkipped(): Boolean + + fun markOnBoardingStorePageSkipped() +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingRepositoryImpl.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingRepositoryImpl.kt new file mode 100644 index 0000000..2caeb87 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingRepositoryImpl.kt @@ -0,0 +1,58 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import android.content.SharedPreferences + +class OnBoardingRepositoryImpl( + private val sharedPreferences: SharedPreferences +) : OnBoardingRepository { + + private var onBoardingEnded = false + private var onBoardingEndedLoaded = false + private var onBoardingStoreSkipped = false + private var onBoardingStoreSkippedLoaded = false + + override fun isOnBoardingEnded(): Boolean { + loadSeen() + return onBoardingEnded + } + + override fun markOnBoardingEnded() { + loadSeen() + onBoardingEnded = true + sharedPreferences.edit().putBoolean(KEY_ON_BOARDING_ENDED, onBoardingEnded).apply() + } + + override fun isOnBoardingStorePageSkipped(): Boolean { + loadSkipped() + return onBoardingStoreSkipped + } + + override fun markOnBoardingStorePageSkipped() { + loadSkipped() + onBoardingStoreSkipped = true + sharedPreferences.edit().putBoolean(KEY_ON_BOARDING_STORE_SKIPPED, onBoardingStoreSkipped).apply() + } + + private fun loadSeen() { + if (onBoardingEndedLoaded) { + return + } + onBoardingEndedLoaded = true + onBoardingEnded = sharedPreferences.getBoolean(KEY_ON_BOARDING_ENDED, onBoardingEnded) + } + + private fun loadSkipped() { + if (onBoardingStoreSkippedLoaded) { + return + } + onBoardingStoreSkippedLoaded = true + onBoardingStoreSkipped = sharedPreferences.getBoolean(KEY_ON_BOARDING_STORE_SKIPPED, onBoardingStoreSkipped) + } + + companion object { + @JvmStatic + val PREFERENCE_NAME = "on-boarding" + private const val KEY_ON_BOARDING_ENDED = "on-boarding-ended" + private const val KEY_ON_BOARDING_STORE_SKIPPED = "on-boarding-store-skipped" + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingThumbnail.java b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingThumbnail.java new file mode 100644 index 0000000..8c3da96 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingThumbnail.java @@ -0,0 +1,108 @@ +package com.mercandalli.android.libs.monetization.on_boarding; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.mercandalli.android.libs.monetization.R; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; + +/** + * Defined width, this {@link ImageView} compute its height to conserve YouTube thumbnail ratio. + */ +public class OnBoardingThumbnail extends AppCompatImageView { + + private Attributes attributes; + + public OnBoardingThumbnail(final Context context) { + super(context); + init(context, null, 0); + } + + public OnBoardingThumbnail(final Context context, final AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public OnBoardingThumbnail(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + /** + * The {@link FrameLayout#onMeasure(int, int)}. Keep the YouTube ratio. + */ + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + if (attributes.ratioWidthReference) { + int originalWidth = View.MeasureSpec.getSize(widthMeasureSpec); + float ratio = attributes.ratioHeight / attributes.ratioWidth; + super.onMeasure( + View.MeasureSpec.makeMeasureSpec( + originalWidth, + View.MeasureSpec.EXACTLY + ), + View.MeasureSpec.makeMeasureSpec( + (int) (originalWidth * ratio), + View.MeasureSpec.EXACTLY + ) + ); + } else { + int originalHeight = View.MeasureSpec.getSize(heightMeasureSpec); + float ratio = attributes.ratioWidth / attributes.ratioHeight; + super.onMeasure( + View.MeasureSpec.makeMeasureSpec( + (int) (originalHeight * ratio), + View.MeasureSpec.EXACTLY + ), + View.MeasureSpec.makeMeasureSpec( + originalHeight, + View.MeasureSpec.EXACTLY + ) + ); + } + } + + private void init(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + attributes = extractAttributes(context, attrs, defStyleAttr); + } + + private Attributes extractAttributes(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.OnBoardingThumbnail, defStyleAttr, 0); + float ratioWidth = typedArray.getFloat(R.styleable.OnBoardingThumbnail_ratio_width, 1f); + float ratioHeight = typedArray.getFloat(R.styleable.OnBoardingThumbnail_ratio_height, 1f); + boolean ratioWidthReference = typedArray.getBoolean(R.styleable.OnBoardingThumbnail_ratio_width_reference, true); + typedArray.recycle(); + return new Attributes( + ratioWidth, + ratioHeight, + ratioWidthReference + ); + } + + private class Attributes { + + final float ratioWidth; + + final float ratioHeight; + + final boolean ratioWidthReference; + + Attributes( + float ratioWidth, + float ratioHeight, + boolean ratioWidthReference + ) { + this.ratioWidth = ratioWidth; + this.ratioHeight = ratioHeight; + this.ratioWidthReference = ratioWidthReference; + } + } +} diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingView.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingView.kt new file mode 100644 index 0000000..7c4994b --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/on_boarding/OnBoardingView.kt @@ -0,0 +1,204 @@ +package com.mercandalli.android.libs.monetization.on_boarding + +import android.content.Context +import android.util.AttributeSet +import android.util.SparseArray +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import androidx.annotation.NonNull +import androidx.viewpager.widget.PagerAdapter +import androidx.viewpager.widget.ViewPager +import com.mercandalli.android.libs.monetization.MonetizationGraph +import com.mercandalli.android.libs.monetization.R +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +class OnBoardingView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), + OnBoardingContract.Screen { + + private val layoutNamesWithoutStore = listOf( + R.layout.view_on_boading_page_1, + R.layout.view_on_boading_page_2 + ) + private val layoutNamesWithStore = listOf( + R.layout.view_on_boading_page_1, + R.layout.view_on_boading_page_2, + R.layout.view_on_boading_page_3 + ) + private var currentLayoutNames = layoutNamesWithoutStore + private val view = LayoutInflater.from(context).inflate(R.layout.view_on_boarding, this) + private val viewPager: ViewPager = view.findViewById(R.id.view_on_boarding_view_pager) + private val store: View = view.findViewById(R.id.view_on_boarding_store) + private val storeBuy: View = view.findViewById(R.id.view_on_boarding_store_buy) + private val storeSkip: View = view.findViewById(R.id.view_on_boarding_store_skip) + private val next: View = view.findViewById(R.id.view_on_boarding_next) + private val onPageChangeListener = createOnPageChangeListener() + private val pages = SparseArray() + private val adapter = createPagerAdapter() + private val userAction = createUserAction() + + private var closeOnBoardingAction: OnBoardingView.CloseOnBoardingAction? = null + private var activityContainer: InAppManager.ActivityContainer? = null + + init { + viewPager.adapter = adapter + viewPager.addOnPageChangeListener(onPageChangeListener) + next.setOnClickListener { + userAction.onNextClicked() + } + storeBuy.setOnClickListener { + userAction.onStoreBuyClicked(activityContainer!!) + } + storeSkip.setOnClickListener { + userAction.onStoreSkipClicked() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + userAction.onAttached() + } + + override fun onDetachedFromWindow() { + userAction.onDetached() + super.onDetachedFromWindow() + } + + @IntRange(from = 0) + override fun getPage() = viewPager.currentItem + + override fun setPage(@IntRange(from = 0) page: Int) { + viewPager.currentItem = page + } + + @IntRange(from = 0) + override fun getPageCount() = currentLayoutNames.size + + override fun enableStorePage() { + currentLayoutNames = layoutNamesWithStore + adapter.notifyDataSetChanged() + } + + override fun disableStorePage() { + currentLayoutNames = layoutNamesWithoutStore + adapter.notifyDataSetChanged() + } + + override fun showNextButton() { + next.visibility = VISIBLE + } + + override fun hideNextButton() { + next.visibility = GONE + } + + override fun showStoreButtons() { + store.visibility = VISIBLE + } + + override fun hideStoreButtons() { + store.visibility = GONE + } + + override fun closeOnBoarding() { + closeOnBoardingAction!!.closeOnBoarding() + } + + override fun startFistActivity() { + MonetizationGraph.startFirstActivity() + } + + fun setCloseOnBoardingAction(action: CloseOnBoardingAction) { + closeOnBoardingAction = action + } + + fun setCloseOnBoardingAction(activityContainer: InAppManager.ActivityContainer) { + this.activityContainer = activityContainer + } + + private fun createPagerAdapter() = object : PagerAdapter() { + + override fun getCount() = getPageCount() + + override fun isViewFromObject(@NonNull view: View, @NonNull item: Any): Boolean { + return view == item + } + + @NonNull + override fun instantiateItem(@NonNull container: ViewGroup, @IntRange(from = 0) position: Int): Any { + val layoutResForPage = currentLayoutNames[position] + val onBoardingPageView = OnBoardingPageView(container.context) + onBoardingPageView.initialize(layoutResForPage) + container.addView(onBoardingPageView) + pages.put(position, onBoardingPageView) + return onBoardingPageView + } + + override fun destroyItem(@NonNull container: ViewGroup, position: Int, @NonNull item: Any) { + val onBoardingPageView = item as OnBoardingPageView + container.removeView(onBoardingPageView) + } + } + + private fun createOnPageChangeListener() = object : ViewPager.OnPageChangeListener { + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + val onBoardingPageViewLeft = pages.get(position) + val onBoardingPageViewRight = pages.get(position + 1) + if (onBoardingPageViewLeft != null) { + @FloatRange(from = -1.0, to = 1.0) + val alpha = (1 - positionOffset) * 2 - 1 + @FloatRange(from = 0.0, to = 1.0) + val securedAlpha = Math.max(alpha, 0f) + onBoardingPageViewLeft.applyAlphaToChildren(securedAlpha) + } + if (onBoardingPageViewRight != null) { + @FloatRange(from = -1.0, to = 1.0) + val alpha = positionOffset * 2 - 1f + @FloatRange(from = 0.0, to = 1.0) + val securedAlpha = Math.max(alpha, 0f) + onBoardingPageViewRight.applyAlphaToChildren(securedAlpha) + } + } + + override fun onPageSelected(position: Int) { + userAction.onPageChanged() + } + + override fun onPageScrollStateChanged(state: Int) { + + } + } + + private fun createUserAction() = if (isInEditMode) { + object : OnBoardingContract.UserAction { + override fun onAttached() {} + override fun onDetached() {} + override fun onPageChanged() {} + override fun onNextClicked() {} + override fun onStoreBuyClicked(activityContainer: InAppManager.ActivityContainer) {} + override fun onStoreSkipClicked() {} + } + } else { + val monetizationManager = MonetizationGraph.getMonetizationManager() + val inAppManager = MonetizationGraph.getInAppManager() + val subscriptionSku = MonetizationGraph.getMonetization().subscriptionSku + val onBoardingRepository = MonetizationGraph.getOnBoardingRepository() + OnBoardingPresenter( + this, + onBoardingRepository, + monetizationManager, + inAppManager, + subscriptionSku + ) + } + + interface CloseOnBoardingAction { + fun closeOnBoarding() + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StoreActivity.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StoreActivity.kt new file mode 100644 index 0000000..948f339 --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StoreActivity.kt @@ -0,0 +1,53 @@ +package com.mercandalli.android.libs.monetization.store + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.mercandalli.android.libs.monetization.Monetization +import com.mercandalli.android.libs.monetization.MonetizationGraph +import com.mercandalli.android.libs.monetization.R +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +class StoreActivity : AppCompatActivity(), + StoreContract.Screen { + + private val userAction = createUserAction() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_store) + + findViewById(R.id.activity_store_buy_subscription).setOnClickListener { + userAction.onBuySubscriptionClicked(object : InAppManager.ActivityContainer { + override fun get() = this@StoreActivity + }) + } + val monetization = Monetization.fromJson(intent.extras.getString(EXTRA_MONETIZATION)) + userAction.onCreate(monetization) + } + + private fun createUserAction(): StoreContract.UserAction { + val inAppManager = MonetizationGraph.getInAppManager() + return StorePresenter( + this, + inAppManager + ) + } + + companion object { + private const val EXTRA_MONETIZATION = "StoreActivity.Extra.EXTRA_MONETIZATION" + + @JvmStatic + fun start(context: Context, monetization: Monetization) { + val intent = Intent(context, StoreActivity::class.java) + if (context !is Activity) { + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + intent.putExtra(EXTRA_MONETIZATION, Monetization.toJson(monetization)) + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StoreContract.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StoreContract.kt new file mode 100644 index 0000000..68ac7af --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StoreContract.kt @@ -0,0 +1,20 @@ +package com.mercandalli.android.libs.monetization.store + +import com.mercandalli.android.libs.monetization.Monetization +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +internal interface StoreContract { + + interface UserAction { + + fun onCreate(monetization: Monetization) + + fun onDestroy() + + fun onBuySubscriptionClicked(activityContainer: InAppManager.ActivityContainer) + } + + interface Screen { + + } +} \ No newline at end of file diff --git a/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StorePresenter.kt b/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StorePresenter.kt new file mode 100644 index 0000000..12f6f3c --- /dev/null +++ b/monetization/src/main/java/com/mercandalli/android/libs/monetization/store/StorePresenter.kt @@ -0,0 +1,26 @@ +package com.mercandalli.android.libs.monetization.store + +import com.android.billingclient.api.BillingClient +import com.mercandalli.android.libs.monetization.Monetization +import com.mercandalli.android.libs.monetization.in_app.InAppManager + +internal class StorePresenter( + private val screen: StoreContract.Screen, + private val inAppManager: InAppManager +) : StoreContract.UserAction { + + private lateinit var monetization: Monetization + + override fun onCreate(monetization: Monetization) { + this.monetization = monetization + inAppManager.initialize() + } + + override fun onDestroy() { + + } + + override fun onBuySubscriptionClicked(activityContainer: InAppManager.ActivityContainer) { + inAppManager.purchase(activityContainer, monetization.subscriptionSku, BillingClient.SkuType.SUBS) + } +} \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_1.png b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_1.png new file mode 100755 index 0000000..ffafa4f Binary files /dev/null and b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_1.png differ diff --git a/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_2.png b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_2.png new file mode 100755 index 0000000..7b2ebec Binary files /dev/null and b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_2.png differ diff --git a/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_3.png b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_3.png new file mode 100644 index 0000000..1044661 Binary files /dev/null and b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_3.png differ diff --git a/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_ic_launcher.png b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_ic_launcher.png new file mode 100755 index 0000000..55447d8 Binary files /dev/null and b/monetization/src/main/res/on_boarding/drawable-nodpi/on_boarding_ic_launcher.png differ diff --git a/monetization/src/main/res/on_boarding/drawable-v21/on_boarding_default_rounded_btn.xml b/monetization/src/main/res/on_boarding/drawable-v21/on_boarding_default_rounded_btn.xml new file mode 100644 index 0000000..c32952a --- /dev/null +++ b/monetization/src/main/res/on_boarding/drawable-v21/on_boarding_default_rounded_btn.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn.xml b/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn.xml new file mode 100644 index 0000000..1a1baec --- /dev/null +++ b/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn_normal.xml b/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn_normal.xml new file mode 100644 index 0000000..0ae126c --- /dev/null +++ b/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn_normal.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn_pressed.xml b/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn_pressed.xml new file mode 100644 index 0000000..f120bc3 --- /dev/null +++ b/monetization/src/main/res/on_boarding/drawable/on_boarding_default_rounded_btn_pressed.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/drawable/on_boarding_shadow_left.xml b/monetization/src/main/res/on_boarding/drawable/on_boarding_shadow_left.xml new file mode 100644 index 0000000..2b70d6a --- /dev/null +++ b/monetization/src/main/res/on_boarding/drawable/on_boarding_shadow_left.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/drawable/on_boarding_shadow_top.xml b/monetization/src/main/res/on_boarding/drawable/on_boarding_shadow_top.xml new file mode 100644 index 0000000..5222474 --- /dev/null +++ b/monetization/src/main/res/on_boarding/drawable/on_boarding_shadow_top.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_1.xml b/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_1.xml new file mode 100644 index 0000000..5468f2f --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_1.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_2.xml b/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_2.xml new file mode 100644 index 0000000..c6b15f3 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_2.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_3.xml b/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_3.xml new file mode 100644 index 0000000..0b43367 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout-land/view_on_boading_page_3.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout-land/view_on_boarding.xml b/monetization/src/main/res/on_boarding/layout-land/view_on_boarding.xml new file mode 100644 index 0000000..989d272 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout-land/view_on_boarding.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/monetization/src/main/res/on_boarding/layout/activity_on_boarding.xml b/monetization/src/main/res/on_boarding/layout/activity_on_boarding.xml new file mode 100644 index 0000000..bd6c390 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout/activity_on_boarding.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/monetization/src/main/res/on_boarding/layout/view_on_boading_page_1.xml b/monetization/src/main/res/on_boarding/layout/view_on_boading_page_1.xml new file mode 100644 index 0000000..2bf200f --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout/view_on_boading_page_1.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout/view_on_boading_page_2.xml b/monetization/src/main/res/on_boarding/layout/view_on_boading_page_2.xml new file mode 100644 index 0000000..6d5b1c1 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout/view_on_boading_page_2.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout/view_on_boading_page_3.xml b/monetization/src/main/res/on_boarding/layout/view_on_boading_page_3.xml new file mode 100644 index 0000000..14037c2 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout/view_on_boading_page_3.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/layout/view_on_boarding.xml b/monetization/src/main/res/on_boarding/layout/view_on_boarding.xml new file mode 100644 index 0000000..3c5f959 --- /dev/null +++ b/monetization/src/main/res/on_boarding/layout/view_on_boarding.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/monetization/src/main/res/on_boarding/values/color.xml b/monetization/src/main/res/on_boarding/values/color.xml new file mode 100644 index 0000000..755105f --- /dev/null +++ b/monetization/src/main/res/on_boarding/values/color.xml @@ -0,0 +1,7 @@ + + + #F0F0F0 + @android:color/white + #1565C0 + #0D47A1 + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/values/dimens.xml b/monetization/src/main/res/on_boarding/values/dimens.xml new file mode 100644 index 0000000..0c11d5c --- /dev/null +++ b/monetization/src/main/res/on_boarding/values/dimens.xml @@ -0,0 +1,6 @@ + + + 10dp + 10dp + 34dp + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/values/on_boarding_thumbnail_view_attrs.xml b/monetization/src/main/res/on_boarding/values/on_boarding_thumbnail_view_attrs.xml new file mode 100644 index 0000000..9b1b5ae --- /dev/null +++ b/monetization/src/main/res/on_boarding/values/on_boarding_thumbnail_view_attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/monetization/src/main/res/on_boarding/values/strings.xml b/monetization/src/main/res/on_boarding/values/strings.xml new file mode 100644 index 0000000..cc62c4e --- /dev/null +++ b/monetization/src/main/res/on_boarding/values/strings.xml @@ -0,0 +1,11 @@ + + Private navigation + • Erase your historic\n• Video streaming\n• Music streaming + Dark theme + Google page in black + Full version + • Ad blocker + NEXT + START FREE TRIAL + Use limited version without ad blocker + diff --git a/monetization/src/main/res/on_boarding/values/style.xml b/monetization/src/main/res/on_boarding/values/style.xml new file mode 100644 index 0000000..2c15d10 --- /dev/null +++ b/monetization/src/main/res/on_boarding/values/style.xml @@ -0,0 +1,6 @@ + + + +