summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNorbel Ambanumben <nambanumben@riseup.net>2024-10-07 21:31:33 +0000
committercyberta <cyberta@riseup.net>2024-10-07 21:31:33 +0000
commit61106968cbdf96a4fa578d16aff03865b985e547 (patch)
tree8ff5d3ea1f2b562e885f4c7e3297ec3bda804fd3
parente15787e844ffe1c74b0ba783a9e37c7daa72b72a (diff)
feat: add language selection
-rw-r--r--app/src/main/AndroidManifest.xml12
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java25
-rw-r--r--app/src/main/res/drawable/translate.xml5
-rw-r--r--app/src/main/res/layout/f_drawer_main.xml8
-rw-r--r--app/src/main/res/layout/f_language_selection.xml17
-rw-r--r--app/src/main/res/layout/v_select_text_list_item.xml3
-rw-r--r--app/src/main/res/resources.properties1
-rw-r--r--app/src/main/res/values/strings.xml1
-rw-r--r--app/src/main/res/values/untranslatable.xml43
-rw-r--r--app/src/main/res/xml/locales_config.xml44
11 files changed, 322 insertions, 3 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8cf2e6f4..cc876ecc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -31,7 +31,8 @@
android:extractNativeLibs="true"
android:appCategory="productivity"
android:logo="@mipmap/ic_launcher"
- android:theme="@style/BitmaskTheme">
+ android:theme="@style/BitmaskTheme"
+ android:localeConfig="@xml/locales_config">
<activity
android:name=".providersetup.activities.SetupActivity"
android:launchMode="singleInstance"
@@ -121,6 +122,15 @@
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
</service>
+ <service
+ android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+ android:enabled="false"
+ android:exported="false">
+ <meta-data
+ android:name="autoStoreLocales"
+ android:value="true" />
+ </service>
+
</application>
</manifest> \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java
new file mode 100644
index 00000000..263a8d46
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java
@@ -0,0 +1,166 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.core.os.LocaleListCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.views.SimpleCheckBox;
+import se.leap.bitmaskclient.databinding.FLanguageSelectionBinding;
+import se.leap.bitmaskclient.databinding.VSelectTextListItemBinding;
+
+public class LanguageSelectionFragment extends BottomSheetDialogFragment {
+ static final String TAG = LanguageSelectionFragment.class.getSimpleName();
+ static final String SYSTEM_LOCALE = "systemLocale";
+ private FLanguageSelectionBinding binding;
+
+ public static LanguageSelectionFragment newInstance(Locale defaultLocale) {
+ LanguageSelectionFragment fragment = new LanguageSelectionFragment();
+ Bundle args = new Bundle();
+ args.putString(SYSTEM_LOCALE, defaultLocale.toLanguageTag());
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ binding = FLanguageSelectionBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setActionBarSubtitle(this, R.string.select_language);
+
+ initRecyclerView(Arrays.asList(getResources().getStringArray(R.array.supported_languages)));
+ }
+
+ private static void customizeSelectionItem(VSelectTextListItemBinding binding) {
+ binding.title.setVisibility(View.GONE);
+ binding.bridgeImage.setVisibility(View.GONE);
+ binding.quality.setVisibility(View.GONE);
+ }
+
+ private void initRecyclerView(List<String> supportedLanguages) {
+ Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ if (defaultLocale == null) {
+ defaultLocale = LocaleListCompat.getDefault().get(0);
+ }
+ // NOTE: Sort the supported languages by their display names.
+ // This would make updating supported languages easier as we don't have to tip toe around the order
+ Collections.sort(supportedLanguages, (lang1, lang2) -> {
+ String displayName1 = Locale.forLanguageTag(lang1).getDisplayName(Locale.ENGLISH);
+ String displayName2 = Locale.forLanguageTag(lang2).getDisplayName(Locale.ENGLISH);
+ return displayName1.compareTo(displayName2);
+ });
+ binding.languages.setAdapter(
+ new LanguageSelectionAdapter(supportedLanguages, this::updateLocale, defaultLocale)
+ );
+ binding.languages.setLayoutManager(new LinearLayoutManager(getContext()));
+ }
+
+ public static Locale getCurrentLocale() {
+ Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ if (defaultLocale == null) {
+ defaultLocale = LocaleListCompat.getDefault().get(0);
+ }
+ return defaultLocale;
+ }
+
+ /**
+ * Update the locale of the application
+ *
+ * @param languageTag the language tag to set the locale to
+ */
+ private void updateLocale(String languageTag) {
+ if (languageTag.isEmpty()) {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
+ } else {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag));
+ }
+ }
+
+ /**
+ * Adapter for the language selection recycler view.
+ */
+ static class LanguageSelectionAdapter extends RecyclerView.Adapter<LanguageSelectionAdapter.LanguageViewHolder> {
+
+ private final List<String> languages;
+ private final LanguageClickListener clickListener;
+ private final Locale selectedLocale;
+
+ public LanguageSelectionAdapter(List<String> languages, LanguageClickListener clickListener, Locale defaultLocale) {
+ this.languages = languages;
+ this.clickListener = clickListener;
+ this.selectedLocale = defaultLocale;
+ }
+
+ @NonNull
+ @Override
+ public LanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ VSelectTextListItemBinding binding = VSelectTextListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new LanguageViewHolder(binding);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull LanguageViewHolder holder, int position) {
+ String languageTag = languages.get(position);
+ holder.languageName.setText(Locale.forLanguageTag(languageTag).getDisplayName(Locale.ENGLISH));
+ if (languages.contains(selectedLocale.toLanguageTag())) {
+ holder.selected.setChecked(selectedLocale.toLanguageTag().equals(languageTag));
+ } else {
+ holder.selected.setChecked(selectedLocale.getLanguage().equals(languageTag));
+ }
+ holder.itemView.setOnClickListener(v -> clickListener.onLanguageClick(languageTag));
+ }
+
+ @Override
+ public int getItemCount() {
+ return languages.size();
+ }
+
+ /**
+ * View holder for the language item
+ */
+ static class LanguageViewHolder extends RecyclerView.ViewHolder {
+ TextView languageName;
+ SimpleCheckBox selected;
+
+ public LanguageViewHolder(@NonNull VSelectTextListItemBinding binding) {
+ super(binding.getRoot());
+ languageName = binding.location;
+ selected = binding.selected;
+ customizeSelectionItem(binding);
+ }
+ }
+ }
+
+
+ /**
+ * Interface for the language click listener
+ */
+ interface LanguageClickListener {
+ void onLanguageClick(String languageTag);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
index cbab1d32..41e102bb 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
@@ -46,15 +46,19 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
+import androidx.core.os.LocaleListCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
+import java.util.Locale;
+import java.util.Map;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
@@ -211,6 +215,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
initSwitchProviderEntry();
initSaveBatteryEntry();
initManualGatewayEntry();
+ initLanguageSettingsEntry();
initAdvancedSettingsEntry();
initDonateEntry();
initLogEntry();
@@ -314,6 +319,26 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
});
}
+
+ private void initLanguageSettingsEntry() {
+ IconTextEntry languageSwitcher = drawerView.findViewById(R.id.language_switcher);
+
+ Locale currentLocale = LanguageSelectionFragment.getCurrentLocale();
+ languageSwitcher.setSubtitle(currentLocale.getDisplayName(Locale.ENGLISH));
+
+ languageSwitcher.setOnClickListener(v -> {
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ closeDrawer();
+ Fragment current = fragmentManager.findFragmentByTag(MainActivity.TAG);
+ if (current instanceof LanguageSelectionFragment) {
+ return;
+ }
+ Fragment fragment = LanguageSelectionFragment.newInstance(Locale.getDefault());
+ setDrawerToggleColor(drawerView.getContext(), ContextCompat.getColor(drawerView.getContext(), R.color.colorActionBarTitleFont));
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
private void initDonateEntry() {
if (ENABLE_DONATION) {
IconTextEntry donate = drawerView.findViewById(R.id.donate);
diff --git a/app/src/main/res/drawable/translate.xml b/app/src/main/res/drawable/translate.xml
new file mode 100644
index 00000000..7826980c
--- /dev/null
+++ b/app/src/main/res/drawable/translate.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#828282" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
+
+</vector>
diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml
index a948d9ce..ca0406ad 100644
--- a/app/src/main/res/layout/f_drawer_main.xml
+++ b/app/src/main/res/layout/f_drawer_main.xml
@@ -80,6 +80,14 @@
/>
<se.leap.bitmaskclient.base.views.IconTextEntry
+ android:id="@+id/language_switcher"
+ app:text="@string/select_language"
+ app:icon="@drawable/translate"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:visibility="visible"/>
+
+ <se.leap.bitmaskclient.base.views.IconTextEntry
android:id="@+id/advancedSettings"
app:icon="@drawable/ic_cog"
android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/f_language_selection.xml b/app/src/main/res/layout/f_language_selection.xml
new file mode 100644
index 00000000..0569c7c1
--- /dev/null
+++ b/app/src/main/res/layout/f_language_selection.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_margin="@dimen/stdpadding"
+ tools:context=".base.fragments.LanguageSelectionFragment">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/languages"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/v_select_text_list_item.xml b/app/src/main/res/layout/v_select_text_list_item.xml
index 47a1f4ad..44e82906 100644
--- a/app/src/main/res/layout/v_select_text_list_item.xml
+++ b/app/src/main/res/layout/v_select_text_list_item.xml
@@ -4,8 +4,7 @@
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
- android:background="?attr/selectableItemBackground"
- tools:viewBindingIgnore="true">
+ android:background="?attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties
new file mode 100644
index 00000000..63b46f93
--- /dev/null
+++ b/app/src/main/res/resources.properties
@@ -0,0 +1 @@
+unqualifiedResLocale=en \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index edbe56bb..b90c4e0f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -238,4 +238,5 @@
<string name="setup_success_description">Click the button below to connect</string>
<string name="permission_rejected">Permission request rejected.</string>
<string name="login_not_supported">The current app version doesn\'t support logins, which you need to update your VPN certificate for this provider.</string>
+ <string name="select_language">Select Language</string>
</resources>
diff --git a/app/src/main/res/values/untranslatable.xml b/app/src/main/res/values/untranslatable.xml
index 80cbf7f3..f326e756 100644
--- a/app/src/main/res/values/untranslatable.xml
+++ b/app/src/main/res/values/untranslatable.xml
@@ -49,4 +49,47 @@
<!-- gateway selector, move to strings.xml, once the wording is clear -->
<string name="no_location" translatable="false">---</string>
+ <string-array name="supported_languages" translatable="false">
+ <item>ar</item>
+ <item>az</item>
+ <item>bg</item>
+ <item>bn</item>
+ <item>br</item>
+ <item>ca</item>
+ <item>cs</item>
+ <item>de</item>
+ <item>el</item>
+ <item>en</item>
+ <item>es</item>
+ <item>es-AR</item>
+ <item>es-CU</item>
+ <item>et</item>
+ <item>eu</item>
+ <item>fa-IR</item>
+ <item>fi</item>
+ <item>fr</item>
+ <item>gl</item>
+ <item>he</item>
+ <item>hr</item>
+ <item>hu</item>
+ <item>id</item>
+ <item>it</item>
+ <item>ja</item>
+ <item>lt</item>
+ <item>my</item>
+ <item>nl</item>
+ <item>no</item>
+ <item>pl</item>
+ <item>pt</item>
+ <item>pt-BR</item>
+ <item>pt-PT</item>
+ <item>ro</item>
+ <item>ru</item>
+ <item>tr</item>
+ <item>ug</item>
+ <item>uk</item>
+ <item>vi</item>
+ <item>zh</item>
+ <item>zh-TW</item>
+ </string-array>
</resources>
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
new file mode 100644
index 00000000..9da2a215
--- /dev/null
+++ b/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+ <locale android:name="ar"/>
+ <locale android:name="az"/>
+ <locale android:name="bg"/>
+ <locale android:name="bn"/>
+ <locale android:name="br"/>
+ <locale android:name="ca"/>
+ <locale android:name="cs"/>
+ <locale android:name="de"/>
+ <locale android:name="el"/>
+ <locale android:name="en"/>
+ <locale android:name="es"/>
+ <locale android:name="es-AR"/>
+ <locale android:name="es-CU"/>
+ <locale android:name="et"/>
+ <locale android:name="eu"/>
+ <locale android:name="fa-IR"/>
+ <locale android:name="fi"/>
+ <locale android:name="fr"/>
+ <locale android:name="gl"/>
+ <locale android:name="he"/>
+ <locale android:name="hr"/>
+ <locale android:name="hu"/>
+ <locale android:name="id"/>
+ <locale android:name="it"/>
+ <locale android:name="ja"/>
+ <locale android:name="lt"/>
+ <locale android:name="my"/>
+ <locale android:name="nl"/>
+ <locale android:name="no"/>
+ <locale android:name="pl"/>
+ <locale android:name="pt"/>
+ <locale android:name="pt-BR"/>
+ <locale android:name="pt-PT"/>
+ <locale android:name="ro"/>
+ <locale android:name="ru"/>
+ <locale android:name="tr"/>
+ <locale android:name="ug"/>
+ <locale android:name="uk"/>
+ <locale android:name="vi"/>
+ <locale android:name="zh"/>
+ <locale android:name="zh-TW"/>
+</locale-config> \ No newline at end of file