diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 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 100755 index 0000000..8049868 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 28 + buildToolsVersion '28.0.3' + + defaultConfig { + applicationId "com.aliasadi.mvvm" + minSdkVersion 23 + targetSdkVersion 28 + versionCode 1 + + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + + implementation fileTree(dir: 'libs', include: ['*.jar']) + + //android support + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + + //gson + implementation 'com.google.code.gson:gson:2.8.5' + + //extensions + implementation "android.arch.lifecycle:extensions:1.1.1" + + //coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3" + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + +} +repositories { + mavenCentral() +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 0000000..cbbceee --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/CSVApp.java b/app/src/main/java/com/speed/mvvm/CSVApp.java new file mode 100755 index 0000000..0ae4d60 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/CSVApp.java @@ -0,0 +1,19 @@ +package com.speed.mvvm; + +import android.app.Application; + +public class CSVApp extends Application { + + private static CSVApp sInstance; + + public static CSVApp getInstance() { + return sInstance; + } + + @Override + public void onCreate() { + super.onCreate(); + sInstance = this; + } + +} diff --git a/app/src/main/java/com/speed/mvvm/CSVReader.kt b/app/src/main/java/com/speed/mvvm/CSVReader.kt new file mode 100644 index 0000000..b46e2cd --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/CSVReader.kt @@ -0,0 +1,37 @@ +package com.speed.mvvm + +import com.speed.mvvm.data.network.model.Person +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.nio.charset.Charset + +class CSVReader { + companion object { + fun createListFromFile(file: InputStream): List? { + val list: ArrayList = ArrayList() + + val reader = BufferedReader(InputStreamReader(file, Charset.forName("UTF-8"))) + reader.readLines().forEach { + + val items = it.split(",") + try { + list.add(Person(replaceQuotationMarks(items[0]), + replaceQuotationMarks(items[1]), + replaceQuotationMarks(items[2]), + replaceQuotationMarks(items[3]))) + } catch (e: IndexOutOfBoundsException) { + println("Item was malformed") + } + } + + //remove first item with column descriptors + if (list.size > 0) list.removeAt(0) + + return list + } + + private fun replaceQuotationMarks(item: String) = + item.replace("\"", "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/DateUtils.kt b/app/src/main/java/com/speed/mvvm/DateUtils.kt new file mode 100644 index 0000000..a4e9cb5 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/DateUtils.kt @@ -0,0 +1,25 @@ +package com.speed.mvvm + +import android.os.Build +import android.support.annotation.RequiresApi +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class DateUtils { + companion object { + + @RequiresApi(Build.VERSION_CODES.O) + fun formatDate(dateOfBirth: String?): String { + try { + if (dateOfBirth.isNullOrBlank()) return "" + + val localDateTime: LocalDateTime = LocalDateTime.parse(dateOfBirth) + val formatter = DateTimeFormatter.ofPattern("dd MMM yyyy '-' hh:mma") + return formatter.format(localDateTime) + } catch (e: Throwable) { + return dateOfBirth ?: "" + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ViewExtensions.kt b/app/src/main/java/com/speed/mvvm/ViewExtensions.kt new file mode 100644 index 0000000..43a54b1 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ViewExtensions.kt @@ -0,0 +1,17 @@ +package com.speed.mvvm + +import android.app.Activity +import android.support.annotation.IdRes +import android.view.View + +fun Activity.bind(@IdRes idRes: Int): Lazy { + @Suppress("UNCHECKED_CAST") + return unsafeLazy { findViewById(idRes) } +} + +fun View.bind(@IdRes idRes: Int): Lazy { + @Suppress("UNCHECKED_CAST") + return unsafeLazy { findViewById(idRes) } +} + +private fun unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer) \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/data/network/model/Person.kt b/app/src/main/java/com/speed/mvvm/data/network/model/Person.kt new file mode 100755 index 0000000..847171a --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/data/network/model/Person.kt @@ -0,0 +1,83 @@ +package com.speed.mvvm.data.network.model + +import android.os.Parcel +import android.os.Parcelable +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +class Person : Parcelable { + @Expose + @SerializedName("First name") + var firstName: String + private set + @Expose + @SerializedName("Sur name") + var surname: String + private set + @Expose + @SerializedName("Issue count") + var issueCount: String + private set + @Expose + @SerializedName("Date of birth") + var dateOfBirth: String + private set + + protected constructor(`in`: Parcel) { + firstName = `in`.readString() ?: "" + surname = `in`.readString() ?: "" + issueCount = `in`.readString() ?: "" + dateOfBirth = `in`.readString() ?: "" + } + + constructor(firstName: String, surname: String, issueCount: String, dateOfBirth: String) { + this.firstName = firstName + this.surname = surname + this.issueCount = issueCount + this.dateOfBirth = dateOfBirth + } + + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(parcel: Parcel, i: Int) { + parcel.writeString(firstName) + parcel.writeString(surname) + parcel.writeString(issueCount) + parcel.writeString(dateOfBirth) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Person + + if (firstName != other.firstName) return false + if (surname != other.surname) return false + if (issueCount != other.issueCount) return false + if (dateOfBirth != other.dateOfBirth) return false + + return true + } + + override fun hashCode(): Int { + var result = firstName.hashCode() + result = 31 * result + surname.hashCode() + result = 31 * result + issueCount.hashCode() + result = 31 * result + dateOfBirth.hashCode() + return result + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Person { + return Person(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/com/speed/mvvm/ui/base/BaseActivity.kt b/app/src/main/java/com/speed/mvvm/ui/base/BaseActivity.kt new file mode 100755 index 0000000..ffe72e3 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/base/BaseActivity.kt @@ -0,0 +1,14 @@ +package com.speed.mvvm.ui.base + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity + +abstract class BaseActivity : AppCompatActivity() { + @JvmField + protected var viewModel: VM? = null + protected abstract fun createViewModel(): VM + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = createViewModel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/base/BaseViewModel.kt b/app/src/main/java/com/speed/mvvm/ui/base/BaseViewModel.kt new file mode 100755 index 0000000..abfdde2 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/base/BaseViewModel.kt @@ -0,0 +1,5 @@ +package com.speed.mvvm.ui.base + +import android.arch.lifecycle.ViewModel + +abstract class BaseViewModel : ViewModel() \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/details/DetailsActivity.kt b/app/src/main/java/com/speed/mvvm/ui/details/DetailsActivity.kt new file mode 100755 index 0000000..a1a3d2a --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/details/DetailsActivity.kt @@ -0,0 +1,59 @@ +package com.speed.mvvm.ui.details + +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.widget.TextView +import com.speed.mvvm.DateUtils +import com.speed.mvvm.R +import com.speed.mvvm.bind +import com.speed.mvvm.data.network.model.Person +import com.speed.mvvm.ui.base.BaseActivity + +class DetailsActivity : BaseActivity() { + + private val firstName by bind(R.id.firstName) + private val surname by bind(R.id.surname) + private val dateOfBirth by bind(R.id.dateOfBirth) + private val issues by bind(R.id.issues) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_details) + viewModel?.loadPersonData() + viewModel?.personLiveData?.observe(this, PersonObserver()) + } + + override fun createViewModel(): DetailsViewModel { + val person: Person = intent.getParcelableExtra(EXTRA_PERSON) + val factory = DetailsViewModelFactory(person) + return ViewModelProviders.of(this, factory).get(DetailsViewModel::class.java) + } + + private inner class PersonObserver : Observer { + override fun onChanged(person: Person?) { + if (person == null) return + firstName.text = person.firstName + surname.text = person.surname + dateOfBirth.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + DateUtils.formatDate(person.dateOfBirth) + } else { + person.dateOfBirth + } + issues.text = person.issueCount + } + } + + companion object { + private const val EXTRA_PERSON = "EXTRA_PERSON" + @JvmStatic + fun start(context: Context, person: Person?) { + val intent = Intent(context, DetailsActivity::class.java) + intent.putExtra(EXTRA_PERSON, person) + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/details/DetailsViewModel.kt b/app/src/main/java/com/speed/mvvm/ui/details/DetailsViewModel.kt new file mode 100755 index 0000000..bba11d3 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/details/DetailsViewModel.kt @@ -0,0 +1,14 @@ +package com.speed.mvvm.ui.details + +import android.arch.lifecycle.MutableLiveData +import com.speed.mvvm.data.network.model.Person +import com.speed.mvvm.ui.base.BaseViewModel + +class DetailsViewModel(private var person: Person) : BaseViewModel() { + val personLiveData = MutableLiveData() + + fun loadPersonData() { + personLiveData.postValue(person) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/details/DetailsViewModelFactory.kt b/app/src/main/java/com/speed/mvvm/ui/details/DetailsViewModelFactory.kt new file mode 100755 index 0000000..ac6d202 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/details/DetailsViewModelFactory.kt @@ -0,0 +1,15 @@ +package com.speed.mvvm.ui.details + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider +import com.speed.mvvm.data.network.model.Person + +class DetailsViewModelFactory internal constructor(private val person: Person) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(DetailsViewModel::class.java)) { + return DetailsViewModel(person) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/main/MainActivity.kt b/app/src/main/java/com/speed/mvvm/ui/main/MainActivity.kt new file mode 100755 index 0000000..4b5cd85 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/main/MainActivity.kt @@ -0,0 +1,74 @@ +package com.speed.mvvm.ui.main + +import android.arch.lifecycle.Observer +import android.arch.lifecycle.ViewModelProviders +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.View +import android.widget.ProgressBar +import android.widget.TextView +import com.speed.mvvm.CSVReader +import com.speed.mvvm.R +import com.speed.mvvm.bind +import com.speed.mvvm.data.network.model.Person +import com.speed.mvvm.ui.base.BaseActivity +import com.speed.mvvm.ui.details.DetailsActivity.Companion.start +import com.speed.mvvm.ui.main.PersonAdapter.OnPersonClickListener + +class MainActivity : BaseActivity(), OnPersonClickListener { + + private val recyclerView by bind(R.id.recycler_view) + private val progressBar by bind(R.id.progress_bar) + private val emptyView by bind(R.id.empty_view) + private var personAdapter: PersonAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + personAdapter = PersonAdapter(this) + recyclerView.adapter = personAdapter + recyclerView.layoutManager = LinearLayoutManager(this) + + //Set observers for loading status and person list + viewModel?.loadingStatusLiveData?.observe(this, LoadingObserver()) + viewModel?.personsLiveData?.observe(this, PersonObserver()) + + viewModel?.loadPersonsLocal(R.raw.issues) + } + + override fun createViewModel(): MainViewModel { + val factory = MainViewModelFactory() + return ViewModelProviders.of(this, factory).get(MainViewModel::class.java) + } + + //Observers + private inner class LoadingObserver : Observer { + override fun onChanged(isLoading: Boolean?) { + if (isLoading == null) return + if (isLoading) { + progressBar.visibility = View.VISIBLE + } else { + progressBar.visibility = View.GONE + } + } + } + + private inner class PersonObserver : Observer?> { + override fun onChanged(personList: List?) { + personAdapter?.setItems(personList) + if (personList?.isEmpty() == true) { + emptyView.visibility = View.VISIBLE + } else { + emptyView.visibility = View.GONE + } + } + } + + override fun onPersonClicked(person: Person?) { + start(this, person) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/main/MainViewModel.kt b/app/src/main/java/com/speed/mvvm/ui/main/MainViewModel.kt new file mode 100755 index 0000000..447550f --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/main/MainViewModel.kt @@ -0,0 +1,38 @@ +package com.speed.mvvm.ui.main + +import android.arch.lifecycle.MutableLiveData +import com.speed.mvvm.CSVApp +import com.speed.mvvm.CSVReader +import com.speed.mvvm.data.network.model.Person +import com.speed.mvvm.ui.base.BaseViewModel +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +class MainViewModel : BaseViewModel() { + val personsLiveData: MutableLiveData?> = MutableLiveData() + val loadingStatusLiveData: MutableLiveData = MutableLiveData() + + private fun setPersons(persons: List?) { + setIsLoading(false) + this.personsLiveData.postValue(persons) + } + + fun loadPersonsLocal(issues: Int) { + setIsLoading(true) + + GlobalScope.launch { + val issuesFile = CSVApp.getInstance().resources.openRawResource(issues) + + val result = async { + CSVReader.createListFromFile(issuesFile) + } + setPersons(result.await()) + } + } + + private fun setIsLoading(loading: Boolean) { + loadingStatusLiveData.postValue(loading) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/main/MainViewModelFactory.kt b/app/src/main/java/com/speed/mvvm/ui/main/MainViewModelFactory.kt new file mode 100755 index 0000000..7f27375 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/main/MainViewModelFactory.kt @@ -0,0 +1,13 @@ +package com.speed.mvvm.ui.main + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider + +class MainViewModelFactory : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MainViewModel::class.java)) { + return MainViewModel() as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/speed/mvvm/ui/main/PersonAdapter.kt b/app/src/main/java/com/speed/mvvm/ui/main/PersonAdapter.kt new file mode 100755 index 0000000..2b0c715 --- /dev/null +++ b/app/src/main/java/com/speed/mvvm/ui/main/PersonAdapter.kt @@ -0,0 +1,79 @@ +package com.speed.mvvm.ui.main + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import com.speed.mvvm.DateUtils +import com.speed.mvvm.R +import com.speed.mvvm.data.network.model.Person + +class PersonAdapter internal constructor(private val mListener: OnPersonClickListener) : RecyclerView.Adapter() { + + private var mItems = arrayListOf() + + fun setItems(items: List?) { + mItems.clear() + items?.let { + mItems.addAll(it) + } + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_person, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val person = getItem(position) + holder.setOnClickListener(person) + person?.dateOfBirth?.let { holder.setDateOfBirth(it) } + person?.firstName?.let { holder.setFirstName(it) } + person?.surname?.let { holder.setSurname(it) } + } + + override fun getItemCount(): Int { + return mItems.size + } + + private fun getItem(position: Int): Person? { + return mItems[position] + } + + interface OnPersonClickListener { + fun onPersonClicked(person: Person?) + } + + inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + + private val dateOfBirth = itemView.findViewById(R.id.dateOfBirth) + private val surname = itemView.findViewById(R.id.surname) + private val firstName = itemView.findViewById(R.id.firstName) + + fun setFirstName(firstName: String?) { + this.firstName?.text = firstName + } + + fun setDateOfBirth(dateOfBirth: String?) { + val date = DateUtils.formatDate(dateOfBirth) + this.dateOfBirth?.text = date + } + + fun setSurname(surname: String) { + this.surname?.text = surname + } + + fun setOnClickListener(person: Person?) { + itemView.tag = person + itemView.setOnClickListener(this) + } + + override fun onClick(view: View) { + mListener.onPersonClicked(view.tag as Person) + } + + } + +} \ No newline at end of file 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 100755 index 0000000..c7bd21d --- /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 100755 index 0000000..d5fccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml new file mode 100644 index 0000000..b2cb337 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml new file mode 100755 index 0000000..7c159e1 --- /dev/null +++ b/app/src/main/res/layout/activity_details.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..e653ac7 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_person.xml b/app/src/main/res/layout/item_person.xml new file mode 100755 index 0000000..9ba0cf8 --- /dev/null +++ b/app/src/main/res/layout/item_person.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100755 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100755 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000..a2f5908 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100755 index 0000000..1b52399 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000..ff10afd Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100755 index 0000000..115a4c7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000..dcd3cd8 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100755 index 0000000..459ca60 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..8ca12fe Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100755 index 0000000..8e19b41 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..b824ebd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100755 index 0000000..4c19a13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/raw/issues.csv b/app/src/main/res/raw/issues.csv new file mode 100755 index 0000000..a0aaa69 --- /dev/null +++ b/app/src/main/res/raw/issues.csv @@ -0,0 +1,4 @@ +"First name","Sur name","Issue count","Date of birth" +"Theo","Jansen",5,"1978-01-02T01:00:00" +"Fiona","de Vries",7,"1950-11-12T00:00:00" +"Petra","Boersma",1,"2001-04-20T00:00:00" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100755 index 0000000..2aa5c29 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #2196F3 + #0D47A1 + #FF4081 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100755 index 0000000..9025f8e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + CSV Reader App + No items avaliable + person image + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100755 index 0000000..5885930 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/CSVReaderTest.kt b/app/src/test/java/CSVReaderTest.kt new file mode 100644 index 0000000..e19e8f5 --- /dev/null +++ b/app/src/test/java/CSVReaderTest.kt @@ -0,0 +1,44 @@ +import com.speed.mvvm.CSVReader +import com.speed.mvvm.data.network.model.Person +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayInputStream + + +class CSVReaderTest { + + private val issuesTest1 = "\"First name\",\"Sur name\",\"Issue count\",\"Date of birth\"\n" + + "\"\",\"\",0,\"\"\n" + //item 1 + "\"\",\"\",1,\"\"\n" + //item 2 + "\"\",\"\",2,\"\"\n" //item 3 + + private val issuesTestMalformed= "\"First name\",\"Sur name\",\"Issue count\",\"Date of birth\"\n" + + "\",\"\"\n" + //item 1 + "\"\"\",1,\"\"\n" + //item 2 + "\"\"\"\n" //item 3 + + private fun getPeopleList(maxIndex: Int): List { + val people = arrayListOf() + for (i in 0..maxIndex) { + people.add(Person("", "", "$i", "")) + } + return people + } + + @Throws(Exception::class) + @Test + fun testDateFormatter() { + val inputStream = ByteArrayInputStream(issuesTest1.toByteArray()) + + assertEquals(getPeopleList(2), CSVReader.createListFromFile(inputStream)) + } + + @Throws(Exception::class) + @Test + fun testDateFormatterMalformed() { + val inputStream = ByteArrayInputStream(issuesTestMalformed.toByteArray()) + + assertEquals(emptyList(), CSVReader.createListFromFile(inputStream)) + } + +} \ No newline at end of file diff --git a/app/src/test/java/DateUtilsTest.kt b/app/src/test/java/DateUtilsTest.kt new file mode 100644 index 0000000..cc2b703 --- /dev/null +++ b/app/src/test/java/DateUtilsTest.kt @@ -0,0 +1,36 @@ +import com.speed.mvvm.DateUtils.Companion.formatDate +import org.junit.Assert.assertEquals +import org.junit.Test + +class DateUtilsTest { + + @Throws(Exception::class) + @Test + fun testDateFormatter() { + assertEquals("12 Nov 1950 - 08:00AM", formatDate("1950-11-12T08:00:00")) + } + + @Throws(Exception::class) + @Test + fun testDateFormatterNegativeNumbersMalformed() { + assertEquals("-1950--11--12T08:00:00", formatDate("-1950--11--12T08:00:00")) + } + + @Throws(Exception::class) + @Test + fun testDateFormatterMalformed() { + assertEquals("195011-12T08:00:00", formatDate("195011-12T08:00:00")) + } + + @Throws(Exception::class) + @Test + fun testDateFormatterEmptyDate() { + assertEquals("", formatDate("")) + } + + @Throws(Exception::class) + @Test + fun testDateFormatterNotADate() { + assertEquals("Hello not a date", formatDate("Hello not a date")) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100755 index 0000000..e6d62de --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.3.61' + + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000..13372ae Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000..aa66cda --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jan 13 20:09:26 CET 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100755 index 0000000..e7b4def --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'