- 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 {
// Kodein Dependecy Injection singletons and providers created
// KODEIN DI components declaration
override val kodein by Kodein.lazy {
import(androidXModule(this@AppClass))

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ import retrofit2.http.GET
import retrofit2.http.Query
/**
* Retrofit2 Network class to create network requests
* Retrofit Network class to currency api calls
*/
interface CurrencyApi {
@@ -21,21 +21,19 @@ interface CurrencyApi {
suspend fun getCurrencyRate(@Query("q") currency: String): Response<ResponseObject>
// interface invokation to be used in application class
companion object{
companion object {
operator fun invoke(
networkConnectionInterceptor: NetworkConnectionInterceptor,
queryInterceptor: QueryInterceptor,
interceptor: HttpLoggingInterceptor
) : CurrencyApi{
): CurrencyApi {
// okkHttpclient with injected interceptors
val okkHttpclient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.addInterceptor(queryInterceptor)
.addNetworkInterceptor(networkConnectionInterceptor)
.build()
// Build retrofit
return Retrofit.Builder()
.client(okkHttpclient)
.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.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import androidx.annotation.RequiresApi
import okhttp3.Interceptor
import okhttp3.Response
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(
context: Context
) : Interceptor {
@@ -29,11 +32,22 @@ class NetworkConnectionInterceptor(
val connectivityManager =
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
else -> false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
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
import android.content.Context
import com.appttude.h_mal.easycc.BuildConfig
import com.appttude.h_mal.easycc.R
import okhttp3.HttpUrl
import okhttp3.Interceptor

View File

@@ -18,8 +18,8 @@ class PreferenceProvider(context: Context) {
private val appContext = context.applicationContext
// Instance of Shared preferences
private val preference: SharedPreferences
= PreferenceManager.getDefaultSharedPreferences(appContext)
private val preference: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(appContext)
// Lazy declaration of default rate if no rate is retrieved from
private val defaultRate: String by lazy {
@@ -29,9 +29,9 @@ class PreferenceProvider(context: Context) {
// Save currency pairs into prefs
fun saveConversionPair(s1: String, s2: String) {
preference.edit()
.putString(CURRENCY_ONE, s1)
.putString(CURRENCY_TWO, s2)
.apply()
.putString(CURRENCY_ONE, s1)
.putString(CURRENCY_TWO, s2)
.apply()
}
// Retrieve Currency pairs from prefs
@@ -46,17 +46,19 @@ class PreferenceProvider(context: Context) {
private fun getConversionString(conversionName: String): String? {
return preference
.getString(conversionName, defaultRate)
.getString(conversionName, defaultRate)
}
// Save currency pairs for widget
fun saveWidgetConversionPair(fromString: String,
toString: String, appWidgetId: Int) {
fun saveWidgetConversionPair(
fromString: String,
toString: String, appWidgetId: Int
) {
preference.edit()
.putString("${appWidgetId}_$CURRENCY_ONE", fromString)
.putString("${appWidgetId}_$CURRENCY_TWO", toString)
.apply()
.putString("${appWidgetId}_$CURRENCY_ONE", fromString)
.putString("${appWidgetId}_$CURRENCY_TWO", toString)
.apply()
}
// Retrieve currency pairs for widget
@@ -68,17 +70,17 @@ class PreferenceProvider(context: Context) {
}
private fun getWidgetConversionString(
appWidgetId: Int, conversionName: String): String? {
appWidgetId: Int, conversionName: String
): String? {
return preference
.getString("${appWidgetId}_$conversionName", defaultRate)
.getString("${appWidgetId}_$conversionName", defaultRate)
}
fun removeWidgetConversion(id: Int) {
preference.edit()
.remove("${id}_$CURRENCY_ONE")
.remove("${id}_$CURRENCY_TWO")
.apply()
.remove("${id}_$CURRENCY_ONE")
.remove("${id}_$CURRENCY_TWO")
.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.
*/
class RepositoryImpl (
class RepositoryImpl(
private val api: CurrencyApi,
private val backUpApi: BackupCurrencyApi,
private val prefs: PreferenceProvider
):Repository, SafeApiRequest(){
) : Repository, SafeApiRequest() {
override suspend fun getDataFromApi(
fromCurrency: String,
toCurrency: String
): ResponseObject{
): ResponseObject {
// Set currency pairs as correct string for api query eg. AUD_GBP
val currencyPair = convertPairsListToString(fromCurrency, toCurrency)
return responseUnwrap{ api.getCurrencyRate(currencyPair)}
return responseUnwrap { api.getCurrencyRate(currencyPair) }
}
override suspend fun getBackupDataFromApi(
fromCurrency: String,
toCurrency: String
): CurrencyResponse {
return responseUnwrap{ backUpApi.getCurrencyRate(fromCurrency, toCurrency)}
return responseUnwrap { backUpApi.getCurrencyRate(fromCurrency, toCurrency) }
}
override fun getConversionPair(): Pair<String?, String?> {
return prefs.getConversionPair()
}
override fun setConversionPair(fromCurrency: String, toCurrency: String){
override fun setConversionPair(fromCurrency: String, toCurrency: String) {
prefs.saveConversionPair(fromCurrency, toCurrency)
}
@@ -47,14 +47,16 @@ class RepositoryImpl (
Resources.getSystem().getStringArray(R.array.currency_arrays)
override fun getWidgetConversionPairs(appWidgetId: Int): Pair<String?, String?> =
prefs.getWidgetConversionPair(appWidgetId)
prefs.getWidgetConversionPair(appWidgetId)
override fun setWidgetConversionPairs(fromCurrency: String,
toCurrency: String, appWidgetId: Int) {
override fun setWidgetConversionPairs(
fromCurrency: String,
toCurrency: String, appWidgetId: Int
) {
return prefs.saveWidgetConversionPair(fromCurrency, toCurrency, appWidgetId)
}
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.models.CurrencyModelInterface
import java.lang.Exception
class CurrencyDataHelper (
class CurrencyDataHelper(
val repository: Repository
){
) {
suspend fun getDataFromApi(from: String, to: String): CurrencyModelInterface{
suspend fun getDataFromApi(from: String, to: String): CurrencyModelInterface {
return try {
repository.getDataFromApi(from, to)
}catch (e: Exception){
} catch (e: Exception) {
e.printStackTrace()
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.utils.trimToThree
import kotlin.Exception
class WidgetHelper (
val helper: CurrencyDataHelper,
class WidgetHelper(
private val helper: CurrencyDataHelper,
val repository: Repository
){
) {
suspend fun getWidgetData(): CurrencyModel? {
try {
@@ -18,13 +16,13 @@ class WidgetHelper (
val s2 = pair.second?.trimToThree() ?: return null
return helper.getDataFromApi(s1, s2).getCurrencyModel()
}catch (e: Exception){
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun removeWidgetData(id: Int){
fun removeWidgetData(id: Int) {
repository.removeWidgetConversionPairs(id)
}
}

View File

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

View File

@@ -2,13 +2,13 @@ package com.appttude.h_mal.easycc.models
import com.google.gson.annotations.SerializedName
data class CurrencyObject (
data class CurrencyObject(
@SerializedName("id")
var id : String,
var id: String,
@SerializedName("fr")
var fr : String,
var fr: String,
@SerializedName("to")
var to : String,
var to: String,
@SerializedName("val")
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
*/
@Suppress("DEPRECATION")
class CustomDialogClass(
context: Context,
private val clickListener: ClickListener
context: Context,
private val clickListener: ClickListener
) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -29,9 +30,10 @@ class CustomDialogClass(
// array adapter for list of currencies in R.Strings
val arrayAdapter =
ArrayAdapter.createFromResource(
context, R.array.currency_arrays,
android.R.layout.simple_list_item_1)
ArrayAdapter.createFromResource(
context, R.array.currency_arrays,
android.R.layout.simple_list_item_1
)
list_view.adapter = arrayAdapter
@@ -41,11 +43,12 @@ class CustomDialogClass(
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
arrayAdapter.filter.filter(charSequence)
}
override fun afterTextChanged(editable: Editable) {}
})
// interface selection back to calling activity
list_view.setOnItemClickListener{ adapterView, _, i, _ ->
list_view.setOnItemClickListener { adapterView, _, i, _ ->
clickListener.onText(adapterView.getItemAtPosition(i).toString())
dismiss()
}
@@ -53,6 +56,6 @@ class CustomDialogClass(
}
// Interface to handle selection within dialog
interface ClickListener{
interface ClickListener {
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.generic.instance
@Suppress("DEPRECATION")
class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener {
// Retrieve MainViewModelFactory via dependency injection
@@ -54,10 +55,10 @@ class MainActivity : AppCompatActivity(), KodeinAware, View.OnClickListener {
}
private fun setUpObservers() {
viewModel.operationStartedListener.observe(this, Observer {
viewModel.operationStartedListener.observe(this, {
progressBar.hideView(false)
})
viewModel.operationFinishedListener.observe(this, Observer { pair ->
viewModel.operationFinishedListener.observe(this, { pair ->
// hide progress bar
progressBar.hideView(true)
if (pair.first) {

View File

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

View File

@@ -10,10 +10,10 @@ import com.appttude.h_mal.easycc.helper.CurrencyDataHelper
* inject repository into viewmodel
*/
@Suppress("UNCHECKED_CAST")
class MainViewModelFactory (
private val repository: RepositoryImpl,
private val helper: CurrencyDataHelper
): ViewModelProvider.NewInstanceFactory(){
class MainViewModelFactory(
private val repository: RepositoryImpl,
private val helper: CurrencyDataHelper
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): 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.
*/
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAware, View.OnClickListener {
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAware,
View.OnClickListener {
override val kodein by kodein()
private val factory: WidgetViewModelFactory by instance()
@@ -47,7 +48,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
val extras = intent.extras
if (extras != null) {
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.
@@ -72,12 +74,12 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
}
private fun setupObserver() {
viewModel.operationFinishedListener.observe(this, Observer {
viewModel.operationFinishedListener.observe(this, {
// it.first is a the success of the operation
if (it.first){
if (it.first) {
displaySubmitDialog()
}else{
} else {
// failed operation - display toast with message from it.second
it.second?.let { message -> displayToast(message) }
}
@@ -87,8 +89,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
private fun setupDataBinding() {
// data binding to @R.layout.currency_app_widget_configure
DataBindingUtil.setContentView<CurrencyAppWidgetConfigureBinding>(
this,
R.layout.currency_app_widget_configure
this,
R.layout.currency_app_widget_configure
).apply {
viewmodel = viewModel
lifecycleOwner = this@CurrencyAppWidgetConfigureActivityKotlin
@@ -125,7 +127,7 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
}).show()
}
fun finishCurrencyWidgetActivity(){
fun finishCurrencyWidgetActivity() {
// Make sure we pass back the original appWidgetId
val resultValue = intent
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId)
@@ -136,8 +138,9 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), KodeinAwar
fun sendUpdateIntent() {
// It is the responsibility of the configuration activity to update the app widget
// Send update broadcast to widget app class
Intent(this@CurrencyAppWidgetConfigureActivityKotlin,
CurrencyAppWidgetKotlin::class.java
Intent(
this@CurrencyAppWidgetConfigureActivityKotlin,
CurrencyAppWidgetKotlin::class.java
).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
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,
private val messageString: String,
private val dialogInterface: DialogSubmit
) :Dialog(context){
) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -33,6 +33,6 @@ class WidgetSubmitDialog(
}
}
interface DialogSubmit{
interface DialogSubmit {
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
class WidgetViewModel(
private val repository: Repository
) : ViewModel(){
private val repository: Repository
) : ViewModel() {
private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] }
var appWidgetId: Int? = null
@@ -21,10 +21,9 @@ class WidgetViewModel(
// Setup viewmodel app widget ID
// Set default values for text views
fun initiate(appId: Int){
fun initiate(appId: Int) {
appWidgetId = appId
val widgetString
= repository.getWidgetConversionPairs(appId)
val widgetString = repository.getWidgetConversionPairs(appId)
rateIdFrom = widgetString.first ?: defaultCurrency
rateIdTo = widgetString.second ?: defaultCurrency
@@ -35,33 +34,35 @@ class WidgetViewModel(
fun getSubmitDialogMessage(): String {
val widgetName = getWidgetStringName()
return StringBuilder().append("Create widget for ")
.append(widgetName)
.append("?").toString()
.append(widgetName)
.append("?").toString()
}
fun submitSelectionOnClick(){
if (rateIdTo == null || rateIdFrom == null){
fun submitSelectionOnClick() {
if (rateIdTo == null || rateIdFrom == null) {
operationFinishedListener.value = Pair(false, "Selections incomplete")
return
}
if (rateIdFrom == rateIdTo){
if (rateIdFrom == rateIdTo) {
operationFinishedListener.value =
Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}")
Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}")
return
}
operationFinishedListener.value = Pair(true, null)
}
fun setWidgetStored() {
repository.setWidgetConversionPairs(rateIdFrom!!,rateIdTo!!,appWidgetId!!)
repository.setWidgetConversionPairs(rateIdFrom!!, rateIdTo!!, appWidgetId!!)
}
// Start operation based on dialog selection
fun setCurrencyName(tag: Any?, currencyName: String){
when(tag.toString()){
fun setCurrencyName(tag: Any?, currencyName: String) {
when (tag.toString()) {
"top" -> rateIdFrom = 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
@Suppress("UNCHECKED_CAST")
class WidgetViewModelFactory (
private val repository: RepositoryImpl
): ViewModelProvider.NewInstanceFactory(){
class WidgetViewModelFactory(
private val repository: RepositoryImpl
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): 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.text.DecimalFormat
import java.util.*
fun transformIntToArray(int: Int): IntArray{
return intArrayOf(int)

View File

@@ -5,14 +5,18 @@ import android.view.View
import android.widget.EditText
import android.widget.Toast
fun EditText.clearEditText(){
fun EditText.clearEditText() {
this.setText("")
}
fun View.hideView(vis : Boolean){
visibility = if (vis){ View.GONE } else { View.VISIBLE }
fun View.hideView(vis: Boolean) {
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()
}

View File

@@ -1,21 +1,11 @@
package com.appttude.h_mal.easycc.widget
import android.app.Activity
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
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.ui.main.MainActivity
import com.appttude.h_mal.easycc.utils.transformIntToArray
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.appttude.h_mal.easycc.widget.WidgetServiceIntent.Companion.enqueueWork
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
@@ -23,9 +13,8 @@ import org.kodein.di.generic.instance
/**
* 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() {
@@ -39,12 +28,7 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
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)
}
loadWidget(context)
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
@@ -58,113 +42,13 @@ class CurrencyAppWidgetKotlin : AppWidgetProvider() {
}
override fun onEnabled(context: Context) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
// 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)
}
loadWidget(context)
super.onEnabled(context)
}
override fun onDisabled(context: Context) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
// Enter relevant functionality for when the last widget is disabled
}
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)
}
private fun loadWidget(context: Context) {
val mIntent = Intent(context, CurrencyAppWidgetKotlin::class.java)
enqueueWork(context, mIntent)
}
}

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

View File

@@ -1,6 +1,5 @@
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.CurrencyApi
import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider
@@ -19,8 +18,10 @@ class RepositoryStorageTest {
@Mock
lateinit var api: CurrencyApi
@Mock
lateinit var apiBackup: BackupCurrencyApi
@Mock
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.repository.Repository
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 kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
@@ -38,7 +38,7 @@ class MainViewModelTest {
}
@Test
fun initiate_validBundleValues_successResponse() = runBlocking{
fun initiate_validBundleValues_successResponse() = runBlocking {
//GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
@@ -48,7 +48,8 @@ class MainViewModelTest {
//WHEN
Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne)
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
viewModel.initiate(bundle)
@@ -62,7 +63,7 @@ class MainViewModelTest {
}
@Test
fun initiate_invalidBundleValues_successfulResponse() = runBlocking{
fun initiate_invalidBundleValues_successfulResponse() = runBlocking {
//GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
@@ -71,7 +72,8 @@ class MainViewModelTest {
//WHEN
Mockito.`when`(repository.getConversionPair()).thenReturn(pair)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN
viewModel.initiate(null)
@@ -85,7 +87,7 @@ class MainViewModelTest {
}
@Test
fun initiate_sameBundleValues_successfulResponse() = runBlocking{
fun initiate_sameBundleValues_successfulResponse() = runBlocking {
//GIVEN
val currencyOne = "AUD - Australian Dollar"
val bundle = mock(Bundle()::class.java)
@@ -128,9 +130,8 @@ class MainViewModelTest {
}
@Test
fun setCurrencyName_validValues_successResponse() = runBlocking{
fun setCurrencyName_validValues_successResponse() = runBlocking {
//GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
@@ -139,7 +140,8 @@ class MainViewModelTest {
val responseObject = mock(ResponseObject::class.java)
//WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN
viewModel.setCurrencyName(tag, currencyOne)
@@ -154,7 +156,7 @@ class MainViewModelTest {
}
@Test
fun setCurrencyName_sameValues_successfulResponse() = runBlocking{
fun setCurrencyName_sameValues_successfulResponse() = runBlocking {
//GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
@@ -163,7 +165,8 @@ class MainViewModelTest {
val responseObject = mock(ResponseObject::class.java)
//WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN
viewModel.setCurrencyName(tag, currencyOne)
@@ -177,7 +180,7 @@ class MainViewModelTest {
}
@Test
fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking{
fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking {
//GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
@@ -185,7 +188,8 @@ class MainViewModelTest {
val responseObject = mock(ResponseObject::class.java)
//WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)).thenReturn(responseObject)
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN
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 com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.utils.observeOnce
import org.junit.Before
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
@@ -14,6 +14,7 @@ import org.mockito.MockitoAnnotations
private const val currencyOne = "AUD - Australian Dollar"
private const val currencyTwo = "GBP - British Pound"
class WidgetViewModelTest {
// Run tasks synchronously
@@ -35,7 +36,7 @@ class WidgetViewModelTest {
fun initiate_validInput_successfulResponse() {
//GIVEN
val appId = 123
val pair = Pair(currencyOne,currencyTwo)
val pair = Pair(currencyOne, currencyTwo)
//WHEN
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.LifecycleOwner

View File

@@ -1,7 +1,6 @@
package com.appttude.h_mal.easycc.utils
import androidx.lifecycle.LiveData
import com.appttude.h_mal.easycc.OneTimeObserver
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
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.
buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.4.10'
repositories {
jcenter()
google()
}
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
// 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
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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