diff --git a/LEGALS.md b/LEGALS.md new file mode 100644 index 0000000..1cb249f --- /dev/null +++ b/LEGALS.md @@ -0,0 +1,15 @@ +# LEGALS + +## Fox's Magisk Module Manager +- Maintained: Yes + - Maintainers + - [Fox2Code](https://github.com/Fox2Code) + - [androidacybot](https://github.com/androidacybot) +- License: [ LGPL-3.0 license](https://github.com/Fox2Code/FoxMagiskModuleManager/blob/master/LICENCE) + +## Rosetta ([Fork](https://github.com/iamjazzar/rosetta)) +- Maintained: No + - Maintainers + - [iamjazzar](https://github.com/iamjazzar) + - Others unknown +- License: [ MIT license](https://github.com/iamjazzar/rosetta/blob/master/LICENSE) diff --git a/app/build.gradle b/app/build.gradle index e70951f..91cb9fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -71,7 +71,10 @@ android { } aboutLibraries { - additionalLicenses = ["LGPL_3_0_only"] + additionalLicenses = [ + "LGPL_3_0_only", + "rosetta" + ] } configurations { @@ -88,7 +91,7 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.webkit:webkit:1.4.0' - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.6.0' implementation "com.mikepenz:aboutlibraries:${latestAboutLibsRelease}" implementation "dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0" implementation "dev.rikka.rikkax.insets:insets:1.2.0" @@ -98,6 +101,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3' implementation 'com.squareup.okhttp3:okhttp-brotli:4.9.3' implementation 'com.github.topjohnwu.libsu:io:5.0.1' + implementation project(":rosetta") // Markdown implementation "io.noties.markwon:core:4.6.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 01792e6..0f0ec7d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,7 +32,8 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:networkSecurityConfig="@xml/network_security_config" android:usesCleartextTraffic="false" - tools:targetApi="s"> + tools:targetApi="s" + tools:replace="android:supportsRtl"> @@ -62,7 +63,9 @@ android:name=".installer.InstallerActivity" android:parentActivityName=".MainActivity" android:exported="false" - android:launchMode="singleTop"> + android:screenOrientation="portrait" + android:launchMode="singleTop" + tools:ignore="LockedOrientationActivity"> diff --git a/app/src/main/java/com/fox2code/mmm/Constants.java b/app/src/main/java/com/fox2code/mmm/Constants.java index 18c21a0..d1d1dff 100644 --- a/app/src/main/java/com/fox2code/mmm/Constants.java +++ b/app/src/main/java/com/fox2code/mmm/Constants.java @@ -23,6 +23,11 @@ public class Constants { public static final String EXTRA_MARKDOWN_URL = "extra_markdown_url"; public static final String EXTRA_MARKDOWN_TITLE = "extra_markdown_title"; public static final String EXTRA_MARKDOWN_CONFIG = "extra_markdown_config"; + public static final String EXTRA_MARKDOWN_CHANGE_BOOT = "extra_markdown_change_boot"; + public static final String EXTRA_MARKDOWN_NEEDS_RAMDISK = "extra_markdown_needs_ramdisk"; + public static final String EXTRA_MARKDOWN_MIN_MAGISK = "extra_markdown_min_magisk"; + public static final String EXTRA_MARKDOWN_MIN_API= "extra_markdown_min_api"; + public static final String EXTRA_MARKDOWN_MAX_API = "extra_markdown_max_api"; public static final String EXTRA_FADE_OUT = "extra_fade_out"; public static final String EXTRA_FROM_MANAGER = "extra_from_manager"; } diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index ac98aca..5ef3082 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -32,6 +32,8 @@ import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.settings.SettingsActivity; import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.progressindicator.LinearProgressIndicator; import eightbitlab.com.blurview.BlurView; @@ -68,10 +70,10 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O protected void onCreate(Bundle savedInstanceState) { this.initMode = true; super.onCreate(savedInstanceState); - this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> { + this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> { IntentHelper.startActivity(this, SettingsActivity.class); - return true; - }, R.string.pref_category_settings); + return true; + }, R.string.pref_category_settings); setContentView(R.layout.activity_main); this.setTitle(R.string.app_name); this.getWindow().setFlags( @@ -106,10 +108,10 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O this.actionBarBlur.setBackground(this.actionBarBackground); this.actionBarBlur.setupWith(this.moduleList).setFrameClearDrawable( this.getWindow().getDecorView().getBackground()) - .setBlurAlgorithm(new RenderScriptBlur(this)) - .setBlurRadius(4F).setBlurAutoUpdate(true) - .setHasFixedTransformationMatrix(true); - this.updateBlurState(); + .setBlurAlgorithm(new RenderScriptBlur(this)) + .setBlurRadius(4F).setBlurAutoUpdate(true) + .setHasFixedTransformationMatrix(true); + this.updateBlurState(); this.moduleList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { @@ -255,7 +257,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O bottomInset + this.searchCard.getHeight()); this.searchCard.setRadius(this.searchCard.getHeight() / 2F); this.moduleViewListBuilder.updateInsets(); - this.actionBarBlur.invalidate(); + //this.actionBarBlur.invalidate(); this.overScrollInsetTop = combinedBarsHeight; this.overScrollInsetBottom = bottomInset; Log.d(TAG, "( " + bottomInset + ", " + diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 059fefe..1e0d745 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -23,6 +23,7 @@ import com.fox2code.mmm.compat.CompatThemeWrapper; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.utils.GMSProviderInstaller; import com.fox2code.mmm.utils.Http; +import com.google.android.material.color.DynamicColors; import com.topjohnwu.superuser.Shell; import java.text.SimpleDateFormat; @@ -115,11 +116,20 @@ public class MainApplication extends CompatApplication { return getSharedPreferences().getBoolean("pref_dns_over_https", true); } + public static boolean isMonetEnabled() { + return getSharedPreferences().getBoolean("pref_enable_monet", false); + } + public static boolean isBlurEnabled() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getSharedPreferences().getBoolean("pref_enable_blur", false); } + public static boolean isChipsDisabled() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + getSharedPreferences().getBoolean("pref_disable_chips", false); + } + public static boolean isDeveloper() { return BuildConfig.DEBUG || getSharedPreferences().getBoolean("developer", false); @@ -275,6 +285,11 @@ public class MainApplication extends CompatApplication { @Override public void onCreate() { super.onCreate(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (MainApplication.isMonetEnabled()) { + DynamicColors.applyToActivitiesIfAvailable(this); + } + } SharedPreferences sharedPreferences = MainApplication.getSharedPreferences(); // We are only one process so it's ok to do this SharedPreferences bootPrefs = MainApplication.bootSharedPreferences = diff --git a/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java b/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java index 9a944d9..d84eb76 100644 --- a/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java +++ b/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java @@ -5,6 +5,7 @@ import android.app.Application; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -36,6 +37,7 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; import com.fox2code.mmm.Constants; import com.fox2code.mmm.R; @@ -55,12 +57,12 @@ public class CompatActivity extends AppCompatActivity { private static final String TAG = "CompatActivity"; public static final CompatActivity.OnBackPressedCallback DISABLE_BACK_BUTTON = new CompatActivity.OnBackPressedCallback() { - @Override - public boolean onBackPressed(CompatActivity compatActivity) { - compatActivity.setOnBackPressedCallback(this); - return true; - } - }; + @Override + public boolean onBackPressed(CompatActivity compatActivity) { + compatActivity.setOnBackPressedCallback(this); + return true; + } + }; final WeakReference selfReference; private final CompatConfigHelper compatConfigHelper = new CompatConfigHelper(this); @@ -68,7 +70,8 @@ public class CompatActivity extends AppCompatActivity { private CompatActivity.OnBackPressedCallback onBackPressedCallback; private MenuItem.OnMenuItemClickListener menuClickListener; private CharSequence menuContentDescription; - @StyleRes private int setThemeDynamic = 0; + @StyleRes + private int setThemeDynamic = 0; private boolean onCreateCalledOnce = false; private boolean onCreateCalled = false; private boolean isRefreshUi = false; @@ -221,7 +224,8 @@ public class CompatActivity extends AppCompatActivity { } } - @Dimension @Px + @Dimension + @Px public int getActionBarHeight() { androidx.appcompat.app.ActionBar compatActionBar; try { @@ -247,6 +251,15 @@ public class CompatActivity extends AppCompatActivity { } } + public int getActionBarHeight(Activity activity) { + TypedValue tv = new TypedValue(); + int actionBarHeight = 0; + if (activity.getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) { + actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics()); + } + return actionBarHeight; + } + public void setActionBarBackground(Drawable drawable) { androidx.appcompat.app.ActionBar compatActionBar; try { @@ -264,7 +277,8 @@ public class CompatActivity extends AppCompatActivity { } } - @Dimension @Px + @Dimension + @Px public int getStatusBarHeight() { int height = WindowInsetsCompat.CONSUMED.getInsets( WindowInsetsCompat.Type.statusBars()).top; @@ -476,7 +490,7 @@ public class CompatActivity extends AppCompatActivity { this.checkResourcesOverrides(forceEnglish, nightModeOverride); } - private void checkResourcesOverrides(boolean forceEnglish,Boolean nightModeOverride) { + private void checkResourcesOverrides(boolean forceEnglish, Boolean nightModeOverride) { if (this.isRefreshUi || !this.onCreateCalled) return; // Wait before reload this.compatConfigHelper.checkResourcesOverrides(forceEnglish, nightModeOverride); } @@ -534,6 +548,14 @@ public class CompatActivity extends AppCompatActivity { return (CompatActivity) context; } + @Override + protected void attachBaseContext(Context newBase) { + SharedPreferences mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(newBase); + Locale locale = new Locale(mSharedPreferences.getString("user_preferred_country", "en")); + Context context = CompatWrapper.setLocale(newBase, locale); + super.attachBaseContext(context); + } + public WeakReference asWeakReference() { return this.selfReference; } diff --git a/app/src/main/java/com/fox2code/mmm/compat/CompatWrapper.java b/app/src/main/java/com/fox2code/mmm/compat/CompatWrapper.java new file mode 100644 index 0000000..04b1b85 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/compat/CompatWrapper.java @@ -0,0 +1,40 @@ +package com.fox2code.mmm.compat; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.os.LocaleList; + +import java.util.Locale; + +public class CompatWrapper extends android.content.ContextWrapper { + + public CompatWrapper(Context base) { + super(base); + } + + public static ContextWrapper setLocale(Context context, Locale newLocale) { + + Resources res = context.getResources(); + Configuration configuration = res.getConfiguration(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + configuration.setLocale(newLocale); + + LocaleList localeList = new LocaleList(newLocale); + LocaleList.setDefault(localeList); + configuration.setLocales(localeList); + + context = context.createConfigurationContext(configuration); + + } else { + configuration.setLocale(newLocale); + context = context.createConfigurationContext(configuration); + + } + + return new ContextWrapper(context); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java index 2adc362..ad289c4 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java @@ -1,30 +1,52 @@ package com.fox2code.mmm.markdown; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.graphics.ColorUtils; import com.fox2code.mmm.Constants; import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.R; import com.fox2code.mmm.XHooks; import com.fox2code.mmm.compat.CompatActivity; +import com.fox2code.mmm.compat.CompatDisplay; import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import eightbitlab.com.blurview.BlurView; +import eightbitlab.com.blurview.RenderScriptBlur; + public class MarkdownActivity extends CompatActivity { private static final String TAG = "MarkdownActivity"; @@ -32,6 +54,12 @@ public class MarkdownActivity extends CompatActivity { private static final String[] variants = new String[]{ "readme.md", "README.MD", ".github/README.md" }; + private BlurView chipHolder; + private TextView actionBarPadding; + private BlurView actionBarBlur; + private ColorDrawable anyBarBackground; + private ScrollView scrollView; + private LinearLayout md_layout; private static byte[] getRawMarkdown(String url) throws IOException { String newUrl = redirects.get(url); @@ -51,7 +79,8 @@ public class MarkdownActivity extends CompatActivity { byte[] rawMarkdown = Http.doHttpGet(prefix + suffix, true); redirects.put(url, newUrl); // Avoid retries return rawMarkdown; - } catch (IOException ignored) {} + } catch (IOException ignored) { + } } } throw e; @@ -74,6 +103,16 @@ public class MarkdownActivity extends CompatActivity { .getString(Constants.EXTRA_MARKDOWN_TITLE); String config = intent.getExtras() .getString(Constants.EXTRA_MARKDOWN_CONFIG); + boolean change_boot = intent.getExtras() + .getBoolean(Constants.EXTRA_MARKDOWN_CHANGE_BOOT); + boolean needs_ramdisk = intent.getExtras() + .getBoolean(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK); + int min_magisk = intent.getExtras() + .getInt(Constants.EXTRA_MARKDOWN_MIN_MAGISK); + int min_api = intent.getExtras() + .getInt(Constants.EXTRA_MARKDOWN_MIN_API); + int max_api = intent.getExtras() + .getInt(Constants.EXTRA_MARKDOWN_MAX_API); if (title != null && !title.isEmpty()) { this.setTitle(title); } @@ -98,9 +137,41 @@ public class MarkdownActivity extends CompatActivity { setContentView(R.layout.markdown_view); final ViewGroup markdownBackground = findViewById(R.id.markdownBackground); final TextView textView = findViewById(R.id.markdownView); + this.chipHolder = findViewById(R.id.chip_holder); + this.anyBarBackground = new ColorDrawable(Color.TRANSPARENT); + this.actionBarPadding = findViewById(R.id.markdown_action_bar_padding); + this.actionBarBlur = findViewById(R.id.markdown_action_bar_blur); + this.scrollView = findViewById(R.id.scrollView2); + this.md_layout = findViewById(R.id.md_layout); final TextView footer = findViewById(R.id.markdownFooter); UiThreadHandler.handler.postDelayed(() -> // Fix footer height footer.setMinHeight(this.getNavigationBarHeight()), 1L); + this.actionBarBlur.setBackground(this.anyBarBackground); + this.setupBlurView(this.chipHolder, markdownBackground, this.anyBarBackground); + this.setupBlurView(this.actionBarBlur, markdownBackground, this.anyBarBackground); + this.updateScreenInsets(); + this.updateUI(); + + // Really bad created (MSG by Der_Googler) + if (MainApplication.isChipsDisabled()) { + this.chipHolder.setVisibility(View.GONE); + } else { + this.chipHolder.setPadding(0,0,0,this.getNavigationBarHeight()); + // set "message" to null to disable dialog + this.setChip(change_boot, + getString(R.string.module_can_change_boot), + "This module may change the boot image"); + this.setChip(needs_ramdisk, + getString(R.string.module_needs_ramdisk), + "This module need boot ramdisk to be installed"); + this.setChip(min_magisk, "Min. Magisk \"" + min_magisk + "\"", + null); + this.setChip(min_api, "Min. Android " + min_api, + null); + this.setChip(max_api, "Max. Android " + max_api, + null); + } + new Thread(() -> { try { Log.d(TAG, "Downloading"); @@ -129,6 +200,122 @@ public class MarkdownActivity extends CompatActivity { }, "Markdown load thread").start(); } + private void setupBlurView(BlurView view, ViewGroup setupWith, ColorDrawable background) { + view.setBackground(background); + view.setupWith(setupWith).setFrameClearDrawable( + this.getWindow().getDecorView().getBackground()) + .setBlurAlgorithm(new RenderScriptBlur(this)) + .setBlurRadius(4F).setBlurAutoUpdate(true) + .setHasFixedTransformationMatrix(true); + } + + private void updateScreenInsets() { + this.runOnUiThread(() -> this.updateScreenInsets( + this.getResources().getConfiguration())); + } + + private void updateScreenInsets(Configuration configuration) { + boolean landscape = configuration.orientation == + Configuration.ORIENTATION_LANDSCAPE; + int bottomInset = (landscape ? 0 : this.getNavigationBarHeight()); + int statusBarHeight = getStatusBarHeight(); + int actionBarHeight = getActionBarHeight(); + int combinedBarsHeight = statusBarHeight + actionBarHeight; + this.actionBarPadding.setMinHeight(combinedBarsHeight); + //this.actionBarBlur.invalidate(); + } + + private void updateUI() { + boolean isLightMode = this.isLightTheme(); + int colorBackground; + try { + colorBackground = this.getColorCompat( + android.R.attr.windowBackground); + } catch (Resources.NotFoundException e) { + colorBackground = this.getColorCompat(isLightMode ? + R.color.white : R.color.black); + } + this.md_layout.setPadding(0,this.getActionBarHeight(this) + this.getStatusBarHeight(),0,this.getNavigationBarHeight() + 56); + if (MainApplication.isBlurEnabled()) { + this.actionBarBlur.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, this.getActionBarHeight(this) + this.getStatusBarHeight())); + this.chipHolder.setBlurEnabled(true); + this.anyBarBackground.setColor(ColorUtils + .setAlphaComponent(colorBackground, 0x02)); + this.anyBarBackground.setColor(Color.TRANSPARENT); + this.actionBarBlur.setBlurEnabled(true); + } else { + this.chipHolder.setBlurEnabled(false); + this.chipHolder.setOverlayColor(Color.TRANSPARENT); + this.anyBarBackground.setColor(colorBackground); + this.actionBarBlur.setBlurEnabled(false); + this.actionBarBlur.setOverlayColor(Color.TRANSPARENT); + } + } + + private void setChip(boolean bool, String title, String message) { + if (bool) { + this.makeChip(title, message); + } + } + + private void setChip(int i, String title, String message) { + if (i != 0) { + this.makeChip(title, message); + } + } + + private void makeChip(String title, String message) { + final ChipGroup chip_group_holder = findViewById(R.id.chip_group_holder); + Chip chip = new Chip(this); + chip.setText(title); + chip.setVisibility(View.VISIBLE); + if (message != null) { + chip.setOnClickListener(_view -> { + MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(this); + + builder + .setTitle(title) + .setMessage(message) + .setCancelable(true) + .setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show(); + + }); + } + chip_group_holder.addView(chip); + } + + private String parseAndroidVersion(int version) { + switch (version) { + case Build.VERSION_CODES.LOLLIPOP: + return "5.0"; + case Build.VERSION_CODES.LOLLIPOP_MR1: + return "5.1"; + case Build.VERSION_CODES.M: + return "6.0"; + case Build.VERSION_CODES.N: + return "7.0"; + case Build.VERSION_CODES.N_MR1: + return "7.1"; + case Build.VERSION_CODES.O: + return "8.0"; + case Build.VERSION_CODES.O_MR1: + return "8.1"; + case Build.VERSION_CODES.P: + return "9.0 (P)"; + case Build.VERSION_CODES.Q: + return "10 (Q)"; + case Build.VERSION_CODES.R: + return "11 (R)"; + case Build.VERSION_CODES.S: + return "12 (S)"; + case Build.VERSION_CODES.S_V2: + return "12L"; + default: + return "false"; + } + } + @Override protected void onResume() { super.onResume(); diff --git a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java index 7f7b215..8399883 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java @@ -1,10 +1,10 @@ package com.fox2code.mmm.module; +import android.annotation.SuppressLint; import android.content.Context; import android.text.Spanned; import android.util.Log; import android.widget.Button; -import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; @@ -20,16 +20,23 @@ import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleManager; -import com.fox2code.mmm.module.ModuleHolder; import com.fox2code.mmm.utils.IntentHelper; +import com.google.android.material.chip.Chip; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import io.noties.markwon.Markwon; +@SuppressLint("UseCompatLoadingForDrawables") public enum ActionButtonType { - INFO(R.drawable.ic_baseline_info_24) { + INFO() { @Override - public void doAction(ImageButton button, ModuleHolder moduleHolder) { + public void update(Chip button, ModuleHolder moduleHolder) { + button.setChipIcon(button.getContext().getResources().getDrawable(R.drawable.ic_baseline_info_24)); + button.setText(R.string.description); + } + + @Override + public void doAction(Chip button, ModuleHolder moduleHolder) { String notesUrl = moduleHolder.repoModule.notesUrl; if (notesUrl.startsWith("https://api.androidacy.com/magisk/readme/?module=") || notesUrl.startsWith("https://www.androidacy.com/")) { @@ -39,36 +46,47 @@ public enum ActionButtonType { } else { IntentHelper.openMarkdown(button.getContext(), notesUrl, moduleHolder.repoModule.moduleInfo.name, - moduleHolder.getMainModuleConfig()); + moduleHolder.getMainModuleConfig(), + moduleHolder.repoModule.moduleInfo.changeBoot, + moduleHolder.repoModule.moduleInfo.needRamdisk, + moduleHolder.repoModule.moduleInfo.minMagisk, + moduleHolder.repoModule.moduleInfo.minApi, + moduleHolder.repoModule.moduleInfo.maxApi + ); } } @Override - public boolean doActionLong(ImageButton button, ModuleHolder moduleHolder) { + public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { Context context = button.getContext(); Toast.makeText(context, context.getString(R.string.module_id_prefix) + - moduleHolder.moduleId, Toast.LENGTH_SHORT).show(); + moduleHolder.moduleId, Toast.LENGTH_SHORT).show(); return true; } }, UPDATE_INSTALL() { @Override - public void update(ImageButton button, ModuleHolder moduleHolder) { + public void update(Chip button, ModuleHolder moduleHolder) { int icon = moduleHolder.hasUpdate() ? R.drawable.ic_baseline_update_24 : R.drawable.ic_baseline_system_update_24; - button.setImageResource(icon); + button.setChipIcon(button.getContext().getResources().getDrawable(icon)); + if (moduleHolder.hasUpdate()) { + button.setText(R.string.update); + } else { + button.setText(R.string.install); + } } @Override - public void doAction(ImageButton button, ModuleHolder moduleHolder) { + public void doAction(Chip button, ModuleHolder moduleHolder) { ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); if (moduleInfo == null) return; String updateZipUrl = moduleHolder.getUpdateZipUrl(); if (updateZipUrl == null) return; // Androidacy manage the selection between download and install if (updateZipUrl.startsWith("https://www.androidacy.com/") || - updateZipUrl.startsWith("https://api.androidacy.com/magisk/info/?module=")) { + updateZipUrl.startsWith("https://api.androidacy.com/magisk/info/?module=")) { IntentHelper.openUrlAndroidacy( button.getContext(), updateZipUrl, true, moduleInfo.name, moduleInfo.config); @@ -125,18 +143,19 @@ public enum ActionButtonType { }, UNINSTALL() { @Override - public void update(ImageButton button, ModuleHolder moduleHolder) { + public void update(Chip button, ModuleHolder moduleHolder) { int icon = moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING) ? R.drawable.ic_baseline_delete_outline_24 : ( !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) || - moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) ? - R.drawable.ic_baseline_delete_24 : - R.drawable.ic_baseline_delete_forever_24; - button.setImageResource(icon); + moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) ? + R.drawable.ic_baseline_delete_24 : + R.drawable.ic_baseline_delete_forever_24; + button.setChipIcon(button.getContext().getResources().getDrawable(icon)); + button.setText(R.string.uninstall); } @Override - public void doAction(ImageButton button, ModuleHolder moduleHolder) { + public void doAction(Chip button, ModuleHolder moduleHolder) { if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UNINSTALLING) && moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) { @@ -151,7 +170,7 @@ public enum ActionButtonType { } @Override - public boolean doActionLong(ImageButton button, ModuleHolder moduleHolder) { + public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { // We can't trust active flag on first boot if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) return false; new AlertDialog.Builder(button.getContext()).setTitle(R.string.master_delete) @@ -163,13 +182,20 @@ public enum ActionButtonType { moduleHolder.moduleInfo = null; CompatActivity.getCompatActivity(button).refreshUI(); } - }).setNegativeButton(R.string.master_delete_no, (v, i) -> {}).create().show(); + }).setNegativeButton(R.string.master_delete_no, (v, i) -> { + }).create().show(); return true; } }, - CONFIG(R.drawable.ic_baseline_app_settings_alt_24) { + CONFIG() { + @Override + public void update(Chip button, ModuleHolder moduleHolder) { + button.setChipIcon(button.getContext().getResources().getDrawable(R.drawable.ic_baseline_app_settings_alt_24)); + button.setText(R.string.config); + } + @Override - public void doAction(ImageButton button, ModuleHolder moduleHolder) { + public void doAction(Chip button, ModuleHolder moduleHolder) { String config = moduleHolder.getMainModuleConfig(); if (config == null) return; if (AndroidacyUtil.isAndroidacyLink(config)) { @@ -181,19 +207,20 @@ public enum ActionButtonType { }, SUPPORT() { @Override - public void update(ImageButton button, ModuleHolder moduleHolder) { + public void update(Chip button, ModuleHolder moduleHolder) { ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); - button.setImageResource(supportIconForUrl(moduleInfo.support)); + button.setChipIcon(button.getContext().getResources().getDrawable(supportIconForUrl(moduleInfo.support))); + button.setText(R.string.support); } @Override - public void doAction(ImageButton button, ModuleHolder moduleHolder) { + public void doAction(Chip button, ModuleHolder moduleHolder) { IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support); } }, DONATE() { @Override - public void update(ImageButton button, ModuleHolder moduleHolder) { + public void update(Chip button, ModuleHolder moduleHolder) { ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); int icon = R.drawable.ic_baseline_monetization_on_24; if (moduleInfo.donate.startsWith("https://www.paypal.me/")) { @@ -201,11 +228,12 @@ public enum ActionButtonType { } else if (moduleInfo.donate.startsWith("https://www.patreon.com/")) { icon = R.drawable.ic_patreon; } - button.setImageResource(icon); + button.setChipIcon(button.getContext().getResources().getDrawable(icon)); + button.setText(R.string.donate); } @Override - public void doAction(ImageButton button, ModuleHolder moduleHolder) { + public void doAction(Chip button, ModuleHolder moduleHolder) { IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate); } }; @@ -237,13 +265,13 @@ public enum ActionButtonType { this.iconId = iconId; } - public void update(ImageButton button, ModuleHolder moduleHolder) { - button.setImageResource(this.iconId); + public void update(Chip button, ModuleHolder moduleHolder) { + button.setChipIcon(button.getContext().getResources().getDrawable(this.iconId)); } - public abstract void doAction(ImageButton button, ModuleHolder moduleHolder); + public abstract void doAction(Chip button, ModuleHolder moduleHolder); - public boolean doActionLong(ImageButton button, ModuleHolder moduleHolder) { + public boolean doActionLong(Chip button, ModuleHolder moduleHolder) { return false; } } diff --git a/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java b/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java index 6331bf2..a5adda9 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java +++ b/app/src/main/java/com/fox2code/mmm/module/ModuleViewAdapter.java @@ -10,6 +10,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; +import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.ColorInt; @@ -25,12 +26,15 @@ import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.repo.RepoModule; +import com.google.android.material.chip.Chip; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.switchmaterial.SwitchMaterial; import com.topjohnwu.superuser.internal.UiThreadHandler; import java.util.ArrayList; import java.util.Objects; + public final class ModuleViewAdapter extends RecyclerView.Adapter { private static final boolean DEBUG = false; public final ArrayList moduleHolders = new ArrayList<>(); @@ -64,13 +68,15 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter actionButtonsTypes; private boolean initState; public ModuleHolder moduleHolder; @@ -80,13 +86,15 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter { + MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(_view.getContext()); + + builder + .setTitle(R.string.low_quality_module) + .setMessage("Actual description for Low-quality module") + .setCancelable(true) + .setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show(); + + }); + this.invalidPropsChip.setVisibility(View.VISIBLE); + // Backup restore + // foregroundAttr = R.attr.colorOnError; + // backgroundAttr = R.attr.colorError; } Resources.Theme theme = this.cardView.getContext().getTheme(); TypedValue value = new TypedValue(); diff --git a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java index 7f3cf14..2a01a0a 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -1,5 +1,6 @@ package com.fox2code.mmm.settings; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.widget.Toast; @@ -19,15 +20,19 @@ import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.OverScrollManager; import com.fox2code.mmm.R; import com.fox2code.mmm.compat.CompatActivity; -import com.fox2code.mmm.compat.CompatThemeWrapper; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.repo.RepoData; import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; + +import com.ahmedjazzar.rosetta.LanguageSwitcher; import com.mikepenz.aboutlibraries.LibsBuilder; import com.topjohnwu.superuser.internal.UiThreadHandler; +import java.util.HashSet; +import java.util.Locale; + public class SettingsActivity extends CompatActivity { private static int devModeStep = 0; @@ -81,25 +86,61 @@ public class SettingsActivity extends CompatActivity { enableBlur.setSummary(R.string.require_android_6); enableBlur.setEnabled(false); } - Preference forceEnglish = findPreference("pref_force_english"); - forceEnglish.setOnPreferenceChangeListener((preference, newValue) -> { - CompatThemeWrapper compatThemeWrapper = - MainApplication.getINSTANCE().getMarkwonThemeContext(); - if (compatThemeWrapper != null) { - compatThemeWrapper.setForceEnglish( - Boolean.parseBoolean(String.valueOf(newValue))); - } - return true; - }); - if (!MainApplication.isDeveloper()) { - forceEnglish.setVisible(false); + + Preference disableMonet = findPreference("pref_enable_monet"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + disableMonet.setSummary(R.string.require_android_12); + disableMonet.setEnabled(false); } + findPreference("pref_dns_over_https").setOnPreferenceChangeListener((p, v) -> { Http.setDoh((Boolean) v); return true; }); - if ("dark".equals(themePreference.getValue())) { - findPreference("pref_force_dark_terminal").setEnabled(false); + + // This is the locale that you wanna your app to launch with. + String firstLaunchLocale = "en"; + + // Warning! Locales that are't exist will crash the app + HashSet supportedLocales = new HashSet<>(); + supportedLocales.add("cs"); + supportedLocales.add("de"); + supportedLocales.add("es-rMX"); + supportedLocales.add("et"); + supportedLocales.add("fr"); + supportedLocales.add("id"); + supportedLocales.add("ja"); + supportedLocales.add("nb-rNO"); + supportedLocales.add("pl"); + supportedLocales.add("pt-rBR"); + supportedLocales.add("ro"); + supportedLocales.add("ru"); + supportedLocales.add("sk"); + supportedLocales.add("tr"); + supportedLocales.add("vi"); + supportedLocales.add("zh-rCH"); + supportedLocales.add("zh-rTW"); + supportedLocales.add(firstLaunchLocale); + + Preference languageSelector = findPreference("pref_language_selector"); + languageSelector.setOnPreferenceClickListener(preference -> { + LanguageSwitcher ls = new LanguageSwitcher(getActivity(), new Locale(firstLaunchLocale)); + ls.showChangeLanguageDialog(getActivity()); + ls.setSupportedStringLocales(supportedLocales); + return true; + }); + + int nightModeFlags = + getContext().getResources().getConfiguration().uiMode & + Configuration.UI_MODE_NIGHT_MASK; + switch (nightModeFlags) { + case Configuration.UI_MODE_NIGHT_YES: + findPreference("pref_force_dark_terminal").setEnabled(false); + break; + case Configuration.UI_MODE_NIGHT_NO: + case Configuration.UI_MODE_NIGHT_UNDEFINED: + findPreference("pref_force_dark_terminal").setEnabled(true); + break; } if (!MainApplication.isDeveloper()) { findPreference("pref_disable_low_quality_module_filter").setVisible(false); @@ -208,7 +249,7 @@ public class SettingsActivity extends CompatActivity { preference.setTitle(R.string.repo_disabled); preference.setEnabled(false); } else { - ((TwoStatePreference)preference).setChecked(repoData.isEnabled()); + ((TwoStatePreference) preference).setChecked(repoData.isEnabled()); preference.setTitle(repoData.isEnabled() ? R.string.repo_enabled : R.string.repo_disabled); preference.setOnPreferenceChangeListener((p, newValue) -> { diff --git a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java index 0026fd1..2a77007 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -165,12 +165,17 @@ public class IntentHelper { } } - public static void openMarkdown(Context context, String url, String title, String config) { + public static void openMarkdown(Context context, String url, String title, String config, Boolean changeBoot, Boolean needsRamdisk,int minMagisk, int minApi, int maxApi) { try { Intent intent = new Intent(context, MarkdownActivity.class); MainApplication.addSecret(intent); intent.putExtra(Constants.EXTRA_MARKDOWN_URL, url); intent.putExtra(Constants.EXTRA_MARKDOWN_TITLE, title); + intent.putExtra(Constants.EXTRA_MARKDOWN_CHANGE_BOOT, changeBoot); + intent.putExtra(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK, needsRamdisk); + intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_MAGISK, minMagisk); + intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_API, minApi); + intent.putExtra(Constants.EXTRA_MARKDOWN_MAX_API, maxApi); if (config != null && !config.isEmpty()) intent.putExtra(Constants.EXTRA_MARKDOWN_CONFIG, config); startActivity(context, intent, true); diff --git a/app/src/main/res/drawable/ic_baseline_chip_24.xml b/app/src/main/res/drawable/ic_baseline_chip_24.xml new file mode 100644 index 0000000..f6f82d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chip_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_design_services_24.xml b/app/src/main/res/drawable/ic_baseline_design_services_24.xml new file mode 100644 index 0000000..872fcba --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_design_services_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_foreground.xml b/app/src/main/res/drawable/ic_foreground.xml new file mode 100644 index 0000000..35f0a68 --- /dev/null +++ b/app/src/main/res/drawable/ic_foreground.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/markdown_view.xml b/app/src/main/res/layout/markdown_view.xml index 81e1391..a0aef44 100644 --- a/app/src/main/res/layout/markdown_view.xml +++ b/app/src/main/res/layout/markdown_view.xml @@ -1,23 +1,36 @@ - + app:fitsSystemWindowsInsets="left|right" + tools:context=".markdown.MarkdownActivity"> + + android:id="@+id/scrollView2" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + - \ No newline at end of file + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/module_entry.xml b/app/src/main/res/layout/module_entry.xml index 8881612..e0a8234 100644 --- a/app/src/main/res/layout/module_entry.xml +++ b/app/src/main/res/layout/module_entry.xml @@ -8,186 +8,190 @@ android:layout_marginRight="8dp" android:layout_marginTop="2dp" android:layout_marginBottom="2dp" - android:gravity="center_vertical"> + android:gravity="center_vertical" + android:orientation="vertical"> + - + + + android:orientation="vertical"> - - - - - - - - - + + android:layout_height="194dp" + android:visibility="gone" + android:scaleType="centerCrop" + tools:ignore="ContentDescription" /> - + android:orientation="vertical" + android:padding="8dp"> + + + + + + + + + + + + + - + + + + + + + + + + tools:ignore="UselessParent"> - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index ca0ddfd..abf424b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,9 +1,5 @@ - - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..f5ff9dd --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e6ce6f6..4e15f94 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -25,28 +25,24 @@ Sperrmodus Sperrmodus verhindert, dass der Manager Aktionen an Modulen ausführt Einstellungen - Info Lizenzen anzeigen Lizenzen Inkompatible Module anzeigen Module anzeigen, die aufgrund ihrer Metadaten nicht mit Ihrem Gerät kompatibel sind Magisk ist veraltet! - Repos Das Repository, das Magisk-Module hostet Eine Alternative zur Magisk-Module-Repo mit weniger Einschränkungen. Löschen der Moduldateien? Dateien behalten Dateien löschen Fehler beim Löschen der Moduldateien - Theme - Module id: - Modul aus dem Speicher installieren + Module aus dem Speicher installieren Das ausgewählte Modul hat ein ungültiges Format Lokale Installation Quellcode Eingebautes Magisk Modul Eingebautes Substratum Modul - Dunkelmodus-Terminal erzwingen + Dunkeles Terminal erzwingen Ihr aktueller Dateimanager konnte keinen Zugriff auf die Datei gewähren. Remote-Installation Ihr Dateimanager hat eine nicht standardmäßige Antwort zurückgegeben. @@ -62,4 +58,49 @@ Einige Module deklarieren ihre Metadaten nicht richtig, was zu visuellen Störungen führt, und/oder auf eine schlechte Modulqualität hinweisen, deaktivieren auf eigene Gefahr! + System WebView konnte nicht geöffnet werden + Keine Beschreibung gefunden + Module herunterladen + Module instalieren + Module aktualisieren + Anderungsprotokoll + Webseite + Hilfe + Spenden + Module einsenden + Erfordert Android 6.0+ + Erfordert Android 12+ + Neustarten + Ja + Nein + Benötigt Ramdisk + Kann Boot ändern + Repos verwalten + Neustart verhindern + Verhindert unerwartete Neustarts + Aktivire Monet + Sicherheit + Ausehen + Allgemein + Theme Modus + DNS über HTTPS + Kann in einigen Fällen Verbindungsprobleme beheben + Deaktivieren Sie die Mmm-Erweiterungen von Fox, um zu verhindern, dass Module Terminalerweiterungen verwenden. Nützlich, wenn ein Modul die Mmm-Erweiterungen von Fox missbraucht + Textumbruch + Zeigen Sie Text in mehreren Zeilen an, anstatt den gesamten Text in derselben Zeile zu platzieren, wenn Sie ein Modul installieren. + Chips in Beschreibung deaktivieren + Repo an + Repo aus + Repo hinzufügen + Repo entfernen + Eigene URL + Das Androidacy-Repo enthält Anzeigen und Tracker. + Backup Modules + Module wiederherstellen + Dieser Vorgang erfordert eine Internetverbindung + Aktualisieren + Installieren + Beschreibung + Deinstallieren + Konfig diff --git a/app/src/main/res/values-v31/colors.xml b/app/src/main/res/values-v31/colors.xml new file mode 100644 index 0000000..04caa4a --- /dev/null +++ b/app/src/main/res/values-v31/colors.xml @@ -0,0 +1,78 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + @android:color/system_neutral1_0 + @android:color/system_neutral1_10 + @android:color/system_neutral1_50 + @android:color/system_neutral1_100 + @android:color/system_neutral1_200 + @android:color/system_neutral1_300 + @android:color/system_neutral1_400 + @android:color/system_neutral1_500 + @android:color/system_neutral1_600 + @android:color/system_neutral1_700 + @android:color/system_neutral1_800 + @android:color/system_neutral1_900 + @android:color/system_neutral1_1000 + @android:color/system_neutral2_0 + @android:color/system_neutral2_10 + @android:color/system_neutral2_50 + @android:color/system_neutral2_100 + @android:color/system_neutral2_200 + @android:color/system_neutral2_300 + @android:color/system_neutral2_400 + @android:color/system_neutral2_500 + @android:color/system_neutral2_600 + @android:color/system_neutral2_700 + @android:color/system_neutral2_800 + @android:color/system_neutral2_900 + @android:color/system_neutral2_1000 + @android:color/system_accent1_0 + @android:color/system_accent1_10 + @android:color/system_accent1_50 + @android:color/system_accent1_100 + @android:color/system_accent1_200 + @android:color/system_accent1_300 + @android:color/system_accent1_400 + @android:color/system_accent1_500 + @android:color/system_accent1_600 + @android:color/system_accent1_700 + @android:color/system_accent1_800 + @android:color/system_accent1_900 + @android:color/system_accent1_1000 + @android:color/system_accent2_0 + @android:color/system_accent2_10 + @android:color/system_accent2_50 + @android:color/system_accent2_100 + @android:color/system_accent2_200 + @android:color/system_accent2_300 + @android:color/system_accent2_400 + @android:color/system_accent2_500 + @android:color/system_accent2_600 + @android:color/system_accent2_700 + @android:color/system_accent2_800 + @android:color/system_accent2_900 + @android:color/system_accent2_1000 + @android:color/system_accent3_0 + @android:color/system_accent3_10 + @android:color/system_accent3_50 + @android:color/system_accent3_100 + @android:color/system_accent3_200 + @android:color/system_accent3_300 + @android:color/system_accent3_400 + @android:color/system_accent3_500 + @android:color/system_accent3_600 + @android:color/system_accent3_700 + @android:color/system_accent3_800 + @android:color/system_accent3_900 + @android:color/system_accent3_1000 + + @android:color/system_accent1_100 + @android:color/system_neutral2_700 + \ No newline at end of file diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml new file mode 100644 index 0000000..99a50b5 --- /dev/null +++ b/app/src/main/res/values-v31/themes.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index f658a6f..0e8c356 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -43,6 +43,11 @@ app:entryValues="@array/theme_values" app:singleLineTitle="false" /> + + - - + + + + + + + diff --git a/rosetta/src/main/java/com/ahmedjazzar/rosetta/LanguageSwitcher.java b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LanguageSwitcher.java new file mode 100644 index 0000000..53d286e --- /dev/null +++ b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LanguageSwitcher.java @@ -0,0 +1,179 @@ +package com.ahmedjazzar.rosetta; + +import android.app.Activity; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + +import java.util.HashSet; +import java.util.Locale; + +/** + * This class is the application door to this Library. It handles the ongoing and outgoing requests, + * initializations, preferences, .. + * I think that there's no need for logging here because other classes already handle logs for these + * actions based on their returned results. + * + * Created by ahmedjazzar on 1/16/16. + */ +public class LanguageSwitcher { + + private Context mContext; + private LocalesPreferenceManager mLocalesPreferences; + private final String TAG = LanguageSwitcher.class.getName(); + + /** + * A constructor that accepts context and sets the base and first launch locales to en_US + * @param context the context of the dealer + */ + public LanguageSwitcher(@NonNull Context context) { + this(context, Locale.US); + } + + /** + * A constructor that accepts context and sets the base and first launch locales to + * firstLaunchLocale. + * + * NOTE: Please do not use unless: + * 1. You wanna set your locales by calling {@link LanguageSwitcher#setSupportedLocales} + * 2. You know for sure that the preferred locale is as same as your base locale + * + * @param context the context of the dealer + * @param firstLaunchLocale the locale that owner wanna use at its first launch + */ + public LanguageSwitcher(@NonNull Context context, Locale firstLaunchLocale) { + this(context, firstLaunchLocale, firstLaunchLocale); + } + + /** + * This is supposed to be more specific; It has three parameters cover all owner needs + * @param context the context of the dealer + * @param firstLaunchLocale the locale that owner wanna use at its first launch + * @param baseLocale the locale that used in the main xml strings file (most likely 'en') + */ + public LanguageSwitcher(@NonNull Context context, Locale firstLaunchLocale, Locale baseLocale) { + this.mContext = context.getApplicationContext(); + + this.mLocalesPreferences = + new LocalesPreferenceManager(context, firstLaunchLocale, baseLocale); + + // initializing Locales utils needed objects (detector, preferences) + LocalesUtils.setDetector(new LocalesDetector(this.mContext)); + LocalesUtils.setLocalesPreferenceManager(mLocalesPreferences); + + // Setting app locale to match the user preferred one + LocalesUtils.setAppLocale(mContext, + mLocalesPreferences + .getPreferredLocale(LocalesPreferenceManager.USER_PREFERRED_LOCALE)); + } + + /** + * Responsible for displaying Change dialog fragment + */ + public void showChangeLanguageDialog(FragmentActivity activity) { + new LanguagesListDialogFragment() + .show(activity.getSupportFragmentManager(), TAG); + } + + /** + * + * @return the application supported locales + */ + public HashSet getLocales() { + return LocalesUtils.getLocales(); + } + + /** + * Sets the app locales from a string Set + * @param sLocales supported locales in a String form + */ + public void setSupportedStringLocales(HashSet sLocales) { + + HashSet locales = new HashSet<>(); + for (String sLocale: sLocales) { + locales.add(new Locale(sLocale)); + } + this.setSupportedLocales(locales); + } + + /** + * set supported locales from the given Set + * @param locales supported locales + */ + public void setSupportedLocales(HashSet locales) { + LocalesUtils.setSupportedLocales(locales); + } + + /** + * Sets the supported locales after fetching there availability using fetchAvailableLocales + * method + * @param stringId the string that this library gonna use to detect current app available + * locales + */ + public void setSupportedLocales(int stringId) { + this.setSupportedLocales(this.fetchAvailableLocales(stringId)); + } + + /** + * Fetching the application available locales inside the resources folder dynamically + * @param stringId the string that this library gonna use to detect current app available + * locales + * @return a set of detected application locales + */ + public HashSet fetchAvailableLocales(int stringId) { + return LocalesUtils.fetchAvailableLocales(stringId); + } + + /** + * Setting the application locale manually + * @param newLocale the locale in a string format + * @param activity the current activity in order to refresh the app + * + * @return true if the operation succeed, false otherwise + */ + public boolean setLocale(String newLocale, Activity activity) { + return setLocale(new Locale(newLocale), activity); + } + + /** + * Setting the application locale manually + * @param newLocale the desired locale + * @param activity the current activity in order to refresh the app + * + * @return true if the operation succeed, false otherwise + */ + public boolean setLocale(Locale newLocale, Activity activity) { + + return LocalesUtils.setLocale(newLocale, activity); + } + + /** + * + * @return the first launch locale + */ + public Locale getLaunchLocale() { + + return LocalesUtils.getLaunchLocale(); + } + + /** + * + * @return the current locale + */ + public Locale getCurrentLocale() { + + return LocalesUtils.getCurrentLocale(this.mContext); + } + + /** + * Return to the first launch locale + * @param activity the current activity in order to refresh the app + * + * @return true if the operation succeed, false otherwise + */ + public boolean switchToLaunch(Activity activity) { + + return setLocale(getLaunchLocale(), activity); + } +} diff --git a/rosetta/src/main/java/com/ahmedjazzar/rosetta/LanguagesListDialogFragment.java b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LanguagesListDialogFragment.java new file mode 100644 index 0000000..b141e36 --- /dev/null +++ b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LanguagesListDialogFragment.java @@ -0,0 +1,180 @@ +package com.ahmedjazzar.rosetta; + +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.Locale; + +/** + * This fragment is responsible for displaying the supported locales and performing any necessary + * action that allows user to select, cancel, and commit changes. + * + * Created by ahmedjazzar on 1/19/16. + */ + +public class LanguagesListDialogFragment extends DialogFragment { + + private final int DIALOG_TITLE_ID = R.string.language; + private final int DIALOG_POSITIVE_ID = R.string.ok; + private final int DIALOG_NEGATIVE_ID = R.string.cancel; + + private int mSelectedLanguage = -1; + private final Logger mLogger; + + public LanguagesListDialogFragment() { + String TAG = LanguagesListDialogFragment.class.getName(); + this.mLogger = new Logger(TAG); + } + + /** + * @return a Dialog fragment + */ + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + mLogger.debug("Building DialogFragment."); + + builder.setTitle(getString(DIALOG_TITLE_ID)) + .setSingleChoiceItems( + getLanguages(), + getCurrentLocaleIndex(), + (dialogInterface, which) -> onLanguageSelectedLocalized(which)) + .setPositiveButton( + getString(DIALOG_POSITIVE_ID).toUpperCase(), + (dialogInterface, which) -> onPositiveClick()) + .setNegativeButton( + getString(DIALOG_NEGATIVE_ID).toUpperCase(), + (dialogInterface, which) -> onNegativeClick()); + + mLogger.verbose("DialogFragment built."); + return builder.create(); + } + + /** + * @param which the position of the selected locale + */ + protected void onLanguageSelected(int which) { + // just update the selected locale + mSelectedLanguage = which; + } + + /** + * Localizing the dialog buttons and title + * @param which the position of the selected locale + */ + protected void onLanguageSelectedLocalized(int which) { + + // update the selected locale + mSelectedLanguage = which; + AlertDialog dialog = (AlertDialog) getDialog(); + + mLogger.debug("Displaying dialog main strings in the selected " + + "locale"); + + onLanguageSelectedLocalized( + which, + null, + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE)); + } + + /** + * the position of the selected locale given the ids + * @param which the position of the selected locale + * @param titleView dialog's title text view + * @param positiveButton positive button + * @param negativeButton negative button + */ + protected void onLanguageSelectedLocalized(int which, TextView titleView, Button positiveButton, + Button negativeButton) { + + // update the selected locale + mSelectedLanguage = which; + Locale locale = LocalesUtils.getLocaleFromIndex(mSelectedLanguage); + AlertDialog dialog = (AlertDialog) getDialog(); + FragmentActivity activity = getActivity(); + + mLogger.debug("Displaying dialog main strings in the selected " + + "locale"); + + assert activity != null; + String LocalizedTitle = LocalesUtils.getInSpecificLocale(activity, locale, DIALOG_TITLE_ID); + if(titleView == null) { + // Display dialog title in the selected locale + assert dialog != null; + dialog.setTitle(LocalizedTitle); + } else { + titleView.setText(LocalizedTitle); + } + + // Display positive button text in the selected locale + positiveButton.setText(LocalesUtils.getInSpecificLocale( + activity, locale, DIALOG_POSITIVE_ID)); + + // Display negative button text in the selected locale + negativeButton.setText(LocalesUtils.getInSpecificLocale( + activity, locale, DIALOG_NEGATIVE_ID)); + } + + /** + * called when the user approved changing locale + */ + protected void onPositiveClick() { + + // if the user did not select the same locale go ahead, else ignore + if (mSelectedLanguage != -1 && + mSelectedLanguage != LocalesUtils.getCurrentLocaleIndex()) { + + // Try changing the locale + if (LocalesUtils.setAppLocale( + getActivity(), mSelectedLanguage)) { + + mLogger.info("App locale changed successfully."); + LocalesUtils.refreshApplication(requireActivity()); + } else { + mLogger.error("Unsuccessful trial to change the App locale."); + // TODO: notify the user that his request not placed + } + } else { + dismiss(); + } + } + + /** + * called when the user discarded changing locale + */ + protected void onNegativeClick() { + mLogger.verbose("User discarded changing language."); + mLogger.debug("Return to the original locale."); + this.onLanguageSelectedLocalized(this.getCurrentLocaleIndex()); + } + + /** + * + * @return available languages + */ + protected String[] getLanguages() { + ArrayList languages = LocalesUtils.getLocalesWithDisplayName(); + return languages.toArray(new String[languages.size()]); + } + + /** + * + * @return the index of the locale that app is using now + */ + protected int getCurrentLocaleIndex() { + return LocalesUtils.getCurrentLocaleIndex(); + } + +} \ No newline at end of file diff --git a/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesDetector.java b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesDetector.java new file mode 100644 index 0000000..61e3171 --- /dev/null +++ b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesDetector.java @@ -0,0 +1,153 @@ +package com.ahmedjazzar.rosetta; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.util.DisplayMetrics; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; + +/** + * This class detects the application available locales inside the resources based on a string id, + * it's not so accurate and expects another methodologies. Next release may hold a better algorithms + * for detecting strings' languages and availability inside apps. + * + * Created by ahmedjazzar on 1/16/16. + */ +class LocalesDetector { + + private final Context mContext; + private Logger mLogger; + private final String TAG = LocalesDetector.class.getName(); + + LocalesDetector(Context context) { + this.mContext = context; + this.mLogger = new Logger(TAG); + } + + /** + * this method takes an experimental string id to see if it's exists in other available + * locales inside the app than default locale. + * NOTE: Even if you have a folder named values-ar it doesn't mean you have any resources + * there + * + * @param stringId experimental string id to discover locales + * @return the discovered locales + */ + HashSet fetchAvailableLocales(int stringId) { + + DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); + Configuration conf = mContext.getResources().getConfiguration(); + Locale originalLocale = conf.locale; + Locale baseLocale = LocalesUtils.getBaseLocale(); + conf.locale = baseLocale; + + ArrayList references = new ArrayList<>(); + references.add(new Resources(mContext.getAssets(), dm, conf).getString(stringId)); + + HashSet result = new HashSet<>(); + result.add(baseLocale); + + for(String loc : mContext.getAssets().getLocales()) { + if(loc.isEmpty()){ + continue; + } + + Locale l; + boolean referencesUpdateLock = false; + + l = Locale.forLanguageTag(loc); + + conf.locale = l; + + //TODO: put it in a method + String tmpString = new Resources(mContext.getAssets(), dm, conf).getString(stringId); + for (String reference: references) { + if(reference.equals(tmpString)){ + // TODO: check its original locale + referencesUpdateLock = true; + break; + } + } + + if(!referencesUpdateLock) { + result.add(l); + references.add(tmpString); + } + } + + conf.locale = originalLocale; // to restore our guy initial state + return result; + } + + /** + * TODO: return the selected one instead + * @return application current locale + */ + Locale getCurrentLocale() { + return mContext.getResources().getConfiguration().locale; + } + + /** + * TODO: what if a user didn't provide a closer email at all? + * TODO: check the closest locale not the first identified + * + * This method should provide a locale that is close to the given one in the parameter, it's + * currently checking the language only if in case the detector detects the string in other + * language. + * + * @param locale mostly the locale that's not detected or provided + * @return the index of the most close locale to the given locale. -1 if not detected + */ + int detectMostClosestLocale(Locale locale) { + + mLogger.debug("Start detecting a close locale to: "); + + int index = 0; + for (Locale loc: LocalesUtils.getLocales()) { + if(loc.getDisplayLanguage().equals(locale.getDisplayLanguage())) { + mLogger.info("The locale: '" + loc + "' has been detected as a closer locale to: '" + + locale + "'"); + return index; + } + index++; + } + + mLogger.debug("No closer locales founded."); + return -1; + } + + /** + * This method validate locales by checking if they are available of they contain wrong letter + * case and adding the valid ones in a clean set. + * @param locales to be checked + * @return valid locales + */ + HashSet validateLocales(HashSet locales) { + + mLogger.debug("Validating given locales.."); + + for (Locale l:LocalesUtils.getPseudoLocales()) { + if(locales.remove(l)) { + mLogger.info("Pseudo locale '" + l + "' has been removed."); + } + } + + HashSet cleanLocales = new HashSet<>(); + Locale[] androidLocales = Locale.getAvailableLocales(); + for (Locale locale: locales) { + if (Arrays.asList(androidLocales).contains(locale)) { + cleanLocales.add(locale); + } else { + mLogger.error("Invalid passed locale: " + locale); + mLogger.warn("Invalid specified locale: '" + locale + "', has been discarded"); + } + } + mLogger.debug("passing validated locales."); + return cleanLocales; + } +} diff --git a/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesPreferenceManager.java b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesPreferenceManager.java new file mode 100644 index 0000000..7228a3b --- /dev/null +++ b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesPreferenceManager.java @@ -0,0 +1,161 @@ +package com.ahmedjazzar.rosetta; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.util.Locale; + +/** + * This class is responsible for setting and getting the preferred locale and manage any related + * actions. I think that there's no need for logging here because the utils class already handles + * logs for these actions based on their returned results. + * + * Created by ahmedjazzar on 1/22/16. + */ +class LocalesPreferenceManager { + + private SharedPreferences mSharedPreferences; + private SharedPreferences.Editor mEditor; + + static final int BASE_LOCALE = 1; + private final String BASE_LANGUAGE_KEY = "base_language"; + private final String BASE_COUNTRY_KEY = "base_country"; + + static final int LAUNCH_LOCALE = 2; + private final String LAUNCH_LANGUAGE_KEY = "launch_language"; + private final String LAUNCH_COUNTRY_KEY = "launch_country"; + + static final int USER_PREFERRED_LOCALE = 3; + private final String USER_PREFERRED_LANGUAGE_KEY = "user_preferred_language"; + private final String USER_PREFERRED_COUNTRY_KEY = "user_preferred_country"; + + LocalesPreferenceManager(Context context, Locale firstLaunchLocale, Locale baseLocale) { + + this.mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.mEditor = this.mSharedPreferences.edit(); + + if (!isLocaleExists(BASE_LOCALE)) { + this.setPreferredLocale(BASE_LOCALE, baseLocale); + } + + if (!isLocaleExists(LAUNCH_LOCALE)) { + this.setPreferredLocale(LAUNCH_LOCALE, firstLaunchLocale); + } + + if (!isLocaleExists(USER_PREFERRED_LOCALE)) { + this.setPreferredLocale(USER_PREFERRED_LOCALE, firstLaunchLocale); + } + } + + boolean isLocaleExists(int key) { + + switch (key) { + case BASE_LOCALE: + return mSharedPreferences.contains(this.BASE_LANGUAGE_KEY); + case LAUNCH_LOCALE: + return mSharedPreferences.contains(this.LAUNCH_LANGUAGE_KEY); + case USER_PREFERRED_LOCALE: + return mSharedPreferences.contains(this.USER_PREFERRED_LANGUAGE_KEY); + default: + return false; + } + } + + /** + * Sets user preferred locale + * + * @param locale user desired locale + * @return true if the preference updated + */ + boolean setPreferredLocale(int key, Locale locale) { + return this.setPreferredLocale(key, locale.getLanguage(), locale.getCountry()); + } + + /** + * + * @return preferred locale after concatenating language and country + */ + Locale getPreferredLocale(int key) { + + String languageKey; + String countryKey; + + switch (key) { + case BASE_LOCALE: + languageKey = this.BASE_LANGUAGE_KEY; + countryKey = this.BASE_COUNTRY_KEY; + break; + case LAUNCH_LOCALE: + languageKey = this.LAUNCH_LANGUAGE_KEY; + countryKey = this.LAUNCH_COUNTRY_KEY; + break; + case USER_PREFERRED_LOCALE: + languageKey = this.USER_PREFERRED_LANGUAGE_KEY; + countryKey = this.USER_PREFERRED_COUNTRY_KEY; + break; + default: + return null; + } + + String language = getPreferredLanguage(languageKey); + String country = getPreferredCountry(countryKey); + + if (language == null) { + return null; + } + + return new Locale(language, country); + } + + /** + * Sets user preferred locale by setting a language preference and a country preference since + * there's no supported preferences for locales + * @param language of the locale; ex. en + * @param country of the locale; ex. US + * @return true if the preferences updated + */ + private boolean setPreferredLocale(int key, String language, String country) { + + String languageKey; + String countryKey; + + switch (key) { + case BASE_LOCALE: + languageKey = this.BASE_LANGUAGE_KEY; + countryKey = this.BASE_COUNTRY_KEY; + break; + case LAUNCH_LOCALE: + languageKey = this.LAUNCH_LANGUAGE_KEY; + countryKey = this.LAUNCH_COUNTRY_KEY; + break; + case USER_PREFERRED_LOCALE: + languageKey = this.USER_PREFERRED_LANGUAGE_KEY; + countryKey = this.USER_PREFERRED_COUNTRY_KEY; + break; + default: + return false; + } + + mEditor.putString(languageKey, language); + mEditor.putString(countryKey, country); + + return mEditor.commit(); + } + + /** + * + * @return preferred language + */ + private String getPreferredLanguage(String key) { + return mSharedPreferences.getString(key, null); + } + + /** + * + * @return preferred country + */ + private String getPreferredCountry(String key) { + return mSharedPreferences.getString(key, null); + } +} diff --git a/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesUtils.java b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesUtils.java new file mode 100644 index 0000000..c93c18c --- /dev/null +++ b/rosetta/src/main/java/com/ahmedjazzar/rosetta/LocalesUtils.java @@ -0,0 +1,302 @@ +package com.ahmedjazzar.rosetta; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.util.DisplayMetrics; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; + +/** + * This class is a helper class that connects all library classes activities together and make it + * easier for every class in the library to use and look at the shared info without a need to + * initialize a new object from the desired class + * + * Created by ahmedjazzar on 1/19/16. + */ +final class LocalesUtils { + + @SuppressLint("StaticFieldLeak") + private static LocalesDetector sDetector; + private static LocalesPreferenceManager sLocalesPreferenceManager; + private static HashSet sLocales; + private static final Locale[] PSEUDO_LOCALES = { + new Locale("en", "XA"), + new Locale("ar", "XB") + }; + private static final String TAG = LocalesDetector.class.getName(); + private static Logger sLogger = new Logger(TAG); + + /** + * + * @param detector just a setter because I don't want to declare any constructors in this class + */ + static void setDetector(@NonNull LocalesDetector detector) { + LocalesUtils.sDetector = detector; + } + + /** + * + * @param localesPreferenceManager just a setter because I don't want to declare any + * constructors in this class + */ + static void setLocalesPreferenceManager( + @NonNull LocalesPreferenceManager localesPreferenceManager) { + + LocalesUtils.sLocalesPreferenceManager = localesPreferenceManager; + } + + /** + * + * @param stringId a string to start discovering sLocales in + * @return a HashSet of discovered sLocales + */ + static HashSet fetchAvailableLocales(int stringId) { + return sDetector.fetchAvailableLocales(stringId); + } + + /** + * + * @param localesSet sLocales user wanna use + */ + static void setSupportedLocales(HashSet localesSet) { + LocalesUtils.sLocales = sDetector.validateLocales(localesSet); + sLogger.debug("Locales have been changed"); + } + + /** + * + * @return a HashSet of the available sLocales discovered in the application + */ + static HashSet getLocales() { + return LocalesUtils.sLocales; + } + + /** + * + * @return a list of locales for displaying on the layout purposes + */ + static ArrayList getLocalesWithDisplayName() { + ArrayList stringLocales = new ArrayList<>(); + + for (Locale loc: LocalesUtils.getLocales()) { + String langDisplay = loc.getDisplayName(loc); + stringLocales.add(langDisplay.substring(0, 1).toUpperCase() + langDisplay.substring(1).toLowerCase()); + } + return stringLocales; + } + + /** + * + * @return the index of the current app locale + */ + static int getCurrentLocaleIndex() { + Locale locale = LocalesUtils.getCurrentLocale(); + int index = -1; + int itr = 0; + + for (Locale l : sLocales) { + if(locale.equals(l)) { + index = itr; + break; + } + itr++; + } + + if (index == -1) { + //TODO: change the index to the most closer available locale + sLogger.warn("Current device locale '" + locale.toString() + + "' does not appear in your given supported locales"); + + index = sDetector.detectMostClosestLocale(locale); + if(index == -1) { + index = 0; + sLogger.warn("Current locale index changed to 0 as the current locale '" + + locale + + "' not supported." + ); + } + } + + return index; + } + + /** + * + * @see Pseudolocalization for + * more information about pseudo localization + * @return pseudo locales list + */ + static List getPseudoLocales() { + return Arrays.asList(LocalesUtils.PSEUDO_LOCALES); + } + + /** + * + * @return the locale at the given index + */ + static Locale getLocaleFromIndex(int index) { + return LocalesUtils.sLocales.toArray(new Locale[LocalesUtils.sLocales.size()])[index]; + } + + /** + * + * @param context + * @param index the selected locale position + * @return true if the application locale changed + */ + static boolean setAppLocale(Context context, int index) { + return setAppLocale(context, getLocaleFromIndex(index)); + } + + /** + * + * @return true if the application locale changed + */ + static boolean setAppLocale(Context context, Locale newLocale) { + + Resources resources = context.getResources(); + DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + Configuration configuration = resources.getConfiguration(); + + Locale oldLocale = new Locale(configuration.locale.getLanguage(), configuration.locale.getCountry()); + configuration.locale = newLocale; + // Sets the layout direction from the Locale + sLogger.debug("Setting the layout direction"); + configuration.setLayoutDirection(newLocale); + resources.updateConfiguration(configuration, displayMetrics); + + if(oldLocale.equals(newLocale)) { + return false; + } + + if (LocalesUtils.updatePreferredLocale(newLocale)) { + sLogger.info("Locale preferences updated to: " + newLocale); + Locale.setDefault(newLocale); + } else { + sLogger.error("Failed to update locale preferences."); + } + + return true; + } + + /** + * + * @return application's base locale + */ + static Locale getBaseLocale() { + return LocalesUtils.sLocalesPreferenceManager.getPreferredLocale(LocalesPreferenceManager.BASE_LOCALE); + } + + /** + * + * @param stringId the target string + * @return a localized string + */ + static String getInSpecificLocale(FragmentActivity activity, Locale locale, int stringId) { + + Configuration conf = activity.getResources().getConfiguration(); + Locale old = conf.locale; + + conf.locale = locale; + DisplayMetrics metrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); + Resources resources = new Resources(activity.getAssets(), metrics, conf); + conf.locale = old; + + return resources.getString(stringId); + } + + /** + * Refreshing the application so no weired results occurred after changing the locale. + */ + static void refreshApplication(Activity activity) { + + Intent app = activity.getBaseContext().getPackageManager() + .getLaunchIntentForPackage(activity.getBaseContext().getPackageName()); + app.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + Intent current = new Intent(activity, activity.getClass()); + sLogger.debug("Refreshing the application: " + + activity.getBaseContext().getPackageName()); + + sLogger.debug("Finishing current activity."); + activity.finish(); + + sLogger.debug("Start the application"); + activity.startActivity(app); + activity.startActivity(current); + + sLogger.debug("Application refreshed"); + } + + /** + * + * @return the first launch locale + */ + static Locale getLaunchLocale() { + + return sLocalesPreferenceManager.getPreferredLocale(LocalesPreferenceManager.LAUNCH_LOCALE); + } + + /** + * Setting the application locale manually + * @param newLocale the desired locale + * @param activity the current activity in order to refresh the app + * + * @return true if the operation succeed, false otherwise + */ + static boolean setLocale(Locale newLocale, Activity activity) { + if (newLocale == null || !getLocales().contains(newLocale)) { + return false; + } + + if (LocalesUtils.setAppLocale(activity.getApplicationContext(), newLocale)) { + LocalesUtils.refreshApplication(activity); + return true; + } + + return false; + } + + /** + * @param context application base context + * @return the current locale + */ + public static Locale getCurrentLocale(Context context) { + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + + return new Locale(configuration.locale.getLanguage(), configuration.locale.getCountry()); + } + + /** + * + * @param locale the new preferred locale + * @return true if the preferred locale updated + */ + private static boolean updatePreferredLocale(Locale locale) { + + return LocalesUtils.sLocalesPreferenceManager + .setPreferredLocale(LocalesPreferenceManager.USER_PREFERRED_LOCALE, locale); + } + + /** + * + * @return current application locale + */ + private static Locale getCurrentLocale() { + return sDetector.getCurrentLocale(); + } +} \ No newline at end of file diff --git a/rosetta/src/main/java/com/ahmedjazzar/rosetta/Logger.java b/rosetta/src/main/java/com/ahmedjazzar/rosetta/Logger.java new file mode 100644 index 0000000..ad40a69 --- /dev/null +++ b/rosetta/src/main/java/com/ahmedjazzar/rosetta/Logger.java @@ -0,0 +1,38 @@ +package com.ahmedjazzar.rosetta; + +import android.util.Log; + +/** + * This class helps logging app events without a need to rewrite the tag name in every time + * Created by ahmedjazzar on 1/16/16. + */ + +class Logger { + + private final String mTag; + + Logger(String tag) { + this.mTag = tag; + this.verbose("Object from " + this.mTag + " has been created."); + } + + void error(String log) { + Log.e(this.mTag, log); + } + + void warn(String log) { + Log.w(this.mTag, log); + } + + void debug(String log) { + Log.d(this.mTag, log); + } + + void info(String log) { + Log.i(this.mTag, log); + } + + void verbose(String log) { + Log.v(this.mTag, log); + } +} diff --git a/rosetta/src/main/res/values/strings.xml b/rosetta/src/main/res/values/strings.xml new file mode 100644 index 0000000..e13140f --- /dev/null +++ b/rosetta/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Language + Ok + Cancel + diff --git a/settings.gradle b/settings.gradle index 5c996dc..4fd643d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,4 @@ dependencyResolutionManagement { } rootProject.name = "MagiskModuleManager" include ':app' +include ':rosetta'