From 9379735a81a8c56a25a727a3241b22fd9f078062 Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Thu, 6 Feb 2020 21:29:45 +0000 Subject: [PATCH] Initial commit --- .gitignore | 14 ++ .idea/.name | 1 + .idea/codeStyles/Project.xml | 125 +++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/dictionaries/h_mal.xml | 3 + .idea/gradle.xml | 19 ++ .idea/misc.xml | 17 ++ .idea/runConfigurations.xml | 12 ++ app/.gitignore | 1 + app/build.gradle | 73 +++++++ app/proguard-rules.pro | 21 ++ .../ui/login/LoginActivityTest.kt | 204 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 29 +++ .../h_mal/sliidenewsreader/AppClass.kt | 32 +++ .../sliidenewsreader/data/db/AppDatabase.kt | 44 ++++ .../h_mal/sliidenewsreader/data/db/UserDao.kt | 41 ++++ .../sliidenewsreader/data/db/entities/User.kt | 13 ++ .../sliidenewsreader/data/models/Image.kt | 6 + .../sliidenewsreader/data/network/MyApi.kt | 36 ++++ .../network/NetworkConnectionInterceptor.kt | 38 ++++ .../data/network/SafeApiRequest.kt | 29 +++ .../data/network/responses/FeedResponse.kt | 14 ++ .../data/repositories/Repository.kt | 45 ++++ .../sliidenewsreader/ui/CompletionListener.kt | 7 + .../ui/login/LoginActivity.kt | 64 ++++++ .../ui/login/LoginViewModel.kt | 61 ++++++ .../ui/login/LoginViewModelFactory.kt | 19 ++ .../ui/main/FeedRecyclerViewAdapter.kt | 73 +++++++ .../sliidenewsreader/ui/main/MainActivity.kt | 97 +++++++++ .../sliidenewsreader/ui/main/MainViewModel.kt | 52 +++++ .../ui/main/MainViewModelFactory.kt | 19 ++ .../ui/main/RecyclerViewAdapter.kt | 2 + .../drawable-v24/ic_launcher_foreground.xml | 34 +++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++++ app/src/main/res/layout/activity_login.xml | 88 ++++++++ app/src/main/res/layout/activity_main.xml | 22 ++ app/src/main/res/layout/feed_item.xml | 25 +++ app/src/main/res/menu/menu.xml | 7 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 15 ++ app/src/main/res/values/styles.xml | 11 + .../h_mal/sliidenewsreader/ExampleUnitTest.kt | 16 ++ build.gradle | 28 +++ gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++++++++++ gradlew.bat | 84 ++++++++ settings.gradle | 2 + 62 files changed, 1938 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/dictionaries/h_mal.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivityTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/AppClass.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/AppDatabase.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/UserDao.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/entities/User.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/models/Image.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/MyApi.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/NetworkConnectionInterceptor.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/SafeApiRequest.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/responses/FeedResponse.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/data/repositories/Repository.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/CompletionListener.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivity.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModel.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModelFactory.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/FeedRecyclerViewAdapter.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainActivity.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModel.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModelFactory.kt create mode 100644 app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/RecyclerViewAdapter.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/feed_item.xml create mode 100644 app/src/main/res/menu/menu.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/example/h_mal/sliidenewsreader/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..ca1696e --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Sliide news reader \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..45b5654 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dictionaries/h_mal.xml b/.idea/dictionaries/h_mal.xml new file mode 100644 index 0000000..f32ad56 --- /dev/null +++ b/.idea/dictionaries/h_mal.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..169fd0d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d5727af --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..edf04bb --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,73 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +//kotlin kapt plugin +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 29 + defaultConfig { + applicationId "com.example.h_mal.sliidenewsreader" + minSdkVersion 26 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + dataBinding { + enabled = true + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.core:core-ktx:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + + //Retrofit and GSON + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + + //Kotlin Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0" + + //New Material Design + implementation 'com.google.android.material:material:1.1.0-alpha10' + + //Kodein Dependency Injection + implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1" + implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1" + + //Android Room + implementation "androidx.room:room-runtime:2.2.0-rc01" + implementation "androidx.room:room-ktx:2.2.0-rc01" + kapt "androidx.room:room-compiler:2.2.0-rc01" + + implementation 'com.xwray:groupie:2.3.0' + implementation 'com.xwray:groupie-kotlin-android-extensions:2.3.0' + implementation 'com.xwray:groupie-databinding:2.3.0' + + implementation "androidx.preference:preference-ktx:1.1.0" + + //image caching and display library + implementation 'com.squareup.picasso:picasso:2.71828' + androidTestImplementation 'androidx.test:rules:1.3.0-alpha03' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivityTest.kt b/app/src/androidTest/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivityTest.kt new file mode 100644 index 0000000..d908357 --- /dev/null +++ b/app/src/androidTest/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivityTest.kt @@ -0,0 +1,204 @@ +package com.example.h_mal.sliidenewsreader.ui.login + + +import android.view.View +import android.view.ViewGroup +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.filters.LargeTest +import androidx.test.rule.ActivityTestRule +import com.example.h_mal.sliidenewsreader.R +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.not +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf +import org.hamcrest.TypeSafeMatcher +import org.junit.Before +import org.junit.Rule +import org.junit.Test + + +@LargeTest +class LoginActivityTest { + + @Rule + @JvmField + var mActivityTestRule = ActivityTestRule(LoginActivity::class.java) + + lateinit var invalidTextString: String + lateinit var failedTextString: String + + @Before + fun setup(){ + invalidTextString = mActivityTestRule.activity.getString(R.string.login_invalid) + failedTextString = mActivityTestRule.activity.getString(R.string.login_failed) + } + + @Test + fun emptyBothTest() { + sign_in_button.perform(click()) + sign_in_button.check(matches(isDisplayed())) + } + + @Test + fun emptyPasswordTest() { + username_edittext.perform(replaceText("user"), closeSoftKeyboard()) + + sign_in_button.perform(click()) + sign_in_button.check(matches(isDisplayed())) + } + + @Test + fun emptyUserNameTest() { + password_edittext.perform(replaceText("password"), closeSoftKeyboard()) + + sign_in_button.perform(click()) + sign_in_button.check(matches(isDisplayed())) + + } + + @Test + fun incorrectDetailsTest() { + username_edittext.perform(replaceText("user"), closeSoftKeyboard()) + password_edittext.perform(replaceText("user"), closeSoftKeyboard()) + + sign_in_button.perform(click()) + sign_in_button.check(matches(isDisplayed())) + } + + @Test + fun validLoginAsUserTest() { + username_edittext.perform(replaceText("user"), closeSoftKeyboard()) + password_edittext.perform(replaceText("password"), closeSoftKeyboard()) + + sign_in_button.perform(click()) + sign_in_button.check(doesNotExist()) + + menu_button.perform(click()) + signout_button.perform(click()) + + sign_in_button.check(matches(isDisplayed())) + } + + @Test + fun validLoginAsPremiumUserTest() { + username_edittext.perform(replaceText("premium"), closeSoftKeyboard()) + password_edittext.perform(replaceText("password"), closeSoftKeyboard()) + + sign_in_button.perform(click()) + sign_in_button.check(doesNotExist()) + + menu_button.perform(click()) + signout_button.perform(click()) + + sign_in_button.check(matches(isDisplayed())) + } + + private val sign_in_button = onView( + allOf( + withId(R.id.login), withText("Sign in"), + childAtPosition( + allOf( + withId(R.id.container), + childAtPosition( + withId(android.R.id.content), + 0 + ) + ), + 2 + ), + isDisplayed() + ) + ) + + private val username_edittext = onView( + allOf( + withId(R.id.username), + childAtPosition( + allOf( + withId(R.id.container), + childAtPosition( + withId(android.R.id.content), + 0 + ) + ), + 0 + ), + isDisplayed() + ) + ) + + private val password_edittext = onView( + allOf( + withId(R.id.password), + childAtPosition( + allOf( + withId(R.id.container), + childAtPosition( + withId(android.R.id.content), + 0 + ) + ), + 1 + ), + isDisplayed() + ) + ) + + val menu_button = onView( + allOf( + withContentDescription("More options"), + childAtPosition( + childAtPosition( + withId(R.id.action_bar), + 1 + ), + 0 + ), + isDisplayed() + ) + ) + + val signout_button = onView( + allOf( + withId(R.id.title), withText("Sign out"), + childAtPosition( + childAtPosition( + withId(R.id.content), + 0 + ), + 0 + ), + isDisplayed() + ) + ) + + private fun testToast(toastText:String){ + onView(withText(toastText)) + .inRoot(withDecorView(not(`is`(mActivityTestRule.activity.window.decorView)))) + .check(matches(isDisplayed())) + } + + private fun childAtPosition( + parentMatcher: Matcher, position: Int + ): Matcher { + + return object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("Child at position $position in parent ") + parentMatcher.describeTo(description) + } + + public override fun matchesSafely(view: View): Boolean { + val parent = view.parent + return parent is ViewGroup && parentMatcher.matches(parent) + && view == parent.getChildAt(position) + } + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fb7a314 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/AppClass.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/AppClass.kt new file mode 100644 index 0000000..488e120 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/AppClass.kt @@ -0,0 +1,32 @@ +package com.example.h_mal.sliidenewsreader + +import android.app.Application +import com.example.h_mal.sliidenewsreader.data.repositories.Repository +import com.example.h_mal.sliidenewsreader.data.db.AppDatabase +import com.example.h_mal.sliidenewsreader.data.network.MyApi +import com.example.h_mal.sliidenewsreader.data.network.NetworkConnectionInterceptor +import com.example.h_mal.sliidenewsreader.ui.login.LoginViewModelFactory +import com.example.h_mal.sliidenewsreader.ui.main.MainViewModelFactory +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.androidXModule +import org.kodein.di.generic.bind +import org.kodein.di.generic.instance +import org.kodein.di.generic.provider +import org.kodein.di.generic.singleton + +class AppClass : Application(), KodeinAware{ + + override val kodein = Kodein.lazy { + import(androidXModule(this@AppClass)) + + bind() from singleton { AppDatabase(instance()) } + bind() from singleton { NetworkConnectionInterceptor(instance()) } + bind() from singleton { MyApi(instance()) } + bind() from singleton { Repository(instance(),instance()) } + bind() from provider { LoginViewModelFactory(instance()) } + bind() from provider { MainViewModelFactory(instance()) } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/AppDatabase.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/AppDatabase.kt new file mode 100644 index 0000000..629998e --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/AppDatabase.kt @@ -0,0 +1,44 @@ +package com.example.h_mal.sliidenewsreader.data.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.example.h_mal.sliidenewsreader.data.db.entities.User +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Database( + entities = [User::class], + version = 1 +) +abstract class AppDatabase : RoomDatabase(){ + + abstract fun getUserDao(): UserDao +// abstract fun getCurrentUserDao(): CurrentUserDao +// abstract fun getWidgetDao(): WidgetDao + + companion object { + + @Volatile + private var instance: AppDatabase? = null + private val LOCK = Any() + + operator fun invoke(context: Context) = instance ?: synchronized(LOCK) { + instance ?: buildDatabase(context).also { + instance = it + CoroutineScope(Dispatchers.Default).launch { + it.getUserDao().initiateUsers() + } + } + } + + private fun buildDatabase(context: Context) = + Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "MyDatabase.db" + ).allowMainThreadQueries().build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/UserDao.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/UserDao.kt new file mode 100644 index 0000000..eeb4cf5 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/UserDao.kt @@ -0,0 +1,41 @@ +package com.example.h_mal.sliidenewsreader.data.db + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.example.h_mal.sliidenewsreader.data.db.entities.User + +@Dao +interface UserDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(user: User) : Long + + @Query("SELECT * FROM User" ) + fun getAllUsers() : Array + + @Query("SELECT * FROM User WHERE username = (:un) AND password = (:pw) LIMIT 1" ) + fun getUser(un: String, pw: String) : User? + + @Query("UPDATE User SET state = 1 WHERE username = (:un)" ) + fun trueLoginState(un: String) : Int + + @Query("UPDATE User SET state = 0 WHERE username = (:un)" ) + fun falseLoginState(un: String) : Int + + @Query("SELECT * FROM User WHERE state = 1 LIMIT 1" ) + fun getCurrentLoginUser() : LiveData + + @Query("SELECT role FROM User WHERE state = 1 LIMIT 1" ) + fun getCurrentRole() : String + + @Transaction + suspend fun initiateUsers(){ + val u1 = User("premium","password","PREMIUM", false) + val u2 = User("user","password","NORMAL", false) + val u3 = User("user1","password","NORMAL", false) + + upsert(u1) + upsert(u2) + upsert(u3) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/entities/User.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/entities/User.kt new file mode 100644 index 0000000..0eb4b55 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/db/entities/User.kt @@ -0,0 +1,13 @@ +package com.example.h_mal.sliidenewsreader.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class User( + @PrimaryKey + val username: String, + val password: String, + val role: String, + val state: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/models/Image.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/models/Image.kt new file mode 100644 index 0000000..655bb01 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/models/Image.kt @@ -0,0 +1,6 @@ +package com.example.h_mal.sliidenewsreader.data.models + +data class Image ( + val id : Int?, + val url: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/MyApi.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/MyApi.kt new file mode 100644 index 0000000..c050e5c --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/MyApi.kt @@ -0,0 +1,36 @@ +package com.example.h_mal.sliidenewsreader.data.network + +import com.example.h_mal.sliidenewsreader.data.network.responses.FeedResponse + +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET + +interface MyApi { + + + @GET("feed") + suspend fun getFeed() : Response> + + companion object{ + operator fun invoke( + networkConnectionInterceptor: NetworkConnectionInterceptor + ) : MyApi{ + + val okkHttpclient = OkHttpClient.Builder() + .addInterceptor(networkConnectionInterceptor) + .build() + + return Retrofit.Builder() + .client(okkHttpclient) + .baseUrl("https://private-9e5bb4-ivorsliide.apiary-mock.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(MyApi::class.java) + } + } + +} + diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/NetworkConnectionInterceptor.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/NetworkConnectionInterceptor.kt new file mode 100644 index 0000000..d897df7 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/NetworkConnectionInterceptor.kt @@ -0,0 +1,38 @@ +package com.example.h_mal.sliidenewsreader.data.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +class NetworkConnectionInterceptor( + context: Context +) : Interceptor { + + private val applicationContext = context.applicationContext + + override fun intercept(chain: Interceptor.Chain): Response { + if (!isInternetAvailable()) + throw IOException("Make sure you have an active data connection") + return chain.proceed(chain.request()) + } + + private fun isInternetAvailable(): Boolean { + var result = false + val connectivityManager = + applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + connectivityManager?.let { + it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply { + result = when { + hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + else -> false + } + } + } + return result + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/SafeApiRequest.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/SafeApiRequest.kt new file mode 100644 index 0000000..f8360c4 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/SafeApiRequest.kt @@ -0,0 +1,29 @@ +package com.example.h_mal.sliidenewsreader.data.network + +import org.json.JSONException +import org.json.JSONObject +import retrofit2.Response +import java.io.IOException + +abstract class SafeApiRequest { + + suspend fun apiRequest(call: suspend () -> Response) : T{ + val response = call.invoke() + if(response.isSuccessful){ + return response.body()!! + }else{ + val error = response.errorBody()?.string() + + val message = StringBuilder() + error?.let{ + try{ + message.append(JSONObject(it).getString("message")) + }catch(e: JSONException){ } + message.append("\n") + } + message.append("Error Code: ${response.code()}") + throw IOException(message.toString()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/responses/FeedResponse.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/responses/FeedResponse.kt new file mode 100644 index 0000000..4b8e6eb --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/network/responses/FeedResponse.kt @@ -0,0 +1,14 @@ +package com.example.h_mal.sliidenewsreader.data.network.responses + +import com.example.h_mal.sliidenewsreader.data.models.Image + + +data class FeedResponse ( + val id: Int?, + val datatype: String?, + val type: String?, + val title: String?, + val description: String?, + val url: String?, + val images: List? +) \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/data/repositories/Repository.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/repositories/Repository.kt new file mode 100644 index 0000000..d7e9713 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/data/repositories/Repository.kt @@ -0,0 +1,45 @@ +package com.example.h_mal.sliidenewsreader.data.repositories + +import com.example.h_mal.sliidenewsreader.data.db.AppDatabase +import com.example.h_mal.sliidenewsreader.data.network.MyApi +import com.example.h_mal.sliidenewsreader.data.network.SafeApiRequest +import com.example.h_mal.sliidenewsreader.data.network.responses.FeedResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +class Repository( + private val db: AppDatabase, + private val api: MyApi +) : SafeApiRequest() { + + fun attemptUser(username: String, password: String) = db.getUserDao().getUser(username,password) + + fun setUserLoggedIn(username: String) = db.getUserDao().trueLoginState(username) + + fun setUserLoggedOut(username: String) = db.getUserDao().falseLoginState(username) + + fun getUser() = db.getUserDao().getCurrentLoginUser() + + fun getCurrentUserRole() = db.getUserDao().getCurrentRole() + + suspend fun fetchData(): MutableList? { + val response = apiRequest { api.getFeed() } + + val remover = removeFinder() + response.removeIf { item -> + item.id == null || item.datatype == remover + } + + return response + } + + fun removeFinder() : String{ + val role = getCurrentUserRole() + return if (role == "PREMIUM"){ + "ADS" + }else{ + "PREMIUM_NEWS" + } + } + +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/CompletionListener.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/CompletionListener.kt new file mode 100644 index 0000000..cb317ef --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/CompletionListener.kt @@ -0,0 +1,7 @@ +package com.example.h_mal.sliidenewsreader.ui + +interface CompletionListener { + fun onStarted() + fun onSuccess() + fun onFailure(message: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivity.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivity.kt new file mode 100644 index 0000000..051d2e9 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginActivity.kt @@ -0,0 +1,64 @@ +package com.example.h_mal.sliidenewsreader.ui.login + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import com.example.h_mal.sliidenewsreader.ui.main.MainActivity +import com.example.h_mal.sliidenewsreader.R +import com.example.h_mal.sliidenewsreader.data.db.entities.User +import com.example.h_mal.sliidenewsreader.databinding.ActivityLoginBinding +import com.example.h_mal.sliidenewsreader.ui.CompletionListener +import kotlinx.android.synthetic.main.activity_login.* +import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein +import org.kodein.di.generic.instance + +class LoginActivity : AppCompatActivity(), + CompletionListener, KodeinAware { + + override val kodein by kodein() + private val factory : LoginViewModelFactory by instance() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding: ActivityLoginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login) + + val loginViewModel: LoginViewModel = ViewModelProviders.of(this, factory) + .get(LoginViewModel::class.java) + binding.viewmodel = loginViewModel + + loginViewModel.authListener = this + + loginViewModel.getCurrentLoggedInUser().observe(this@LoginActivity, Observer { user -> + + if(user != null){ + Intent(this, MainActivity::class.java).also { + it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(it) + } + } + }) + + } + + + + override fun onStarted() { + progress_bar.visibility = View.VISIBLE + } + + override fun onSuccess() { + progress_bar.visibility = View.GONE + } + + override fun onFailure(message: String) { + Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show() + progress_bar.visibility = View.GONE + } +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModel.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModel.kt new file mode 100644 index 0000000..5dcb935 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModel.kt @@ -0,0 +1,61 @@ +package com.example.h_mal.sliidenewsreader.ui.login + +import android.view.View +import androidx.lifecycle.ViewModel +import com.example.h_mal.sliidenewsreader.R +import com.example.h_mal.sliidenewsreader.data.repositories.Repository +import com.example.h_mal.sliidenewsreader.ui.CompletionListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.Exception + +class LoginViewModel( + private val repository: Repository +) : ViewModel() { + + var name: String? = null + var password: String? = null + + var authListener: CompletionListener? = null + + fun getCurrentLoggedInUser() = repository.getUser() + + + fun onLoginButtonClick(view: View){ + authListener?.onStarted() + if(name.isNullOrEmpty() || password.isNullOrEmpty()){ + authListener?.onFailure(view.context.getString(R.string.login_invalid)) + return + } + + CoroutineScope(Dispatchers.IO).launch { + try { + + val user = repository.attemptUser(name!!, password!!) + + user?.let { + withContext(Dispatchers.Main){ + authListener?.onSuccess() + } + + repository.setUserLoggedIn(name!!) + return@launch + } + + withContext(Dispatchers.Main){ + authListener?.onFailure(view.context.getString(R.string.login_failed)) + } + + }catch(e: Exception){ + withContext(Dispatchers.Main){ + authListener?.onFailure(e.message!!) + } + + } + } + + } + +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModelFactory.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModelFactory.kt new file mode 100644 index 0000000..539d724 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/login/LoginViewModelFactory.kt @@ -0,0 +1,19 @@ +package com.example.h_mal.sliidenewsreader.ui.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.h_mal.sliidenewsreader.data.repositories.Repository + +/** + * ViewModel provider factory to instantiate LoginViewModel. + * Required given LoginViewModel has a non-empty constructor + */ +class LoginViewModelFactory( + private val repository: Repository +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return LoginViewModel(repository) as T + } +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/FeedRecyclerViewAdapter.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/FeedRecyclerViewAdapter.kt new file mode 100644 index 0000000..2002500 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/FeedRecyclerViewAdapter.kt @@ -0,0 +1,73 @@ +package com.example.h_mal.sliidenewsreader.ui.main + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import com.example.h_mal.sliidenewsreader.R +import com.example.h_mal.sliidenewsreader.data.network.responses.FeedResponse +import com.squareup.picasso.Picasso +import kotlinx.android.synthetic.main.feed_item.view.* + + +class ListAdapter(context: Context, objects: MutableList) : + ArrayAdapter(context, 0, objects) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + var view: View? = convertView + if (view == null) { + view = LayoutInflater.from(context).inflate(R.layout.feed_item, null)!! + } + + val currentFeedItem = getItem(position) + + view.textView.text = currentFeedItem?.title + + val slider = view.seekBar + val imageView = view.imageView + + slider.visibility = View.GONE + imageView.visibility = View.GONE + + val imgArray = currentFeedItem?.images + + imgArray?.let { + imageView.visibility = View.VISIBLE + it[0].url?.apply { + displayImage(this,imageView) + } + + val count = it.count() -1 + + if (count > 0){ + slider.visibility = View.VISIBLE + slider.max = count + slider.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) { + it[seekBar.progress].url?.let {url -> + displayImage(url,view.imageView) + } + } + }) + } + + } + return view + } + + fun displayImage(url: String, imageView: ImageView){ + Picasso.get() + .load(url) + .placeholder(R.mipmap.ic_launcher) + .resize(250, 250) + .centerCrop() + .into(imageView) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainActivity.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainActivity.kt new file mode 100644 index 0000000..d7c4afa --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainActivity.kt @@ -0,0 +1,97 @@ +package com.example.h_mal.sliidenewsreader.ui.main + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.h_mal.sliidenewsreader.R +import com.example.h_mal.sliidenewsreader.data.db.entities.User +import com.example.h_mal.sliidenewsreader.data.network.responses.FeedResponse +import com.example.h_mal.sliidenewsreader.ui.CompletionListener +import com.example.h_mal.sliidenewsreader.ui.login.LoginActivity +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.ViewHolder +import kotlinx.android.synthetic.main.activity_login.* +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.activity_main.progress_bar +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein +import org.kodein.di.generic.instance + +class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { + + override val kodein by kodein() + private val factory : MainViewModelFactory by instance() + + lateinit var viewModel: MainViewModel + + private var currentUser: User? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + viewModel = ViewModelProviders.of(this,factory).get(MainViewModel::class.java) + viewModel.completionLister = this + + viewModel.setList() + + viewModel.feed.observe(this, Observer { + val adapter = ListAdapter(this,it) + list_view.adapter = adapter + }) + + + viewModel.getCurrentLoggedInUser().observe(this, Observer {user -> + if (user != null){ + currentUser = user + } + + }) + } + + //create a menu to navigate to other activities + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu, menu) + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId){ + R.id.sign_out ->{ + viewModel.logOut(currentUser) + Intent(this@MainActivity, LoginActivity::class.java).also { + it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(it) + + } + } + } + + return super.onOptionsItemSelected(item) + } + + override fun onStarted() { + progress_bar.visibility = View.VISIBLE + } + + override fun onSuccess() { + progress_bar.visibility = View.GONE + } + + override fun onFailure(message: String) { + Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show() + progress_bar.visibility = View.GONE + } +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModel.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModel.kt new file mode 100644 index 0000000..d95f3f1 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModel.kt @@ -0,0 +1,52 @@ +package com.example.h_mal.sliidenewsreader.ui.main + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.h_mal.sliidenewsreader.data.db.entities.User +import com.example.h_mal.sliidenewsreader.data.network.responses.FeedResponse +import com.example.h_mal.sliidenewsreader.data.repositories.Repository +import com.example.h_mal.sliidenewsreader.ui.CompletionListener +import kotlinx.coroutines.* +import java.io.IOException + +class MainViewModel( + private val repository: Repository +) : ViewModel() { + + fun getCurrentLoggedInUser() = repository.getUser() + + var completionLister : CompletionListener? = null + + val feed = MutableLiveData>() + + fun setList(){ + completionLister?.onStarted() + + CoroutineScope(Dispatchers.Main).launch { + try { + val feedResponse = repository.fetchData() + + feedResponse?.let { + completionLister?.onSuccess() + feed.value = it + return@launch + } + + completionLister?.onFailure("Failed to retrieve data") + }catch(e: IOException){ + completionLister?.onFailure(e.message!!) + } + } + + } + + fun logOut(user: User?){ + CoroutineScope(Dispatchers.IO).launch { + user?.username?.let { + repository.setUserLoggedOut(it) + } + + } + } + +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModelFactory.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModelFactory.kt new file mode 100644 index 0000000..8e4bda3 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/MainViewModelFactory.kt @@ -0,0 +1,19 @@ +package com.example.h_mal.sliidenewsreader.ui.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.h_mal.sliidenewsreader.data.repositories.Repository + +/** + * ViewModel provider factory to instantiate LoginViewModel. + * Required given LoginViewModel has a non-empty constructor + */ +class MainViewModelFactory( + private val repository: Repository +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return MainViewModel(repository) as T + } +} diff --git a/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/RecyclerViewAdapter.kt b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/RecyclerViewAdapter.kt new file mode 100644 index 0000000..dab85c2 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/sliidenewsreader/ui/main/RecyclerViewAdapter.kt @@ -0,0 +1,2 @@ +package com.example.h_mal.sliidenewsreader.ui.main + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..b8d9b20 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + +