Compare commits

...

11 Commits

17 changed files with 1210 additions and 777 deletions

View File

@ -11,7 +11,7 @@ steps:
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" - ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\""
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Building..." - echo "Building..."
- ./gradlew :androidApp:build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false - ./gradlew build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"
- echo "Testing..." - echo "Testing..."
- echo "---------------------------------------------------------" - echo "---------------------------------------------------------"

View File

@ -58,7 +58,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
compileSdk = 31 compileSdk = 32
buildToolsVersion = "31.0.0" buildToolsVersion = "31.0.0"
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
@ -66,7 +66,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android" applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 21 minSdk = 21
targetSdk = 31 targetSdk = 32
versionCode = versionCodeFromGit() versionCode = versionCodeFromGit()
versionName = versionNameFromGit() versionName = versionNameFromGit()
@ -79,6 +79,11 @@ android {
// tests // tests
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes { buildTypes {
getByName("release") { getByName("release") {
isMinifyEnabled = true isMinifyEnabled = true
@ -98,9 +103,6 @@ android {
dimension = "build" dimension = "build"
} }
} }
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "bou.amine.apps.readerforselfossv2.android" namespace = "bou.amine.apps.readerforselfossv2.android"
} }
@ -114,13 +116,6 @@ dependencies {
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.1.1")
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0-alpha02")
androidTestImplementation("androidx.test:runner:1.3.1-alpha02")
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.4.0-alpha02")
// Espresso-intents for validation and stubbing of Intents
androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0-alpha02")
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs"))) implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
// Android Support // Android Support
@ -188,9 +183,6 @@ dependencies {
implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.core:core-ktx:1.8.0")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
// implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
// implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
// Network information // Network information
@ -198,4 +190,23 @@ dependencies {
// SQLDELIGHT // SQLDELIGHT
implementation("com.squareup.sqldelight:android-driver:1.5.3") implementation("com.squareup.sqldelight:android-driver:1.5.3")
//test
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
}
tasks.withType<Test> {
outputs.upToDateWhen { false }
useJUnit()
testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
)
showStandardStreams = true
}
} }

View File

@ -90,3 +90,5 @@
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. # @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-dontwarn io.mockk.**
-keep class io.mockk.** { *; }

View File

@ -93,6 +93,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
} }
private fun goToMain() { private fun goToMain() {
CoroutineScope(Dispatchers.Main).launch {
repository.updateApiVersion()
}
val intent = Intent(this, HomeActivity::class.java) val intent = Intent(this, HomeActivity::class.java)
startActivity(intent) startActivity(intent)
finish() finish()

View File

@ -14,6 +14,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
@ -28,6 +29,7 @@ import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.* import org.kodein.di.*
@ -37,7 +39,7 @@ class MyApp : MultiDexApplication(), DIAware {
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus, instance()) } bind<Repository>() with singleton { Repository(instance(), instance(), isConnectionAvailable, instance()) }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
@ -47,6 +49,9 @@ class MyApp : MultiDexApplication(), DIAware {
private val connectivityStatus: ConnectivityStatus by instance() private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance() private val driverFactory: DriverFactory by instance()
// TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Napier.base(DebugAntilog()) Napier.base(DebugAntilog())

View File

@ -354,6 +354,7 @@ class ArticleFragment : Fragment(), DIAware {
binding.webcontent.settings.javaScriptEnabled = false binding.webcontent.settings.javaScriptEnabled = false
binding.webcontent.webViewClient = object : WebViewClient() { binding.webcontent.webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean { override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
@ -361,6 +362,7 @@ class ArticleFragment : Fragment(), DIAware {
return true return true
} }
@Deprecated("Deprecated in Java")
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) { if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {

View File

@ -1,21 +1,19 @@
buildscript { buildscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies { dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
classpath("com.android.tools.build:gradle:7.3.0")
// sonarquve
classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513")
// SqlDelight // SqlDelight
classpath("com.squareup.sqldelight:gradle-plugin:1.5.3") classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")
} }
} }
plugins {
//trick: for the same plugin versions in all sub-modules
id("com.android.application").version("7.3.1").apply(false)
id("com.android.library").version("7.3.1").apply(false)
kotlin("android").version("1.7.20").apply(false)
kotlin("multiplatform").version("1.7.20").apply(false)
id("org.sonarqube").version("3.4.0.2513").apply(false)
}
apply(plugin = "org.sonarqube") apply(plugin = "org.sonarqube")
allprojects { allprojects {
@ -27,6 +25,7 @@ allprojects {
} }
} }
tasks.register("clean", Delete::class) { tasks.register("clean", Delete::class) {
delete(rootProject.buildDir) delete(rootProject.buildDir)
} }

View File

@ -11,14 +11,27 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
#Tue Mar 22 16:50:00 CET 2022 #Tue Mar 22 16:50:00 CET 2022
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
#Kotlin
kotlin.code.style=official kotlin.code.style=official
kotlin.mpp.enableCInteropCommonization=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" #Android
kotlin.native.enableDependencyPropagation=false
android.useAndroidX=true android.useAndroidX=true
kotlin.native.enableDependencyPropagation=false
#android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=true
#MPP
kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.mpp.enableGranularSourceSetsMetadata=true
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
ignoreGitVersion=false ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none
pushCache=true pushCache=true

View File

@ -1,19 +1,19 @@
// !$*UTF8*$! // !$*UTF8*$!
{ {
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 50; objectVersion = 50;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
7555FFB4242A642300829871 /* Embed Frameworks */ = { 7555FFB4242A642300829871 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -24,18 +24,18 @@
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; }; 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
7555FF78242A565900829871 /* Frameworks */ = { 7555FF78242A565900829871 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -43,9 +43,9 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
058557D7273AAEEB004C7B11 /* Preview Content */ = { 058557D7273AAEEB004C7B11 /* Preview Content */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -53,7 +53,7 @@
); );
path = "Preview Content"; path = "Preview Content";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7555FF72242A565900829871 = { 7555FF72242A565900829871 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -90,9 +90,9 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
7555FF7A242A565900829871 /* iosApp */ = { 7555FF7A242A565900829871 /* iosApp */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
@ -112,9 +112,9 @@
productReference = 7555FF7B242A565900829871 /* iosApp.app */; productReference = 7555FF7B242A565900829871 /* iosApp.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
7555FF73242A565900829871 /* Project object */ = { 7555FF73242A565900829871 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
@ -143,9 +143,9 @@
7555FF7A242A565900829871 /* iosApp */, 7555FF7A242A565900829871 /* iosApp */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
7555FF79242A565900829871 /* Resources */ = { 7555FF79242A565900829871 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -155,9 +155,9 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
7555FFB5242A651A00829871 /* ShellScript */ = { 7555FFB5242A651A00829871 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -175,9 +175,9 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
7555FF77242A565900829871 /* Sources */ = { 7555FF77242A565900829871 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -187,9 +187,9 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
7555FFA3242A565B00829871 /* Debug */ = { 7555FFA3242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@ -356,9 +356,9 @@
}; };
name = Release; name = Release;
}; };
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@ -377,7 +377,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 7555FF73242A565900829871 /* Project object */; rootObject = 7555FF73242A565900829871 /* Project object */;
} }

View File

@ -2,12 +2,9 @@ import SwiftUI
import shared import shared
struct ContentView: View { struct ContentView: View {
let greet = Greeting().greeting()
let toto = SelfossApi().getItems()
var body: some View { var body: some View {
Text(greet) Text("ototot")
} }
} }

View File

@ -8,6 +8,14 @@ pluginManagement {
} }
} }
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
buildCache { buildCache {
remote<HttpBuildCache> { remote<HttpBuildCache> {
url = uri("http://18.0.0.7:3071/cache/") url = uri("http://18.0.0.7:3071/cache/")

View File

@ -18,7 +18,7 @@ kotlin {
listOf( listOf(
iosX64(), iosX64(),
iosArm64(), iosArm64(),
//iosSimulatorArm64() sure all ios dependencies support this target // iosSimulatorArm64()
).forEach { ).forEach {
it.binaries.framework { it.binaries.framework {
baseName = "shared" baseName = "shared"
@ -40,14 +40,11 @@ kotlin {
implementation("org.kodein.di:kodein-di:7.12.0") implementation("org.kodein.di:kodein-di:7.12.0")
//Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9") implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
//Logging //Logging
implementation("io.github.aakira:napier:2.6.1") implementation("io.github.aakira:napier:2.6.1")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// Sql // Sql
implementation(SqlDelight.runtime) implementation(SqlDelight.runtime)
} }
@ -56,8 +53,6 @@ kotlin {
dependencies { dependencies {
implementation(kotlin("test-common")) implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common")) implementation(kotlin("test-annotations-common"))
implementation("io.mockk:mockk:1.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
} }
} }
val androidMain by getting { val androidMain by getting {
@ -76,39 +71,36 @@ kotlin {
} }
val iosX64Main by getting val iosX64Main by getting
val iosArm64Main by getting val iosArm64Main by getting
//val iosSimulatorArm64Main by getting // val iosSimulatorArm64Main by getting
val iosMain by creating { val iosMain by creating {
dependsOn(commonMain) dependsOn(commonMain)
iosX64Main.dependsOn(this) iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this) iosArm64Main.dependsOn(this)
//iosSimulatorArm64Main.dependsOn(this) // iosSimulatorArm64Main.dependsOn(this)
// Sql
dependencies { dependencies {
implementation(SqlDelight.native) implementation(SqlDelight.native)
implementation("io.ktor:ktor-client-ios:2.1.1")
} }
} }
val iosX64Test by getting val iosX64Test by getting
val iosArm64Test by getting val iosArm64Test by getting
//val iosSimulatorArm64Test by getting // val iosSimulatorArm64Test by getting
val iosTest by creating { val iosTest by creating {
dependsOn(commonTest) dependsOn(commonTest)
iosX64Test.dependsOn(this) iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this) iosArm64Test.dependsOn(this)
dependencies { // iosSimulatorArm64Test.dependsOn(this)
implementation("io.ktor:ktor-client-ios:2.1.1")
}
//iosSimulatorArm64Test.dependsOn(this)
} }
} }
} }
android { android {
compileSdk = 31 compileSdk = 32
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
targetSdk = 31 targetSdk = 32
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
@ -117,10 +109,11 @@ android {
namespace = "bou.amine.apps.readerforselfossv2" namespace = "bou.amine.apps.readerforselfossv2"
} }
sqldelight { sqldelight {
database("ReaderForSelfossDB") { database("ReaderForSelfossDB") {
packageName = "bou.amine.apps.readerforselfossv2.dao" packageName = "bou.amine.apps.readerforselfossv2.dao"
sourceFolders = listOf("sqldelight") sourceFolders = listOf("sqldelight")
} }
} }

View File

@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
import android.os.Build
import android.text.format.DateUtils import android.text.format.DateUtils
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import java.time.Instant import java.time.Instant
@ -15,10 +16,18 @@ actual class DateUtils actual constructor(actual val appSettingsService: AppSett
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss" val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (appSettingsService.getApiVersion() >= 4) { return if (appSettingsService.getApiVersion() >= 4) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
OffsetDateTime.parse(dateString).toInstant().toEpochMilli() OffsetDateTime.parse(dateString).toInstant().toEpochMilli()
} else { } else {
TODO("VERSION.SDK_INT < O")
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant( LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(
ZoneOffset.UTC).toEpochMilli() ZoneOffset.UTC).toEpochMilli()
} else {
TODO("VERSION.SDK_INT < O")
}
} }
} }
@ -26,11 +35,15 @@ actual class DateUtils actual constructor(actual val appSettingsService: AppSett
val date = parseDate(dateString) val date = parseDate(dateString)
return " " + DateUtils.getRelativeTimeSpanString( return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
" " + DateUtils.getRelativeTimeSpanString(
date, date,
Instant.now().toEpochMilli(), Instant.now().toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS, DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE DateUtils.FORMAT_ABBREV_RELATIVE
) )
} else {
TODO("VERSION.SDK_INT < O")
}
} }
} }

View File

@ -6,17 +6,16 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.* import bou.amine.apps.readerforselfossv2.utils.*
import com.github.ln_12.library.ConnectivityStatus
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) { class Repository(private val api: SelfossApi, private val appSettingsService: AppSettingsService, val isConnectionAvailable: MutableStateFlow<Boolean>, private val db: ReaderForSelfossDB) {
var items = ArrayList<SelfossModel.Item>() var items = ArrayList<SelfossModel.Item>()
val isConnectionAvailable = connectivityStatus.isNetworkConnected
var connectionMonitored = false var connectionMonitored = false
var baseUrl = appSettingsService.getBaseUrl() var baseUrl = appSettingsService.getBaseUrl()
@ -40,16 +39,6 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
private var fetchedSources = false private var fetchedSources = false
private var fetchedTags = false private var fetchedTags = false
init {
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
connectivityStatus.start()
runBlocking {
updateApiVersion()
dateUtils = DateUtils(appSettingsService)
reloadBadges()
}
}
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
// TODO: Use the updatedSince parameter // TODO: Use the updatedSince parameter
var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error() var fetchedItems: SelfossModel.StatusAndData<List<SelfossModel.Item>> = SelfossModel.StatusAndData.error()
@ -383,9 +372,6 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
try { try {
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
if (result) {
updateApiVersion()
}
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote")
} }
@ -399,7 +385,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
api.refreshLoginInformation() api.refreshLoginInformation()
} }
private suspend fun updateApiVersion() { suspend fun updateApiVersion() {
val apiMajorVersion = appSettingsService.getApiVersion() val apiMajorVersion = appSettingsService.getApiVersion()
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
@ -408,6 +394,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
appSettingsService.updateApiVersion(fetchedVersion.data.getApiMajorVersion()) appSettingsService.updateApiVersion(fetchedVersion.data.getApiMajorVersion())
} }
} }
dateUtils = DateUtils(appSettingsService)
} }
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride

View File

@ -54,8 +54,8 @@ class AppSettingsService {
return _apiVersion return _apiVersion
} }
fun refreshApiVersion() { private fun refreshApiVersion() {
_apiVersion = settings.getInt("apiVersionMajor", -1) _apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
} }
fun getBaseUrl(): String { fun getBaseUrl(): String {
@ -86,8 +86,8 @@ class AppSettingsService {
return _itemsNumber!! return _itemsNumber!!
} }
fun refreshItemsNumber() { private fun refreshItemsNumber() {
_itemsNumber = settings.getString("prefer_api_items_number", "20").toInt() _itemsNumber = settings.getString(API_ITEMS_NUMBER, "20").toInt()
} }
fun getApiTimeout(): Long { fun getApiTimeout(): Long {
@ -98,24 +98,24 @@ class AppSettingsService {
} }
private fun refreshApiTimeout() { private fun refreshApiTimeout() {
val settingsTimeout = settings.getLong("api_timeout", HttpTimeout.INFINITE_TIMEOUT_MS) val settingsTimeout = settings.getLong(API_TIMEOUT, HttpTimeout.INFINITE_TIMEOUT_MS)
_apiTimeout = if (settingsTimeout > 0) settingsTimeout else HttpTimeout.INFINITE_TIMEOUT_MS _apiTimeout = if (settingsTimeout > 0) settingsTimeout else HttpTimeout.INFINITE_TIMEOUT_MS
} }
private fun refreshBaseUrl() { private fun refreshBaseUrl() {
_baseUrl = settings.getString("url", "") _baseUrl = settings.getString(BASE_URL, "")
} }
private fun refreshUsername() { private fun refreshUsername() {
_userName = settings.getString("login", "") _userName = settings.getString(LOGIN, "")
} }
private fun refreshPassword() { private fun refreshPassword() {
_password = settings.getString("password", "") _password = settings.getString(PASSWORD, "")
} }
private fun refreshArticleViewerEnabled() { private fun refreshArticleViewerEnabled() {
_articleViewer = settings.getBoolean("prefer_article_viewer", true) _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
} }
fun isArticleViewerEnabled(): Boolean { fun isArticleViewerEnabled(): Boolean {
@ -125,7 +125,7 @@ class AppSettingsService {
return _articleViewer == true return _articleViewer == true
} }
private fun refreshShouldBeCardViewEnabled() { private fun refreshShouldBeCardViewEnabled() {
_shouldBeCardView = settings.getBoolean("card_view_active", false) _shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
} }
fun isCardViewEnabled(): Boolean { fun isCardViewEnabled(): Boolean {
@ -135,7 +135,7 @@ class AppSettingsService {
return _shouldBeCardView == true return _shouldBeCardView == true
} }
private fun refreshDisplayUnreadCountEnabled() { private fun refreshDisplayUnreadCountEnabled() {
_displayUnreadCount = settings.getBoolean("display_unread_count", true) _displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
} }
fun isDisplayUnreadCountEnabled(): Boolean { fun isDisplayUnreadCountEnabled(): Boolean {
@ -145,7 +145,7 @@ class AppSettingsService {
return _displayUnreadCount == true return _displayUnreadCount == true
} }
private fun refreshDisplayAllCountEnabled() { private fun refreshDisplayAllCountEnabled() {
_displayAllCount = settings.getBoolean("display_other_count", false) _displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
} }
fun isDisplayAllCountEnabled(): Boolean { fun isDisplayAllCountEnabled(): Boolean {
@ -155,7 +155,7 @@ class AppSettingsService {
return _displayAllCount == true return _displayAllCount == true
} }
private fun refreshFullHeightCardsEnabled() { private fun refreshFullHeightCardsEnabled() {
_fullHeightCards = settings.getBoolean("full_height_cards", false) _fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
} }
fun isFullHeightCardsEnabled(): Boolean { fun isFullHeightCardsEnabled(): Boolean {
@ -165,7 +165,7 @@ class AppSettingsService {
return _fullHeightCards == true return _fullHeightCards == true
} }
private fun refreshUpdateSourcesEnabled() { private fun refreshUpdateSourcesEnabled() {
_updateSources = settings.getBoolean("update_sources", true) _updateSources = settings.getBoolean(UPDATE_SOURCES, true)
} }
fun isUpdateSourcesEnabled(): Boolean { fun isUpdateSourcesEnabled(): Boolean {
@ -175,7 +175,7 @@ class AppSettingsService {
return _updateSources == true return _updateSources == true
} }
private fun refreshPeriodicRefreshEnabled() { private fun refreshPeriodicRefreshEnabled() {
_periodicRefresh = settings.getBoolean("periodic_refresh", false) _periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
} }
fun isPeriodicRefreshEnabled(): Boolean { fun isPeriodicRefreshEnabled(): Boolean {
@ -186,7 +186,7 @@ class AppSettingsService {
} }
private fun refreshRefreshWhenChargingOnlyEnabled() { private fun refreshRefreshWhenChargingOnlyEnabled() {
_refreshWhenChargingOnly = settings.getBoolean("refresh_when_charging", false) _refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
} }
fun isRefreshWhenChargingOnlyEnabled(): Boolean { fun isRefreshWhenChargingOnlyEnabled(): Boolean {
@ -197,22 +197,22 @@ class AppSettingsService {
} }
private fun refreshRefreshMinutes() { private fun refreshRefreshMinutes() {
_refreshMinutes = settings.getString("periodic_refresh_minutes", "360").toLong() _refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (_refreshMinutes <= 15) { if (_refreshMinutes <= 15) {
_refreshMinutes = 15 _refreshMinutes = 15
} }
} }
fun getRefreshMinutes(): Long { fun getRefreshMinutes(): Long {
if (_refreshMinutes != null) { if (_refreshMinutes != 360L) {
refreshRefreshMinutes() refreshRefreshMinutes()
} }
return _refreshMinutes return _refreshMinutes
} }
private fun refreshHiddenTags() { private fun refreshHiddenTags() {
if (settings.getString("hidden_tags", "").isNotEmpty()) { if (settings.getString(HIDDEN_TAGS, "").isNotEmpty()) {
_hiddenTags = settings.getString("hidden_tags", "").replace("\\s".toRegex(), "").split(",") _hiddenTags = settings.getString(HIDDEN_TAGS, "").replace("\\s".toRegex(), "").split(",")
} }
} }
@ -224,7 +224,7 @@ class AppSettingsService {
} }
private fun refreshInfiniteLoadingEnabled() { private fun refreshInfiniteLoadingEnabled() {
_infiniteLoading = settings.getBoolean("infinite_loading", false) _infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
} }
fun isInfiniteLoadingEnabled(): Boolean { fun isInfiniteLoadingEnabled(): Boolean {
@ -235,7 +235,7 @@ class AppSettingsService {
} }
private fun refreshItemCachingEnabled() { private fun refreshItemCachingEnabled() {
_itemsCaching = settings.getBoolean("items_caching", false) _itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
} }
fun isItemCachingEnabled(): Boolean { fun isItemCachingEnabled(): Boolean {
@ -246,7 +246,7 @@ class AppSettingsService {
} }
private fun refreshNotifyNewItemsEnabled() { private fun refreshNotifyNewItemsEnabled() {
_notifyNewItems = settings.getBoolean("notify_new_items", false) _notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
} }
fun isNotifyNewItemsEnabled(): Boolean { fun isNotifyNewItemsEnabled(): Boolean {
@ -258,7 +258,7 @@ class AppSettingsService {
private fun refreshMarkOnScrollEnabled() { private fun refreshMarkOnScrollEnabled() {
_markOnScroll = settings.getBoolean("mark_on_scroll", false) _markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
} }
fun isMarkOnScrollEnabled(): Boolean { fun isMarkOnScrollEnabled(): Boolean {
@ -270,7 +270,7 @@ class AppSettingsService {
private fun refreshActiveAllignment() { private fun refreshActiveAllignment() {
_activeAlignment = settings.getInt("text_align", JUSTIFY) _activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
} }
fun getActiveAllignment(): Int { fun getActiveAllignment(): Int {
@ -281,12 +281,12 @@ class AppSettingsService {
} }
fun changeAllignment(allignment: Int) { fun changeAllignment(allignment: Int) {
settings.putInt("text_align", allignment) settings.putInt(TEXT_ALIGN, allignment)
_activeAlignment = allignment _activeAlignment = allignment
} }
private fun refreshFontSize() { private fun refreshFontSize() {
_fontSize = settings.getString("reader_font_size", "16").toInt() _fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
} }
fun getFontSize(): Int { fun getFontSize(): Int {
@ -297,7 +297,7 @@ class AppSettingsService {
} }
private fun refreshStaticBarEnabled() { private fun refreshStaticBarEnabled() {
_staticBar = settings.getBoolean("reader_static_bar", false) _staticBar = settings.getBoolean(READER_STATIC_BAR, false)
} }
fun isStaticBarEnabled(): Boolean { fun isStaticBarEnabled(): Boolean {
@ -308,11 +308,11 @@ class AppSettingsService {
} }
private fun refreshFont() { private fun refreshFont() {
_font = settings.getString("reader_font", "") _font = settings.getString(READER_FONT, "")
} }
fun getFont(): String { fun getFont(): String {
if (_font != null) { if (_font.isEmpty()) {
refreshFont() refreshFont()
} }
return _font return _font
@ -353,21 +353,21 @@ class AppSettingsService {
login: String, login: String,
password: String password: String
) { ) {
settings.putString("url", url) settings.putString(BASE_URL, url)
settings.putString("login", login) settings.putString(LOGIN, login)
settings.putString("password", password) settings.putString(PASSWORD, password)
refreshApiSettings() refreshApiSettings()
} }
fun resetLoginInformation() { fun resetLoginInformation() {
settings.remove("url") settings.remove(BASE_URL)
settings.remove("login") settings.remove(LOGIN)
settings.remove("password") settings.remove(PASSWORD)
refreshApiSettings() refreshApiSettings()
} }
fun updateApiVersion(apiMajorVersion: Int) { fun updateApiVersion(apiMajorVersion: Int) {
settings.putInt("apiVersionMajor", apiMajorVersion) settings.putInt(API_VERSION_MAJOR, apiMajorVersion)
refreshApiVersion() refreshApiVersion()
} }
@ -378,7 +378,7 @@ class AppSettingsService {
} }
fun disableArticleViewer() { fun disableArticleViewer() {
settings.putBoolean("prefer_article_viewer", false) settings.putBoolean(PREFER_ARTICLE_VIEWER, false)
refreshArticleViewerEnabled() refreshArticleViewerEnabled()
} }
@ -396,5 +396,53 @@ class AppSettingsService {
const val JUSTIFY = 1 const val JUSTIFY = 1
const val ALIGN_LEFT = 2 const val ALIGN_LEFT = 2
const val API_VERSION_MAJOR = "apiVersionMajor"
const val API_ITEMS_NUMBER = "prefer_api_items_number"
const val API_TIMEOUT = "api_timeout"
const val BASE_URL = "url"
const val LOGIN = "login"
const val PASSWORD = "password"
const val PREFER_ARTICLE_VIEWER = "prefer_article_viewer"
const val CARD_VIEW_ACTIVE = "card_view_active"
const val DISPLAY_UNREAD_COUNT = "display_unread_count"
const val DISPLAY_OTHER_COUNT = "display_other_count"
const val FULL_HEIGHT_CARDS = "full_height_cards"
const val UPDATE_SOURCES = "update_sources"
const val PERIODIC_REFRESH = "periodic_refresh"
const val REFRESH_WHEN_CHARGING = "refresh_when_charging"
const val READER_FONT = "reader_font"
const val READER_STATIC_BAR = "reader_static_bar"
const val READER_FONT_SIZE = "reader_font_size"
const val TEXT_ALIGN = "text_align"
const val MARK_ON_SCROLL = "mark_on_scroll"
const val NOTIFY_NEW_ITEMS = "notify_new_items"
const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
const val HIDDEN_TAGS = "hidden_tags"
const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching"
} }
} }

View File

@ -1,12 +0,0 @@
package bou.amine.apps.readerforselfossv2
import kotlin.test.Test
import kotlin.test.assertTrue
class IosGreetingTest {
@Test
fun testExample() {
assertTrue(Greeting().greeting().contains("iOS"), "Check iOS is mentioned")
}
}