From 7caa0866f765c26505df96712b8db9a9687b4e85 Mon Sep 17 00:00:00 2001 From: VaiTon Date: Sun, 29 Aug 2021 20:25:34 +0200 Subject: [PATCH] fix: not working language selection (#4192) * ref: general refactor to use kotlin inline for building fix: not working language selection * fix: clicking profile in drawer crashes the app * fix: wrong serving regex crashes the app * chore: removed unused method * fix: tests --- app/build.gradle.kts | 4 +- .../repositories/ProductRepositoryTest.kt | 6 +- .../github/scrachx/openfood/AppFlavors.kt | 2 +- .../openfood/features/ImagesManageActivity.kt | 4 +- .../scrachx/openfood/features/MainActivity.kt | 443 ++++++++++------- .../openfood/features/PreferencesFragment.kt | 433 ---------------- .../allergensalert/AllergensAlertFragment.kt | 20 +- .../AnalyticsUsageDialogFragment.kt | 8 +- .../fragment/CategoryListFragment.kt | 15 +- .../openfood/features/login/LoginActivity.kt | 2 +- .../preferences/PreferencesFragment.kt | 468 ++++++++++++++++++ .../preferences/PreferencesListener.kt | 23 + .../ProductEditNutritionFactsFragment.kt | 12 +- .../features/scan/ContinuousScanActivity.kt | 4 +- .../scanhistory/ScanHistoryAdapter.kt | 2 +- .../adapters/NutrientLevelListAdapter.kt | 8 +- .../features/splash/SplashActivity.kt | 3 +- .../openfood/jobs/LoadTaxonomiesWorker.kt | 3 +- .../openfood/jobs/ProductUploaderWorker.kt | 43 +- .../openfood/models/MeasurementUnit.kt | 2 + .../openfood/network/OpenFoodAPIClient.kt | 2 +- .../repositories/ProductRepository.kt | 6 +- .../repositories/TaxonomiesManager.kt | 9 +- .../github/scrachx/openfood/utils/Activity.kt | 17 + .../scrachx/openfood/utils/Constraints.kt | 7 + .../utils/{ContextExt.kt => Context.kt} | 44 +- .../github/scrachx/openfood/utils/Data.kt | 7 + .../utils/{DateExtensions.kt => Date.kt} | 2 +- .../scrachx/openfood/utils/DrawerBuilder.kt | 36 ++ .../scrachx/openfood/utils/FileUtils.kt | 22 - .../utils/{FragmentExt.kt => Fragment.kt} | 18 +- .../scrachx/openfood/utils/LocaleManager.kt | 9 +- .../utils/{UnitUtils.kt => Measurement.kt} | 16 +- .../github/scrachx/openfood/utils/Number.kt | 10 + .../openfood/utils/OFFDatabaseHelper.kt | 2 +- .../openfood/utils/OneTimeWorkRequest.kt | 11 + .../openfood/utils/PreferencesUtils.kt | 29 +- .../utils/{ProductExt.kt => Product.kt} | 0 .../scrachx/openfood/utils/{RxEx.kt => Rx.kt} | 0 .../res/drawable/ic_baseline_language_24.xml | 10 + app/src/main/res/values/pref_keys.xml | 9 +- app/src/main/res/xml/preferences.xml | 37 +- .../scrachx/openfood/utils/UnitUtilsTest.kt | 13 + 43 files changed, 1034 insertions(+), 787 deletions(-) delete mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesFragment.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesListener.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Activity.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Constraints.kt rename app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/{ContextExt.kt => Context.kt} (67%) create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Data.kt rename app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/{DateExtensions.kt => Date.kt} (93%) create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DrawerBuilder.kt rename app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/{FragmentExt.kt => Fragment.kt} (53%) rename app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/{UnitUtils.kt => Measurement.kt} (86%) create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Number.kt create mode 100644 app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OneTimeWorkRequest.kt rename app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/{ProductExt.kt => Product.kt} (100%) rename app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/{RxEx.kt => Rx.kt} (100%) create mode 100644 app/src/main/res/drawable/ic_baseline_language_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2128c8141736..ce24738533e8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,9 +154,11 @@ dependencies { } // UI Component : Material Drawer + // https://github.com/mikepenz/MaterialDrawer/commit/3b2cb1db4c3b6afe639b0f3c21c03c1de68648a3 + // TODO: We need minSdk 16 to update implementation("com.mikepenz:materialdrawer:7.0.0") { isTransitive = true } - //DO NOT UPDATE : RecyclerViewCacheUtil removed, needs rework + // DO NOT UPDATE : RecyclerViewCacheUtil removed, needs rework implementation("com.mikepenz:fastadapter-commons:3.3.1@aar") // UI Component : Font Icons diff --git a/app/src/androidTest/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepositoryTest.kt b/app/src/androidTest/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepositoryTest.kt index a20537b29fc5..c3c1424b9ce5 100644 --- a/app/src/androidTest/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepositoryTest.kt +++ b/app/src/androidTest/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepositoryTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.test.runBlockingTest import openfoodfacts.github.scrachx.openfood.models.DaoSession import openfoodfacts.github.scrachx.openfood.models.entities.allergen.Allergen import openfoodfacts.github.scrachx.openfood.models.entities.allergen.AllergenName +import openfoodfacts.github.scrachx.openfood.utils.getAppPreferences import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -50,8 +51,9 @@ class ProductRepositoryTest { @Test fun testGetAllergens() = runBlockingTest { - val mSettings = instance.getSharedPreferences("prefs", 0) - val isDownloadActivated = mSettings.getBoolean(Taxonomy.Allergens.getDownloadActivatePreferencesId(), false) + val appPrefs = instance.getAppPreferences() + + val isDownloadActivated = appPrefs.getBoolean(Taxonomy.Allergens.getDownloadActivatePreferencesId(), false) val allergens = productRepository.reloadAllergensFromServer() assertNotNull(allergens) if (!isDownloadActivated) { diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/AppFlavors.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/AppFlavors.kt index 4ff9dfaa7ce1..8631ee7a3e9e 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/AppFlavors.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/AppFlavors.kt @@ -22,6 +22,6 @@ object AppFlavors { const val OBF = "obf" @JvmStatic - fun isFlavors(vararg flavors: String) = flavors.contains(BuildConfig.FLAVOR_versionCode) + fun isFlavors(vararg flavors: String) = BuildConfig.FLAVOR_versionCode in flavors } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/ImagesManageActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/ImagesManageActivity.kt index ca50bab02acf..c67bbb3f1c2b 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/ImagesManageActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/ImagesManageActivity.kt @@ -96,7 +96,7 @@ class ImagesManageActivity : BaseActivity() { private var lastViewedImage: File? = null private lateinit var attacher: PhotoViewAttacher - private val settings by lazy { getSharedPreferences("prefs", 0) } + private val settings by lazy { getAppPreferences() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -185,7 +185,7 @@ class ImagesManageActivity : BaseActivity() { 4 -> startShowCase(getString(R.string.title_edit_photo), getString(R.string.content_edit_photo), R.id.btnEditImage, 5) 5 -> startShowCase(getString(R.string.title_unselect_photo), getString(R.string.content_unselect_photo), R.id.btnUnselectImage, 6) 6 -> startShowCase(getString(R.string.title_exit), getString(R.string.content_exit), R.id.btn_done, 7) - 7 -> settings!!.edit { putBoolean(getString(R.string.check_first_time), false) } + 7 -> settings.edit { putBoolean(getString(R.string.check_first_time), false) } } } .build() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/MainActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/MainActivity.kt index c347924f6492..84a4d4271372 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/MainActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/MainActivity.kt @@ -19,7 +19,7 @@ import android.Manifest import android.app.SearchManager import android.content.* import android.content.pm.ActivityInfo -import android.content.pm.PackageManager +import android.content.pm.PackageManager.PERMISSION_GRANTED import android.graphics.BitmapFactory import android.graphics.drawable.ColorDrawable import android.net.Uri @@ -51,15 +51,12 @@ import com.google.zxing.* import com.google.zxing.common.HybridBinarizer import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.materialdrawer.AccountHeader -import com.mikepenz.materialdrawer.AccountHeaderBuilder import com.mikepenz.materialdrawer.Drawer -import com.mikepenz.materialdrawer.DrawerBuilder import com.mikepenz.materialdrawer.holder.StringHolder import com.mikepenz.materialdrawer.model.* import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem import com.mikepenz.materialdrawer.model.interfaces.IProfile import dagger.hilt.android.AndroidEntryPoint -import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -84,6 +81,7 @@ import openfoodfacts.github.scrachx.openfood.features.changelog.ChangelogDialog import openfoodfacts.github.scrachx.openfood.features.compare.ProductCompareActivity import openfoodfacts.github.scrachx.openfood.features.login.LoginActivity import openfoodfacts.github.scrachx.openfood.features.login.LoginActivity.Companion.LoginContract +import openfoodfacts.github.scrachx.openfood.features.preferences.PreferencesFragment import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity import openfoodfacts.github.scrachx.openfood.features.productlists.ProductListsActivity import openfoodfacts.github.scrachx.openfood.features.scanhistory.ScanHistoryActivity @@ -144,8 +142,6 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { @Inject lateinit var localeManager: LocaleManager - private val disp = CompositeDisposable() - private val contributeUri: Uri by lazy { Uri.parse(getString(R.string.website_contribute)) } private val discoverUri: Uri by lazy { Uri.parse(getString(R.string.website_discover)) } private fun getUserContributeUri(): Uri = Uri.parse(getString(R.string.website_contributor) + getUserLogin()) @@ -194,12 +190,13 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { // Create the AccountHeader val profile = getUserProfile() - var accountHeaderBuilder = AccountHeaderBuilder() - .withActivity(this) - .withTranslucentStatusBar(true) - .withTextColorRes(R.color.white) - .addProfiles(profile) - .withOnAccountHeaderProfileImageListener(object : AccountHeader.OnAccountHeaderProfileImageListener { + + headerResult = buildAccountHeader { + withActivity(this@MainActivity) + withTranslucentStatusBar(true) + withTextColorRes(R.color.white) + addProfiles(profile) + withOnAccountHeaderProfileImageListener(object : AccountHeader.OnAccountHeaderProfileImageListener { override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean { if (!isUserSet()) startActivity(Intent(this@MainActivity, LoginActivity::class.java)) return false @@ -207,14 +204,14 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { override fun onProfileImageLongClick(view: View, profile: IProfile<*>, current: Boolean) = false }) - .withOnAccountHeaderSelectionViewClickListener(object : AccountHeader.OnAccountHeaderSelectionViewClickListener { + withOnAccountHeaderSelectionViewClickListener(object : AccountHeader.OnAccountHeaderSelectionViewClickListener { override fun onClick(view: View, profile: IProfile<*>): Boolean { if (!isUserSet()) startActivity(Intent(this@MainActivity, LoginActivity::class.java)) return false } }) - .withSelectionListEnabledForSingleProfile(selectionListEnabledForSingleProfile = false) - .withOnAccountHeaderListener(object : AccountHeader.OnAccountHeaderListener { + withSelectionListEnabledForSingleProfile(false) + withOnAccountHeaderListener(object : AccountHeader.OnAccountHeaderListener { override fun onProfileChanged(view: View?, profile: IProfile<*>, current: Boolean): Boolean { if (profile is IDrawerItem<*> && profile.identifier == ITEM_MANAGE_ACCOUNT.toLong()) { CustomTabActivityHelper.openCustomTab( @@ -227,148 +224,21 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { return false } }) - .withSavedInstance(savedInstanceState) - accountHeaderBuilder = try { - accountHeaderBuilder.withHeaderBackground(R.drawable.header) - } catch (e: OutOfMemoryError) { - Log.w(LOG_TAG, "Device has too low memory, loading color drawer header...", e) - accountHeaderBuilder.withHeaderBackground(ColorDrawable(ContextCompat.getColor(this, R.color.primary_dark))) + withSavedInstance(savedInstanceState) + + try { + withHeaderBackground(R.drawable.header) + } catch (e: OutOfMemoryError) { + Log.w(LOG_TAG, "Device has too low memory, loading color drawer header...", e) + withHeaderBackground(ColorDrawable(ContextCompat.getColor(this@MainActivity, R.color.primary_dark))) + } } - headerResult = accountHeaderBuilder.build() // Add Manage Account profile if the user is connected if (isUserSet() && getUserSession() != null) updateProfileForCurrentUser() // Create the drawer - drawerResult = DrawerBuilder() - .withActivity(this) - .withToolbar(binding.toolbarInclude.toolbar) - .withHasStableIds(true) - .withAccountHeader(headerResult) //set the AccountHeader we created earlier for the header - .withOnDrawerListener(object : Drawer.OnDrawerListener { - override fun onDrawerSlide(drawerView: View, slideOffset: Float) = hideKeyboard() - override fun onDrawerOpened(drawerView: View) = hideKeyboard() - override fun onDrawerClosed(drawerView: View) = Unit - }) - .addDrawerItems( - PrimaryDrawerItem().withName(R.string.home_drawer).withIcon(GoogleMaterial.Icon.gmd_home).withIdentifier(ITEM_HOME.toLong()), - - SectionDrawerItem().withName(R.string.search_drawer), - - PrimaryDrawerItem().withName(R.string.search_by_barcode_drawer).withIcon(GoogleMaterial.Icon.gmd_dialpad) - .withIdentifier(ITEM_SEARCH_BY_CODE.toLong()), - PrimaryDrawerItem().withName(R.string.search_by_category).withIcon(GoogleMaterial.Icon.gmd_filter_list).withIdentifier(ITEM_CATEGORIES.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.additives).withIcon(R.drawable.ic_additives).withIdentifier(ITEM_ADDITIVES.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.scan_search).withIcon(R.drawable.barcode_grey_24dp).withIdentifier(ITEM_SCAN.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.compare_products).withIcon(GoogleMaterial.Icon.gmd_swap_horiz).withIdentifier(ITEM_COMPARE.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.advanced_search_title).withIcon(GoogleMaterial.Icon.gmd_insert_chart) - .withIdentifier(ITEM_ADVANCED_SEARCH.toLong()).withSelectable(false), - PrimaryDrawerItem().withName(R.string.scan_history_drawer).withIcon(GoogleMaterial.Icon.gmd_history).withIdentifier(ITEM_HISTORY.toLong()) - .withSelectable(false), - - SectionDrawerItem().withName(R.string.user_drawer).withIdentifier(USER_ID), - - PrimaryDrawerItem().withName(getString(R.string.action_contributes)).withIcon(GoogleMaterial.Icon.gmd_rate_review) - .withIdentifier(ITEM_MY_CONTRIBUTIONS.toLong()).withSelectable(false), - PrimaryDrawerItem().withName(R.string.your_lists).withIcon(GoogleMaterial.Icon.gmd_list).withIdentifier(ITEM_YOUR_LISTS.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.products_to_be_completed).withIcon(GoogleMaterial.Icon.gmd_edit) - .withIdentifier(ITEM_INCOMPLETE_PRODUCTS.toLong()).withSelectable(false), - PrimaryDrawerItem().withName(R.string.alert_drawer).withIcon(GoogleMaterial.Icon.gmd_warning).withIdentifier(ITEM_ALERT.toLong()), - PrimaryDrawerItem().withName(R.string.action_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ITEM_PREFERENCES.toLong()), - - DividerDrawerItem(), - - PrimaryDrawerItem().withName(R.string.action_discover).withIcon(GoogleMaterial.Icon.gmd_info).withIdentifier(ITEM_ABOUT.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.contribute).withIcon(GoogleMaterial.Icon.gmd_group).withIdentifier(ITEM_CONTRIBUTE.toLong()) - .withSelectable(false), - PrimaryDrawerItem().withName(R.string.open_other_flavor_drawer).withIcon(GoogleMaterial.Icon.gmd_shop).withIdentifier(ITEM_OBF.toLong()) - .withSelectable(false) - ) - .withOnDrawerItemClickListener(object : Drawer.OnDrawerItemClickListener { - override fun onItemClick(view: View?, position: Int, drawerItem: IDrawerItem<*>): Boolean { - var newFragment: Fragment? = null - when (drawerItem.identifier.toInt()) { - ITEM_HOME -> newFragment = HomeFragment.newInstance() - ITEM_SEARCH_BY_CODE -> { - newFragment = SearchByCodeFragment.newInstance() - binding.bottomNavigationInclude.bottomNavigation.selectNavigationItem(0) - } - ITEM_CATEGORIES -> CategoryActivity.start(this@MainActivity) - ITEM_ADDITIVES -> AdditiveListActivity.start(this@MainActivity) - ITEM_COMPARE -> ProductCompareActivity.start(this@MainActivity) - ITEM_HISTORY -> ScanHistoryActivity.start(this@MainActivity) - ITEM_SCAN -> checkThenStartScanActivity() - ITEM_LOGIN -> loginThenUpdate.launch(Unit) - ITEM_ALERT -> newFragment = AllergensAlertFragment.newInstance() - ITEM_PREFERENCES -> newFragment = PreferencesFragment.newInstance() - ITEM_ABOUT -> CustomTabActivityHelper.openCustomTab(this@MainActivity, customTabsIntent, discoverUri, WebViewFallback()) - ITEM_CONTRIBUTE -> CustomTabActivityHelper.openCustomTab(this@MainActivity, customTabsIntent, contributeUri, WebViewFallback()) - ITEM_INCOMPLETE_PRODUCTS -> startSearch( - this@MainActivity, - SearchType.INCOMPLETE_PRODUCT, - "" - ) // Search and display the products to be completed by moving to ProductBrowsingListActivity - ITEM_OBF -> { - - val otherOFAppInstalled = isApplicationInstalled(this@MainActivity, BuildConfig.OFOTHERLINKAPP) - if (otherOFAppInstalled) { - val launchIntent = packageManager.getLaunchIntentForPackage(BuildConfig.OFOTHERLINKAPP) - if (launchIntent != null) { - startActivity(launchIntent) - } else { - Toast.makeText(this@MainActivity, R.string.app_disabled_text, Toast.LENGTH_SHORT).show() - startActivity(Intent().apply { - action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - data = Uri.fromParts( - "package", - BuildConfig.OFOTHERLINKAPP, - null - ) - }) - } - } else { - try { - startActivity(Intent(Intent.ACTION_VIEW, "market://details?id=${BuildConfig.OFOTHERLINKAPP}".toUri())) - } catch (anfe: ActivityNotFoundException) { - startActivity( - Intent( - Intent.ACTION_VIEW, - "https://play.google.com/store/apps/details?id=${BuildConfig.OFOTHERLINKAPP}".toUri() - ) - ) - } - } - } - ITEM_ADVANCED_SEARCH -> { - CustomTabActivityHelper.openCustomTab( - this@MainActivity, - CustomTabsIntent.Builder().build(), - getString(R.string.advanced_search_url).toUri(), - WebViewFallback() - ) - } - ITEM_MY_CONTRIBUTIONS -> openMyContributions() - ITEM_YOUR_LISTS -> ProductListsActivity.start(this@MainActivity) - ITEM_LOGOUT -> MaterialAlertDialogBuilder(this@MainActivity) - .setTitle(R.string.confirm_logout) - .setMessage(R.string.logout_dialog_content) - .setPositiveButton(android.R.string.ok) { _, _ -> logout() } - .setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() } - .show() - } - newFragment?.let(::swapToFragment) - return false - } - }) - .withSavedInstance(savedInstanceState) - .withShowDrawerOnFirstLaunch(false) - .build() + drawerResult = setupDrawer(savedInstanceState) drawerResult.actionBarDrawerToggle!!.isDrawerIndicatorEnabled = true // Add Drawer items for the connected user @@ -437,9 +307,9 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { scheduleProductUpload(this, sharedPreferences) // Adds nutriscore and quantity values in old history for schema 5 update - val mSharedPref = getSharedPreferences("prefs", 0) + val appPrefs = getAppPreferences() - val isOldHistoryDataSynced = mSharedPref.getBoolean("is_old_history_data_synced", false) + val isOldHistoryDataSynced = appPrefs.getBoolean("is_old_history_data_synced", false) if (!isOldHistoryDataSynced && this.isNetworkConnected()) { historySyncJob?.cancel() historySyncJob = lifecycleScope.launch { apiClient.syncOldHistory() } @@ -455,6 +325,202 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { } } + + private fun setupDrawer(savedInstanceState: Bundle?) = buildDrawer(this) { + withToolbar(binding.toolbarInclude.toolbar) + withHasStableIds(true) + withAccountHeader(headerResult) //set the AccountHeader we created earlier for the header + + withOnDrawerListener(object : Drawer.OnDrawerListener { + override fun onDrawerSlide(drawerView: View, slideOffset: Float) = hideKeyboard() + override fun onDrawerOpened(drawerView: View) = hideKeyboard() + override fun onDrawerClosed(drawerView: View) = Unit + }) + + addDrawerItems( + + primaryItem { + withName(R.string.home_drawer) + withIcon(GoogleMaterial.Icon.gmd_home) + withIdentifier(ITEM_HOME.toLong()) + }, + + sectionItem { withName(R.string.search_drawer) }, + primaryItem { + withName(R.string.search_by_barcode_drawer) + withIcon(GoogleMaterial.Icon.gmd_dialpad) + withIdentifier(ITEM_SEARCH_BY_CODE.toLong()) + }, + primaryItem { + withName(R.string.search_by_category) + withIcon(GoogleMaterial.Icon.gmd_filter_list) + withIdentifier(ITEM_CATEGORIES.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.additives) + withIcon(R.drawable.ic_additives).withIdentifier(ITEM_ADDITIVES.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.scan_search) + withIcon(R.drawable.barcode_grey_24dp) + withIdentifier(ITEM_SCAN.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.compare_products) + withIcon(GoogleMaterial.Icon.gmd_swap_horiz) + withIdentifier(ITEM_COMPARE.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.advanced_search_title) + withIcon(GoogleMaterial.Icon.gmd_insert_chart) + withIdentifier(ITEM_ADVANCED_SEARCH.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.scan_history_drawer) + withIcon(GoogleMaterial.Icon.gmd_history) + withIdentifier(ITEM_HISTORY.toLong()) + withSelectable(false) + }, + + sectionItem { withName(R.string.user_drawer).withIdentifier(USER_ID) }, + primaryItem { + withName(getString(R.string.action_contributes)) + withIcon(GoogleMaterial.Icon.gmd_rate_review) + withIdentifier(ITEM_MY_CONTRIBUTIONS.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.your_lists) + withIcon(GoogleMaterial.Icon.gmd_list) + withIdentifier(ITEM_YOUR_LISTS.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.products_to_be_completed) + withIcon(GoogleMaterial.Icon.gmd_edit) + withIdentifier(ITEM_INCOMPLETE_PRODUCTS.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.alert_drawer) + withIcon(GoogleMaterial.Icon.gmd_warning) + withIdentifier(ITEM_ALERT.toLong()) + }, + primaryItem { + withName(R.string.action_preferences) + withIcon(GoogleMaterial.Icon.gmd_settings) + withIdentifier(ITEM_PREFERENCES.toLong()) + }, + + dividerItem(), + + primaryItem { + withName(R.string.action_discover) + withIcon(GoogleMaterial.Icon.gmd_info) + withIdentifier(ITEM_ABOUT.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.contribute) + withIcon(GoogleMaterial.Icon.gmd_group) + withIdentifier(ITEM_CONTRIBUTE.toLong()) + withSelectable(false) + }, + primaryItem { + withName(R.string.open_other_flavor_drawer) + withIcon(GoogleMaterial.Icon.gmd_shop) + withIdentifier(ITEM_OBF.toLong()) + withSelectable(false) + } + ) + withOnDrawerItemClickListener(object : Drawer.OnDrawerItemClickListener { + override fun onItemClick(view: View?, position: Int, drawerItem: IDrawerItem<*>): Boolean { + var newFragment: Fragment? = null + when (drawerItem.identifier.toInt()) { + ITEM_HOME -> newFragment = HomeFragment.newInstance() + ITEM_SEARCH_BY_CODE -> { + newFragment = SearchByCodeFragment.newInstance() + binding.bottomNavigationInclude.bottomNavigation.selectNavigationItem(0) + } + ITEM_CATEGORIES -> CategoryActivity.start(this@MainActivity) + ITEM_ADDITIVES -> AdditiveListActivity.start(this@MainActivity) + ITEM_COMPARE -> ProductCompareActivity.start(this@MainActivity) + ITEM_HISTORY -> ScanHistoryActivity.start(this@MainActivity) + ITEM_SCAN -> checkThenStartScanActivity() + ITEM_LOGIN -> loginThenUpdate.launch(Unit) + ITEM_ALERT -> newFragment = AllergensAlertFragment.newInstance() + ITEM_PREFERENCES -> newFragment = PreferencesFragment.newInstance() + ITEM_ABOUT -> CustomTabActivityHelper.openCustomTab(this@MainActivity, customTabsIntent, discoverUri, WebViewFallback()) + ITEM_CONTRIBUTE -> CustomTabActivityHelper.openCustomTab(this@MainActivity, customTabsIntent, contributeUri, WebViewFallback()) + ITEM_INCOMPLETE_PRODUCTS -> startSearch( + this@MainActivity, + SearchType.INCOMPLETE_PRODUCT, + "" + ) // Search and display the products to be completed by moving to ProductBrowsingListActivity + ITEM_OBF -> { + + val otherOFAppInstalled = isApplicationInstalled(this@MainActivity, BuildConfig.OFOTHERLINKAPP) + if (otherOFAppInstalled) { + val launchIntent = packageManager.getLaunchIntentForPackage(BuildConfig.OFOTHERLINKAPP) + if (launchIntent != null) { + startActivity(launchIntent) + } else { + Toast.makeText(this@MainActivity, R.string.app_disabled_text, Toast.LENGTH_SHORT).show() + startActivity(Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts( + "package", + BuildConfig.OFOTHERLINKAPP, + null + ) + }) + } + } else { + try { + startActivity(Intent(Intent.ACTION_VIEW, "market://details?id=${BuildConfig.OFOTHERLINKAPP}".toUri())) + } catch (anfe: ActivityNotFoundException) { + startActivity( + Intent( + Intent.ACTION_VIEW, + "https://play.google.com/store/apps/details?id=${BuildConfig.OFOTHERLINKAPP}".toUri() + ) + ) + } + } + } + ITEM_ADVANCED_SEARCH -> { + CustomTabActivityHelper.openCustomTab( + this@MainActivity, + CustomTabsIntent.Builder().build(), + getString(R.string.advanced_search_url).toUri(), + WebViewFallback() + ) + } + ITEM_MY_CONTRIBUTIONS -> openMyContributions() + ITEM_YOUR_LISTS -> ProductListsActivity.start(this@MainActivity) + ITEM_LOGOUT -> MaterialAlertDialogBuilder(this@MainActivity) + .setTitle(R.string.confirm_logout) + .setMessage(R.string.logout_dialog_content) + .setPositiveButton(android.R.string.ok) { _, _ -> logout() } + .setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() } + .show() + } + newFragment?.let(::swapToFragment) + return false + } + }) + + + withSavedInstance(savedInstanceState) + withShowDrawerOnFirstLaunch(false) + } + + private fun swapToFragment(fragment: Fragment) { val currentFragment = supportFragmentManager.fragments.lastOrNull() if (currentFragment == null || currentFragment::class.java != fragment::class.java) { @@ -468,7 +534,7 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { private fun checkThenStartScanActivity() { when { - checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> { + checkSelfPermission(this, Manifest.permission.CAMERA) == PERMISSION_GRANTED -> { startScanActivity() } shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) -> { @@ -507,7 +573,7 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { .setMessage(R.string.contribution_without_account) .setPositiveButton(R.string.create_account_button) { _, _ -> CustomTabActivityHelper.openCustomTab( - this@MainActivity, + this, customTabsIntent, "${getString(R.string.website)}cgi/user.pl".toUri(), WebViewFallback() @@ -527,7 +593,8 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { val userSession = getUserSession() userSettingsURI = "${getString(R.string.website)}cgi/user.pl?type=edit&userid=$userLogin&user_id=$userLogin&user_session=$userSession".toUri() customTabActivityHelper.mayLaunchUrl(userSettingsURI, null, null) - return ProfileSettingDrawerItem().apply { + + return profileSettingItem { withName(getString(R.string.action_manage_account)) withIcon(GoogleMaterial.Icon.gmd_settings) withIdentifier(ITEM_MANAGE_ACCOUNT.toLong()) @@ -542,7 +609,7 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { * Remove user login info */ private fun logout() { - getSharedPreferences(PreferencesFragment.LOGIN_PREF, MODE_PRIVATE).edit { clear() } + getLoginPreferences().edit { clear() } updateConnectedState() matomoAnalytics.trackEvent(AnalyticsEvent.UserLogout) } @@ -604,22 +671,25 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { return true } - private fun getLogoutDrawerItem() = PrimaryDrawerItem() - .withName(getString(R.string.logout_drawer)) - .withIcon(GoogleMaterial.Icon.gmd_settings_power) - .withIdentifier(ITEM_LOGOUT.toLong()) - .withSelectable(false) + private fun getLogoutDrawerItem() = primaryItem { + withName(getString(R.string.logout_drawer)) + withIcon(GoogleMaterial.Icon.gmd_settings_power) + withIdentifier(ITEM_LOGOUT.toLong()) + withSelectable(false) + } - private fun getLoginDrawerItem() = PrimaryDrawerItem() - .withName(R.string.sign_in_drawer) - .withIcon(GoogleMaterial.Icon.gmd_account_circle) - .withIdentifier(ITEM_LOGIN.toLong()) - .withSelectable(false) + private fun getLoginDrawerItem() = primaryItem { + withName(R.string.sign_in_drawer) + withIcon(GoogleMaterial.Icon.gmd_account_circle) + withIdentifier(ITEM_LOGIN.toLong()) + withSelectable(false) + } - private fun getUserProfile() = ProfileDrawerItem() - .withName(getLoginPreferences().getString("user", resources.getString(R.string.txt_anonymous))) - .withIcon(R.drawable.img_home) - .withIdentifier(ITEM_USER.toLong()) + private fun getUserProfile(): ProfileDrawerItem = profileItem { + withName(getLoginPreferences().getString("user", resources.getString(R.string.txt_anonymous))) + withIcon(R.drawable.img_home) + withIdentifier(ITEM_USER.toLong()) + } @ExperimentalTime override fun onStart() { @@ -643,22 +713,21 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { */ private fun showFeedbackDialog() { //dialog for rating the app on play store - val rateDialog = MaterialAlertDialogBuilder(this).apply { - setTitle(R.string.app_name) - setMessage(R.string.user_ask_rate_app) - setPositiveButton(R.string.rate_app) { dialog, _ -> + val rateDialog = MaterialAlertDialogBuilder(this) + .setTitle(R.string.app_name) + .setMessage(R.string.user_ask_rate_app) + .setPositiveButton(R.string.rate_app) { dialog, _ -> //open app page in play store startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName"))) dialog.dismiss() } - setNegativeButton(R.string.no_thx) { dialog, _ -> dialog.dismiss() } - } + .setNegativeButton(R.string.no_thx) { dialog, _ -> dialog.dismiss() } //dialog for giving feedback - val feedbackDialog = MaterialAlertDialogBuilder(this).apply { - setTitle(R.string.app_name) - setMessage(R.string.user_ask_show_feedback_form) - setPositiveButton(android.R.string.ok) { dialog, _ -> + val feedbackDialog = MaterialAlertDialogBuilder(this) + .setTitle(R.string.app_name) + .setMessage(R.string.user_ask_show_feedback_form) + .setPositiveButton(android.R.string.ok) { dialog, _ -> //show feedback form CustomTabActivityHelper.openCustomTab( this@MainActivity, @@ -668,8 +737,7 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { ) dialog.dismiss() } - setNegativeButton(R.string.txtNo) { dialog, _ -> dialog.dismiss() } - } + .setNegativeButton(R.string.txtNo) { dialog, _ -> dialog.dismiss() } MaterialAlertDialogBuilder(this) @@ -695,7 +763,6 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { override fun onDestroy() { customTabActivityHelper.connectionCallback = null - disp.dispose() _binding = null super.onDestroy() } @@ -794,10 +861,10 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { val bMap = try { contentResolver.openInputStream(uri).use { BitmapFactory.decodeStream(it) } } catch (e: FileNotFoundException) { - Log.e(MainActivity::class.java.simpleName, "Could not resolve file from Uri $uri", e) + Log.e(MainActivity::class.simpleName, "Could not resolve file from Uri $uri", e) null } catch (e: IOException) { - Log.e(MainActivity::class.java.simpleName, "IO error during bitmap stream decoding: " + e.message, e) + Log.e(MainActivity::class.simpleName, "IO error during bitmap stream decoding: ${e.message}", e) null } ?: return@mapNotNull null @@ -813,8 +880,7 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { DecodeHintType.TRY_HARDER to true, DecodeHintType.PURE_BARCODE to true ) - val decodedResult = reader.decode(bitmap, decodeHints) - decodedResult?.text + reader.decode(bitmap, decodeHints)?.text } catch (e: FormatException) { Toast.makeText(this@MainActivity, getString(R.string.format_error), Toast.LENGTH_SHORT).show() Log.e(MainActivity::class.simpleName, "Error decoding bitmap into barcode: ${e.message}") @@ -877,7 +943,6 @@ class MainActivity : BaseActivity(), NavigationDrawerListener { } } setNegativeButton(R.string.txtNo) { d, _ -> d.cancel() } - create() show() } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt deleted file mode 100644 index 05f094ba92b3..000000000000 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/PreferencesFragment.kt +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright 2016-2020 Open Food Facts - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package openfoodfacts.github.scrachx.openfood.features - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.content.Intent.ACTION_VIEW -import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.SearchRecentSuggestions -import android.util.Log -import android.view.Menu -import android.view.MenuInflater -import android.widget.Toast -import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate -import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.content.edit -import androidx.core.content.pm.PackageInfoCompat -import androidx.core.net.toUri -import androidx.preference.* -import androidx.preference.Preference.OnPreferenceClickListener -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkInfo -import androidx.work.WorkManager -import com.afollestad.materialdialogs.MaterialDialog -import dagger.hilt.android.AndroidEntryPoint -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo -import io.reactivex.schedulers.Schedulers -import openfoodfacts.github.scrachx.openfood.AppFlavors -import openfoodfacts.github.scrachx.openfood.AppFlavors.OBF -import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF -import openfoodfacts.github.scrachx.openfood.AppFlavors.OPFF -import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors -import openfoodfacts.github.scrachx.openfood.BuildConfig -import openfoodfacts.github.scrachx.openfood.R -import openfoodfacts.github.scrachx.openfood.analytics.AnalyticsEvent -import openfoodfacts.github.scrachx.openfood.analytics.MatomoAnalytics -import openfoodfacts.github.scrachx.openfood.customtabs.CustomTabActivityHelper -import openfoodfacts.github.scrachx.openfood.customtabs.WebViewFallback -import openfoodfacts.github.scrachx.openfood.jobs.LoadTaxonomiesWorker -import openfoodfacts.github.scrachx.openfood.jobs.ProductUploaderWorker.Companion.scheduleProductUpload -import openfoodfacts.github.scrachx.openfood.models.DaoSession -import openfoodfacts.github.scrachx.openfood.models.entities.analysistag.AnalysisTagNameDao -import openfoodfacts.github.scrachx.openfood.models.entities.analysistagconfig.AnalysisTagConfig -import openfoodfacts.github.scrachx.openfood.models.entities.analysistagconfig.AnalysisTagConfigDao -import openfoodfacts.github.scrachx.openfood.models.entities.country.CountryName -import openfoodfacts.github.scrachx.openfood.models.entities.country.CountryNameDao -import openfoodfacts.github.scrachx.openfood.utils.* -import openfoodfacts.github.scrachx.openfood.utils.NavigationDrawerListener.NavigationDrawerType -import org.greenrobot.greendao.async.AsyncOperation -import org.greenrobot.greendao.async.AsyncOperationListener -import org.greenrobot.greendao.query.WhereCondition.StringCondition -import java.util.* -import javax.inject.Inject - -@AndroidEntryPoint -class PreferencesFragment : PreferenceFragmentCompat(), INavigationItem, OnSharedPreferenceChangeListener { - private val disp = CompositeDisposable() - - @Inject - lateinit var daoSession: DaoSession - - @Inject - lateinit var matomoAnalytics: MatomoAnalytics - - @Inject - lateinit var localeManager: LocaleManager - - override val navigationDrawerListener: NavigationDrawerListener? by lazy { - if (activity is NavigationDrawerListener) activity as NavigationDrawerListener - else null - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - menu.findItem(R.id.action_search).isVisible = false - } - - override fun onCreatePreferences(bundle: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.preferences, rootKey) - setHasOptionsMenu(true) - - val settings = requireActivity().getSharedPreferences("prefs", 0) - - initLanguageCell() - - requirePreference(getString(R.string.pref_app_theme_key)).let { - it.setEntries(R.array.application_theme_entries) - it.setEntryValues(R.array.application_theme_entries) - it.setOnPreferenceChangeListener { _, value -> - when (value) { - getString(R.string.day) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - getString(R.string.night) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - getString(R.string.follow_system) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - } - true - } - } - - requirePreference(getString(R.string.pref_delete_history_key)).setOnPreferenceChangeListener { _, _ -> - MaterialDialog.Builder(requireActivity()).run { - content(R.string.pref_delete_history_dialog_content) - positiveText(R.string.delete_txt) - onPositive { _, _ -> - Toast.makeText(requireContext(), getString(R.string.pref_delete_history), Toast.LENGTH_SHORT).show() - SearchRecentSuggestions(requireContext(), SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE).clearHistory() - } - neutralText(R.string.dialog_cancel) - onNeutral { dialog, _ -> dialog.dismiss() } - show() - } - true - } - - requirePreference(getString(R.string.pref_scanner_type_key)).let { - it.isVisible = BuildConfig.USE_MLKIT - it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - if (newValue == true) { - MaterialDialog.Builder(requireActivity()).run { - title(R.string.preference_choose_scanner_dialog_title) - content(R.string.preference_choose_scanner_dialog_body) - positiveText(R.string.proceed) - onPositive { _, _ -> - it.isChecked = true - settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } - Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - } - negativeText(R.string.dialog_cancel) - onNegative { dialog, _ -> - dialog.dismiss() - it.isChecked = false - settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), false) } - } - show() - } - } else { - it.isChecked = false - settings.edit { putBoolean(getString(R.string.pref_scanner_type_key), newValue as Boolean) } - Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - } - true - } - } - - - val countryPreference = requirePreference(getString(R.string.pref_country_key)) - - val asyncSessionCountries = daoSession.startAsyncSession() - val countryNameDao = daoSession.countryNameDao - - // Set query finish listener - asyncSessionCountries.listenerMainThread = AsyncOperationListener { operation: AsyncOperation -> - val countryNames = operation.result as List - countryPreference.entries = countryNames.map { it.name }.toTypedArray() - countryPreference.entryValues = countryNames.map { it.countyTag }.toTypedArray() - } - // Execute query - asyncSessionCountries.queryList(countryNameDao.queryBuilder() - .where(CountryNameDao.Properties.LanguageCode.eq(localeManager.getLanguage())) - .orderAsc(CountryNameDao.Properties.Name).build()) - - countryPreference.setOnPreferenceChangeListener { preference, newValue -> - val country = newValue as String? - settings.edit { putString(preference.key, country) } - Toast.makeText(context, getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - true - } - - requirePreference(getString(R.string.pref_contact_us_key)).setOnPreferenceChangeListener { _, _ -> - try { - startActivity(Intent(Intent.ACTION_SENDTO).apply { - data = Uri.parse(getString(R.string.off_mail)) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - }) - } catch (e: ActivityNotFoundException) { - Toast.makeText(requireActivity(), R.string.email_not_found, Toast.LENGTH_SHORT).show() - } - true - } - requirePreference(getString(R.string.pref_rate_us_key)).setOnPreferenceChangeListener { _, _ -> - try { - startActivity(Intent(ACTION_VIEW, - Uri.parse("market://details?id=${requireActivity().packageName}"))) - } catch (e: ActivityNotFoundException) { - startActivity(Intent(ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=${requireActivity().packageName}"))) - } - true - } - - requirePreference(getString(R.string.pref_faq_key)) - .setOnPreferenceClickListener { openWebCustomTab(R.string.faq_url) } - - requirePreference(getString(R.string.pref_terms_key)) - .setOnPreferenceClickListener { openWebCustomTab(R.string.terms_url) } - - requirePreference(getString(R.string.pref_help_translate_key)) - .setOnPreferenceClickListener { openWebCustomTab(R.string.translate_url) } - - requirePreference(getString(R.string.pref_energy_unit_key)).let { - it.setEntries(R.array.energy_units) - it.setEntryValues(R.array.energy_units) - it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - settings.edit { putString(getString(R.string.pref_energy_unit_key), newValue as String?) } - Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - true - } - } - - requirePreference(getString(R.string.pref_volume_unit_key)).let { - it.setEntries(R.array.volume_units) - it.setEntryValues(R.array.volume_units) - it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - settings.edit { putString(getString(R.string.pref_volume_unit_key), newValue as String?) } - Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - true - } - } - - requirePreference(getString(R.string.pref_resolution_key)).let { - it.setEntries(R.array.upload_image) - it.setEntryValues(R.array.upload_image) - it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - settings.edit { putString(getString(R.string.pref_resolution_key), newValue as String?) } - Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - true - } - } - - // Disable photo mode for OpenProductFacts - if (isFlavors(AppFlavors.OPF)) { - requirePreference(getString(R.string.pref_show_product_photos_key)).isVisible = false - } - - // Preference to show version name - requirePreference(getString(R.string.pref_version_key)).let { - try { - val pInfo = requireActivity().packageManager.getPackageInfo(requireActivity().packageName, 0) - val version = pInfo.versionName - val versionCode = PackageInfoCompat.getLongVersionCode(pInfo) - it.summary = "${getString(R.string.version_string)} $version ($versionCode)" - } catch (e: PackageManager.NameNotFoundException) { - Log.e(PreferencesFragment::class.simpleName, "onCreatePreferences", e) - } - - if (isFlavors(OFF, OBF, OPFF)) { - getAnalysisTagConfigs(daoSession) - } else { - preferenceScreen.removePreference(preferenceScreen.requirePreference(getString(R.string.pref_key_display))) - } - } - - requirePreference(getString(R.string.pref_analytics_reporting_key)).let { - it.setOnPreferenceChangeListener { _, newValue -> - matomoAnalytics.setEnabled(newValue == true) - true - } - } - } - - private fun buildDisplayCategory(configs: List) { - if (!isAdded) return - - val displayCategory = preferenceScreen.requirePreference(getString(R.string.pref_key_display)) - displayCategory.removeAll() - preferenceScreen.addPreference(displayCategory) - - // If analysis tag is empty show "Load ingredient detection data" option in order to manually reload taxonomies - if (configs.isNotEmpty()) { - configs.forEach { config -> - displayCategory.addPreference(CheckBoxPreference(this.context).apply { - key = config.type - setDefaultValue(true) - summary = null - summaryOn = null - summaryOff = null - title = getString(R.string.display_analysis_tag_status, config.typeName.lowercase(Locale.getDefault())) - setOnPreferenceChangeListener { _, newValue -> - val event = if (newValue == true) { - AnalyticsEvent.IngredientAnalysisEnabled(config.type) - } else { - AnalyticsEvent.IngredientAnalysisDisabled(config.type) - } - matomoAnalytics.trackEvent(event) - true - } - }) - } - } else { - val preference = Preference(preferenceScreen.context).apply { - setTitle(R.string.load_ingredient_detection_data) - setSummary(R.string.load_ingredient_detection_data_summary) - onPreferenceClickListener = OnPreferenceClickListener { pref -> - pref.onPreferenceClickListener = null - val request = OneTimeWorkRequest.from(LoadTaxonomiesWorker::class.java) - - // The service will load server resources only if newer than already downloaded... - WorkManager.getInstance(requireContext()).let { manager -> - manager.enqueue(request) - manager.getWorkInfoByIdLiveData(request.id).observe(this@PreferencesFragment, { workInfo: WorkInfo? -> - if (workInfo != null) { - if (workInfo.state == WorkInfo.State.RUNNING) { - pref.setTitle(R.string.please_wait) - pref.setIcon(R.drawable.ic_cloud_download_black_24dp) - pref.summary = null - pref.widgetLayoutResource = R.layout.loading - } else if (workInfo.state == WorkInfo.State.SUCCEEDED) { - getAnalysisTagConfigs(daoSession) - } - } - }) - } - true - } - } - displayCategory.addPreference(preference) - } - displayCategory.isVisible = true - } - - private fun openWebCustomTab(@StringRes resId: Int): Boolean { - val customTabsIntent = CustomTabsIntent.Builder().build().apply { - intent.putExtra("android.intent.extra.REFERRER", "android-app://${requireContext().packageName}".toUri()) - } - CustomTabActivityHelper.openCustomTab(requireActivity(), customTabsIntent, getString(resId).toUri(), WebViewFallback()) - return true - } - - @NavigationDrawerType - override fun getNavigationDrawerType() = NavigationDrawerListener.ITEM_PREFERENCES - - override fun onResume() { - super.onResume() - try { - (this.activity as? AppCompatActivity)?.supportActionBar!!.title = getString(R.string.action_preferences) - } catch (e: NullPointerException) { - throw IllegalStateException("Preference fragment not attached to AppCompatActivity.") - } - preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) - } - - override fun onPause() { - preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - super.onPause() - } - - override fun onDestroy() { - disp.dispose() - super.onDestroy() - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - when (key) { - getString(R.string.pref_enable_mobile_data_key) -> scheduleProductUpload(requireContext(), sharedPreferences) - } - } - - private fun getAnalysisTagConfigs(daoSession: DaoSession) { - val language = localeManager.getLanguage() - Single.fromCallable { - val analysisTagConfigDao = daoSession.analysisTagConfigDao - val analysisTagConfigs = analysisTagConfigDao.queryBuilder() - .where(StringCondition("1 GROUP BY type")) - .orderAsc(AnalysisTagConfigDao.Properties.Type).build().list() - val analysisTagNameDao = daoSession.analysisTagNameDao - analysisTagConfigs.forEach { config -> - val type = "en:${config.type}" - var analysisTagTypeName = analysisTagNameDao.queryBuilder().where( - AnalysisTagNameDao.Properties.AnalysisTag.eq(type), - AnalysisTagNameDao.Properties.LanguageCode.eq(language), - ).unique() - if (analysisTagTypeName == null) { - analysisTagTypeName = analysisTagNameDao.queryBuilder().where( - AnalysisTagNameDao.Properties.AnalysisTag.eq(type), - AnalysisTagNameDao.Properties.LanguageCode.eq("en") - ).unique() - } - config.typeName = if (analysisTagTypeName != null) analysisTagTypeName.name else config.type - } - analysisTagConfigs - }.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { configs: List -> buildDisplayCategory(configs) } - .addTo(disp) - } - - private fun initLanguageCell() { - val localesWithNames = SupportedLanguages.codes() - .map { lc -> - val locale = LocaleUtils.parseLocale(lc) - lc to locale.getDisplayName(locale).replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } - } - - requirePreference(getString(R.string.pref_language_key)).let { preference -> - preference.entries = localesWithNames.map { it.second }.toTypedArray() - preference.entryValues = localesWithNames.map { it.first }.toTypedArray() - preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, locale: Any? -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && locale != null) { - val configuration = requireActivity().resources.configuration - configuration.setLocale(LocaleUtils.parseLocale(locale as String)) - Toast.makeText(context, getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() - requireActivity().recreate() - } - true - } - } - } - - companion object { - const val LOGIN_PREF = "login" - fun newInstance() = PreferencesFragment().apply { arguments = Bundle() } - } -} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/allergensalert/AllergensAlertFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/allergensalert/AllergensAlertFragment.kt index dd095ad3ce87..1cac3a0d5ef2 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/allergensalert/AllergensAlertFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/allergensalert/AllergensAlertFragment.kt @@ -21,6 +21,8 @@ import android.view.* import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import androidx.core.content.res.ResourcesCompat +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver @@ -39,6 +41,7 @@ import openfoodfacts.github.scrachx.openfood.repositories.ProductRepository import openfoodfacts.github.scrachx.openfood.utils.LocaleManager import openfoodfacts.github.scrachx.openfood.utils.NavigationDrawerListener import openfoodfacts.github.scrachx.openfood.utils.NavigationDrawerListener.NavigationDrawerType +import openfoodfacts.github.scrachx.openfood.utils.getAppPreferences import openfoodfacts.github.scrachx.openfood.utils.isNetworkConnected import javax.inject.Inject @@ -64,7 +67,7 @@ class AllergensAlertFragment : NavigationBaseFragment() { private var allergensFromDao: List? = null private lateinit var adapter: AllergensAdapter - private val mSettings by lazy { requireActivity().getSharedPreferences("prefs", 0) } + private val mSettings by lazy { requireActivity().getAppPreferences() } private val dataObserver by lazy { AllergensObserver() } private val appLang: String by lazy { localeManager.getLanguage() } @@ -208,20 +211,19 @@ class AllergensAlertFragment : NavigationBaseFragment() { * Data observer of the Recycler Views */ internal inner class AllergensObserver : AdapterDataObserver() { - override fun onChanged() = setAppropriateView() - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = setAppropriateView() - override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) = setAppropriateView() + override fun onChanged() = updateView() + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = updateView() + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) = updateView() - private fun setAppropriateView() { + private fun updateView() { val isListEmpty = adapter.itemCount == 0 - binding.emptyAllergensView.visibility = if (isListEmpty) View.VISIBLE else View.GONE - binding.allergensRecycle.visibility = if (isListEmpty) View.GONE else View.VISIBLE + + binding.emptyAllergensView.isVisible = isListEmpty + binding.allergensRecycle.isGone = isListEmpty } } companion object { - - @JvmStatic fun newInstance() = AllergensAlertFragment().apply { arguments = Bundle() } } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/analyticsusage/AnalyticsUsageDialogFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/analyticsusage/AnalyticsUsageDialogFragment.kt index 2644b2445373..6ad86c633b26 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/analyticsusage/AnalyticsUsageDialogFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/analyticsusage/AnalyticsUsageDialogFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.edit import androidx.databinding.DataBindingUtil import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint @@ -46,9 +47,8 @@ class AnalyticsUsageDialogFragment : BottomSheetDialogFragment() { } private fun saveAnalyticsReportingPref(value: Boolean) { - sharedPreferences - .edit() - .putBoolean(getString(R.string.pref_analytics_reporting_key), value) - .apply() + sharedPreferences.edit { + putBoolean(getString(R.string.pref_analytics_reporting_key), value) + } } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/categories/fragment/CategoryListFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/categories/fragment/CategoryListFragment.kt index 3cccdfbb49b7..5c51a681dbc1 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/categories/fragment/CategoryListFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/categories/fragment/CategoryListFragment.kt @@ -45,7 +45,7 @@ class CategoryListFragment : BaseFragment() { binding.fastScroller.setRecyclerView(binding.recycler) binding.recycler.viewTreeObserver.addOnGlobalLayoutListener { - if (this.viewModel.shownCategories.isEmpty()) { + if (viewModel.shownCategories.isEmpty()) { binding.fastScroller.visibility = View.GONE } else { binding.fastScroller.visibility = View.VISIBLE @@ -79,12 +79,13 @@ class CategoryListFragment : BaseFragment() { searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName)) searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - val suggestions = SearchRecentSuggestions( - context, - SearchSuggestionProvider.AUTHORITY, - SearchSuggestionProvider.MODE - ) - suggestions.saveRecentQuery(query, null) + + SearchRecentSuggestions( + context, + SearchSuggestionProvider.AUTHORITY, + SearchSuggestionProvider.MODE + ).saveRecentQuery(query, null) + return false } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/login/LoginActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/login/LoginActivity.kt index 809ead5364fa..e256327c55bf 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/login/LoginActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/login/LoginActivity.kt @@ -139,7 +139,7 @@ class LoginActivity : BaseActivity() { null } } ?: return@launch - val pref = this@LoginActivity.getSharedPreferences("login", 0) + val pref = this@LoginActivity.getLoginPreferences() if (isHtmlNotValid(htmlNoParsed)) { loadingSnackbar.dismiss() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesFragment.kt new file mode 100644 index 000000000000..0e1d4af51ce6 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesFragment.kt @@ -0,0 +1,468 @@ +/* + * Copyright 2016-2020 Open Food Facts + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package openfoodfacts.github.scrachx.openfood.features.preferences + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.content.pm.PackageManager +import android.os.Bundle +import android.provider.SearchRecentSuggestions +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit +import androidx.core.content.pm.PackageInfoCompat +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import androidx.preference.* +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import openfoodfacts.github.scrachx.openfood.AppFlavors.OBF +import openfoodfacts.github.scrachx.openfood.AppFlavors.OFF +import openfoodfacts.github.scrachx.openfood.AppFlavors.OPF +import openfoodfacts.github.scrachx.openfood.AppFlavors.OPFF +import openfoodfacts.github.scrachx.openfood.AppFlavors.isFlavors +import openfoodfacts.github.scrachx.openfood.BuildConfig +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.analytics.AnalyticsEvent +import openfoodfacts.github.scrachx.openfood.analytics.MatomoAnalytics +import openfoodfacts.github.scrachx.openfood.customtabs.CustomTabActivityHelper +import openfoodfacts.github.scrachx.openfood.customtabs.CustomTabsHelper +import openfoodfacts.github.scrachx.openfood.customtabs.WebViewFallback +import openfoodfacts.github.scrachx.openfood.jobs.LoadTaxonomiesWorker +import openfoodfacts.github.scrachx.openfood.models.DaoSession +import openfoodfacts.github.scrachx.openfood.models.entities.analysistag.AnalysisTagNameDao +import openfoodfacts.github.scrachx.openfood.models.entities.analysistagconfig.AnalysisTagConfig +import openfoodfacts.github.scrachx.openfood.models.entities.analysistagconfig.AnalysisTagConfigDao +import openfoodfacts.github.scrachx.openfood.models.entities.country.CountryNameDao +import openfoodfacts.github.scrachx.openfood.utils.* +import openfoodfacts.github.scrachx.openfood.utils.NavigationDrawerListener.NavigationDrawerType +import org.greenrobot.greendao.query.WhereCondition.StringCondition +import java.util.* +import javax.inject.Inject + +@AndroidEntryPoint +class PreferencesFragment : PreferenceFragmentCompat(), INavigationItem { + + @Inject + lateinit var daoSession: DaoSession + + @Inject + lateinit var matomoAnalytics: MatomoAnalytics + + @Inject + lateinit var localeManager: LocaleManager + + @Inject + lateinit var preferencesListener: PreferencesListener + + @NavigationDrawerType + override fun getNavigationDrawerType() = NavigationDrawerListener.ITEM_PREFERENCES + + override val navigationDrawerListener: NavigationDrawerListener? by lazy { + if (activity is NavigationDrawerListener) activity as NavigationDrawerListener + else null + } + + + override fun onCreatePreferences(bundle: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.preferences, rootKey) + setHasOptionsMenu(true) + + val settings = requireActivity().getAppPreferences() + + setupLanguagePref() + + requirePreference(getString(R.string.pref_app_theme_key)) { + setEntries(R.array.application_theme_entries) + setEntryValues(R.array.application_theme_entries) + setOnPreferenceChangeListener { _, value -> + when (value) { + getString(R.string.day) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + getString(R.string.night) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + getString(R.string.follow_system) -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + true + } + } + + requirePreference(getString(R.string.pref_delete_history_key)) { + setOnPreferenceChangeListener { _, _ -> + MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.pref_delete_history_dialog_content) + .setPositiveButton(R.string.delete_txt) { _, _ -> + + // Clear history + SearchRecentSuggestions( + requireContext(), + SearchSuggestionProvider.AUTHORITY, + SearchSuggestionProvider.MODE + ).clearHistory() + + Toast.makeText(requireContext(), getString(R.string.pref_delete_history), Toast.LENGTH_SHORT).show() + } + .setNegativeButton(R.string.dialog_cancel) { d, _ -> d.dismiss() } + .show() + + true + } + } + + requirePreference(getString(R.string.pref_scanner_mlkit_key)) { + + if (!BuildConfig.USE_MLKIT) { + // We're on F-Droid + isEnabled = false + + setSummary(R.string.pref_scanner_mlkit_fdroid) + return@requirePreference + } + + setOnPreferenceChangeListener { _, newValue -> + if (newValue == true) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.preference_choose_scanner_dialog_title) + .setMessage(R.string.preference_choose_scanner_dialog_body) + .setPositiveButton(R.string.proceed) { d, _ -> + d.dismiss() + isChecked = true + settings.edit { putBoolean(getString(R.string.pref_scanner_mlkit_key), true) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + } + .setNegativeButton(android.R.string.cancel) { d, _ -> + d.dismiss() + isChecked = false + settings.edit { putBoolean(getString(R.string.pref_scanner_mlkit_key), false) } + } + .show() + + } else { + isChecked = false + settings.edit { putBoolean(getString(R.string.pref_scanner_mlkit_key), false) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + } + true + } + + } + + + requirePreference(getString(R.string.pref_country_key)) { + + lifecycleScope.launch(IO) { + + val countryNames = daoSession.countryNameDao.list { + where(CountryNameDao.Properties.LanguageCode.eq(localeManager.getLanguage())) + .orderAsc(CountryNameDao.Properties.Name) + } + + withContext(Main) { + entries = countryNames.map { it.name }.toTypedArray() + entryValues = countryNames.map { it.countyTag }.toTypedArray() + } + } + + setOnPreferenceChangeListener { preference, newValue -> + val country = newValue as String? + settings.edit { putString(preference.key, country) } + Toast.makeText(context, getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + true + } + } + + requirePreference(getString(R.string.pref_contact_us_key)) { + setOnPreferenceChangeListener { _, _ -> + try { + startActivity(Intent(Intent.ACTION_SENDTO).apply { + data = getString(R.string.off_mail).toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }) + } catch (e: ActivityNotFoundException) { + Toast.makeText(requireActivity(), R.string.email_not_found, Toast.LENGTH_SHORT).show() + } + true + } + } + + requirePreference(getString(R.string.pref_rate_us_key)) { + setOnPreferenceChangeListener { _, _ -> + try { + startActivity( + Intent( + ACTION_VIEW, + "market://details?id=${requireActivity().packageName}".toUri() + ) + ) + } catch (e: ActivityNotFoundException) { + startActivity( + Intent( + ACTION_VIEW, + "https://play.google.com/store/apps/details?id=${requireActivity().packageName}".toUri() + ) + ) + } + true + } + } + + requirePreference(getString(R.string.pref_faq_key)) + .setOnPreferenceClickListener { openWebCustomTab(R.string.faq_url) } + + requirePreference(getString(R.string.pref_terms_key)) + .setOnPreferenceClickListener { openWebCustomTab(R.string.terms_url) } + + requirePreference(getString(R.string.pref_help_translate_key)) + .setOnPreferenceClickListener { openWebCustomTab(R.string.translate_url) } + + requirePreference(getString(R.string.pref_energy_unit_key)) { + setEntries(R.array.energy_units) + setEntryValues(R.array.energy_units) + + setOnPreferenceChangeListener { _, newValue -> + settings.edit { putString(getString(R.string.pref_energy_unit_key), newValue as String?) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + true + } + } + + requirePreference(getString(R.string.pref_volume_unit_key)) { + setEntries(R.array.volume_units) + setEntryValues(R.array.volume_units) + + setOnPreferenceChangeListener { _, newValue -> + settings.edit { putString(getString(R.string.pref_volume_unit_key), newValue as String?) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + true + } + } + + requirePreference(getString(R.string.pref_resolution_key)) { + setEntries(R.array.upload_image) + setEntryValues(R.array.upload_image) + + setOnPreferenceChangeListener { _, newValue -> + settings.edit { putString(getString(R.string.pref_resolution_key), newValue as String?) } + Toast.makeText(requireActivity(), getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + true + } + } + + // Disable photo mode for OpenProductFacts + if (isFlavors(OPF)) { + requirePreference(getString(R.string.pref_show_product_photos_key)).isVisible = false + } + + // Preference to show version name + requirePreference(getString(R.string.pref_version_key)) { + try { + val pInfo = requireActivity().packageManager.getPackageInfo(requireActivity().packageName, 0) + val version = pInfo.versionName + val versionCode = PackageInfoCompat.getLongVersionCode(pInfo) + + summary = "${getString(R.string.version_string)} $version ($versionCode)" + } catch (e: PackageManager.NameNotFoundException) { + Log.e(PreferencesFragment::class.simpleName, "onCreatePreferences", e) + } + + } + + + if (isFlavors(OFF, OBF, OPFF)) { + setupAnalysisTagConfigs() + } else { + preferenceScreen.removePreference(preferenceScreen.requirePreference(getString(R.string.pref_key_display))) + } + + requirePreference(getString(R.string.pref_analytics_reporting_key)) { + setOnPreferenceChangeListener { _, newValue -> + matomoAnalytics.setEnabled(newValue == true) + true + } + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.findItem(R.id.action_search).isVisible = false + } + + override fun onResume() { + super.onResume() + try { + (activity as AppCompatActivity).supportActionBar!!.title = getString(R.string.action_preferences) + } catch (e: NullPointerException) { + throw IllegalStateException("Preference fragment not attached to AppCompatActivity.") + } + preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(preferencesListener) + } + + override fun onPause() { + preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferencesListener) + super.onPause() + } + + + private fun openWebCustomTab(@StringRes urlRes: Int): Boolean { + val helper = CustomTabActivityHelper() + val customTabsIntent = CustomTabsHelper.getCustomTabsIntent(requireContext(), helper.session) + CustomTabActivityHelper.openCustomTab( + requireActivity(), + customTabsIntent, + getString(urlRes).toUri(), + WebViewFallback() + ) + return true + } + + private fun setupDisplayCategory(analysisTagConfigs: List) { + if (!isAdded) return + + val displayCategory = requirePreference(getString(R.string.pref_key_display)) + .apply { removeAll() } + + preferenceScreen.addPreference(displayCategory) + + // If analysis tag is empty show "Load ingredient detection data" option in order to manually reload taxonomies + if (analysisTagConfigs.isNotEmpty()) { + analysisTagConfigs + .map(::createAnalysisTagPreference) + .forEach(displayCategory::addPreference) + } else { + Preference(preferenceScreen.context).apply { + setTitle(R.string.load_ingredient_detection_data) + setSummary(R.string.load_ingredient_detection_data_summary) + + setOnPreferenceClickListener { pref -> + pref.onPreferenceClickListener = null + val request = buildOneTimeWorkRequest() + + // The service will load server resources only if newer than already downloaded... + WorkManager.getInstance(requireContext()).let { + it.enqueue(request) + it.getWorkInfoByIdLiveData(request.id).observe(this@PreferencesFragment, { workInfo: WorkInfo? -> + when (workInfo?.state) { + WorkInfo.State.RUNNING -> { + pref.setTitle(R.string.please_wait) + pref.setIcon(R.drawable.ic_cloud_download_black_24dp) + pref.summary = null + pref.widgetLayoutResource = R.layout.loading + } + WorkInfo.State.SUCCEEDED -> { + setupAnalysisTagConfigs() + } + else -> Unit // Nothing + } + }) + } + true + } + displayCategory.addPreference(this) + } + } + + displayCategory.isVisible = true + } + + private fun createAnalysisTagPreference(config: AnalysisTagConfig) = CheckBoxPreference(requireContext()).apply { + setDefaultValue(true) + + key = config.type + title = getString(R.string.display_analysis_tag_status, config.typeName.lowercase(Locale.getDefault())) + + summary = null + summaryOn = null + summaryOff = null + + setOnPreferenceChangeListener { _, newValue -> + val event = if (newValue == true) { + AnalyticsEvent.IngredientAnalysisEnabled(config.type) + } else { + AnalyticsEvent.IngredientAnalysisDisabled(config.type) + } + matomoAnalytics.trackEvent(event) + true + } + } + + private fun setupAnalysisTagConfigs() { + val language = localeManager.getLanguage() + + lifecycleScope.launch(IO) { + val analysisTagConfigs = daoSession.analysisTagConfigDao.list { + where(StringCondition("1 GROUP BY type")) + orderAsc(AnalysisTagConfigDao.Properties.Type) + } + + analysisTagConfigs.forEach { config -> + val type = "en:${config.type}" + + val analysisTagTypeName = daoSession.analysisTagNameDao.unique { + where(AnalysisTagNameDao.Properties.AnalysisTag.eq(type)) + where(AnalysisTagNameDao.Properties.LanguageCode.eq(language)) + } ?: daoSession.analysisTagNameDao.unique { + where(AnalysisTagNameDao.Properties.AnalysisTag.eq(type)) + where(AnalysisTagNameDao.Properties.LanguageCode.eq("en")) + } + + config.typeName = if (analysisTagTypeName != null) analysisTagTypeName.name else config.type + } + + withContext(Main) { setupDisplayCategory(analysisTagConfigs) } + } + } + + private fun setupLanguagePref() { + val localeCodes = SupportedLanguages.codes() + val localeNames = localeCodes.map { lc -> + val locale = LocaleUtils.parseLocale(lc) + // Get the locale name in the locale language (eg. "English", "Italiano", "Français") + locale.getDisplayName(locale) + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } + } + + requirePreference(getString(R.string.pref_language_key)) { + entries = localeNames.toTypedArray() + entryValues = localeCodes.toTypedArray() + + setOnPreferenceChangeListener { _, lc -> + // Change app language + if (lc is String) { + localeManager.saveLanguageToPrefs(requireContext(), Locale(lc)) + Toast.makeText(context, getString(R.string.changes_saved), Toast.LENGTH_SHORT).show() + requireActivity().recreate() + } + true + } + } + } + + companion object { + const val LOGIN_SHARED_PREF = "login" + const val APP_SHARED_PREF = "prefs" + + fun newInstance() = PreferencesFragment().apply { arguments = Bundle() } + } +} diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesListener.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesListener.kt new file mode 100644 index 000000000000..b6b38db79002 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/preferences/PreferencesListener.kt @@ -0,0 +1,23 @@ +package openfoodfacts.github.scrachx.openfood.features.preferences + +import android.content.Context +import android.content.SharedPreferences +import dagger.hilt.android.qualifiers.ApplicationContext +import openfoodfacts.github.scrachx.openfood.R +import openfoodfacts.github.scrachx.openfood.jobs.ProductUploaderWorker +import javax.inject.Inject + + +class PreferencesListener @Inject constructor( + @ApplicationContext private val context: Context +) : SharedPreferences.OnSharedPreferenceChangeListener { + + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + when (key) { + context.getString(R.string.pref_enable_mobile_data_key) -> { + ProductUploaderWorker.scheduleProductUpload(context, sharedPreferences) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt index ba7abba3399b..20c6c6e256f0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/product/edit/nutrition/ProductEditNutritionFactsFragment.kt @@ -304,12 +304,12 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { getModifierIndex(nutriments[nutriment]?.modifier) private fun updateServingSize(servingSize: String) { - val parts = servingSize.split(Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")) - binding.servingSize.setText(parts[0]) - if (parts.size > 1) { - val symbol = parts[1].trim { it <= ' ' } - val unit = MeasurementUnit.findBySymbol(symbol)!! + val (value, unit) = parseServing(servingSize) + + binding.servingSize.setText(value) + + if (unit != null) { binding.servingSize.unitSpinner?.setSelection(getServingUnitIndex(unit)) } } @@ -980,5 +980,7 @@ class ProductEditNutritionFactsFragment : ProductEditFragment() { override fun onNothingSelected(parent: AdapterView<*>?) = Unit // This is not possible } } + } } + diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt index c2ab3aded66d..857fc3b263fd 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scan/ContinuousScanActivity.kt @@ -130,7 +130,7 @@ class ContinuousScanActivity : BaseActivity(), IProductView { private val bottomSheetCallback by lazy { QuickViewCallback(this) } private val cameraPref by lazy { getSharedPreferences("camera", 0) } - private val settings by lazy { getSharedPreferences("prefs", 0) } + private val settings by lazy { getAppPreferences() } private var productDisp: Job? = null private var hintBarcodeDisp: Disposable? = null @@ -489,7 +489,7 @@ class ContinuousScanActivity : BaseActivity(), IProductView { _binding = ActivityContinuousScanBinding.inflate(layoutInflater) setContentView(binding.root) - useMLScanner = BuildConfig.USE_MLKIT && settings.getBoolean(getString(R.string.pref_scanner_type_key), false) + useMLScanner = BuildConfig.USE_MLKIT && settings.getBoolean(getString(R.string.pref_scanner_mlkit_key), false) binding.toggleFlash.setOnClickListener { toggleFlash() } binding.buttonMore.setOnClickListener { showMoreSettings() } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryAdapter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryAdapter.kt index 787e87c8021f..902db02c75cb 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryAdapter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/scanhistory/ScanHistoryAdapter.kt @@ -82,7 +82,7 @@ class ScanHistoryAdapter( binding.productImage.setImageResource(R.drawable.placeholder_thumb) binding.imgProgress.isVisible = false } - binding.lastScan.text = product.lastSeen.timeFormatted(context) + binding.lastScan.text = product.lastSeen.durationToNowFormatted(context) if (isFlavors(OFF)) { binding.nutriscore.setImageResource(product.getNutriScoreResource()) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt index 4f4b931ec8f5..db340110b317 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/shared/adapters/NutrientLevelListAdapter.kt @@ -1,7 +1,6 @@ package openfoodfacts.github.scrachx.openfood.features.shared.adapters import android.content.Context -import android.view.LayoutInflater import android.view.View import android.view.View.NO_ID import android.view.ViewGroup @@ -14,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.features.shared.adapters.NutrientLevelListAdapter.NutrientViewHolder import openfoodfacts.github.scrachx.openfood.models.NutrientLevelItem +import android.view.LayoutInflater.from as inflateFrom class NutrientLevelListAdapter( private val context: Context, @@ -21,10 +21,8 @@ class NutrientLevelListAdapter( ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - NutrientViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.nutrient_lvl_list_item, parent, false) - ) + NutrientViewHolder(inflateFrom(parent.context).inflate(R.layout.nutrient_lvl_list_item, parent, false)) + override fun onBindViewHolder(holder: NutrientViewHolder, position: Int) { val (category, value, label, icon) = nutrientLevelItems[position] diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/splash/SplashActivity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/splash/SplashActivity.kt index ef7d1c54284b..5fd011c2c238 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/splash/SplashActivity.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/features/splash/SplashActivity.kt @@ -14,6 +14,7 @@ import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.databinding.ActivitySplashBinding import openfoodfacts.github.scrachx.openfood.features.shared.BaseActivity import openfoodfacts.github.scrachx.openfood.features.welcome.WelcomeActivity +import openfoodfacts.github.scrachx.openfood.utils.getAppPreferences import pl.aprilapps.easyphotopicker.EasyImage import kotlin.time.ExperimentalTime @@ -23,7 +24,7 @@ class SplashActivity : BaseActivity() { private val controller by lazy { - SplashController(getSharedPreferences("prefs", 0), this, this) + SplashController(getAppPreferences(), this, this) } @ExperimentalTime diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/LoadTaxonomiesWorker.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/LoadTaxonomiesWorker.kt index f4d12fe4e637..15f67ceee4fb 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/LoadTaxonomiesWorker.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/LoadTaxonomiesWorker.kt @@ -25,6 +25,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import openfoodfacts.github.scrachx.openfood.repositories.ProductRepository import openfoodfacts.github.scrachx.openfood.utils.Utils +import openfoodfacts.github.scrachx.openfood.utils.getAppPreferences /** * @param appContext The application [Context] @@ -37,7 +38,7 @@ class LoadTaxonomiesWorker @AssistedInject constructor( private val repo: ProductRepository ) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { - val settings = appContext.getSharedPreferences("prefs", 0) + val settings = appContext.getAppPreferences() return try { repo.reloadLabelsFromServer() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/ProductUploaderWorker.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/ProductUploaderWorker.kt index 7971997d7772..562a9e4e0ab6 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/ProductUploaderWorker.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/jobs/ProductUploaderWorker.kt @@ -9,6 +9,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import openfoodfacts.github.scrachx.openfood.R import openfoodfacts.github.scrachx.openfood.utils.OfflineProductService +import openfoodfacts.github.scrachx.openfood.utils.buildConstraints +import openfoodfacts.github.scrachx.openfood.utils.buildData +import openfoodfacts.github.scrachx.openfood.utils.buildOneTimeWorkRequest import javax.inject.Inject @HiltWorker @@ -38,36 +41,32 @@ class ProductUploaderWorker @AssistedInject constructor( companion object { private const val WORK_TAG = "OFFLINE_WORKER_TAG" const val KEY_INCLUDE_IMAGES = "includeImages" - private fun inputData(includeImages: Boolean) = Data.Builder() - .putBoolean(KEY_INCLUDE_IMAGES, includeImages) - .build() - fun scheduleProductUpload(context: Context, sharedPreferences: SharedPreferences) { + fun scheduleProductUpload(context: Context, pref: SharedPreferences) { - val constData = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - val uploadDataWorkRequest = OneTimeWorkRequest.Builder(ProductUploaderWorker::class.java) - .setInputData(inputData(false)) - .setConstraints(constData) - .build() + val constData = buildConstraints { + setRequiredNetworkType(NetworkType.CONNECTED) + } + val uploadDataWorkRequest = buildUploadRequest(constData, false) - val constPics = Constraints.Builder() - .setRequiredNetworkType( - if (sharedPreferences.getBoolean(context.getString(R.string.pref_enable_mobile_data_key), true)) - NetworkType.CONNECTED - else - NetworkType.UNMETERED - ) - val uploadPicturesWorkRequest = OneTimeWorkRequest.Builder(ProductUploaderWorker::class.java) - .setInputData(inputData(true)) - .setConstraints(constPics.build()) - .build() + val constPics = buildConstraints { + val uploadIfMobile = pref.getBoolean(context.getString(R.string.pref_enable_mobile_data_key), true) + + setRequiredNetworkType(if (uploadIfMobile) NetworkType.CONNECTED else NetworkType.UNMETERED) + } + val uploadPicturesWorkRequest = buildUploadRequest(constPics, true) WorkManager.getInstance(context) .beginUniqueWork(WORK_TAG, ExistingWorkPolicy.REPLACE, uploadDataWorkRequest) .then(uploadPicturesWorkRequest) .enqueue() } + + private fun buildUploadRequest(constPics: Constraints, includeImages: Boolean) = buildOneTimeWorkRequest { + setInputData(buildData { putBoolean(KEY_INCLUDE_IMAGES, includeImages) }) + setConstraints(constPics) + } + } } + diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt index 742653f1954b..7f3c2a5abeb1 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/models/MeasurementUnit.kt @@ -23,6 +23,8 @@ enum class MeasurementUnit(val sym: String) { companion object { fun findBySymbol(symbol: String) = values().find { it.sym == symbol } + fun requireBySymbol(symbol: String) = findBySymbol(symbol) + ?: throw IllegalArgumentException("Could not find unit with symbol '$symbol'.") } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt index 94bd16ff6d35..662198539741 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/network/OpenFoodAPIClient.kt @@ -381,7 +381,7 @@ class OpenFoodAPIClient @Inject constructor( } - context.getSharedPreferences("prefs", 0).edit { + context.getAppPreferences().edit { putBoolean("is_old_history_data_synced", true) } } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt index b0e94e09ed8e..61de96cb0bf0 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/ProductRepository.kt @@ -62,6 +62,7 @@ import openfoodfacts.github.scrachx.openfood.models.entities.tag.Tag import openfoodfacts.github.scrachx.openfood.network.ApiFields import openfoodfacts.github.scrachx.openfood.network.services.AnalysisDataAPI import openfoodfacts.github.scrachx.openfood.network.services.RobotoffAPI +import openfoodfacts.github.scrachx.openfood.utils.getAppPreferences import openfoodfacts.github.scrachx.openfood.utils.getLoginPreferences import org.greenrobot.greendao.query.WhereCondition.StringCondition import javax.inject.Inject @@ -278,8 +279,9 @@ class ProductRepository @Inject constructor( * @param lastDownload Date of last update on Long format */ private fun updateLastDownloadDateInSettings(taxonomy: Taxonomy, lastDownload: Long) { - context.getSharedPreferences("prefs", 0) - .edit { putLong(taxonomy.getLastDownloadTimeStampPreferenceId(), lastDownload) } + context.getAppPreferences().edit { + putLong(taxonomy.getLastDownloadTimeStampPreferenceId(), lastDownload) + } Log.i(LOG_TAG, "Set lastDownload of $taxonomy to $lastDownload") } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt index d32d71c63cd0..0150bb5034ab 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/repositories/TaxonomiesManager.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import openfoodfacts.github.scrachx.openfood.BuildConfig import openfoodfacts.github.scrachx.openfood.utils.Utils +import openfoodfacts.github.scrachx.openfood.utils.getAppPreferences import openfoodfacts.github.scrachx.openfood.utils.isEmpty import openfoodfacts.github.scrachx.openfood.utils.logDownload import org.greenrobot.greendao.AbstractDao @@ -60,14 +61,14 @@ class TaxonomiesManager @Inject constructor( dao: AbstractDao, productRepository: ProductRepository ): List = withContext(Dispatchers.Default) { - val mSettings = context.getSharedPreferences("prefs", 0) + val appPrefs = context.getAppPreferences() // First check if this taxonomy is to be loaded for this flavor, else return empty list - val isTaxonomyActivated = mSettings.getBoolean(taxonomy.getDownloadActivatePreferencesId(), false) + val isTaxonomyActivated = appPrefs.getBoolean(taxonomy.getDownloadActivatePreferencesId(), false) if (!isTaxonomyActivated) return@withContext emptyList() // If the database scheme changed, this settings should be true - val forceUpdate = mSettings.getBoolean(Utils.FORCE_REFRESH_TAXONOMIES, false) + val forceUpdate = appPrefs.getBoolean(Utils.FORCE_REFRESH_TAXONOMIES, false) // If database is empty or we have to force update, download it val empty = dao.isEmpty() @@ -76,7 +77,7 @@ class TaxonomiesManager @Inject constructor( download(taxonomy, productRepository) } else if (checkUpdate) { // Get local last downloaded time - val localDownloadTime = mSettings.getLong(taxonomy.getLastDownloadTimeStampPreferenceId(), 0L) + val localDownloadTime = appPrefs.getLong(taxonomy.getLastDownloadTimeStampPreferenceId(), 0L) // We need to check for update. Test if file on server is more recent than last download. checkAndDownloadIfNewer(taxonomy, localDownloadTime, productRepository) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Activity.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Activity.kt new file mode 100644 index 000000000000..f818da31e785 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Activity.kt @@ -0,0 +1,17 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import android.app.Activity +import android.content.Context +import android.view.inputmethod.InputMethodManager +import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity +import openfoodfacts.github.scrachx.openfood.models.ProductState + +fun Activity.hideKeyboard() { + val view = currentFocus ?: return + (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .hideSoftInputFromWindow(view.windowToken, 0) +} + +fun Activity.getProductState() = intent.getSerializableExtra(ProductEditActivity.KEY_STATE) as ProductState? +fun Activity.requireProductState() = this.getProductState() + ?: error("Activity ${this::class.simpleName} started without '${ProductEditActivity.KEY_STATE}' serializable in intent.") \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Constraints.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Constraints.kt new file mode 100644 index 000000000000..64fbc94b4113 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Constraints.kt @@ -0,0 +1,7 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import androidx.work.Constraints + +inline fun buildConstraints(buildAction: Constraints.Builder.() -> Unit): Constraints { + return Constraints.Builder().apply(buildAction).build() +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ContextExt.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Context.kt similarity index 67% rename from app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ContextExt.kt rename to app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Context.kt index fe6ae22e8f6f..f0b60c1e130f 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ContextExt.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Context.kt @@ -1,39 +1,32 @@ package openfoodfacts.github.scrachx.openfood.utils -import android.app.Activity import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.BatteryManager import android.util.Log -import android.util.TypedValue -import android.view.inputmethod.InputMethodManager import androidx.preference.PreferenceManager -import openfoodfacts.github.scrachx.openfood.features.PreferencesFragment +import java.io.File import kotlin.math.ceil private const val LOG_TAG = "ContextExt" -fun Context.isUserSet() = !getLoginPreferences().getString("user", null).isNullOrBlank() - -fun Context.getLoginPreferences(mode: Int = 0): SharedPreferences = - getSharedPreferences(PreferencesFragment.LOGIN_PREF, mode) - /** * Function which returns true if the battery level is low * * @return true if battery is low or false if battery in not low */ -fun Context.isBatteryLevelLow(): Boolean { +fun Context.isBatteryLevelLow(percent: Int = 15): Boolean { val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) val batteryStatus = registerReceiver(null, ifilter) ?: throw IllegalStateException("cannot get battery level") + val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val batteryPct = level / scale.toFloat() * 100 Log.i("BATTERYSTATUS", batteryPct.toString()) - return ceil(batteryPct.toDouble()) <= 15 + return ceil(batteryPct.toDouble()) <= percent } fun Context.isDisableImageLoad(defValue: Boolean = false) = PreferenceManager.getDefaultSharedPreferences(this) @@ -46,12 +39,6 @@ fun Context.isFastAdditionMode(defValue: Boolean = false) = PreferenceManager.ge fun Context.dpsToPixel(dps: Int) = dps.toPx(this) -fun Number.toPx(context: Context) = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this.toFloat(), - context.resources.displayMetrics -).toInt() - /** * @return Returns the version name of the app */ @@ -66,9 +53,22 @@ fun Context.getVersionName(): String = try { } fun Context.isHardwareCameraInstalled() = isHardwareCameraInstalled(this) +fun Context.clearCameraCache() { + (getCameraCacheLocation().listFiles() ?: return).forEach { + if (it.delete()) Log.i(LOG_TAG, "Deleted cached photo '${it.absolutePath}'.") + else Log.i(LOG_TAG, "Couldn't delete cached photo '${it.absolutePath}'.") + } +} -fun Activity.hideKeyboard() { - val view = currentFocus ?: return - (getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(view.windowToken, 0) +fun Context.getCameraCacheLocation(): File { + var cacheDir = cacheDir + if (Utils.isExternalStorageWritable()) { + cacheDir = externalCacheDir + } + val picDir = File(cacheDir, "EasyImage") + if (!picDir.exists()) { + if (picDir.mkdirs()) Log.i(LOG_TAG, "Directory '${picDir.absolutePath}' created.") + else Log.i(LOG_TAG, "Couldn't create directory '${picDir.absolutePath}'.") + } + return picDir } \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Data.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Data.kt new file mode 100644 index 000000000000..27a7c9dc493b --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Data.kt @@ -0,0 +1,7 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import androidx.work.Data + +inline fun buildData(buildAction: Data.Builder.() -> Unit): Data { + return Data.Builder().apply(buildAction).build() +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Date.kt similarity index 93% rename from app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt rename to app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Date.kt index 9f8524e81f38..cf16e79f5c8b 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DateExtensions.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Date.kt @@ -5,7 +5,7 @@ import openfoodfacts.github.scrachx.openfood.R import java.util.* import java.util.concurrent.TimeUnit -fun Date.timeFormatted(context: Context): String { +fun Date.durationToNowFormatted(context: Context): String { val duration = Date().time - time val seconds = TimeUnit.MILLISECONDS.toSeconds(duration) val minutes = TimeUnit.MILLISECONDS.toMinutes(duration) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DrawerBuilder.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DrawerBuilder.kt new file mode 100644 index 000000000000..c754609c65c3 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/DrawerBuilder.kt @@ -0,0 +1,36 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import android.app.Activity +import com.mikepenz.materialdrawer.AccountHeader +import com.mikepenz.materialdrawer.AccountHeaderBuilder +import com.mikepenz.materialdrawer.Drawer +import com.mikepenz.materialdrawer.DrawerBuilder +import com.mikepenz.materialdrawer.model.* + +inline fun buildDrawer(activity: Activity, builderAction: DrawerBuilder.() -> Unit = {}): Drawer { + return DrawerBuilder(activity).apply(builderAction).build() +} + +inline fun primaryItem(builderAction: PrimaryDrawerItem.() -> Unit = {}): PrimaryDrawerItem { + return PrimaryDrawerItem().apply(builderAction) +} + +inline fun sectionItem(builderAction: SectionDrawerItem.() -> Unit = {}): SectionDrawerItem { + return SectionDrawerItem().apply(builderAction) +} + +fun dividerItem(builderAction: DividerDrawerItem.() -> Unit = {}): DividerDrawerItem { + return DividerDrawerItem().apply(builderAction) +} + +inline fun profileSettingItem(builderAction: ProfileSettingDrawerItem.() -> Unit = {}): ProfileSettingDrawerItem { + return ProfileSettingDrawerItem().apply(builderAction) +} + +inline fun profileItem(builderAction: ProfileDrawerItem.() -> Unit = {}): ProfileDrawerItem { + return ProfileDrawerItem().apply(builderAction) +} + +inline fun buildAccountHeader(builder: AccountHeaderBuilder.() -> Unit = {}): AccountHeader { + return AccountHeaderBuilder().apply(builder).build() +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FileUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FileUtils.kt index 9a2da239d580..98e2205e2b90 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FileUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FileUtils.kt @@ -25,7 +25,6 @@ import openfoodfacts.github.scrachx.openfood.models.entities.ProductLists import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVPrinter import org.jetbrains.annotations.Contract -import java.io.File import java.io.IOException private const val LOG_TAG = "FileUtils" @@ -135,26 +134,5 @@ fun createNotificationManager(context: Context): NotificationManager { } -fun Context.clearCameraCache() { - (getCameraCacheLocation().listFiles() ?: return).forEach { - if (it.delete()) Log.i(LOG_TAG, "Deleted cached photo '${it.absolutePath}'.") - else Log.i(LOG_TAG, "Couldn't delete cached photo '${it.absolutePath}'.") - } -} - -fun Context.getCameraCacheLocation(): File { - var cacheDir = cacheDir - if (Utils.isExternalStorageWritable()) { - cacheDir = externalCacheDir - } - val picDir = File(cacheDir, "EasyImage") - if (!picDir.exists()) { - if (picDir.mkdirs()) Log.i(LOG_TAG, "Directory '${picDir.absolutePath}' created.") - else Log.i(LOG_TAG, "Couldn't create directory '${picDir.absolutePath}'.") - } - return picDir -} - - diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FragmentExt.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Fragment.kt similarity index 53% rename from app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FragmentExt.kt rename to app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Fragment.kt index 457055eb443a..97c3cb2f85b4 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/FragmentExt.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Fragment.kt @@ -1,31 +1,19 @@ package openfoodfacts.github.scrachx.openfood.utils -import android.app.Activity -import android.os.Bundle import androidx.fragment.app.Fragment import openfoodfacts.github.scrachx.openfood.features.product.edit.ProductEditActivity.Companion.KEY_STATE import openfoodfacts.github.scrachx.openfood.models.ProductState import openfoodfacts.github.scrachx.openfood.models.entities.SendProduct -fun T.applyBundle(bundle: Bundle): T { - this.arguments = bundle - return this -} +internal const val KEY_SEND_PRODUCT = "sendProduct" fun Fragment.getProductState() = arguments?.getSerializable(KEY_STATE) as ProductState? fun Fragment.requireProductState() = this.getProductState() - ?: error("Fragment ${this::class.simpleName} started without '$KEY_STATE' argument.") - + ?: error("Fragment ${this::class.simpleName} started without '$KEY_STATE' argument.") -internal const val KEY_SEND_PRODUCT = "sendProduct" fun Fragment.getSendProduct() = arguments?.getSerializable(KEY_SEND_PRODUCT) as SendProduct? fun Fragment.requireSendProduct() = this.getSendProduct() - ?: error("Fragment ${this::class.simpleName} started without 'sendProduct' argument.") - -fun Activity.getProductState() = intent.getSerializableExtra(KEY_STATE) as ProductState? - -fun Activity.requireProductState() = this.getProductState() - ?: error("Activity ${this::class.simpleName} started without '$KEY_STATE' serializable in intent.") \ No newline at end of file + ?: error("Fragment ${this::class.simpleName} started without 'sendProduct' argument.") diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/LocaleManager.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/LocaleManager.kt index e67e1488d727..d69951e1cc34 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/LocaleManager.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/LocaleManager.kt @@ -36,9 +36,7 @@ class LocaleManager @Inject constructor( private val sharedPreferences: SharedPreferences ) { - private val selectedLanguageKey by lazy { - context.getString(R.string.pref_language_key) - } + private val selectedLanguagePrefKey by lazy { context.getString(R.string.pref_language_key) } private var currentLocale: Locale init { @@ -61,7 +59,6 @@ class LocaleManager @Inject constructor( fun getLocale(): Locale = currentLocale - @Deprecated("Only for UI tests.") fun saveLanguageToPrefs(context: Context, locale: Locale): Context { saveLanguageToPrefs(locale.language) return changeAppLanguage(context, locale) @@ -93,9 +90,9 @@ class LocaleManager @Inject constructor( private fun saveLanguageToPrefs(language: String) { sharedPreferences.edit { - putString(selectedLanguageKey, language) + putString(selectedLanguagePrefKey, language) } } - private fun getLanguageFromPrefs() = sharedPreferences.getString(selectedLanguageKey, null) + private fun getLanguageFromPrefs() = sharedPreferences.getString(selectedLanguagePrefKey, null) } diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Measurement.kt similarity index 86% rename from app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt rename to app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Measurement.kt index 4e180c91d64d..50409f4a3d56 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Measurement.kt @@ -83,8 +83,10 @@ fun Float.sodiumToSalt() = this * SALT_PER_SODIUM fun Measurement.saltToSodium() = Measurement(value.saltToSodium(), unit) fun Measurement.sodiumToSalt() = Measurement(value.sodiumToSalt(), unit) +private val SIZE_REGEX = Regex("(\\d+[.,]?\\d*)\\s*([A-z]+)?") + fun getServingIn(servingSize: String, unit: MeasurementUnit): Measurement? { - val match = Regex("(\\d+(?:\\.\\d+)?) *(\\w+)").find(servingSize) ?: return null + val match = SIZE_REGEX.find(servingSize) ?: return null val value = match.groupValues[1].toFloat() val servingUnit = match.groupValues[2].let { Companion.findBySymbol(it) } ?: return null @@ -93,5 +95,15 @@ fun getServingIn(servingSize: String, unit: MeasurementUnit): Measurement? { return measurement.convertTo(unit) } +fun parseServing(servingSize: String): Pair { + val match = SIZE_REGEX.find(servingSize) + ?: throw IllegalArgumentException("Could not parse serving size '$servingSize'") + + val value = match.groupValues[1] + val unit = match.groupValues[2].let { Companion.findBySymbol(it) } + + return value to unit +} + fun getServingInOz(servingSize: String) = getServingIn(servingSize, UNIT_OZ) -fun getServingInL(servingSize: String) = getServingIn(servingSize, UNIT_LITER) \ No newline at end of file +fun getServingInL(servingSize: String) = getServingIn(servingSize, UNIT_LITER) diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Number.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Number.kt new file mode 100644 index 000000000000..fbc28efaec89 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Number.kt @@ -0,0 +1,10 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import android.content.Context +import android.util.TypedValue + +fun Number.toPx(context: Context) = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + context.resources.displayMetrics +).toInt() \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OFFDatabaseHelper.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OFFDatabaseHelper.kt index 4b4d1499ef5a..1ea9998b394d 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OFFDatabaseHelper.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OFFDatabaseHelper.kt @@ -42,7 +42,7 @@ class OFFDatabaseHelper @JvmOverloads constructor( name: String, factory: CursorFactory? = null ) : OpenHelper(context, name, factory) { - private val settings: SharedPreferences by lazy { context.getSharedPreferences("prefs", 0) } + private val settings: SharedPreferences by lazy { context.getAppPreferences() } override fun onCreate(db: Database) { Log.i(LOG_TAG, "Creating tables for schema version ${DaoMaster.SCHEMA_VERSION}") diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OneTimeWorkRequest.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OneTimeWorkRequest.kt new file mode 100644 index 000000000000..9b6b9a11bd46 --- /dev/null +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/OneTimeWorkRequest.kt @@ -0,0 +1,11 @@ +package openfoodfacts.github.scrachx.openfood.utils + +import androidx.work.ListenableWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder + +inline fun buildOneTimeWorkRequest( + builderAction: OneTimeWorkRequest.Builder.() -> Unit = {} +): OneTimeWorkRequest { + return OneTimeWorkRequestBuilder().apply(builderAction).build() +} \ No newline at end of file diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/PreferencesUtils.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/PreferencesUtils.kt index dbef9ec4482f..c55e826445e2 100644 --- a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/PreferencesUtils.kt +++ b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/PreferencesUtils.kt @@ -1,11 +1,34 @@ package openfoodfacts.github.scrachx.openfood.utils +import android.content.Context +import android.content.SharedPreferences import androidx.preference.Preference import androidx.preference.PreferenceScreen -import openfoodfacts.github.scrachx.openfood.features.PreferencesFragment +import openfoodfacts.github.scrachx.openfood.features.preferences.PreferencesFragment fun PreferencesFragment.requirePreference(key: String): T = - findPreference(key) ?: error("$key preference does not exist.") + findPreference(key) ?: error("$key preference does not exist.") fun PreferenceScreen.requirePreference(key: String): T = - findPreference(key) ?: error("$key preference does not exist.") \ No newline at end of file + findPreference(key) ?: error("$key preference does not exist.") + + +inline fun PreferencesFragment.requirePreference(key: String, action: T.() -> Unit) { + requirePreference(key).run(action) +} + +inline fun PreferenceScreen.requirePreference(key: String, action: T.() -> Unit) { + requirePreference(key).run(action) +} + + +fun Context.getLoginPreferences(mode: Int = Context.MODE_PRIVATE): SharedPreferences = + getSharedPreferences(PreferencesFragment.LOGIN_SHARED_PREF, mode) + +fun Context.getAppPreferences(mode: Int = Context.MODE_PRIVATE): SharedPreferences = + getSharedPreferences(PreferencesFragment.APP_SHARED_PREF, mode) + +fun Context.getCameraPreferences(mode: Int = Context.MODE_PRIVATE): SharedPreferences = + getSharedPreferences("camera", mode) + +fun Context.isUserSet() = !getLoginPreferences().getString("user", null).isNullOrBlank() diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductExt.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Product.kt similarity index 100% rename from app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/ProductExt.kt rename to app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Product.kt diff --git a/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/RxEx.kt b/app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Rx.kt similarity index 100% rename from app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/RxEx.kt rename to app/src/main/java/openfoodfacts/github/scrachx/openfood/utils/Rx.kt diff --git a/app/src/main/res/drawable/ic_baseline_language_24.xml b/app/src/main/res/drawable/ic_baseline_language_24.xml new file mode 100644 index 000000000000..47df05f356c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_language_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/pref_keys.xml b/app/src/main/res/values/pref_keys.xml index 4a27b8c03271..948c1d8fdb49 100644 --- a/app/src/main/res/values/pref_keys.xml +++ b/app/src/main/res/values/pref_keys.xml @@ -100,9 +100,10 @@ Crop new images Enables crop action on new images - Use MLKit Scanner - Increases Scanning efficiency. - Switch off to use Classic Barcode Scanner. - select_scanner + select_scanner + Use MLKit Scanner + Increases Scanning efficiency. + Switch off to use Classic Barcode Scanner. + Only available on PlayStore version. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index e2a3a7807092..28fe0c524141 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -7,6 +7,7 @@ android:title="@string/preference_header_general"> + android:key="@string/pref_scanner_mlkit_key" + android:title="@string/pref_scanner_mlkit_title" + app:summaryOff="@string/pref_scanner_mlkit_summaryOff" + app:summaryOn="@string/pref_scanner_mlkit_summaryOn" /> + android:summaryOn="@string/pref_low_battery_summary" + android:title="@string/pref_low_battery_title" /> android:summaryOff="@string/pref_low_battery_summary" /> @@ -114,36 +115,36 @@ + android:summary="@string/pref_help_translate_summary" + android:title="@string/pref_help_translate_title" /> + android:summaryOn="@string/pref_crop_new_images_summary" + android:title="@string/pref_crop_new_images_title" /> android:summaryOff="@string/pref_crop_new_images_summary" /> + android:summaryOn="@string/pref_fast_addition_summary" + android:title="@string/pref_fast_addition_title" /> android:summaryOff="@string/pref_fast_addition_summary" /> + android:summaryOn="@string/pref_contribution_tab_summary" + android:title="@string/pref_contribution_tab_title" /> android:summaryOff="@string/pref_contribution_tab_summary" /> + android:summaryOn="@string/pref_show_product_photos_summary" + android:title="@string/pref_show_product_photos_title" /> android:summaryOff="@string/pref_show_product_photos_summary" /> @@ -155,15 +156,15 @@ diff --git a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt index a1609392da3f..c078bebecc9e 100644 --- a/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt +++ b/app/src/test/java/openfoodfacts/github/scrachx/openfood/utils/UnitUtilsTest.kt @@ -1,6 +1,7 @@ package openfoodfacts.github.scrachx.openfood.utils import com.google.common.truth.Truth.assertThat +import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit import openfoodfacts.github.scrachx.openfood.models.MeasurementUnit.* import org.junit.Assert.assertThrows import org.junit.Test @@ -107,6 +108,18 @@ class UnitUtilsTest { assertThat(measure(5f, UNIT_GRAM).saltToSodium().value).isWithin(TOL).of(1.96850393701f) } + @Test + fun `test serving size parsing`() { + assertThat(parseServing("25g")).isEqualTo("25" to UNIT_GRAM) + assertThat(parseServing("25 g")).isEqualTo("25" to UNIT_GRAM) + assertThat(parseServing("25.7g")).isEqualTo("25.7" to UNIT_GRAM) + assertThat(parseServing("25.7 g")).isEqualTo("25.7" to UNIT_GRAM) + assertThat(parseServing("25,7g")).isEqualTo("25,7" to UNIT_GRAM) + assertThat(parseServing("25,7 g")).isEqualTo("25,7" to UNIT_GRAM) + assertThat(parseServing("25.5.7g")).isEqualTo(Pair("25.5", null)) + assertThat(parseServing("25.5.7")).isEqualTo(Pair("25.5", null)) + } + companion object { private const val TOL = 1e-5f }