Commit 8d1ac18c authored by Amit Kumar's avatar Amit Kumar 💻

Initial implementation of MVI and clean architecture

parent de25a81b
Pipeline #43129 failed with stage
in 4 minutes and 16 seconds
<component name="ProjectDictionaryState">
<dictionary name="amit">
<words>
<w>badging</w>
<w>flowable</w>
<w>interactor</w>
<w>interactors</w>
<w>unsuspend</w>
</words>
</dictionary>
</component>
\ No newline at end of file
......@@ -111,7 +111,7 @@ dependencies {
// Rx Relay
implementation "com.jakewharton.rxrelay2:rxrelay:2.1.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61"
// Blur Library
implementation 'com.hoko:hoko-blur:1.3.4'
......
package foundation.e.blisslauncher.core.customviews;
import android.animation.LayoutTransition;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import foundation.e.blisslauncher.core.customviews.pageindicators.PageIndicatorDots;
import foundation.e.blisslauncher.features.launcher.LauncherActivity;
public class Workspace extends PagedView<PageIndicatorDots> implements View.OnTouchListener{
private static final String TAG = "Workspace";
private static final int DEFAULT_PAGE = 0;
private final LauncherActivity mLauncher;
private LayoutTransition mLayoutTransition;
public Workspace(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public Workspace(Context context, AttributeSet attributeSet, int defStyle) {
super(context, attributeSet, defStyle);
mLauncher = LauncherActivity.getLauncher(context);
setHapticFeedbackEnabled(false);
initWorkspace();
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
}
private void initWorkspace() {
mCurrentPage = DEFAULT_PAGE;
setClipToPadding(false);
setupLayoutTransition();
//setWallpaperDimension();
}
private void setupLayoutTransition() {
// We want to show layout transitions when pages are deleted, to close the gap.
mLayoutTransition = new LayoutTransition();
mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
setLayoutTransition(mLayoutTransition);
}
void enableLayoutTransitions() {
setLayoutTransition(mLayoutTransition);
}
void disableLayoutTransitions() {
setLayoutTransition(null);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
}
import foundation.e.blisslauncher.buildsrc.Libs
import foundation.e.blisslauncher.buildsrc.Versions
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion Versions.compile_sdk
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "foundation.e.blisslauncher.v2"
minSdkVersion Versions.min_sdk
targetSdkVersion Versions.target_sdk
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(path: ':common')
implementation project(path: ':data-bridge')
implementation project(path: ':domain')
implementation Libs.Kotlin.stdlib
implementation Libs.AndroidX.appcompat
implementation Libs.AndroidX.recyclerview
implementation Libs.AndroidX.coreKtx
implementation Libs.AndroidX.constraintlayout
// Rx
implementation Libs.RxJava.rxKotlin
implementation Libs.RxJava.rxJava
implementation Libs.RxJava.rxAndroid
implementation Libs.Dagger.dagger
implementation Libs.Dagger.android
kapt Libs.Dagger.compiler
kapt Libs.Dagger.androidProcessor
implementation Libs.timber
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
# 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
package foundation.e.blisslauncher
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("foundation.e.blisslauncher", appContext.packageName)
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="foundation.e.blisslauncher">
<application
android:name=".BlissLauncher"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".features.launcher.LauncherActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package foundation.e.blisslauncher
import android.app.Application
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import foundation.e.blisslauncher.databridge.DataBridgeInitializer
import foundation.e.blisslauncher.domain.inject.DomainComponent
import foundation.e.blisslauncher.inject.DaggerAppComponent
import timber.log.Timber
import javax.inject.Inject
class BlissLauncher : Application(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun onCreate() {
super.onCreate()
DataBridgeInitializer.initialize(this)
DaggerAppComponent.factory().create(
this, DomainComponent.INSTANCE
).inject(this)
setupTimber()
}
private fun setupTimber() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
override fun androidInjector(): AndroidInjector<Any> {
return androidInjector
}
}
\ No newline at end of file
package foundation.e.blisslauncher.base
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import androidx.annotation.IntDef
import foundation.e.blisslauncher.common.util.SystemUiController
import javax.inject.Inject
open class BaseActivity : Activity() {
/*val dpChangeListeners = ArrayList<DeviceProfile.OnDeviceProfileChangeListener>()
@Inject
lateinit var deviceProfile: DeviceProfile*/
@Inject
lateinit var systemUiController: SystemUiController
@Retention(AnnotationRetention.SOURCE)
@IntDef(
flag = true,
value = [ACTIVITY_STATE_STARTED, ACTIVITY_STATE_RESUMED, ACTIVITY_STATE_USER_ACTIVE]
)
annotation class ActivityFlags
@ActivityFlags
private var activityFlags: Int = 0
val isStarted: Boolean
get() = activityFlags and ACTIVITY_STATE_STARTED != 0
val hasBeenResumed: Boolean
get() = activityFlags and ACTIVITY_STATE_RESUMED != 0
override fun onStart() {
activityFlags = activityFlags or ACTIVITY_STATE_STARTED
super.onStart()
}
override fun onResume() {
activityFlags = activityFlags or ACTIVITY_STATE_RESUMED or ACTIVITY_STATE_USER_ACTIVE
super.onResume()
}
override fun onUserLeaveHint() {
activityFlags = activityFlags and ACTIVITY_STATE_USER_ACTIVE.inv()
super.onUserLeaveHint()
}
override fun onPause() {
activityFlags = activityFlags and ACTIVITY_STATE_RESUMED.inv()
super.onPause()
}
override fun onStop() {
super.onStop()
activityFlags =
activityFlags and ACTIVITY_STATE_STARTED.inv() and ACTIVITY_STATE_USER_ACTIVE.inv()
}
protected fun dispatchDeviceProfileChanged() {
//dpChangeListeners.forEach { it.onDeviceProfileChanged(deviceProfile) }
}
companion object {
private const val ACTIVITY_STATE_STARTED = 1 shl 0
private const val ACTIVITY_STATE_RESUMED = 1 shl 1
private const val ACTIVITY_STATE_USER_ACTIVE = 1 shl 2
fun fromContext(context: Context): BaseActivity =
if (context is BaseActivity) context
else ((context as ContextWrapper).baseContext) as BaseActivity
}
}
\ No newline at end of file
package foundation.e.blisslauncher.base
import android.app.ActivityOptions
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.os.Process
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import android.view.ActionMode
import android.view.View
import android.widget.Toast
import foundation.e.blisslauncher.R
import foundation.e.blisslauncher.common.Utilities
import foundation.e.blisslauncher.common.compat.LauncherAppsCompat
import foundation.e.blisslauncher.domain.entity.LauncherConstants
import foundation.e.blisslauncher.domain.entity.LauncherItem
import javax.inject.Inject
/**
* BaseActivity Extension with the support of Drag and Drop
*/
abstract class BaseDraggingActivity : BaseActivity() {
private var currentActionMode: ActionMode? = null
protected var isSafeModeEnabled = false
@Inject
lateinit var launcherAppsRepository: LauncherAppsCompat
// TODO Replace with LauncherTheme
var themeRes: Int = R.style.AppTheme
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isSafeModeEnabled = packageManager.isSafeMode
setTheme(themeRes)
}
override fun onActionModeStarted(mode: ActionMode?) {
super.onActionModeStarted(mode)
currentActionMode = mode
}
override fun onActionModeFinished(mode: ActionMode?) {
super.onActionModeFinished(mode)
currentActionMode = null
}
abstract fun getRootView(): View
abstract fun invalidateParent(launcherItem: LauncherItem)
fun getViewBounds(v: View): Rect {
val pos = IntArray(2)
v.getLocationOnScreen(pos)
return Rect(pos[0], pos[1], pos[0] + v.width, pos[1] + v.height)
}
abstract fun getActivityLaunchOptions(v: View): ActivityOptions?
fun getActivityLaunchOptionsAsBundle(v: View): Bundle? {
val activityOptions = getActivityLaunchOptions(v)
return activityOptions?.toBundle()
}
fun startActivitySafely(v: View, intent: Intent, item: LauncherItem?): Boolean {
if (isSafeModeEnabled && !Utilities.isSystemApp(this, intent)) {
Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show()
return false
}
val useLaunchAnimation = !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION)
val optsBundle = if (useLaunchAnimation) getActivityLaunchOptionsAsBundle(v) else null
val user = item?.user
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.sourceBounds = getViewBounds(v)
try {
//TODO
/*val isShortcut = (item is ShortcutInfo
&& (item!!.itemType === Favorites.ITEM_TYPE_SHORTCUT
|| item!!.itemType === Favorites.ITEM_TYPE_DEEP_SHORTCUT)
&& !(item as ShortcutInfo).isPromise()) */
val isShortcut = false
if (isShortcut)
startShortcutIntentSafely(intent, optsBundle!!, item!!)
else if (user == null || user == Process.myUserHandle()) {
startActivity(intent, optsBundle)
} else launcherAppsRepository.startActivityForProfile(
intent.component,
user,
intent.sourceBounds,
optsBundle
)
return true
} catch (e: Exception) {
when (e) {
is SecurityException, is ActivityNotFoundException -> Toast.makeText(
this,
R.string.activity_not_found,
Toast.LENGTH_SHORT
).show()
else -> throw e
}
}
return false
}
private fun startShortcutIntentSafely(
intent: Intent,
optsBundle: Bundle,
item: LauncherItem
) {
try {
val oldPolicy = StrictMode.getVmPolicy()
try {
// Temporarily disable deathPenalty on all default checks. For eg, shortcuts
// containing file Uri's would cause a crash as penaltyDeathOnFileUriExposure
// is enabled by default on NYC.
StrictMode.setVmPolicy(
VmPolicy.Builder().detectAll()
.penaltyLog().build()
)
if (item.itemType == LauncherConstants.ItemType.DEEP_SHORTCUT) {
/*val id: String = (info as ShortcutInfo).getDeepShortcutId()
val packageName = intent.getPackage()
DeepShortcutManager.getInstance(this).startShortcut(
packageName, id, intent.sourceBounds, optsBundle, info.user
)*/
} else { // Could be launching some bookkeeping activity
startActivity(intent, optsBundle)
}
} finally {
StrictMode.setVmPolicy(oldPolicy)
}
} catch (e: SecurityException) {
throw e
}
}
companion object {
private const val TAG = "BaseDraggingActivity"
const val INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
"foundation.e.blisslauncher.intent.extra.shortcut.IGNORE_LAUNCH_ANIMATION"
val AUTO_CANCEL_ACTION_MODE = Any()
fun fromContext(context: Context): BaseDraggingActivity =
if (context is BaseDraggingActivity) context
else ((context as ContextWrapper).baseContext) as BaseDraggingActivity
}
}
\ No newline at end of file
package foundation.e.blisslauncher.base.presentation
interface BaseIntent<T> {
fun reduce(oldState: T): T
}
/**
*
* NOTE: Magic of extension functions, (T)->T and T.()->T interchangeable.
*/
fun <T> intent(block: T.() -> T): BaseIntent<T> = object :
BaseIntent<T> {
override fun reduce(oldState: T): T = block(oldState)
}
/**
* By delegating work to other models, repositories or services, we
* end up with situations where we don't need to update our ModelStore
* state until the delegated work completes.
*
* Use the `sideEffect {}` DSL function for those situations.
*/
fun <T> sideEffect(block: T.() -> Unit): BaseIntent<T> = object :
BaseIntent<T> {
override fun reduce(oldState: T): T = oldState.apply(block)
}
\ No newline at end of file
package foundation.e.blisslauncher.base.presentation
interface BaseView<in State : BaseViewState> {
fun render(state: State)
}
\ No newline at end of file
package foundation.e.blisslauncher.base.presentation
interface BaseViewEvent
\ No newline at end of file
package foundation.e.blisslauncher.base.presentation
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.subjects.PublishSubject
import timber.log.Timber
abstract class BaseViewModel<S : BaseViewState>(initialState: S) :
Model<S> {
// State reducers
private val intents = PublishSubject.create<BaseIntent<S>>()