diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a5d456928e..f39734eb4b 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,16 +1,12 @@ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a9c0c498cf..bb3309714d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,6 +16,11 @@ if (isRunningOnTravisAndIsNotPRBuild) { dependencies { + def appcompat_version = "1.7.0" + implementation "androidx.appcompat:appcompat:$appcompat_version" + // For loading and tinting drawables on older versions of the platform + implementation "androidx.appcompat:appcompat-resources:$appcompat_version" + // Utils implementation 'in.yuvi:http.fluent:1.3' implementation 'com.google.code.gson:gson:2.8.5' @@ -380,13 +385,16 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.3.2' + kotlinCompilerExtensionVersion '1.5.8' } namespace 'fr.free.nrw.commons' lint { abortOnError false disable 'MissingTranslation', 'ExtraTranslation' } + androidResources { + generateLocaleConfig true + } } String getTestUserName() { diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ui/AppLanguagesSystemTest.java b/app/src/androidTest/java/fr/free/nrw/commons/ui/AppLanguagesSystemTest.java new file mode 100644 index 0000000000..3abe28e365 --- /dev/null +++ b/app/src/androidTest/java/fr/free/nrw/commons/ui/AppLanguagesSystemTest.java @@ -0,0 +1,58 @@ +package fr.free.nrw.commons.ui; + +import androidx.test.uiautomator.UiScrollable; +import androidx.test.uiautomator.UiSelector; +import androidx.test.uiautomator.UiObject; +import androidx.test.uiautomator.UiDevice; +import androidx.test.platform.app.InstrumentationRegistry; +import android.content.Intent; +import android.provider.Settings; + +import org.junit.Before; +import org.junit.Test; + +public class AppLanguagesSystemTest { + + private UiDevice device; + + @Before + public void setUp() { + // Initiate UI Automator + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + // Use Intent to start system application + Intent intent = new Intent(Settings.ACTION_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent); + + // wait Settings starting + device.waitForIdle(); + } + + @Test + public void testCommonsAppLanguageOptionExists() throws Exception { + // 1. Find and click "System" + UiScrollable settingsList = new UiScrollable(new UiSelector().scrollable(true)); + UiObject systemOption = settingsList.getChildByText(new UiSelector().text("System"), "System"); + systemOption.click(); + + // 2. scroll and find "Languages & input" + UiObject languagesInputOption = device.findObject(new UiSelector() + .className("android.widget.TextView") + .textContains("Gboard")); + languagesInputOption.clickAndWaitForNewWindow(); + + + + + // 3. detect "App languages" and click + UiObject appLanguagesOption = device.findObject(new UiSelector().text("App languages")); + appLanguagesOption.clickAndWaitForNewWindow(); + + // 4. detect "Commons" APP is there + UiScrollable appLanguagesList = new UiScrollable(new UiSelector().scrollable(true)); + UiObject commonsAppOption = appLanguagesList.getChildByText(new UiSelector().text("Commons"), "Commons"); + + assert(commonsAppOption.exists()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a47a46447..ee7cebde26 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -193,6 +193,13 @@ + + + { onSupportNavigateUp(); @@ -485,13 +485,13 @@ public enum ActiveFragment { /** * Load default language in onCreate from SharedPreferences */ - private void loadLocale() { - final SharedPreferences preferences = getSharedPreferences("Settings", - Activity.MODE_PRIVATE); - final String language = preferences.getString("language", ""); - final SettingsFragment settingsFragment = new SettingsFragment(); - settingsFragment.setLocale(this, language); - } +// private void loadLocale() { +// final SharedPreferences preferences = getSharedPreferences("Settings", +// Activity.MODE_PRIVATE); +// final String language = preferences.getString("language", ""); +// final SettingsFragment settingsFragment = new SettingsFragment(); +// settingsFragment.setLocale(this, language); +// } public NavTabLayout.OnNavigationItemSelectedListener getNavListener() { return navListener; diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt index b75a6e1d4f..872388f40d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt @@ -15,19 +15,19 @@ abstract class NotForUploadStatusDao { * Insert into Not For Upload status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(notForUploadStatus: NotForUploadStatus) + abstract fun insert(notForUploadStatus: NotForUploadStatus) /** * Delete Not For Upload status entry. */ @Delete - abstract suspend fun delete(notForUploadStatus: NotForUploadStatus) + abstract fun delete(notForUploadStatus: NotForUploadStatus) /** * Query Not For Upload status with image sha1. */ @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? + abstract fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? /** * Asynchronous image sha1 query. @@ -38,7 +38,7 @@ abstract class NotForUploadStatusDao { * Deletion Not For Upload status with image sha1. */ @Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun deleteWithImageSHA1(imageSHA1: String) + abstract fun deleteWithImageSHA1(imageSHA1: String) /** * Asynchronous image sha1 deletion. @@ -49,5 +49,5 @@ abstract class NotForUploadStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun find(imageSHA1: String): Int + abstract fun find(imageSHA1: String): Int } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt index 378af5b8db..03cbb176fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt @@ -17,31 +17,31 @@ abstract class UploadedStatusDao { * Insert into uploaded status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(uploadedStatus: UploadedStatus) + abstract fun insert(uploadedStatus: UploadedStatus) /** * Update uploaded status entry. */ @Update - abstract suspend fun update(uploadedStatus: UploadedStatus) + abstract fun update(uploadedStatus: UploadedStatus) /** * Delete uploaded status entry. */ @Delete - abstract suspend fun delete(uploadedStatus: UploadedStatus) + abstract fun delete(uploadedStatus: UploadedStatus) /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus? + abstract fun getFromImageSHA1(imageSHA1: String): UploadedStatus? /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? + abstract fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? /** * Asynchronous insert into uploaded status table. @@ -55,7 +55,7 @@ abstract class UploadedStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ") - abstract suspend fun findByImageSHA1( + abstract fun findByImageSHA1( imageSHA1: String, imageResult: Boolean, ): Int @@ -66,7 +66,7 @@ abstract class UploadedStatusDao { @Query( "SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ", ) - abstract suspend fun findByModifiedImageSHA1( + abstract fun findByModifiedImageSHA1( modifiedImageSHA1: String, modifiedImageResult: Boolean, ): Int diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 7336c1b407..384e7f1a9a 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -707,7 +707,8 @@ public void updateCategories() { */ private void buildDepictionList(List idAndCaptions) { binding.mediaDetailDepictionContainer.removeAllViews(); - String locale = Locale.getDefault().getLanguage(); + String fullCode = Locale.getDefault().getLanguage(); + String locale = fullCode.contains(",") ? fullCode.substring(0, fullCode.indexOf(',')).trim() : fullCode; for (IdAndCaptions idAndCaption : idAndCaptions) { binding.mediaDetailDepictionContainer.addView(buildDepictLabel( getDepictionCaption(idAndCaption, locale), diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java index 8d6b742319..dd041d8bc6 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -409,15 +409,55 @@ public List getNearbyPlaces( */ @Nullable public List getPlaces( - final List placeList, final String language) throws IOException { + final List placeList, final String language, final String secondaryLanguages) throws IOException { final String wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq"); + final String[] secondaryLanguageArray = secondaryLanguages.split(",\\s*"); // could be used to generate backup SparQL Queries + String qids = ""; for (final Place place : placeList) { qids += "\n" + ("wd:" + place.getWikiDataEntityId()); } + + StringBuilder fallBackDescription = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallBackDescription.append("OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?itemDescriptionPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + + StringBuilder fallbackLabel = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallbackLabel.append("OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?itemLabelPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + + StringBuilder fallbackClassLabel = new StringBuilder(); + for (int i = 0; i < secondaryLanguageArray.length; i++) { + fallbackClassLabel.append("OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage_") + .append(i + 1) + .append(". FILTER (lang(?classLabelPreferredLanguage_") + .append(i + 1) + .append(") = \"") + .append(secondaryLanguageArray[i]) + .append("\")}\n"); + } + final String query = wikidataQuery .replace("${ENTITY}", qids) - .replace("${LANG}", language); + .replace("${LANG}", language) + .replace("${SECONDARYDESCRIPTION}", fallBackDescription.toString()) + .replace("${SECONDARYLABEL}", fallbackLabel.toString()) + .replace("${SECONDARYCLASSLABEL}", fallbackClassLabel.toString()); + final HttpUrl.Builder urlBuilder = HttpUrl .parse(sparqlQueryUrl) .newBuilder() diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 7bb3119617..d5f551e27f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -8,6 +8,8 @@ import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.MapController; import fr.free.nrw.commons.location.LatLng; +import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.settings.Prefs; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -16,6 +18,7 @@ import java.util.Locale; import java.util.Map; import javax.inject.Inject; +import javax.inject.Named; import timber.log.Timber; public class NearbyController extends MapController { @@ -34,6 +37,9 @@ public NearbyController(NearbyPlaces nearbyPlaces) { this.nearbyPlaces = nearbyPlaces; } + @Inject + @Named("default_preferences") + JsonKvStore defaultKvStore; /** * Prepares Place list to make their distance information update later. @@ -56,6 +62,7 @@ public NearbyPlacesInfo loadAttractionsFromLocation(final LatLng currentLatLng, Timber.d("Loading attractions nearby, but currentLatLng is null"); return null; } + List places = nearbyPlaces .radiusExpander(searchLatLng, Locale.getDefault().getLanguage(), returnClosestResult, customQuery); @@ -139,7 +146,10 @@ public String getPlacesAsGPX(LatLng currentLocation) throws Exception { * @throws Exception If an error occurs during the retrieval process. */ public List getPlaces(List placeList) throws Exception { - return nearbyPlaces.getPlaces(placeList, Locale.getDefault().getLanguage()); + + String secondaryLanguages = defaultKvStore.getString(Prefs.SECONDARY_LANGUAGE, ""); + + return nearbyPlaces.getPlaces(placeList, Locale.getDefault().getLanguage(), secondaryLanguages); } public static LatLng calculateNorthEast(double latitude, double longitude, double distance) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index 46f0a2a9eb..cfc45945ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -131,9 +131,9 @@ public List getFromWikidataQuery( * @throws Exception If an error occurs during the retrieval process. */ public List getPlaces(final List placeList, - final String lang) throws Exception { + final String lang, final String lang2) throws Exception { return okHttpJsonApiClient - .getPlaces(placeList, lang); + .getPlaces(placeList, lang, lang2); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt index 81ef5533d9..b71670e2a5 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt @@ -27,7 +27,8 @@ class RecentLanguagesAdapter constructor( override fun isEnabled(position: Int) = recentLanguages[position].languageCode.let { - it.isNotEmpty() && !selectedLanguages.containsValue(it) && it != selectedLangCode +// it.isNotEmpty() && !it.contains(selectedLanguages.values.first()) + true } override fun getCount() = recentLanguages.size diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/SavedLanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/SavedLanguagesAdapter.kt new file mode 100644 index 0000000000..c067a6e49e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/SavedLanguagesAdapter.kt @@ -0,0 +1,77 @@ +package fr.free.nrw.commons.recentlanguages + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import fr.free.nrw.commons.R +import fr.free.nrw.commons.databinding.RowItemLanguagesSpinnerBinding +import fr.free.nrw.commons.utils.LangCodeUtils +import org.apache.commons.lang3.StringUtils +import java.util.HashMap + +/** + * Array adapter for saved languages + */ +class SavedLanguagesAdapter constructor( + context: Context, + var savedLanguages: List, // List of saved languages + private val selectedLanguages: HashMap<*, String>, // Selected languages map +) : ArrayAdapter(context, R.layout.row_item_languages_spinner) { + /** + * Selected language code in SavedLanguagesAdapter + * Used for marking selected ones + */ + var selectedLangCode = "" + + override fun isEnabled(position: Int) = + savedLanguages[position].languageCode.let { + it.isNotEmpty() && !selectedLanguages.containsValue(it) && it != selectedLangCode + } + + override fun getCount() = savedLanguages.size + + override fun getView( + position: Int, + convertView: View?, + parent: ViewGroup, + ): View { + val binding: RowItemLanguagesSpinnerBinding + var rowView = convertView + + if (rowView == null) { + val layoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + binding = RowItemLanguagesSpinnerBinding.inflate(layoutInflater, parent, false) + rowView = binding.root + } else { + binding = RowItemLanguagesSpinnerBinding.bind(rowView) + } + + val languageCode = savedLanguages[position].languageCode + val languageName = savedLanguages[position].languageName + binding.tvLanguage.let { + it.isEnabled = isEnabled(position) + if (languageCode.isEmpty()) { + it.text = StringUtils.capitalize(languageName) + it.textAlignment = View.TEXT_ALIGNMENT_CENTER + } else { + it.text = + "${StringUtils.capitalize(languageName)}" + + " [${LangCodeUtils.fixLanguageCode(languageCode)}]" + } + } + return rowView + } + + /** + * Provides code of a language from saved languages for a specific position + */ + fun getLanguageCode(position: Int): String = savedLanguages[position].languageCode + + /** + * Provides name of a language from saved languages for a specific position + */ + fun getLanguageName(position: Int): String = savedLanguages[position].languageName +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java index 3342143479..d0451e24ca 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java @@ -8,6 +8,8 @@ public class Prefs { public static final String UPLOADS_SHOWING = "uploadsshowing"; public static final String MANAGED_EXIF_TAGS = "managed_exif_tags"; public static final String DESCRIPTION_LANGUAGE = "languageDescription"; + public static final String SECONDARY_LANGUAGE = "languageSecondary"; + public static final String APP_UI_LANGUAGE = "appUiLanguage"; public static final String KEY_THEME_VALUE = "appThemePref"; diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 94e799aa29..cd40d5b5e2 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -3,6 +3,7 @@ import static android.content.Context.MODE_PRIVATE; import android.Manifest.permission; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.Intent; @@ -18,9 +19,13 @@ import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; +import android.widget.ArrayAdapter; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; import androidx.preference.ListPreference; import androidx.preference.MultiSelectListPreference; import androidx.preference.Preference; @@ -47,10 +52,13 @@ import fr.free.nrw.commons.recentlanguages.Language; import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; +import fr.free.nrw.commons.recentlanguages.SavedLanguagesAdapter; import fr.free.nrw.commons.upload.LanguagesAdapter; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -79,13 +87,16 @@ public class SettingsFragment extends PreferenceFragmentCompat { private ListPreference themeListPreference; private Preference descriptionLanguageListPreference; + private Preference descriptionSecondaryLanguageListPreference; private Preference appUiLanguageListPreference; private String keyLanguageListPreference; private TextView recentLanguagesTextView; private View separator; private ListView languageHistoryListView; private static final String GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"; - private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { + private ActivityResultLauncher + inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), + new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { boolean areAllGranted = true; @@ -108,6 +119,9 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { // Set the preferences from an XML resource setPreferencesFromResource(R.xml.preferences, rootKey); + System.out.println("Locale"); + System.out.println(Locale.getDefault().getLanguage()); + themeListPreference = findPreference(Prefs.KEY_THEME_VALUE); prepareTheme(); @@ -145,6 +159,10 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { // If current language code is empty, means none selected by user yet so use phone local appUiLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage()); } else { + if(!languageCode.equals(Locale.getDefault().getLanguage())){ + languageCode = Locale.getDefault().getLanguage(); + saveLanguageValue(languageCode, "appUiDefaultLanguagePref"); + } // If any language is selected by user previously, use it Locale defLocale = createLocale(languageCode); appUiLanguageListPreference.setSummary((defLocale).getDisplayLanguage(defLocale)); @@ -178,6 +196,21 @@ public boolean onPreferenceClick(Preference preference) { } }); + + descriptionSecondaryLanguageListPreference = findPreference("descriptionSecondaryLanguagePref"); + assert descriptionSecondaryLanguageListPreference != null; + keyLanguageListPreference = descriptionSecondaryLanguageListPreference.getKey(); + languageCode = getCurrentLanguageCode(keyLanguageListPreference); + assert languageCode != null; + + descriptionSecondaryLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + prepareSecondaryLanguageDialog(); + return true; + } + }); + Preference betaTesterPreference = findPreference("becomeBetaTester"); betaTesterPreference.setOnPreferenceClickListener(preference -> { Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link))); @@ -205,6 +238,7 @@ public boolean onPreferenceClick(Preference preference) { findPreference("useAuthorName").setEnabled(false); findPreference("displayNearbyCardView").setEnabled(false); findPreference("descriptionDefaultLanguagePref").setEnabled(false); + findPreference("descriptionSecondaryLanguagePref").setEnabled(false); findPreference("displayLocationPermissionForCardView").setEnabled(false); findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE).setEnabled(false); findPreference("managed_exif_tags").setEnabled(false); @@ -242,6 +276,7 @@ private void showLocationLossWarning() { ); } + @SuppressLint("RestrictedApi") @Override protected Adapter onCreateAdapter(final PreferenceScreen preferenceScreen) { return new PreferenceGroupAdapter(preferenceScreen) { @@ -267,6 +302,155 @@ private void prepareTheme() { }); } + private void updateSavedLanguages(ListView savedLanguageListView, List savedLanguages, HashMap selectedLanguages) { + // Use SavedLanguagesAdapter to display saved languages + SavedLanguagesAdapter savedLanguagesAdapter = new SavedLanguagesAdapter( + getActivity(), + savedLanguages, // List of saved Language objects + selectedLanguages // Pass the map of selected languages + ); + + // Set the adapter to the ListView to display the saved languages + savedLanguageListView.setAdapter(savedLanguagesAdapter); + } + + private ArrayList deSerialise(String languageCodes) { + // Check if the stored string is empty or null + if (languageCodes == null || languageCodes.isEmpty()) { + return new ArrayList<>(); // Return an empty list if there's no data + } + + // Split the string by commas and store it in a list + String[] languageArray = languageCodes.split(",\\s*"); // Split by comma and optional space + return new ArrayList<>(Arrays.asList(languageArray)); // Convert array to ArrayList and return + } + + + private void prepareSecondaryLanguageDialog() { + final String languageCode = getCurrentLanguageCode("descriptionSecondaryLanguagePref"); + HashMap selectedLanguages = new HashMap<>(); + assert languageCode != null; + selectedLanguages.put(0, Locale.getDefault().getLanguage()); + + // Deserializing saved language codes to Language objects + ArrayList savedLanguages = new ArrayList<>(); + for (String code : deSerialise(languageCode)) { + System.out.println(code); + if(code.equals(Locale.getDefault().getLanguage())){ + System.out.println("match"); + continue; + } + Locale locale = new Locale(code); + savedLanguages.add(new Language(locale.getDisplayLanguage(locale), code)); + } + + // Create the new dialog for secondary language + Dialog dialog = new Dialog(getActivity()); + dialog.setContentView(R.layout.dialog_select_secondary_language); + dialog.setCanceledOnTouchOutside(true); + dialog.getWindow().setLayout( + (int) (getActivity().getResources().getDisplayMetrics().widthPixels * 0.90), + (int) (getActivity().getResources().getDisplayMetrics().heightPixels * 0.90) + ); + dialog.show(); + + // Bind UI elements + EditText editText = dialog.findViewById(R.id.search_language); + ListView listView = dialog.findViewById(R.id.language_list); + ListView savedLanguageListView = dialog.findViewById(R.id.language_history_list); + View separator = dialog.findViewById(R.id.separator); + + // Setup saved languages with the new SavedLanguagesAdapter + updateSavedLanguages(savedLanguageListView, savedLanguages, selectedLanguages); + + // Set an onItemClickListener to remove a language when clicked + savedLanguageListView.setOnItemClickListener((adapterView, view, position, id) -> { + // Remove the clicked language from Saved_Languages + savedLanguages.remove(position); + + // Update the saved language list view after removing the language + updateSavedLanguages(savedLanguageListView, savedLanguages, selectedLanguages); + + // Update the shared preferences to reflect the removal + String updatedLanguageCodes = ""; + for (Language language : savedLanguages) { + updatedLanguageCodes += language.getLanguageCode() + ", "; + } + // Remove the trailing comma and space if present + if (!updatedLanguageCodes.isEmpty()) { + updatedLanguageCodes = updatedLanguageCodes.substring(0, updatedLanguageCodes.length() - 2); + } + saveLanguageValue(updatedLanguageCodes, "descriptionSecondaryLanguagePref"); +// descriptionSecondaryLanguageListPreference.setSummary(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); + }); + + // Set up the adapter for new languages using the selectedLanguages map + LanguagesAdapter languagesAdapter = new LanguagesAdapter(getActivity(), selectedLanguages); + listView.setAdapter(languagesAdapter); + + // Add search functionality + editText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + hideRecentLanguagesSection(); + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + languagesAdapter.getFilter().filter(charSequence); + } + + @Override + public void afterTextChanged(Editable editable) { + } + }); + + // Handle item click for language selection in the main list + listView.setOnItemClickListener((adapterView, view, i, l) -> { + String selectedLanguageCode = languagesAdapter.getLanguageCode(i); + String selectedLanguageName = languagesAdapter.getLanguageName(i); + + if (deSerialise(getCurrentLanguageCode("descriptionSecondaryLanguagePref")).contains(selectedLanguageCode)) { + Toast.makeText(getActivity(), "Language already selected", Toast.LENGTH_SHORT).show(); + return; + } + + savedLanguages.add(new Language(selectedLanguageName, selectedLanguageCode)); + updateSavedLanguages(savedLanguageListView, savedLanguages, selectedLanguages); + + // Update the shared preferences to reflect the addition + String updatedLanguageCodes = ""; + for (Language language : savedLanguages) { + updatedLanguageCodes += language.getLanguageCode() + ", "; + } + // Remove the trailing comma and space if present + if (!updatedLanguageCodes.isEmpty()) { + updatedLanguageCodes = updatedLanguageCodes.substring(0, updatedLanguageCodes.length() - 2); + } + + saveLanguageValue(updatedLanguageCodes, "descriptionSecondaryLanguagePref"); + +// descriptionSecondaryLanguageListPreference.setSummary(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); + }); + + dialog.setOnDismissListener(dialogInterface -> { + // Update the shared preferences to reflect changes + String updatedLanguageCodes = ""; + for (Language language : savedLanguages) { + updatedLanguageCodes += language.getLanguageCode() + ", "; + } + // Remove the trailing comma and space if present + if (!updatedLanguageCodes.isEmpty()) { + updatedLanguageCodes = updatedLanguageCodes.substring(0, updatedLanguageCodes.length() - 2); + } + saveLanguageValue(updatedLanguageCodes, "descriptionSecondaryLanguagePref"); + + }); + } + + + + /** * Prepare and Show language selection dialog box * Uses previously saved language if there is any, if not uses phone locale as initial language. @@ -278,6 +462,7 @@ private void prepareTheme() { */ private void prepareAppLanguages(final String keyListPreference) { + // Gets current language code from shared preferences final String languageCode = getCurrentLanguageCode(keyListPreference); final List recentLanguages = recentLanguagesDao.getRecentLanguages(); @@ -287,7 +472,7 @@ private void prepareAppLanguages(final String keyListPreference) { assert languageCode != null; if (languageCode.equals("")) { - selectedLanguages.put(0, Locale.getDefault().getLanguage()); + selectedLanguages.put(0, AppCompatDelegate.getApplicationLocales().toLanguageTags()); } else { selectedLanguages.put(0, languageCode); } @@ -300,7 +485,16 @@ private void prepareAppLanguages(final String keyListPreference) { } else { selectedLanguages.put(0, languageCode); } + } else if (keyListPreference.equals("descriptionSecondaryLanguagePref")) { + + assert languageCode != null; + if (languageCode.equals("")) { + selectedLanguages.put(0, Locale.getDefault().getLanguage()); + + } else { + selectedLanguages.put(0, languageCode); } + } LanguagesAdapter languagesAdapter = new LanguagesAdapter( getActivity(), @@ -309,6 +503,7 @@ private void prepareAppLanguages(final String keyListPreference) { Dialog dialog = new Dialog(getActivity()); dialog.setContentView(R.layout.dialog_select_language); + dialog.setCanceledOnTouchOutside(true); dialog.getWindow().setLayout((int)(getActivity().getResources().getDisplayMetrics().widthPixels*0.90), (int)(getActivity().getResources().getDisplayMetrics().heightPixels*0.90)); @@ -360,15 +555,12 @@ public void onItemClick(AdapterView adapterView, View view, int i, recentLanguagesDao.deleteRecentLanguage(languageCode); } recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); - saveLanguageValue(languageCode, keyListPreference); Locale defLocale = createLocale(languageCode); if(keyListPreference.equals("appUiDefaultLanguagePref")) { appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - setLocale(requireActivity(), languageCode); - getActivity().recreate(); - final Intent intent = new Intent(getActivity(), MainActivity.class); - startActivity(intent); - }else { + LocaleListCompat appLocale = LocaleListCompat.forLanguageTags(languageCode); + AppCompatDelegate.setApplicationLocales(appLocale); + }else if(keyListPreference.equals("descriptionDefaultLanguagePref")){ descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); } dialog.dismiss(); @@ -429,10 +621,8 @@ private void onRecentLanguageClicked(String keyListPreference, Dialog dialog, Ad final Locale defLocale = createLocale(recentLanguageCode); if (keyListPreference.equals("appUiDefaultLanguagePref")) { appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - setLocale(requireActivity(), recentLanguageCode); - getActivity().recreate(); - final Intent intent = new Intent(getActivity(), MainActivity.class); - startActivity(intent); + LocaleListCompat appLocale = LocaleListCompat.forLanguageTags(recentLanguageCode); + AppCompatDelegate.setApplicationLocales(appLocale); } else { descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); } @@ -448,13 +638,43 @@ private void hideRecentLanguagesSection() { separator.setVisibility(View.GONE); } + private String reSerialise(ArrayList languageCodes) { + // Join the elements of the list into a single string, separated by a comma and a space + return String.join(", ", languageCodes); + } + /** * Changing the default app language with selected one and save it to SharedPreferences */ public void setLocale(final Activity activity, String userSelectedValue) { - if (userSelectedValue.equals("")) { - userSelectedValue = Locale.getDefault().getLanguage(); - } +// if (userSelectedValue.equals("")) { +// userSelectedValue = Locale.getDefault().getLanguage(); +// } +// +// String current = Locale.getDefault().getLanguage(); +// ArrayList languageCodes = deSerialise(current); +// if(appUI) { +// languageCodes.set(0, userSelectedValue); +// userSelectedValue = reSerialise(languageCodes); +// } +// else{ +// ArrayList newLanguageCodes = new ArrayList<>(); +// ArrayList userSelctedCode = deSerialise(userSelectedValue); +// +// newLanguageCodes.add(languageCodes.get(0)); +// for(String code : userSelctedCode){ +// newLanguageCodes.add(code); +// } +// userSelectedValue = reSerialise(newLanguageCodes); +// } +// +// System.out.println("Final locale"); +// System.out.println(userSelectedValue); +// +// System.out.println("vs"); +// System.out.println(getCurrentLanguageCode("appUiDefaultLanguagePref")); +// System.out.println(getCurrentLanguageCode("descriptionSecondaryLanguagePref")); + final Locale locale = createLocale(userSelectedValue); Locale.setDefault(locale); final Configuration configuration = new Configuration(); @@ -496,6 +716,8 @@ private void saveLanguageValue(final String userSelectedValue, final String pref defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue); } else if (preferenceKey.equals("descriptionDefaultLanguagePref")) { defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue); + } else if (preferenceKey.equals("descriptionSecondaryLanguagePref")) { + defaultKvStore.putString(Prefs.SECONDARY_LANGUAGE, userSelectedValue); } } @@ -511,6 +733,9 @@ private String getCurrentLanguageCode(final String preferenceKey) { if (preferenceKey.equals("descriptionDefaultLanguagePref")) { return defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""); } + if (preferenceKey.equals("descriptionSecondaryLanguagePref")) { + return defaultKvStore.getString(Prefs.SECONDARY_LANGUAGE, ""); + } return null; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt index 2847fa0c0b..ebbe8452e7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt @@ -51,7 +51,8 @@ class LanguagesAdapter constructor( override fun isEnabled(position: Int) = languageCodesList[position].let { - it.isNotEmpty() && !selectedLanguages.containsValue(it) && it != selectedLangCode +// it.isNotEmpty() && !it.contains(selectedLanguages.values.first()) + true } override fun getCount() = languageNamesList.size diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt index f1e4917a0d..d20129475c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/BaseDelegateAdapter.kt @@ -10,16 +10,14 @@ abstract class BaseDelegateAdapter( areContentsTheSame: (T, T) -> Boolean = { old, new -> old == new }, ) : AsyncListDifferDelegationAdapter( object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: T, - newItem: T, - ) = areItemsTheSame(oldItem, newItem) + override fun areItemsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { + return areItemsTheSame(oldItem, newItem) + } - override fun areContentsTheSame( - oldItem: T, - newItem: T, - ) = areContentsTheSame(oldItem, newItem) - }, + override fun areContentsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { + return areContentsTheSame(oldItem, newItem) + } + }, *delegates, ) { fun addAll(newResults: List) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt index 6844003014..c20d65abf6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt @@ -22,16 +22,16 @@ abstract class DepictsDao { private val maxItemsAllowed = 10 @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun insert(depictedItem: Depicts) + abstract fun insert(depictedItem: Depicts) @Query("Select * From depicts_table order by lastUsed DESC") - abstract suspend fun getAllDepicts(): List + abstract fun getAllDepicts(): List @Query("Select * From depicts_table order by lastUsed DESC LIMIT :n OFFSET 10") - abstract suspend fun getDepictsForDeletion(n: Int): List + abstract fun getDepictsForDeletion(n: Int): List @Delete - abstract suspend fun delete(depicts: Depicts) + abstract fun delete(depicts: Depicts) /** * Gets all Depicts objects from the database, ordered by lastUsed in descending order. diff --git a/app/src/main/res/layout/dialog_select_secondary_language.xml b/app/src/main/res/layout/dialog_select_secondary_language.xml new file mode 100644 index 0000000000..06de2b01f9 --- /dev/null +++ b/app/src/main/res/layout/dialog_select_secondary_language.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000000..d5a3ddc92a --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index c098533734..7c105feab3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -25,6 +25,17 @@ @string/exif_tag_name_serialNumbers @string/exif_tag_name_software + + + @string/exif_tag_name_author + @string/exif_tag_name_copyright + @string/exif_tag_name_location + @string/exif_tag_name_cameraModel + @string/exif_tag_name_lensModel + @string/exif_tag_name_serialNumbers + @string/exif_tag_name_software + + @string/exif_tag_author @string/exif_tag_copyright diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c6be31292..9cf667e460 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -544,6 +544,7 @@ Upload your first media by tapping on the add button. Why should %1$s be deleted? %1$s is uploaded by: %2$s Default description language + Secondary Description Language Nominating for deletion Success Nominated %1$s for deletion. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 8ac890545d..aeca0d45c9 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -25,12 +25,20 @@ app:singleLineTitle="false" android:title="@string/app_ui_language" /> + + + +