diff --git a/.travis/before_install.sh b/.travis/before_install.sh index 2badc4e7ada..9036c0d6b8c 100644 --- a/.travis/before_install.sh +++ b/.travis/before_install.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -ev + if [ "$TRAVIS_PULL_REQUEST" = "false" ] then openssl aes-256-cbc -K $encrypted_6c4fc944fe71_key -iv $encrypted_6c4fc944fe71_iv -in .travis/secrets.tar.enc -out .travis/secrets.tar -d diff --git a/.travis/script.sh b/.travis/script.sh index 9df25b10fe7..fcdb2e2ab89 100644 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -1,9 +1,12 @@ #!/bin/bash +set -ev + echo $TRAVIS_COMMIT_MESSAGE > CHANGES.md export VERSION_CODE=`git rev-list --count HEAD` -./gradlew testReleaseUnitTest +./gradlew test +./gradlew lint if [ "$TRAVIS_PULL_REQUEST" = "false" ] then diff --git a/app/build.gradle b/app/build.gradle index a0470a368c2..8133bdedd9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,9 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' apply plugin: 'com.google.firebase.appdistribution' apply plugin: 'com.github.triplet.play' +apply plugin: 'de.mannodermaus.android-junit5' buildscript { repositories { @@ -11,15 +13,16 @@ buildscript { dependencies { classpath 'com.google.firebase:firebase-appdistribution-gradle:1.1.0' classpath 'com.github.triplet.gradle:play-publisher:2.5.0' + classpath 'de.mannodermaus.gradle.plugins:android-junit5:1.5.2.0' } } android { - compileSdkVersion 29 + compileSdkVersion projectSdkVersion.toInteger() defaultConfig { applicationId "io.homeassistant.companion.android" - minSdkVersion 21 + minSdkVersion projectMinSdkVersion targetSdkVersion 29 versionCode "${System.env.VERSION_CODE ?: 1}".toInteger() versionName "1.0.0" @@ -49,6 +52,17 @@ android { signingConfig signingConfigs.release } } + + testOptions { + unitTests.returnDefaultValues = true + junitPlatform { + filters { + engines { + include 'spek2' + } + } + } + } } play { @@ -56,13 +70,28 @@ play { } dependencies { + implementation project(':common') + implementation project(':domain') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:1.1.3" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + + implementation "com.google.dagger:dagger:${daggerVersion}" + kapt "com.google.dagger:dagger-compiler:${daggerVersion}" + + implementation "androidx.appcompat:appcompat:$appCompatVersion" + implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutversion" + + implementation("com.jakewharton.threetenabp:threetenabp:$threeTenAbpVersion") { + exclude group: 'org.threeten' + } - implementation "com.squareup.retrofit2:retrofit:2.6.2" - implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' - implementation "com.squareup.retrofit2:converter-jackson:2.6.2" + testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek2Version" + testImplementation "org.spekframework.spek2:spek-runner-junit5:$spek2Version" + testImplementation "org.assertj:assertj-core:$assertJVersion" + testImplementation "io.mockk:mockk:$mockkVersion" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/HomeAssistantApplication.kt b/app/src/main/java/io/homeassistant/companion/android/HomeAssistantApplication.kt index a5da9f0b797..256f3d80279 100644 --- a/app/src/main/java/io/homeassistant/companion/android/HomeAssistantApplication.kt +++ b/app/src/main/java/io/homeassistant/companion/android/HomeAssistantApplication.kt @@ -1,14 +1,27 @@ package io.homeassistant.companion.android import android.app.Application -import io.homeassistant.companion.android.api.Session +import com.jakewharton.threetenabp.AndroidThreeTen +import io.homeassistant.companion.android.common.dagger.AppComponent +import io.homeassistant.companion.android.common.dagger.Graph +import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor -class HomeAssistantApplication : Application() { +class HomeAssistantApplication : Application(), GraphComponentAccessor { + + lateinit var graph: Graph override fun onCreate() { super.onCreate() - Session.init(this) + AndroidThreeTen.init(this) + graph = Graph(this) + } + + override val appComponent: AppComponent + get() = graph.appComponent + + override fun urlUpdated() { + graph.urlUpdated() } } \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/PresenterComponent.kt b/app/src/main/java/io/homeassistant/companion/android/PresenterComponent.kt new file mode 100644 index 00000000000..31cfe54f5cd --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/PresenterComponent.kt @@ -0,0 +1,21 @@ +package io.homeassistant.companion.android + +import dagger.Component +import io.homeassistant.companion.android.common.dagger.AppComponent +import io.homeassistant.companion.android.launch.LaunchActivity +import io.homeassistant.companion.android.onboarding.authentication.AuthenticationFragment +import io.homeassistant.companion.android.onboarding.manual.ManualSetupFragment +import io.homeassistant.companion.android.webview.WebViewActivity + +@Component(dependencies = [AppComponent::class], modules = [PresenterModule::class]) +interface PresenterComponent { + + fun inject(activity: LaunchActivity) + + fun inject(fragment: AuthenticationFragment) + + fun inject(fragment: ManualSetupFragment) + + fun inject(activity: WebViewActivity) + +} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/PresenterModule.kt b/app/src/main/java/io/homeassistant/companion/android/PresenterModule.kt new file mode 100644 index 00000000000..d59d95e6c3f --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/PresenterModule.kt @@ -0,0 +1,72 @@ +package io.homeassistant.companion.android + +import dagger.Binds +import dagger.Module +import dagger.Provides +import io.homeassistant.companion.android.launch.LaunchPresenter +import io.homeassistant.companion.android.launch.LaunchPresenterImpl +import io.homeassistant.companion.android.launch.LaunchView +import io.homeassistant.companion.android.onboarding.authentication.AuthenticationPresenter +import io.homeassistant.companion.android.onboarding.authentication.AuthenticationPresenterImpl +import io.homeassistant.companion.android.onboarding.authentication.AuthenticationView +import io.homeassistant.companion.android.onboarding.manual.ManualSetupPresenter +import io.homeassistant.companion.android.onboarding.manual.ManualSetupPresenterImpl +import io.homeassistant.companion.android.onboarding.manual.ManualSetupView +import io.homeassistant.companion.android.webview.WebView +import io.homeassistant.companion.android.webview.WebViewPresenter +import io.homeassistant.companion.android.webview.WebViewPresenterImpl + +@Module(includes = [PresenterModule.Declaration::class]) +class PresenterModule { + + private lateinit var launchView: LaunchView + private lateinit var authenticationView: AuthenticationView + private lateinit var manualSetupView: ManualSetupView + private lateinit var webView: WebView + + constructor(launchView: LaunchView) { + this.launchView = launchView + } + + constructor(authenticationView: AuthenticationView) { + this.authenticationView = authenticationView + } + + constructor(manualSetupView: ManualSetupView) { + this.manualSetupView = manualSetupView + } + + constructor(webView: WebView) { + this.webView = webView + } + + @Provides + fun provideLaunchView() = launchView + + @Provides + fun provideAuthenticationView() = authenticationView + + @Provides + fun provideManualSetupView() = manualSetupView + + @Provides + fun provideWebView() = webView + + @Module + interface Declaration { + + @Binds + fun bindLaunchPresenter(presenter: LaunchPresenterImpl): LaunchPresenter + + @Binds + fun bindAuthenticationPresenterImpl(presenter: AuthenticationPresenterImpl): AuthenticationPresenter + + @Binds + fun bindManualSetupPresenter(presenter: ManualSetupPresenterImpl): ManualSetupPresenter + + @Binds + fun bindWebViewPresenterImpl(presenter: WebViewPresenterImpl): WebViewPresenter + + + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/api/HomeAssistantApi.kt b/app/src/main/java/io/homeassistant/companion/android/api/HomeAssistantApi.kt deleted file mode 100644 index 24e5ae9b475..00000000000 --- a/app/src/main/java/io/homeassistant/companion/android/api/HomeAssistantApi.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.homeassistant.companion.android.api - -import io.homeassistant.companion.android.BuildConfig -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.jackson.JacksonConverterFactory - - -class HomeAssistantApi(url: String) { - - private val retrofit = Retrofit.Builder() - .baseUrl(url) - .addConverterFactory(JacksonConverterFactory.create()) - .client( - OkHttpClient.Builder() - .addInterceptor(HttpLoggingInterceptor().apply { - level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE - }) - .build() - ) - .build() - - val authenticationService: AuthenticationService = retrofit.create(AuthenticationService::class.java) - -} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/api/RefreshToken.kt b/app/src/main/java/io/homeassistant/companion/android/api/RefreshToken.kt deleted file mode 100644 index 53b6ce7be92..00000000000 --- a/app/src/main/java/io/homeassistant/companion/android/api/RefreshToken.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.homeassistant.companion.android.api - -import com.fasterxml.jackson.annotation.JsonProperty - - -data class RefreshToken( - @JsonProperty("access_token") - val accessToken: String, - @JsonProperty("expires_in") - val expiresIn: Int, - @JsonProperty("token_type") - val tokenType: String -) \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/api/Session.kt b/app/src/main/java/io/homeassistant/companion/android/api/Session.kt deleted file mode 100644 index d6c9b3cd7e5..00000000000 --- a/app/src/main/java/io/homeassistant/companion/android/api/Session.kt +++ /dev/null @@ -1,86 +0,0 @@ -package io.homeassistant.companion.android.api - -import android.app.Application -import android.content.Context -import java.util.* - - -class Session private constructor(application: Application) { - - companion object { - private const val PREF_URL = "url" - private const val PREF_ACCESS_TOKEN = "access_token" - private const val PREF_EXPIRED_DATE = "expires_date" - private const val PREF_REFRESH_TOKEN = "refresh_token" - private const val PREF_TOKEN_TYPE = "token_type" - - @Volatile - private var INSTANCE: Session? = null - - fun init(application: Application) { - INSTANCE = Session(application) - } - - fun getInstance(): Session = INSTANCE ?: throw IllegalStateException("You should init the singleton first") - } - - private val sharedPreferences = application.getSharedPreferences("session", Context.MODE_PRIVATE) - - var token: Token? = null - private set - var url: String? = null - private set - - init { - if (sharedPreferences.contains(PREF_URL) && - sharedPreferences.contains(PREF_ACCESS_TOKEN) && sharedPreferences.contains(PREF_EXPIRED_DATE) && - sharedPreferences.contains(PREF_REFRESH_TOKEN) && sharedPreferences.contains(PREF_TOKEN_TYPE) - ) { - url = sharedPreferences.getString(PREF_URL, null)!! - token = Token( - sharedPreferences.getString(PREF_ACCESS_TOKEN, "")!!, - sharedPreferences.getLong(PREF_EXPIRED_DATE, 0), - sharedPreferences.getString(PREF_REFRESH_TOKEN, null)!!, - sharedPreferences.getString(PREF_TOKEN_TYPE, null)!! - ) - } - } - - fun registerSession(token: AuthorizationCode, url: String) { - this.token = Token(token.accessToken, expiresInToTimestamp(token.expiresIn), token.refreshToken, token.tokenType).apply { - saveSession(this) - } - this.url = url.apply { - saveUrl(this) - } - } - - fun registerRefreshToken(refreshToken: RefreshToken, url: String) { - token = (token?.copy( - accessToken = refreshToken.accessToken, - expiresTimestamp = expiresInToTimestamp(refreshToken.expiresIn), - tokenType = refreshToken.tokenType - ) ?: throw IllegalStateException("Unable ")) - .apply { - saveSession(this) - saveUrl(url) - } - } - - private fun saveSession(token: Token) { - sharedPreferences.edit() - .putString(PREF_ACCESS_TOKEN, token.accessToken) - .putLong(PREF_EXPIRED_DATE, token.expiresTimestamp) - .putString(PREF_REFRESH_TOKEN, token.refreshToken) - .putString(PREF_TOKEN_TYPE, token.tokenType) - .apply() - } - - private fun saveUrl(url: String) { - sharedPreferences.edit() - .putString(PREF_URL, url) - .apply() - } - - private fun expiresInToTimestamp(expiresIn: Int) = Calendar.getInstance().timeInMillis / 1000 + expiresIn -} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/api/Token.kt b/app/src/main/java/io/homeassistant/companion/android/api/Token.kt deleted file mode 100644 index a5b95b9e02d..00000000000 --- a/app/src/main/java/io/homeassistant/companion/android/api/Token.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.homeassistant.companion.android.api - -import java.util.* - - -data class Token( - val accessToken: String, - val expiresTimestamp: Long, - val refreshToken: String, - val tokenType: String -) { - fun isExpired() = expiresIn() < 0 - - fun expiresIn() = expiresTimestamp - Calendar.getInstance().timeInMillis / 1000 -} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/launch/LaunchActivity.kt b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchActivity.kt index f5a9b1c3e66..b915bbb09c4 100644 --- a/app/src/main/java/io/homeassistant/companion/android/launch/LaunchActivity.kt +++ b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchActivity.kt @@ -2,21 +2,43 @@ package io.homeassistant.companion.android.launch import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import io.homeassistant.companion.android.api.Session +import io.homeassistant.companion.android.DaggerPresenterComponent +import io.homeassistant.companion.android.PresenterModule +import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor import io.homeassistant.companion.android.onboarding.OnboardingActivity import io.homeassistant.companion.android.webview.WebViewActivity +import javax.inject.Inject -class LaunchActivity : AppCompatActivity() { +class LaunchActivity : AppCompatActivity(), LaunchView { + + @Inject lateinit var presenter: LaunchPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (Session.getInstance().url.isNullOrBlank()) { - startActivity(OnboardingActivity.newInstance(this)) - } else { - startActivity(WebViewActivity.newInstance(this)) - } + DaggerPresenterComponent + .builder() + .appComponent((application as GraphComponentAccessor).appComponent) + .presenterModule(PresenterModule(this)) + .build() + .inject(this) + + presenter.onViewReady() + } + + override fun displayWebview() { + startActivity(WebViewActivity.newInstance(this)) finish() } + + override fun displayOnBoarding() { + startActivity(OnboardingActivity.newInstance(this)) + finish() + } + + override fun onDestroy() { + presenter.onFinish() + super.onDestroy() + } } \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/launch/LaunchPresenter.kt b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchPresenter.kt new file mode 100644 index 00000000000..efea1aa866b --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchPresenter.kt @@ -0,0 +1,10 @@ +package io.homeassistant.companion.android.launch + + +interface LaunchPresenter { + + fun onViewReady() + + fun onFinish() + +} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/launch/LaunchPresenterImpl.kt b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchPresenterImpl.kt new file mode 100644 index 00000000000..02dd2a5021c --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchPresenterImpl.kt @@ -0,0 +1,29 @@ +package io.homeassistant.companion.android.launch + +import io.homeassistant.companion.android.domain.authentication.AuthenticationUseCase +import io.homeassistant.companion.android.domain.authentication.SessionState +import kotlinx.coroutines.* +import javax.inject.Inject + +class LaunchPresenterImpl @Inject constructor( + private val view: LaunchView, + private val authenticationUseCase: AuthenticationUseCase +) : LaunchPresenter { + + private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + + override fun onViewReady() { + mainScope.launch { + if (authenticationUseCase.getSessionState() == SessionState.CONNECTED) { + view.displayWebview() + } else { + view.displayOnBoarding() + } + } + } + + override fun onFinish() { + mainScope.cancel() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/homeassistant/companion/android/launch/LaunchView.kt b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchView.kt new file mode 100644 index 00000000000..b843966e6e4 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/launch/LaunchView.kt @@ -0,0 +1,10 @@ +package io.homeassistant.companion.android.launch + + +interface LaunchView { + + fun displayWebview() + + fun displayOnBoarding() + +} diff --git a/app/src/main/java/io/homeassistant/companion/android/onboarding/ManualSetupFragment.kt b/app/src/main/java/io/homeassistant/companion/android/onboarding/ManualSetupFragment.kt deleted file mode 100644 index 3469b20a48d..00000000000 --- a/app/src/main/java/io/homeassistant/companion/android/onboarding/ManualSetupFragment.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.homeassistant.companion.android.onboarding - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.EditText -import androidx.fragment.app.Fragment -import io.homeassistant.companion.android.R - - -class ManualSetupFragment : Fragment() { - - companion object { - fun newInstance(): ManualSetupFragment { - return ManualSetupFragment() - } - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_manual_setup, container, false).apply { - findViewById