diff --git a/android/app/build.gradle b/android/app/build.gradle index c44b2e2..662ef74 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.appttude.h_mal.easycc" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion localProperties.getProperty('flutter.minSdkVersion').toInteger() targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/kotlin/com/appttude/h_mal/easycc/AppWidgetProvider.kt b/android/app/src/main/kotlin/com/appttude/h_mal/easycc/AppWidgetProvider.kt index 093f328..20f59c8 100644 --- a/android/app/src/main/kotlin/com/appttude/h_mal/easycc/AppWidgetProvider.kt +++ b/android/app/src/main/kotlin/com/appttude/h_mal/easycc/AppWidgetProvider.kt @@ -1,14 +1,19 @@ package com.appttude.h_mal.easycc import android.appwidget.AppWidgetManager +import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.net.Uri import android.widget.RemoteViews +import android.widget.Toast import es.antonborri.home_widget.HomeWidgetBackgroundIntent import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetPlugin import es.antonborri.home_widget.HomeWidgetProvider + class AppWidgetProvider : HomeWidgetProvider(){ override fun onUpdate( context: Context, @@ -19,16 +24,22 @@ class AppWidgetProvider : HomeWidgetProvider(){ appWidgetIds.forEach { widgetId -> val views = RemoteViews(context.packageName, R.layout.currency_app_widget).apply { // Data from background operation received - val from: String? = widgetData.getString("from", null) - val to: String? = widgetData.getString("to", null) - val rate: String? = widgetData.getString("rate", null) + val from: String? = widgetData.getString("${widgetId}_from", null) + val to: String? = widgetData.getString("${widgetId}_to", null) + val rate: String? = widgetData.getString("${widgetId}_rate", null) + + if (from.isNullOrBlank() or to.isNullOrBlank() or rate.isNullOrBlank()) { + Toast.makeText(context, "Unable to review data for widget", Toast.LENGTH_SHORT).show() + return@apply + } val titleString = "${from}${to}" setTextViewText(R.id.exchangeName, titleString) setTextViewText(R.id.exchangeRate, rate.toString()) - val uri = Uri.parse("myAppWidget://update") - uri.buildUpon().query(widgetId.toString()).build() + val uri = Uri.parse("myAppWidget://updatewidget").buildUpon() + .appendQueryParameter("id", widgetId.toString()) + .build() setImageViewResource(R.id.refresh_icon, R.drawable.ic_refresh_white_24dp) val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, uri) @@ -42,4 +53,36 @@ class AppWidgetProvider : HomeWidgetProvider(){ appWidgetManager.updateAppWidget(widgetId, views) } } + + override fun onReceive(context: Context?, intent: Intent?) { + super.onReceive(context, intent) + + when (intent?.action) { + + } + val appWidgetManager = AppWidgetManager.getInstance(context); + val appWidgetIds = appWidgetManager.getAppWidgetIds( + context?.let { ComponentName(it, this::class.java) }) + val widgetDate = context?.let { HomeWidgetPlugin.getData(it) } + + appWidgetIds.forEach { widgetId -> + + + } + } + + override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { + super.onDeleted(context, appWidgetIds) + + appWidgetIds?.forEach { widgetId -> + val uri = Uri.parse("myAppWidget://deletewidget") + uri.buildUpon().appendQueryParameter("id", widgetId.toString()).build() + + context?.let { + HomeWidgetBackgroundIntent + .getBroadcast(it, uri) + .send() + } + } + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/appttude/h_mal/easycc/CurrencyAppWidgetConfigureActivity.kt b/android/app/src/main/kotlin/com/appttude/h_mal/easycc/CurrencyAppWidgetConfigureActivity.kt index 3ed4817..edf8223 100644 --- a/android/app/src/main/kotlin/com/appttude/h_mal/easycc/CurrencyAppWidgetConfigureActivity.kt +++ b/android/app/src/main/kotlin/com/appttude/h_mal/easycc/CurrencyAppWidgetConfigureActivity.kt @@ -1,24 +1,13 @@ package com.appttude.h_mal.easycc import android.app.Activity -import android.app.PendingIntent import android.appwidget.AppWidgetManager -import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.View import android.widget.TextView -import androidx.activity.viewModels -import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding -import com.appttude.h_mal.easycc.ui.BaseActivity -import com.appttude.h_mal.easycc.ui.main.CustomDialogClass -import com.appttude.h_mal.easycc.utils.transformIntToArray -import com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin -import dagger.hilt.android.AndroidEntryPoint +import android.widget.Toast import es.antonborri.home_widget.HomeWidgetBackgroundIntent -import es.antonborri.home_widget.HomeWidgetBackgroundReceiver -import es.antonborri.home_widget.HomeWidgetPlugin /** * The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget. @@ -28,6 +17,9 @@ class CurrencyAppWidgetConfigureActivity : Activity(), private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + private var top: String? = null + private var bottom: String? = null + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.currency_app_widget_configure) @@ -61,18 +53,23 @@ class CurrencyAppWidgetConfigureActivity : Activity(), override fun onClick(view: View?) { when (view?.tag.toString()) { "top", "bottom" -> showCustomDialog(view) - "submit" -> viewModel.submitSelectionOnClick() - else -> { - return - } + "submit" -> displaySubmitDialog() } } private fun displaySubmitDialog() { - val message = viewModel.getSubmitDialogMessage() - WidgetSubmitDialog(this, message, object : DialogSubmit { + if (top == null || bottom == null) { + Toast.makeText(this, "Selections incomplete", Toast.LENGTH_SHORT).show() + return + } + if (top == bottom) { + Toast.makeText(this, "Selected rates cannot be the same", Toast.LENGTH_SHORT).show() + return + } + + WidgetSubmitDialog(this, getSubmitDialogMessage(), object : DialogSubmit { override fun onSubmit() { - sendUpdateIntent() + sendUpdateIntent(top!!, bottom!!) finishCurrencyWidgetActivity() } }).show() @@ -82,7 +79,10 @@ class CurrencyAppWidgetConfigureActivity : Activity(), private fun showCustomDialog(view: View?) { CustomDialogClass(this) { (view as TextView).text = it - viewModel.setCurrencyName(view.tag, it) + when (view.tag.toString()) { + "top" -> top = it + "bottom" -> bottom = it + } }.show() } @@ -97,7 +97,7 @@ class CurrencyAppWidgetConfigureActivity : Activity(), fun sendUpdateIntent(from: String, to: String) { // It is the responsibility of the configuration activity to update the app widget // Send update broadcast to widget app class - val uri = Uri.parse("myAppWidget://createWidget").buildUpon() + val uri = Uri.parse("myAppWidget://createwidget").buildUpon() .appendQueryParameter("id", mAppWidgetId.toString()) .appendQueryParameter("from", from) .appendQueryParameter("to", to) @@ -107,4 +107,13 @@ class CurrencyAppWidgetConfigureActivity : Activity(), backgroundIntent.send() } + private fun getSubmitDialogMessage(): String { + val widgetName = getWidgetStringName() + return StringBuilder().append("Create widget for ") + .append(widgetName) + .append("?").toString() + } + + private fun getWidgetStringName() = "${top!!.substring(0, 3)}${bottom!!.substring(0, 3)}" + } diff --git a/android/app/src/main/kotlin/com/appttude/h_mal/easycc/WidgetViewModel.kt b/android/app/src/main/kotlin/com/appttude/h_mal/easycc/WidgetViewModel.kt deleted file mode 100644 index 068eee4..0000000 --- a/android/app/src/main/kotlin/com/appttude/h_mal/easycc/WidgetViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.appttude.h_mal.easycc - -import androidx.lifecycle.ViewModel -import com.appttude.h_mal.easycc.data.repository.Repository -import com.appttude.h_mal.easycc.ui.BaseViewModel -import com.appttude.h_mal.easycc.utils.trimToThree -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -class WidgetViewModel: ViewModel() { - - var appWidgetId: Int? = null - - var rateIdFrom: String? = null - var rateIdTo: String? = null - - // Setup viewmodel app widget ID - // Set default values for text views - fun initiate(appId: Int) { - appWidgetId = appId - } - - // Retrieve name for submit dialog (eg. AUDGBP) - fun getSubmitDialogMessage(): String { - val widgetName = getWidgetStringName() - return StringBuilder().append("Create widget for ") - .append(widgetName) - .append("?").toString() - } - - fun submitSelectionOnClick() { - if (rateIdTo == null || rateIdFrom == null) { - onError("Selections incomplete") - return - } - if (rateIdFrom == rateIdTo) { - onError("Selected rates cannot be the same") - return - } - onSuccess(Unit) - } - - fun setWidgetStored() { - - } - - // Start operation based on dialog selection - fun setCurrencyName(tag: Any?, currencyName: String) { - when (tag.toString()) { - "top" -> rateIdFrom = currencyName - "bottom" -> rateIdTo = currencyName - } - } - - private fun getWidgetStringName() = "${rateIdFrom!!.substring(0, 3)}${rateIdTo!!.substring(0, 3)}" - -} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/easyycc_widget_preview.png b/android/app/src/main/res/drawable/easyycc_widget_preview.png new file mode 100644 index 0000000..144292e Binary files /dev/null and b/android/app/src/main/res/drawable/easyycc_widget_preview.png differ diff --git a/android/app/src/main/res/xml/currency_app_widget_info.xml b/android/app/src/main/res/xml/currency_app_widget_info.xml new file mode 100644 index 0000000..1e0409c --- /dev/null +++ b/android/app/src/main/res/xml/currency_app_widget_info.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/lib/Utils/currency_utils.dart b/lib/Utils/currency_utils.dart index fb739a6..3169ac9 100644 --- a/lib/Utils/currency_utils.dart +++ b/lib/Utils/currency_utils.dart @@ -2,6 +2,9 @@ extension CurrencyExtension on String { /// Convert currency string into currency code /// eg. "AUD - Australian Dollar" to "AUD" String getCurrencyCode(){ + if (length == 3) { + return this; + } return substring(0,3); } } \ No newline at end of file diff --git a/lib/base_widget.dart b/lib/base_widget.dart index 1bd296e..11ab430 100644 --- a/lib/base_widget.dart +++ b/lib/base_widget.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:toast/toast.dart'; -import 'base_viewmodel.dart'; import 'Utils/constants.dart'; import 'Utils/view_state.dart'; import 'Utils/view_utils.dart'; +import 'base_viewmodel.dart'; abstract class BaseStatelessWidget extends StatelessWidget { diff --git a/lib/data/network/app_dio.dart b/lib/data/network/app_dio.dart index 3b4c6df..7414e6a 100644 --- a/lib/data/network/app_dio.dart +++ b/lib/data/network/app_dio.dart @@ -5,8 +5,8 @@ class AppDio { static Dio createDio() { Dio dio = Dio( BaseOptions( - receiveTimeout: 60000, - connectTimeout: 120000, + receiveTimeout: 30000, + connectTimeout: 30000, ) ); dio.interceptors.add(LogInterceptor()); diff --git a/lib/data/repository/repository_impl.dart b/lib/data/repository/repository_impl.dart index 54ce8b8..5d6f3ba 100644 --- a/lib/data/repository/repository_impl.dart +++ b/lib/data/repository/repository_impl.dart @@ -4,7 +4,6 @@ import 'package:easy_cc_flutter/Utils/currency_utils.dart'; import 'package:easy_cc_flutter/data/model/currency.dart'; import 'package:easy_cc_flutter/data/prefs/currency_pair.dart'; -import '../../locator.dart'; import '../../main.dart'; import '../network/backup_currency_api.dart'; import '../network/currency_api.dart'; @@ -13,9 +12,11 @@ import '../prefs/preference_provider.dart'; import 'repository.dart'; class RepositoryImpl extends Repository with SafeApiCall { - final PreferenceProvider _prefs = locator(); - final CurrencyApi _api = locator(); - final BackupCurrencyApi _backupApi = locator(); + final PreferenceProvider _prefs; + final CurrencyApi _api; + final BackupCurrencyApi _backupApi; + + RepositoryImpl(this._prefs, this._api, this._backupApi); @override CurrencyPair getConversionPair() { diff --git a/lib/home.dart b/lib/home.dart index a2fac5a..dc891cb 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -1,19 +1,20 @@ -import 'package:easy_cc_flutter/main_view_model.dart'; import 'package:easy_cc_flutter/Utils/selection_type.dart'; -import 'package:easy_cc_flutter/views/drop_down_box.dart'; +import 'package:easy_cc_flutter/locator.dart'; +import 'package:easy_cc_flutter/main_view_model.dart'; import 'package:easy_cc_flutter/views/converter_edit_text.dart'; +import 'package:easy_cc_flutter/views/drop_down_box.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'base_widget.dart'; import 'Utils/constants.dart'; +import 'base_widget.dart'; class HomePage extends BaseStatelessWidget { const HomePage({super.key}); @override MainViewModel createViewModel() { - return MainViewModel(); + return locator(); } @override diff --git a/lib/locator.dart b/lib/locator.dart index edb970c..8ba82fb 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,10 +1,10 @@ -import 'package:easy_cc_flutter/main_view_model.dart'; import 'package:easy_cc_flutter/data/network/backup_currency_api.dart'; import 'package:easy_cc_flutter/data/network/currency_api.dart'; import 'package:easy_cc_flutter/data/repository/repository_impl.dart'; +import 'package:easy_cc_flutter/main_view_model.dart'; import 'package:get_it/get_it.dart'; - import 'data/prefs/preference_provider.dart'; +import 'data/repository/repository.dart'; GetIt locator = GetIt.instance; @@ -12,6 +12,6 @@ void setupLocator() { locator.registerLazySingleton(() => PreferenceProvider()); locator.registerLazySingleton(() => CurrencyApi.create()); locator.registerLazySingleton(() => BackupCurrencyApi.create()); - locator.registerLazySingleton(() => RepositoryImpl()); - locator.registerFactory(() => MainViewModel()); + locator.registerLazySingleton(() => RepositoryImpl(locator(), locator(),locator())); + locator.registerFactory(() => MainViewModel(locator())); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 15af659..06bf8d6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,14 @@ +import 'package:easy_cc_flutter/Utils/currency_utils.dart'; +import 'package:easy_cc_flutter/data/network/backup_currency_api.dart'; +import 'package:easy_cc_flutter/data/network/currency_api.dart'; import 'package:flutter/material.dart'; import 'package:home_widget/home_widget.dart'; import 'package:logger/logger.dart'; +import 'data/model/currency.dart'; import 'data/prefs/preference_provider.dart'; +import 'data/repository/repository.dart'; +import 'data/repository/repository_impl.dart'; import 'home.dart'; import 'locator.dart'; @@ -10,34 +16,56 @@ var logger = Logger( printer: PrettyPrinter(), ); -void main() async { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); setupLocator(); await locator().init(); + await HomeWidget.registerBackgroundCallback(backgroundCallback); runApp(const MyApp()); } Future backgroundCallback(Uri? uri) async { - if (uri?.host == 'updatecounter') { + PreferenceProvider prefs = PreferenceProvider(); + await prefs.init(); + CurrencyApi api = CurrencyApi.create(); + BackupCurrencyApi backupApi = BackupCurrencyApi.create(); + RepositoryImpl repository = RepositoryImpl(prefs, api, backupApi); + + if (uri?.host == 'updatewidget') { Map? querys = uri?.queryParameters; + String? widgetId = querys?["id"]; - int _counter = 0; - await HomeWidget.getWidgetData('_counter', defaultValue: 0).then((int? value) { - _counter = value ?? 0; - _counter++; - }); - await HomeWidget.saveWidgetData('_counter', _counter); - await HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider'); - } else if (uri?.host == 'createWidget') { + await updateWidget(widgetId, repository); + } else if (uri?.host == 'createwidget') { Map? querys = uri?.queryParameters; - String? id = querys?["id"]; - String? from = querys?["from"]; - String? to = querys?["to"]; + String? widgetId = querys?["id"]; + String? from = querys?["from"]?.getCurrencyCode(); + String? to = querys?["to"]?.getCurrencyCode(); + await HomeWidget.saveWidgetData("${widgetId}_from", from); + await HomeWidget.saveWidgetData("${widgetId}_to", to); + await updateWidget(widgetId, repository); } } +Future updateWidget(String? widgetId, Repository repository) async { + String? from = await HomeWidget.getWidgetData("${widgetId}_from"); + String? to = await HomeWidget.getWidgetData("${widgetId}_to"); + + if (from == null || to == null) { + return; + } + + Currency currency = await repository.getConversationRateFromApi(from, to); + + await HomeWidget.saveWidgetData("${widgetId}_from", from); + await HomeWidget.saveWidgetData("${widgetId}_to", to); + await HomeWidget.saveWidgetData("${widgetId}_rate", currency.rate.toString()); + + await HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider'); +} + class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -58,7 +86,7 @@ class MyApp extends StatelessWidget { // is not restarted. primarySwatch: Colors.blue, ), - home: const HomePage(), + home: HomePage(), ); } } diff --git a/lib/main_view_model.dart b/lib/main_view_model.dart index 7dfb31b..98e522f 100644 --- a/lib/main_view_model.dart +++ b/lib/main_view_model.dart @@ -1,14 +1,14 @@ -import 'package:easy_cc_flutter/base_viewmodel.dart'; import 'package:easy_cc_flutter/Utils/selection_type.dart'; +import 'package:easy_cc_flutter/base_viewmodel.dart'; import 'Utils/constants.dart'; import 'data/prefs/currency_pair.dart'; import 'data/repository/repository.dart'; -import 'data/repository/repository_impl.dart'; -import 'locator.dart'; class MainViewModel extends BaseViewmodel { - final Repository _repository = locator(); + final Repository _repository; + + MainViewModel(this._repository); double conversionRate = 1.0; diff --git a/test/unit_test/repository_test.dart b/test/unit_test/repository_test.dart index db8d905..cc3c59a 100644 --- a/test/unit_test/repository_test.dart +++ b/test/unit_test/repository_test.dart @@ -8,7 +8,6 @@ import 'package:easy_cc_flutter/data/network/currency_api.dart'; import 'package:easy_cc_flutter/data/prefs/currency_pair.dart'; import 'package:easy_cc_flutter/data/prefs/preference_provider.dart'; import 'package:easy_cc_flutter/data/repository/repository_impl.dart'; -import 'package:easy_cc_flutter/locator.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -29,7 +28,8 @@ import 'repository_test.mocks.dart'; MockSpec(onMissingStub: OnMissingStub.returnDefault) ]) void main() { - late RepositoryImpl repository; + late RepositoryImpl sut; + late PreferenceProvider preferenceProvider; late CurrencyApi currencyApi; late BackupCurrencyApi backupCurrencyApi; @@ -44,11 +44,7 @@ void main() { currencyApi = MockCurrencyApi(); backupCurrencyApi = MockBackupCurrencyApi(); - locator.registerLazySingleton(() => preferenceProvider); - locator.registerLazySingleton(() => currencyApi); - locator.registerLazySingleton(() => backupCurrencyApi); - - repository = RepositoryImpl(); + sut = RepositoryImpl(preferenceProvider, currencyApi, backupCurrencyApi); }); test('get currency pair from prefs', () { @@ -59,7 +55,7 @@ void main() { when(preferenceProvider.getConversionPair()).thenReturn(pair); // Then - expect(repository.getConversionPair(), pair); + expect(sut.getConversionPair(), pair); }); test('get currency rate from API', () async { @@ -77,7 +73,7 @@ void main() { // Then Currency retrieved = - await repository.getConversationRateFromApi(fromCurrency, toCurrency); + await sut.getConversationRateFromApi(fromCurrency, toCurrency); expect(retrieved.toString(), currencyObject.toString()); }); @@ -98,7 +94,7 @@ void main() { // Then Currency retrieved = - await repository.getConversationRateFromApi(fromCurrency, toCurrency); + await sut.getConversationRateFromApi(fromCurrency, toCurrency); expect(retrieved.toString(), currencyObject.toString()); }); @@ -115,7 +111,7 @@ void main() { .thenAnswer((_) async => Future.error(backUpError)); // Then - expect(() async => await repository.getConversationRateFromApi(fromCurrency, toCurrency), + expect(() async => await sut.getConversationRateFromApi(fromCurrency, toCurrency), throwsA(predicate((e) => e is HttpException && e.message == 'Error message'))); diff --git a/test/unit_test/viewmodel_test.dart b/test/unit_test/viewmodel_test.dart index 738f1af..9e11ef7 100644 --- a/test/unit_test/viewmodel_test.dart +++ b/test/unit_test/viewmodel_test.dart @@ -1,15 +1,14 @@ import 'dart:io'; -import 'package:easy_cc_flutter/main_view_model.dart'; import 'package:easy_cc_flutter/Utils/selection_type.dart'; +import 'package:easy_cc_flutter/Utils/view_state.dart'; import 'package:easy_cc_flutter/data/model/currency.dart'; import 'package:easy_cc_flutter/data/prefs/currency_pair.dart'; import 'package:easy_cc_flutter/data/repository/repository_impl.dart'; -import 'package:easy_cc_flutter/locator.dart'; +import 'package:easy_cc_flutter/main_view_model.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:easy_cc_flutter/Utils/view_state.dart'; import 'viewmodel_test.mocks.dart'; @@ -29,9 +28,7 @@ void main() { // Create mock object. repository = MockRepositoryImpl(); - locator.registerLazySingleton(() => repository); - - mainViewModel = MainViewModel(); + mainViewModel = MainViewModel(repository); }); test('get currency pair from prefs', () {