- lint checks done

- added service intent

Took 1 hour 7 minutes
This commit is contained in:
2021-06-12 22:50:52 +01:00
parent 33f1738d1e
commit 9160551cb4
35 changed files with 322 additions and 365 deletions

Binary file not shown.

View File

@@ -22,7 +22,7 @@ import org.kodein.di.generic.singleton
class AppClass : Application(), KodeinAware { class AppClass : Application(), KodeinAware {
// Kodein Dependecy Injection singletons and providers created // KODEIN DI components declaration
override val kodein by Kodein.lazy { override val kodein by Kodein.lazy {
import(androidXModule(this@AppClass)) import(androidXModule(this@AppClass))

View File

@@ -1,6 +1,5 @@
package com.appttude.h_mal.easycc.data.network package com.appttude.h_mal.easycc.data.network
import android.util.Log
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import retrofit2.Response import retrofit2.Response
@@ -10,11 +9,10 @@ import java.io.IOException
* This abstract class extract objects from Retrofit [Response] * This abstract class extract objects from Retrofit [Response]
* or throws IOException if object does not exist * or throws IOException if object does not exist
*/ */
private const val TAG = "SafeApiRequest"
abstract class SafeApiRequest { abstract class SafeApiRequest {
suspend fun <T : Any> responseUnwrap( suspend fun <T : Any> responseUnwrap(
call: suspend () -> Response<T> call: suspend () -> Response<T>
): T { ): T {
val response = call.invoke() val response = call.invoke()
@@ -34,21 +32,17 @@ abstract class SafeApiRequest {
val errorMessageString = errorBody.getError() val errorMessageString = errorBody.getError()
//build a log message to log in console //build a log message to log in console
val log = if (errorMessageString.isNullOrEmpty()){ val log = if (errorMessageString.isNullOrEmpty()) {
errorCode errorCode
}else{ } else {
StringBuilder() StringBuilder()
.append(errorCode) .append(errorCode)
.append("\n") .append("\n")
.append(errorMessageString) .append(errorMessageString)
.toString() .toString()
} }
print(log) print(log)
// Log.e("Api Response Error", log)
//return error message
//if null return error code
return errorMessageString ?: errorCode return errorMessageString ?: errorCode
} }

View File

@@ -11,39 +11,34 @@ import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
/** /**
* Retrofit2 Network class to create network requests * Retrofit Network class to currency api calls
*/ */
interface BackupCurrencyApi { interface BackupCurrencyApi {
// Get rate from server with arguments passed in Repository
@GET("latest?") @GET("latest?")
suspend fun getCurrencyRate( suspend fun getCurrencyRate(
@Query("from") currencyFrom: String, @Query("from") currencyFrom: String,
@Query("to") currencyTo: String @Query("to") currencyTo: String
): Response<CurrencyResponse> ): Response<CurrencyResponse>
// interface invokation to be used in application class companion object {
companion object{
operator fun invoke( operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor, networkConnectionInterceptor: NetworkConnectionInterceptor,
interceptor: HttpLoggingInterceptor interceptor: HttpLoggingInterceptor
) : BackupCurrencyApi{ ): BackupCurrencyApi {
val okkHttpclient = OkHttpClient.Builder() val okkHttpclient = OkHttpClient.Builder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.addNetworkInterceptor(networkConnectionInterceptor) .addNetworkInterceptor(networkConnectionInterceptor)
.build() .build()
// Build retrofit
return Retrofit.Builder() return Retrofit.Builder()
.client(okkHttpclient) .client(okkHttpclient)
.baseUrl("https://api.frankfurter.app/") .baseUrl("https://api.frankfurter.app/")
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
.create(BackupCurrencyApi::class.java) .create(BackupCurrencyApi::class.java)
} }
} }
} }

View File

@@ -12,7 +12,7 @@ import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
/** /**
* Retrofit2 Network class to create network requests * Retrofit Network class to currency api calls
*/ */
interface CurrencyApi { interface CurrencyApi {
@@ -21,21 +21,19 @@ interface CurrencyApi {
suspend fun getCurrencyRate(@Query("q") currency: String): Response<ResponseObject> suspend fun getCurrencyRate(@Query("q") currency: String): Response<ResponseObject>
// interface invokation to be used in application class // interface invokation to be used in application class
companion object{ companion object {
operator fun invoke( operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor, networkConnectionInterceptor: NetworkConnectionInterceptor,
queryInterceptor: QueryInterceptor, queryInterceptor: QueryInterceptor,
interceptor: HttpLoggingInterceptor interceptor: HttpLoggingInterceptor
) : CurrencyApi{ ): CurrencyApi {
// okkHttpclient with injected interceptors
val okkHttpclient = OkHttpClient.Builder() val okkHttpclient = OkHttpClient.Builder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.addInterceptor(queryInterceptor) .addInterceptor(queryInterceptor)
.addNetworkInterceptor(networkConnectionInterceptor) .addNetworkInterceptor(networkConnectionInterceptor)
.build() .build()
// Build retrofit
return Retrofit.Builder() return Retrofit.Builder()
.client(okkHttpclient) .client(okkHttpclient)
.baseUrl("https://free.currencyconverterapi.com/api/v3/") .baseUrl("https://free.currencyconverterapi.com/api/v3/")

View File

@@ -3,14 +3,17 @@ package com.appttude.h_mal.easycc.data.network.interceptors
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build
import androidx.annotation.RequiresApi
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import java.io.IOException import java.io.IOException
/** /**
* Interceptor used in [CurrencyApi] to intercept network status * Interceptor used in network classes to check network status
* *
*/ */
@Suppress("DEPRECATION")
class NetworkConnectionInterceptor( class NetworkConnectionInterceptor(
context: Context context: Context
) : Interceptor { ) : Interceptor {
@@ -29,11 +32,22 @@ class NetworkConnectionInterceptor(
val connectivityManager = val connectivityManager =
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let { connectivityManager?.let {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
result = when { it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true result = when {
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
else -> false
}
}
} else {
it.activeNetworkInfo?.run {
result = when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
else -> false
}
} }
} }
} }

View File

@@ -1,7 +1,6 @@
package com.appttude.h_mal.easycc.data.network.interceptors package com.appttude.h_mal.easycc.data.network.interceptors
import android.content.Context import android.content.Context
import com.appttude.h_mal.easycc.BuildConfig
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor

View File

@@ -18,8 +18,8 @@ class PreferenceProvider(context: Context) {
private val appContext = context.applicationContext private val appContext = context.applicationContext
// Instance of Shared preferences // Instance of Shared preferences
private val preference: SharedPreferences private val preference: SharedPreferences =
= PreferenceManager.getDefaultSharedPreferences(appContext) PreferenceManager.getDefaultSharedPreferences(appContext)
// Lazy declaration of default rate if no rate is retrieved from // Lazy declaration of default rate if no rate is retrieved from
private val defaultRate: String by lazy { private val defaultRate: String by lazy {
@@ -29,9 +29,9 @@ class PreferenceProvider(context: Context) {
// Save currency pairs into prefs // Save currency pairs into prefs
fun saveConversionPair(s1: String, s2: String) { fun saveConversionPair(s1: String, s2: String) {
preference.edit() preference.edit()
.putString(CURRENCY_ONE, s1) .putString(CURRENCY_ONE, s1)
.putString(CURRENCY_TWO, s2) .putString(CURRENCY_TWO, s2)
.apply() .apply()
} }
// Retrieve Currency pairs from prefs // Retrieve Currency pairs from prefs
@@ -46,17 +46,19 @@ class PreferenceProvider(context: Context) {
private fun getConversionString(conversionName: String): String? { private fun getConversionString(conversionName: String): String? {
return preference return preference
.getString(conversionName, defaultRate) .getString(conversionName, defaultRate)
} }
// Save currency pairs for widget // Save currency pairs for widget
fun saveWidgetConversionPair(fromString: String, fun saveWidgetConversionPair(
toString: String, appWidgetId: Int) { fromString: String,
toString: String, appWidgetId: Int
) {
preference.edit() preference.edit()
.putString("${appWidgetId}_$CURRENCY_ONE", fromString) .putString("${appWidgetId}_$CURRENCY_ONE", fromString)
.putString("${appWidgetId}_$CURRENCY_TWO", toString) .putString("${appWidgetId}_$CURRENCY_TWO", toString)
.apply() .apply()
} }
// Retrieve currency pairs for widget // Retrieve currency pairs for widget
@@ -68,17 +70,17 @@ class PreferenceProvider(context: Context) {
} }
private fun getWidgetConversionString( private fun getWidgetConversionString(
appWidgetId: Int, conversionName: String): String? { appWidgetId: Int, conversionName: String
): String? {
return preference return preference
.getString("${appWidgetId}_$conversionName", defaultRate) .getString("${appWidgetId}_$conversionName", defaultRate)
} }
fun removeWidgetConversion(id: Int) { fun removeWidgetConversion(id: Int) {
preference.edit() preference.edit()
.remove("${id}_$CURRENCY_ONE") .remove("${id}_$CURRENCY_ONE")
.remove("${id}_$CURRENCY_TWO") .remove("${id}_$CURRENCY_TWO")
.apply() .apply()
} }
} }

View File

@@ -13,33 +13,33 @@ import com.appttude.h_mal.easycc.utils.convertPairsListToString
/** /**
* Default implementation of [Repository]. Single entry point for managing currency' data. * Default implementation of [Repository]. Single entry point for managing currency' data.
*/ */
class RepositoryImpl ( class RepositoryImpl(
private val api: CurrencyApi, private val api: CurrencyApi,
private val backUpApi: BackupCurrencyApi, private val backUpApi: BackupCurrencyApi,
private val prefs: PreferenceProvider private val prefs: PreferenceProvider
):Repository, SafeApiRequest(){ ) : Repository, SafeApiRequest() {
override suspend fun getDataFromApi( override suspend fun getDataFromApi(
fromCurrency: String, fromCurrency: String,
toCurrency: String toCurrency: String
): ResponseObject{ ): ResponseObject {
// Set currency pairs as correct string for api query eg. AUD_GBP // Set currency pairs as correct string for api query eg. AUD_GBP
val currencyPair = convertPairsListToString(fromCurrency, toCurrency) val currencyPair = convertPairsListToString(fromCurrency, toCurrency)
return responseUnwrap{ api.getCurrencyRate(currencyPair)} return responseUnwrap { api.getCurrencyRate(currencyPair) }
} }
override suspend fun getBackupDataFromApi( override suspend fun getBackupDataFromApi(
fromCurrency: String, fromCurrency: String,
toCurrency: String toCurrency: String
): CurrencyResponse { ): CurrencyResponse {
return responseUnwrap{ backUpApi.getCurrencyRate(fromCurrency, toCurrency)} return responseUnwrap { backUpApi.getCurrencyRate(fromCurrency, toCurrency) }
} }
override fun getConversionPair(): Pair<String?, String?> { override fun getConversionPair(): Pair<String?, String?> {
return prefs.getConversionPair() return prefs.getConversionPair()
} }
override fun setConversionPair(fromCurrency: String, toCurrency: String){ override fun setConversionPair(fromCurrency: String, toCurrency: String) {
prefs.saveConversionPair(fromCurrency, toCurrency) prefs.saveConversionPair(fromCurrency, toCurrency)
} }
@@ -47,14 +47,16 @@ class RepositoryImpl (
Resources.getSystem().getStringArray(R.array.currency_arrays) Resources.getSystem().getStringArray(R.array.currency_arrays)
override fun getWidgetConversionPairs(appWidgetId: Int): Pair<String?, String?> = override fun getWidgetConversionPairs(appWidgetId: Int): Pair<String?, String?> =
prefs.getWidgetConversionPair(appWidgetId) prefs.getWidgetConversionPair(appWidgetId)
override fun setWidgetConversionPairs(fromCurrency: String, override fun setWidgetConversionPairs(
toCurrency: String, appWidgetId: Int) { fromCurrency: String,
toCurrency: String, appWidgetId: Int
) {
return prefs.saveWidgetConversionPair(fromCurrency, toCurrency, appWidgetId) return prefs.saveWidgetConversionPair(fromCurrency, toCurrency, appWidgetId)
} }
override fun removeWidgetConversionPairs(id: Int) = override fun removeWidgetConversionPairs(id: Int) =
prefs.removeWidgetConversion(id) prefs.removeWidgetConversion(id)
} }

View File

@@ -2,16 +2,15 @@ package com.appttude.h_mal.easycc.helper
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyModelInterface import com.appttude.h_mal.easycc.models.CurrencyModelInterface
import java.lang.Exception
class CurrencyDataHelper ( class CurrencyDataHelper(
val repository: Repository val repository: Repository
){ ) {
suspend fun getDataFromApi(from: String, to: String): CurrencyModelInterface{ suspend fun getDataFromApi(from: String, to: String): CurrencyModelInterface {
return try { return try {
repository.getDataFromApi(from, to) repository.getDataFromApi(from, to)
}catch (e: Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
repository.getBackupDataFromApi(from, to) repository.getBackupDataFromApi(from, to)
} }

View File

@@ -4,12 +4,10 @@ import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyModel import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
import kotlin.Exception class WidgetHelper(
private val helper: CurrencyDataHelper,
class WidgetHelper (
val helper: CurrencyDataHelper,
val repository: Repository val repository: Repository
){ ) {
suspend fun getWidgetData(): CurrencyModel? { suspend fun getWidgetData(): CurrencyModel? {
try { try {
@@ -18,13 +16,13 @@ class WidgetHelper (
val s2 = pair.second?.trimToThree() ?: return null val s2 = pair.second?.trimToThree() ?: return null
return helper.getDataFromApi(s1, s2).getCurrencyModel() return helper.getDataFromApi(s1, s2).getCurrencyModel()
}catch (e: Exception){ } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return null return null
} }
} }
fun removeWidgetData(id: Int){ fun removeWidgetData(id: Int) {
repository.removeWidgetConversionPairs(id) repository.removeWidgetConversionPairs(id)
} }
} }

View File

@@ -6,6 +6,6 @@ data class CurrencyModel(
var rate: Double = 0.0 var rate: Double = 0.0
) )
interface CurrencyModelInterface{ interface CurrencyModelInterface {
fun getCurrencyModel(): CurrencyModel fun getCurrencyModel(): CurrencyModel
} }

View File

@@ -2,13 +2,13 @@ package com.appttude.h_mal.easycc.models
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class CurrencyObject ( data class CurrencyObject(
@SerializedName("id") @SerializedName("id")
var id : String, var id: String,
@SerializedName("fr") @SerializedName("fr")
var fr : String, var fr: String,
@SerializedName("to") @SerializedName("to")
var to : String, var to: String,
@SerializedName("val") @SerializedName("val")
var value: Double var value: Double
) )

View File

@@ -13,9 +13,10 @@ import kotlinx.android.synthetic.main.custom_dialog.*
/** /**
* Custom dialog when selecting currencies from list with filter * Custom dialog when selecting currencies from list with filter
*/ */
@Suppress("DEPRECATION")
class CustomDialogClass( class CustomDialogClass(
context: Context, context: Context,
private val clickListener: ClickListener private val clickListener: ClickListener
) : Dialog(context) { ) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -29,9 +30,10 @@ class CustomDialogClass(
// array adapter for list of currencies in R.Strings // array adapter for list of currencies in R.Strings
val arrayAdapter = val arrayAdapter =
ArrayAdapter.createFromResource( ArrayAdapter.createFromResource(
context, R.array.currency_arrays, context, R.array.currency_arrays,
android.R.layout.simple_list_item_1) android.R.layout.simple_list_item_1
)
list_view.adapter = arrayAdapter list_view.adapter = arrayAdapter
@@ -41,11 +43,12 @@ class CustomDialogClass(
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
arrayAdapter.filter.filter(charSequence) arrayAdapter.filter.filter(charSequence)
} }
override fun afterTextChanged(editable: Editable) {} override fun afterTextChanged(editable: Editable) {}
}) })
// interface selection back to calling activity // interface selection back to calling activity
list_view.setOnItemClickListener{ adapterView, _, i, _ -> list_view.setOnItemClickListener { adapterView, _, i, _ ->
clickListener.onText(adapterView.getItemAtPosition(i).toString()) clickListener.onText(adapterView.getItemAtPosition(i).toString())
dismiss() dismiss()
} }
@@ -53,6 +56,6 @@ class CustomDialogClass(
} }
// Interface to handle selection within dialog // Interface to handle selection within dialog
interface ClickListener{ interface ClickListener {
fun onText(currencyName: String) fun onText(currencyName: String)
} }

View File

@@ -20,6 +20,7 @@ import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein import org.kodein.di.android.kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
@Suppress("DEPRECATION")
class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener { class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener {
// Retrieve MainViewModelFactory via dependency injection // Retrieve MainViewModelFactory via dependency injection
@@ -54,10 +55,10 @@ class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener {
} }
private fun setUpObservers() { private fun setUpObservers() {
viewModel.operationStartedListener.observe(this, Observer { viewModel.operationStartedListener.observe(this, {
progressBar.hideView(false) progressBar.hideView(false)
}) })
viewModel.operationFinishedListener.observe(this, Observer { pair -> viewModel.operationFinishedListener.observe(this, { pair ->
// hide progress bar // hide progress bar
progressBar.hideView(true) progressBar.hideView(true)
if (pair.first) { if (pair.first) {

View File

@@ -16,9 +16,9 @@ import java.io.IOException
* ViewModel for the task Main Activity Screen * ViewModel for the task Main Activity Screen
*/ */
class MainViewModel( class MainViewModel(
private val currencyDataHelper: CurrencyDataHelper, private val currencyDataHelper: CurrencyDataHelper,
private val repository: Repository private val repository: Repository
) : ViewModel(){ ) : ViewModel() {
private val conversionPairs by lazy { repository.getConversionPair() } private val conversionPairs by lazy { repository.getConversionPair() }
@@ -32,17 +32,17 @@ class MainViewModel(
private var conversionRate: Double = 1.00 private var conversionRate: Double = 1.00
private fun getExchangeRate(){ private fun getExchangeRate() {
operationStartedListener.postValue(true) operationStartedListener.postValue(true)
// view binded exchange rates selected null checked // view binded exchange rates selected null checked
if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()){ if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()) {
operationFinishedListener.postValue(Pair(false, "Select currencies")) operationFinishedListener.postValue(Pair(false, "Select currencies"))
return return
} }
// No need to call api as it will return exchange rate as 1 // No need to call api as it will return exchange rate as 1
if (rateIdFrom == rateIdTo){ if (rateIdFrom == rateIdTo) {
conversionRate = 1.00 conversionRate = 1.00
operationFinishedListener.postValue(Pair(true, null)) operationFinishedListener.postValue(Pair(true, null))
return return
@@ -63,7 +63,7 @@ class MainViewModel(
operationFinishedListener.postValue(Pair(true, null)) operationFinishedListener.postValue(Pair(true, null))
return@launch return@launch
} }
}catch(e: IOException){ } catch (e: IOException) {
e.message?.let { e.message?.let {
operationFinishedListener.postValue(Pair(false, it)) operationFinishedListener.postValue(Pair(false, it))
return@launch return@launch
@@ -78,7 +78,7 @@ class MainViewModel(
val fromValDouble = fromValue.toDouble() val fromValDouble = fromValue.toDouble()
val bottomVal1 = (fromValDouble * conversionRate) val bottomVal1 = (fromValDouble * conversionRate)
bottomVal1.toTwoDpString() bottomVal1.toTwoDpString()
}catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
null null
} }
} }
@@ -86,7 +86,7 @@ class MainViewModel(
fun getReciprocalConversion(toValue: String): String? { fun getReciprocalConversion(toValue: String): String? {
return try { return try {
val toDoubleVal = toValue.toDouble() val toDoubleVal = toValue.toDouble()
val newTopVal = toDoubleVal.times((1/conversionRate)) val newTopVal = toDoubleVal.times((1 / conversionRate))
newTopVal.toTwoDpString() newTopVal.toTwoDpString()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
null null
@@ -94,11 +94,13 @@ class MainViewModel(
} }
// Start operation based on dialog selection // Start operation based on dialog selection
fun setCurrencyName(tag: Any?, currencyName: String){ fun setCurrencyName(tag: Any?, currencyName: String) {
when(tag.toString()){ when (tag.toString()) {
"top" -> rateIdFrom = currencyName "top" -> rateIdFrom = currencyName
"bottom" -> rateIdTo = currencyName "bottom" -> rateIdTo = currencyName
else -> { return } else -> {
return
}
} }
getExchangeRate() getExchangeRate()

View File

@@ -10,10 +10,10 @@ import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
* inject repository into viewmodel * inject repository into viewmodel
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class MainViewModelFactory ( class MainViewModelFactory(
private val repository: RepositoryImpl, private val repository: RepositoryImpl,
private val helper: CurrencyDataHelper private val helper: CurrencyDataHelper
): ViewModelProvider.NewInstanceFactory(){ ) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(helper, repository) as T return MainViewModel(helper, repository) as T

View File

@@ -25,7 +25,8 @@ import org.kodein.di.generic.instance
/** /**
* The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget. * The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget.
*/ */
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAware, View.OnClickListener { class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAware,
View.OnClickListener {
override val kodein by kodein() override val kodein by kodein()
private val factory: WidgetViewModelFactory by instance() private val factory: WidgetViewModelFactory by instance()
@@ -47,7 +48,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
val extras = intent.extras val extras = intent.extras
if (extras != null) { if (extras != null) {
mAppWidgetId = extras.getInt( mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
)
} }
// If this activity was started with an intent without an app widget ID, finish with an error. // If this activity was started with an intent without an app widget ID, finish with an error.
@@ -72,12 +74,12 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
} }
private fun setupObserver() { private fun setupObserver() {
viewModel.operationFinishedListener.observe(this, Observer { viewModel.operationFinishedListener.observe(this, {
// it.first is a the success of the operation // it.first is a the success of the operation
if (it.first){ if (it.first) {
displaySubmitDialog() displaySubmitDialog()
}else{ } else {
// failed operation - display toast with message from it.second // failed operation - display toast with message from it.second
it.second?.let { message -> displayToast(message) } it.second?.let { message -> displayToast(message) }
} }
@@ -87,8 +89,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
private fun setupDataBinding() { private fun setupDataBinding() {
// data binding to @R.layout.currency_app_widget_configure // data binding to @R.layout.currency_app_widget_configure
DataBindingUtil.setContentView<CurrencyAppWidgetConfigureBinding>( DataBindingUtil.setContentView<CurrencyAppWidgetConfigureBinding>(
this, this,
R.layout.currency_app_widget_configure R.layout.currency_app_widget_configure
).apply { ).apply {
viewmodel = viewModel viewmodel = viewModel
lifecycleOwner = this@CurrencyAppWidgetConfigureActivityKotlin lifecycleOwner = this@CurrencyAppWidgetConfigureActivityKotlin
@@ -125,7 +127,7 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
}).show() }).show()
} }
fun finishCurrencyWidgetActivity(){ fun finishCurrencyWidgetActivity() {
// Make sure we pass back the original appWidgetId // Make sure we pass back the original appWidgetId
val resultValue = intent val resultValue = intent
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId) resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)
@@ -136,8 +138,9 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
fun sendUpdateIntent() { fun sendUpdateIntent() {
// It is the responsibility of the configuration activity to update the app widget // It is the responsibility of the configuration activity to update the app widget
// Send update broadcast to widget app class // Send update broadcast to widget app class
Intent(this@CurrencyAppWidgetConfigureActivityKotlin, Intent(
CurrencyAppWidgetKotlin::class.java this@CurrencyAppWidgetConfigureActivityKotlin,
CurrencyAppWidgetKotlin::class.java
).apply { ).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
viewModel.setWidgetStored() viewModel.setWidgetStored()

View File

@@ -1,48 +0,0 @@
package com.appttude.h_mal.easycc.ui.widget
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.WindowManager
import android.widget.ArrayAdapter
import com.appttude.h_mal.easycc.R
import kotlinx.android.synthetic.main.custom_dialog.*
/*
widget for when submitting the completed selections
*/
class WidgetItemSelectDialog(
context: Context,
private val dialogResult: DialogResult
) :Dialog(context){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.custom_dialog)
window!!.setBackgroundDrawableResource(android.R.color.transparent)
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
val arrayAdapter = ArrayAdapter.createFromResource(context, R.array.currency_arrays, android.R.layout.simple_list_item_1)
list_view.adapter = arrayAdapter
search_text.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
arrayAdapter.filter.filter(charSequence)
}
override fun afterTextChanged(editable: Editable) {}
})
list_view.setOnItemClickListener{ adapterView, _, i, _ ->
dialogResult.result(adapterView.getItemAtPosition(i).toString())
dismiss()
}
}
}
interface DialogResult{
fun result(result : String)
}

View File

@@ -15,7 +15,7 @@ class WidgetSubmitDialog(
context: Context, context: Context,
private val messageString: String, private val messageString: String,
private val dialogInterface: DialogSubmit private val dialogInterface: DialogSubmit
) :Dialog(context){ ) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -33,6 +33,6 @@ class WidgetSubmitDialog(
} }
} }
interface DialogSubmit{ interface DialogSubmit {
fun onSubmit() fun onSubmit()
} }

View File

@@ -6,8 +6,8 @@ import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
class WidgetViewModel( class WidgetViewModel(
private val repository: Repository private val repository: Repository
) : ViewModel(){ ) : ViewModel() {
private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] } private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] }
var appWidgetId: Int? = null var appWidgetId: Int? = null
@@ -21,10 +21,9 @@ class WidgetViewModel(
// Setup viewmodel app widget ID // Setup viewmodel app widget ID
// Set default values for text views // Set default values for text views
fun initiate(appId: Int){ fun initiate(appId: Int) {
appWidgetId = appId appWidgetId = appId
val widgetString val widgetString = repository.getWidgetConversionPairs(appId)
= repository.getWidgetConversionPairs(appId)
rateIdFrom = widgetString.first ?: defaultCurrency rateIdFrom = widgetString.first ?: defaultCurrency
rateIdTo = widgetString.second ?: defaultCurrency rateIdTo = widgetString.second ?: defaultCurrency
@@ -35,33 +34,35 @@ class WidgetViewModel(
fun getSubmitDialogMessage(): String { fun getSubmitDialogMessage(): String {
val widgetName = getWidgetStringName() val widgetName = getWidgetStringName()
return StringBuilder().append("Create widget for ") return StringBuilder().append("Create widget for ")
.append(widgetName) .append(widgetName)
.append("?").toString() .append("?").toString()
} }
fun submitSelectionOnClick(){ fun submitSelectionOnClick() {
if (rateIdTo == null || rateIdFrom == null){ if (rateIdTo == null || rateIdFrom == null) {
operationFinishedListener.value = Pair(false, "Selections incomplete") operationFinishedListener.value = Pair(false, "Selections incomplete")
return return
} }
if (rateIdFrom == rateIdTo){ if (rateIdFrom == rateIdTo) {
operationFinishedListener.value = operationFinishedListener.value =
Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}") Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}")
return return
} }
operationFinishedListener.value = Pair(true, null) operationFinishedListener.value = Pair(true, null)
} }
fun setWidgetStored() { fun setWidgetStored() {
repository.setWidgetConversionPairs(rateIdFrom!!,rateIdTo!!,appWidgetId!!) repository.setWidgetConversionPairs(rateIdFrom!!, rateIdTo!!, appWidgetId!!)
} }
// Start operation based on dialog selection // Start operation based on dialog selection
fun setCurrencyName(tag: Any?, currencyName: String){ fun setCurrencyName(tag: Any?, currencyName: String) {
when(tag.toString()){ when (tag.toString()) {
"top" -> rateIdFrom = currencyName "top" -> rateIdFrom = currencyName
"bottom" -> rateIdTo = currencyName "bottom" -> rateIdTo = currencyName
else -> { return } else -> {
return
}
} }
} }

View File

@@ -5,9 +5,9 @@ import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.easycc.data.repository.RepositoryImpl import com.appttude.h_mal.easycc.data.repository.RepositoryImpl
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class WidgetViewModelFactory ( class WidgetViewModelFactory(
private val repository: RepositoryImpl private val repository: RepositoryImpl
): ViewModelProvider.NewInstanceFactory(){ ) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return WidgetViewModel(repository) as T return WidgetViewModel(repository) as T

View File

@@ -2,7 +2,6 @@ package com.appttude.h_mal.easycc.utils
import java.lang.Double.valueOf import java.lang.Double.valueOf
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.*
fun transformIntToArray(int: Int): IntArray{ fun transformIntToArray(int: Int): IntArray{
return intArrayOf(int) return intArrayOf(int)

View File

@@ -5,14 +5,18 @@ import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
fun EditText.clearEditText(){ fun EditText.clearEditText() {
this.setText("") this.setText("")
} }
fun View.hideView(vis : Boolean){ fun View.hideView(vis: Boolean) {
visibility = if (vis){ View.GONE } else { View.VISIBLE } visibility = if (vis) {
View.GONE
} else {
View.VISIBLE
}
} }
fun Context.displayToast(message: String){ fun Context.displayToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show() Toast.makeText(this, message, Toast.LENGTH_LONG).show()
} }

View File

@@ -1,21 +1,11 @@
package com.appttude.h_mal.easycc.widget package com.appttude.h_mal.easycc.widget
import android.app.Activity
import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import android.widget.RemoteViews
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.helper.WidgetHelper import com.appttude.h_mal.easycc.helper.WidgetHelper
import com.appttude.h_mal.easycc.ui.main.MainActivity import com.appttude.h_mal.easycc.widget.WidgetServiceIntent.Companion.enqueueWork
import com.appttude.h_mal.easycc.utils.transformIntToArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
@@ -23,9 +13,8 @@ import org.kodein.di.generic.instance
/** /**
* Implementation of App Widget functionality. * Implementation of App Widget functionality.
* App Widget Configuration implemented in [CurrencyAppWidgetConfigureActivityKotlin] * App Widget Configuration implemented in [CurrencyAppWidgetKotlin]
*/ */
private const val TAG = "CurrencyAppWidgetKotlin"
class CurrencyAppWidgetKotlin : AppWidgetProvider() { class CurrencyAppWidgetKotlin : AppWidgetProvider() {
@@ -39,12 +28,7 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() {
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray appWidgetIds: IntArray
) { ) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein loadWidget(context)
Log.i(TAG, "onUpdate() appWidgetIds = ${appWidgetIds.size}")
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
super.onUpdate(context, appWidgetManager, appWidgetIds) super.onUpdate(context, appWidgetManager, appWidgetIds)
} }
@@ -58,113 +42,13 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() {
} }
override fun onEnabled(context: Context) { override fun onEnabled(context: Context) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein loadWidget(context)
// Enter relevant functionality for when the first widget is created
AppWidgetManager.getInstance(context).apply {
val thisAppWidget =
ComponentName(context.packageName, CurrencyAppWidgetKotlin::class.java.name)
val appWidgetIds = getAppWidgetIds(thisAppWidget)
onUpdate(context, this, appWidgetIds)
}
super.onEnabled(context) super.onEnabled(context)
} }
override fun onDisabled(context: Context) { private fun loadWidget(context: Context) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein val mIntent = Intent(context, CurrencyAppWidgetKotlin::class.java)
// Enter relevant functionality for when the last widget is disabled enqueueWork(context, mIntent)
}
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.currency_app_widget)
CoroutineScope(Dispatchers.Main).launch {
val exchangeResponse = repository.getWidgetData()
exchangeResponse?.let {
val titleString = "${it.from}${it.to}"
views.setTextViewText(R.id.exchangeName, titleString)
views.setTextViewText(R.id.exchangeRate, it.rate.toString())
setUpdateIntent(context, appWidgetId).let { intent ->
//set the pending intent to the icon
views.setImageViewResource(R.id.refresh_icon, R.drawable.ic_refresh_white_24dp)
views.setOnClickPendingIntent(R.id.refresh_icon, intent)
}
val clickIntentTemplate = clickingIntent(context)
val configPendingIntent =
PendingIntent.getActivity(
context, appWidgetId, clickIntentTemplate,
PendingIntent.FLAG_UPDATE_CURRENT
)
views.setOnClickPendingIntent(R.id.widget_view, configPendingIntent)
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
private fun setUpdateIntent(context: Context, appWidgetId: Int): PendingIntent? {
//Create update intent for refresh icon
val updateIntent = Intent(
context, CurrencyAppWidgetKotlin::class.java
).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, transformIntToArray(appWidgetId))
}
//add previous intent to this pending intent
return PendingIntent.getBroadcast(
context,
appWidgetId,
updateIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
private fun clickingIntent(
context: Context
): Intent {
val pair = repository.repository.getConversionPair()
val s1 = pair.first
val s2 = pair.second
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("parse_1", s1)
putExtra("parse_2", s2)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
private fun <T: Activity> clickingIntent(
context: Context,
activity: Class<T>,
vararg argPairs: Pair<String, Any?>
): Intent {
return Intent(context, activity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
argPairs.forEach {
putExtra(it.first, it.second)
}
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
private fun <T: Any> Intent.putExtra(s: String, second: T?) {
when(second){
is String -> putExtra(s,second)
}
} }
} }

View File

@@ -0,0 +1,121 @@
package com.appttude.h_mal.easycc.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import androidx.core.app.JobIntentService
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.helper.WidgetHelper
import com.appttude.h_mal.easycc.ui.main.MainActivity
import com.appttude.h_mal.easycc.utils.transformIntToArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
class WidgetServiceIntent : JobIntentService() {
//DI with kodein to use in CurrencyAppWidgetKotlin
private val kodein = LateInitKodein()
private val repository: WidgetHelper by kodein.instance()
override fun onHandleWork(intent: Intent) {
kodein.baseKodein = this.kodein
val appWidgetManager = AppWidgetManager.getInstance(this)
val thisAppWidget = ComponentName(packageName, CurrencyAppWidgetKotlin::class.java.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
for (appWidgetId in appWidgetIds) {
updateAppWidget(this, appWidgetManager, appWidgetId)
}
}
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.currency_app_widget)
CoroutineScope(Dispatchers.Main).launch {
val exchangeResponse = repository.getWidgetData()
exchangeResponse?.let {
val titleString = "${it.from}${it.to}"
views.setTextViewText(R.id.exchangeName, titleString)
views.setTextViewText(R.id.exchangeRate, it.rate.toString())
setUpdateIntent(context, appWidgetId).let { intent ->
//set the pending intent to the icon
views.setImageViewResource(R.id.refresh_icon, R.drawable.ic_refresh_white_24dp)
views.setOnClickPendingIntent(R.id.refresh_icon, intent)
}
val clickIntentTemplate = clickingIntent(context)
val configPendingIntent =
PendingIntent.getActivity(
context, appWidgetId, clickIntentTemplate,
PendingIntent.FLAG_UPDATE_CURRENT
)
views.setOnClickPendingIntent(R.id.widget_view, configPendingIntent)
}
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
private fun setUpdateIntent(context: Context, appWidgetId: Int): PendingIntent? {
//Create update intent for refresh icon
val updateIntent = Intent(
context, CurrencyAppWidgetKotlin::class.java
).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, transformIntToArray(appWidgetId))
}
//add previous intent to this pending intent
return PendingIntent.getBroadcast(
context,
appWidgetId,
updateIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
private fun clickingIntent(
context: Context
): Intent {
val pair = repository.repository.getConversionPair()
val s1 = pair.first
val s2 = pair.second
return Intent(context, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("parse_1", s1)
putExtra("parse_2", s2)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
companion object {
/**
* Unique job ID for this service.
*/
private const val JOB_ID = 1000
/**
* Convenience method for enqueuing work in to this service.
*/
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(context, WidgetServiceIntent::class.java, JOB_ID, work)
}
}
}

View File

@@ -1,20 +0,0 @@
package com.appttude.h_mal.easycc;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -2,7 +2,6 @@ package com.appttude.h_mal.easycc.repository
import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi
import com.appttude.h_mal.easycc.data.network.api.CurrencyApi import com.appttude.h_mal.easycc.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse
import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
@@ -23,14 +22,16 @@ import java.io.IOException
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class RepositoryNetworkTest{ class RepositoryNetworkTest {
lateinit var repository: Repository lateinit var repository: Repository
@Mock @Mock
lateinit var api: CurrencyApi lateinit var api: CurrencyApi
@Mock @Mock
lateinit var apiBackup: BackupCurrencyApi lateinit var apiBackup: BackupCurrencyApi
@Mock @Mock
lateinit var prefs: PreferenceProvider lateinit var prefs: PreferenceProvider
@@ -54,7 +55,7 @@ class RepositoryNetworkTest{
Mockito.`when`(api.getCurrencyRate(currencyPair)).thenReturn(re) Mockito.`when`(api.getCurrencyRate(currencyPair)).thenReturn(re)
//THEN - the unwrapped login response contains the correct values //THEN - the unwrapped login response contains the correct values
val currencyResponse = repository.getDataFromApi(s1,s2) val currencyResponse = repository.getDataFromApi(s1, s2)
assertNotNull(currencyResponse) assertNotNull(currencyResponse)
assertEquals(currencyResponse, mockCurrencyResponse) assertEquals(currencyResponse, mockCurrencyResponse)
} }

View File

@@ -1,6 +1,5 @@
package com.appttude.h_mal.easycc.repository package com.appttude.h_mal.easycc.repository
import android.content.Context
import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi
import com.appttude.h_mal.easycc.data.network.api.CurrencyApi import com.appttude.h_mal.easycc.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider
@@ -19,8 +18,10 @@ class RepositoryStorageTest {
@Mock @Mock
lateinit var api: CurrencyApi lateinit var api: CurrencyApi
@Mock @Mock
lateinit var apiBackup: BackupCurrencyApi lateinit var apiBackup: BackupCurrencyApi
@Mock @Mock
lateinit var prefs: PreferenceProvider lateinit var prefs: PreferenceProvider

View File

@@ -6,10 +6,10 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
import kotlinx.coroutines.runBlocking
import org.junit.Before
import com.appttude.h_mal.easycc.utils.observeOnce import com.appttude.h_mal.easycc.utils.observeOnce
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.Mock import org.mockito.Mock
@@ -38,7 +38,7 @@ class MainViewModelTest {
} }
@Test @Test
fun initiate_validBundleValues_successResponse() = runBlocking{ fun initiate_validBundleValues_successResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound" val currencyTwo = "GBP - British Pound"
@@ -48,7 +48,8 @@ class MainViewModelTest {
//WHEN //WHEN
Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne) Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne)
Mockito.`when`(bundle.getString("parse_2")).thenReturn(currencyTwo) Mockito.`when`(bundle.getString("parse_2")).thenReturn(currencyTwo)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN //THEN
viewModel.initiate(bundle) viewModel.initiate(bundle)
@@ -62,7 +63,7 @@ class MainViewModelTest {
} }
@Test @Test
fun initiate_invalidBundleValues_successfulResponse() = runBlocking{ fun initiate_invalidBundleValues_successfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound" val currencyTwo = "GBP - British Pound"
@@ -71,7 +72,8 @@ class MainViewModelTest {
//WHEN //WHEN
Mockito.`when`(repository.getConversionPair()).thenReturn(pair) Mockito.`when`(repository.getConversionPair()).thenReturn(pair)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN //THEN
viewModel.initiate(null) viewModel.initiate(null)
@@ -85,7 +87,7 @@ class MainViewModelTest {
} }
@Test @Test
fun initiate_sameBundleValues_successfulResponse() = runBlocking{ fun initiate_sameBundleValues_successfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val currencyOne = "AUD - Australian Dollar"
val bundle = mock(Bundle()::class.java) val bundle = mock(Bundle()::class.java)
@@ -128,9 +130,8 @@ class MainViewModelTest {
} }
@Test @Test
fun setCurrencyName_validValues_successResponse() = runBlocking{ fun setCurrencyName_validValues_successResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound" val currencyTwo = "GBP - British Pound"
@@ -139,7 +140,8 @@ class MainViewModelTest {
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(ResponseObject::class.java)
//WHEN //WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN //THEN
viewModel.setCurrencyName(tag, currencyOne) viewModel.setCurrencyName(tag, currencyOne)
@@ -154,7 +156,7 @@ class MainViewModelTest {
} }
@Test @Test
fun setCurrencyName_sameValues_successfulResponse() = runBlocking{ fun setCurrencyName_sameValues_successfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound" val currencyTwo = "GBP - British Pound"
@@ -163,7 +165,8 @@ class MainViewModelTest {
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(ResponseObject::class.java)
//WHEN //WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN //THEN
viewModel.setCurrencyName(tag, currencyOne) viewModel.setCurrencyName(tag, currencyOne)
@@ -177,7 +180,7 @@ class MainViewModelTest {
} }
@Test @Test
fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking{ fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound" val currencyTwo = "GBP - British Pound"
@@ -185,7 +188,8 @@ class MainViewModelTest {
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(ResponseObject::class.java)
//WHEN //WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN //THEN
viewModel.setCurrencyName(tag, currencyOne) viewModel.setCurrencyName(tag, currencyOne)

View File

@@ -3,8 +3,8 @@ package com.appttude.h_mal.easycc.ui.widget
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.utils.observeOnce import com.appttude.h_mal.easycc.utils.observeOnce
import org.junit.Before
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.Mock import org.mockito.Mock
@@ -14,6 +14,7 @@ import org.mockito.MockitoAnnotations
private const val currencyOne = "AUD - Australian Dollar" private const val currencyOne = "AUD - Australian Dollar"
private const val currencyTwo = "GBP - British Pound" private const val currencyTwo = "GBP - British Pound"
class WidgetViewModelTest { class WidgetViewModelTest {
// Run tasks synchronously // Run tasks synchronously
@@ -35,7 +36,7 @@ class WidgetViewModelTest {
fun initiate_validInput_successfulResponse() { fun initiate_validInput_successfulResponse() {
//GIVEN //GIVEN
val appId = 123 val appId = 123
val pair = Pair(currencyOne,currencyTwo) val pair = Pair(currencyOne, currencyTwo)
//WHEN //WHEN
Mockito.`when`(repository.getWidgetConversionPairs(appId)).thenReturn(pair) Mockito.`when`(repository.getWidgetConversionPairs(appId)).thenReturn(pair)

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.easycc package com.appttude.h_mal.easycc.utils
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner

View File

@@ -1,7 +1,6 @@
package com.appttude.h_mal.easycc.utils package com.appttude.h_mal.easycc.utils
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.appttude.h_mal.easycc.OneTimeObserver
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) { fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler) val observer = OneTimeObserver(handler = onChangeHandler)

View File

@@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.3.61' ext.kotlin_version = '1.4.10'
repositories { repositories {
jcenter() jcenter()
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.1' classpath 'com.android.tools.build:gradle:4.0.2'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@@ -1,6 +1,6 @@
#Thu Mar 05 18:27:33 UTC 2020 #Sat Jun 12 22:27:25 BST 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip