From 5f6e92174d49005be46a421ac8e515faeb4d09f6 Mon Sep 17 00:00:00 2001 From: Fox2Code Date: Mon, 27 Dec 2021 17:19:44 +0100 Subject: [PATCH] 0.2.5 Release --- DEVELOPERS.md | 7 +- README.md | 4 + app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 3 + .../com/fox2code/mmm/AppUpdateManager.java | 6 +- .../java/com/fox2code/mmm/MainActivity.java | 11 +- .../com/fox2code/mmm/ModuleViewAdapter.java | 3 + .../fox2code/mmm/ModuleViewListBuilder.java | 4 +- .../fox2code/mmm/manager/ModuleManager.java | 32 +----- .../java/com/fox2code/mmm/repo/RepoData.java | 2 +- .../java/com/fox2code/mmm/utils/Http.java | 67 +++++++---- .../com/fox2code/mmm/utils/IntentHelper.java | 2 +- .../com/fox2code/mmm/utils/PropUtils.java | 108 +++++++++++++++--- app/src/main/res/values-de/arrays.xml | 5 - 14 files changed, 172 insertions(+), 86 deletions(-) diff --git a/DEVELOPERS.md b/DEVELOPERS.md index b3a868d..47c3f42 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -12,7 +12,7 @@ Index: ## Properties -In addition to the following magisk properties +In addition to the following required magisk properties ```properties # Magisk supported properties id= @@ -22,8 +22,9 @@ versionCode= author= description= ``` +(Note: The Fox's mmm will not show the module if theses values are not filled properly) -This the manager support these new properties +This the manager support these new optional properties ```properties # Fox's Mmm supported properties minApi= @@ -38,7 +39,7 @@ config= - `minApi` and `maxApi` tell the manager which is the SDK version range the module support (See: [Codenames, Tags, and Build Numbers](https://source.android.com/setup/start/build-numbers)) - `minMagisk` tell the manager which is the minimum Magisk version required for the module - (Often for magisk `xx.y` the version code is `xxy00`) + (Often for magisk `xx.y` the version code is `xxyzz`, `zz` being non `00` on canary builds) - `support` support link to direct users when they need support for you modules - `donate` donate link to direct users to where they can financially support your project - `config` package name of the application that configure your module diff --git a/README.md b/README.md index 067104a..314bdc3 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,14 @@ So I made my own app to do that! :3 Minimum: - Android 5.0+ - Magisk 19.0+ +- An internet connection Recommended: - Android 6.0+ - Magisk 21.2+ +- An internet connection + +Note: This app may require the use of a VPN in countries with a state wide firewall. ## For users diff --git a/app/build.gradle b/app/build.gradle index b07c182..e9b66f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.fox2code.mmm" minSdk 21 targetSdk 31 - versionCode 14 - versionName "0.2.4" + versionCode 15 + versionName "0.2.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95b5313..70ca125 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ package="com.fox2code.mmm" tools:ignore="QueryAllPackagesPermission"> + + + diff --git a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java index 2b941a8..b601f45 100644 --- a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java +++ b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java @@ -37,13 +37,13 @@ public class AppUpdateManager { // Return true if should show a notification public boolean checkUpdate(boolean force) { - if (this.peekShouldUpdate()) + if (!force && this.peekShouldUpdate()) return true; long lastChecked = this.lastChecked; - if (!force && lastChecked != 0 && + if (lastChecked != 0 && // Avoid spam calls by putting a 10 seconds timer lastChecked < System.currentTimeMillis() - 10000L) - return false; + return force && this.peekShouldUpdate(); synchronized (this.updateLock) { if (lastChecked != this.lastChecked) return this.peekShouldUpdate(); diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 5cc7465..d859744 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -21,6 +21,7 @@ import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.manager.ModuleManager; 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.progressindicator.LinearProgressIndicator; @@ -180,7 +181,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE); if (!RepoManager.getINSTANCE().hasConnectivity()) moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); - else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true)) + else if (AppUpdateManager.getAppUpdateManager().checkUpdate(false)) moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); moduleViewListBuilder.appendRemoteModules(); Log.i(TAG, "Common Before applyTo"); @@ -201,8 +202,14 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O this.progressIndicator.setProgressCompat(0, false); // this.swipeRefreshLayout.setRefreshing(true); ?? new Thread(() -> { + Http.cleanDnsCache(); // Allow DNS reload from network RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> - this.progressIndicator.setProgressCompat((int) (value * PRECISION), true))); + this.progressIndicator.setProgressCompat( + (int) (value * PRECISION), true))); + if (!RepoManager.getINSTANCE().hasConnectivity()) + moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); + else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true)) + moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); runOnUiThread(() -> { this.progressIndicator.setVisibility(View.GONE); this.swipeRefreshLayout.setRefreshing(false); diff --git a/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java b/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java index c16ec5d..56f1f59 100644 --- a/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java +++ b/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java @@ -261,6 +261,9 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter headerTypes = EnumSet.of( - ModuleHolder.Type.NOTIFICATION, ModuleHolder.Type.SEPARATOR); + EnumSet headerTypes = EnumSet.of(ModuleHolder.Type.SEPARATOR, + ModuleHolder.Type.NOTIFICATION, ModuleHolder.Type.FOOTER); Iterator moduleHolderIterator = this.mappedModuleHolders.values().iterator(); synchronized (this.queryLock) { while (moduleHolderIterator.hasNext()) { diff --git a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java index dcfb033..f54d719 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java @@ -21,10 +21,9 @@ public final class ModuleManager { ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE; private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED; private final HashMap moduleInfos; - private final HashMap invalidModules; private final SharedPreferences bootPrefs; private final Object scanLock = new Object(); - private boolean scanning, lastScanResult; + private boolean scanning; private static final ModuleManager INSTANCE = new ModuleManager(); @@ -34,19 +33,17 @@ public final class ModuleManager { private ModuleManager() { this.moduleInfos = new HashMap<>(); - this.invalidModules = new HashMap<>(); this.bootPrefs = MainApplication.getBootSharedPreferences(); } // MultiThread friendly method - public final boolean scan() { + public final void scan() { if (!this.scanning) { // Do scan synchronized (scanLock) { this.scanning = true; try { - this.lastScanResult = - this.scanInternal(); + this.scanInternal(); } finally { this.scanning = false; } @@ -55,7 +52,6 @@ public final class ModuleManager { // Wait for current scan synchronized (scanLock) {} } - return this.lastScanResult; } // Pause execution until the scan is completed if one is currently running @@ -69,13 +65,9 @@ public final class ModuleManager { } } - private boolean scanInternal() { + private void scanInternal() { boolean firstScan = this.bootPrefs.getBoolean("mm_first_scan", true); - boolean changed = false; SharedPreferences.Editor editor = firstScan ? this.bootPrefs.edit() : null; - // Reset existing ModuleInfo - this.moduleInfos.putAll(this.invalidModules); - this.invalidModules.clear(); for (ModuleInfo v : this.moduleInfos.values()) { v.flags |= FLAG_MM_UNPROCESSED; v.flags &= ~FLAGS_RESET_INIT; @@ -94,7 +86,6 @@ public final class ModuleManager { if (moduleInfo == null) { moduleInfo = new ModuleInfo(module); moduleInfos.put(module, moduleInfo); - changed = true; // Shis should not really happen, but let's handles theses cases anyway moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY; } @@ -118,7 +109,7 @@ public final class ModuleManager { } try { PropUtils.readProperties(moduleInfo, - "/data/adb/modules/" + module + "/module.prop"); + "/data/adb/modules/" + module + "/module.prop", true); } catch (Exception e) { Log.d(TAG, "Failed to parse metadata!", e); moduleInfo.flags |= FLAG_MM_INVALID; @@ -132,13 +123,12 @@ public final class ModuleManager { if (moduleInfo == null) { moduleInfo = new ModuleInfo(module); moduleInfos.put(module, moduleInfo); - changed = true; } moduleInfo.flags &= ~FLAGS_RESET_UPDATE; moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING; try { PropUtils.readProperties(moduleInfo, - "/data/adb/modules_update/" + module + "/module.prop"); + "/data/adb/modules_update/" + module + "/module.prop", true); } catch (Exception e) { Log.d(TAG, "Failed to parse metadata!", e); moduleInfo.flags |= FLAG_MM_INVALID; @@ -152,10 +142,6 @@ public final class ModuleManager { if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) { moduleInfoIterator.remove(); continue; // Don't process fallbacks if unreferenced - } else if ((moduleInfo.flags & FLAG_MM_INVALID) != 0) { - moduleInfo.flags &=~ FLAG_MM_INVALID; - this.invalidModules.put(moduleInfo.id, moduleInfo); - moduleInfoIterator.remove(); } if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) { moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) + @@ -169,7 +155,6 @@ public final class ModuleManager { editor.putBoolean("mm_first_scan", false); editor.apply(); } - return changed; } public HashMap getModules() { @@ -177,11 +162,6 @@ public final class ModuleManager { return this.moduleInfos; } - public HashMap getInvalidModules() { - this.afterScan(); - return invalidModules; - } - public boolean setEnabledState(ModuleInfo moduleInfo, boolean checked) { if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false; SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/disable"); diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index 02a6573..e0a59bb 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -112,7 +112,7 @@ public class RepoData { if (file.exists()) { try { ModuleInfo moduleInfo = repoModule.moduleInfo; - PropUtils.readProperties(moduleInfo, file.getAbsolutePath()); + PropUtils.readProperties(moduleInfo, file.getAbsolutePath(), false); moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID; if (moduleInfo.version == null) { moduleInfo.version = "v" + moduleInfo.versionCode; diff --git a/app/src/main/java/com/fox2code/mmm/utils/Http.java b/app/src/main/java/com/fox2code/mmm/utils/Http.java index 98e8141..58fca55 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -41,6 +41,7 @@ public class Http { private static final String TAG = "Http"; private static final OkHttpClient httpClient; private static final OkHttpClient httpClientWithCache; + private static final FallBackDNS fallbackDNS; static { OkHttpClient.Builder httpclientBuilder = new OkHttpClient.Builder(); @@ -80,10 +81,11 @@ public class Http { httpclientBuilder.cookieJar(CookieJar.NO_COOKIES); MainApplication mainApplication = MainApplication.getINSTANCE(); if (mainApplication != null) { - httpclientBuilder.dns(new FallBackDNS(mainApplication, dns, "github.com", - "api.github.com", "raw.githubusercontent.com", "camo.githubusercontent.com", - "user-images.githubusercontent.com", "cdn.jsdelivr.net", "img.shields.io", - "magisk-modules-repo.github.io", "www.androidacy.com")); + httpclientBuilder.dns(fallbackDNS = new FallBackDNS(mainApplication, dns, + "github.com", "api.github.com", "raw.githubusercontent.com", + "camo.githubusercontent.com", "user-images.githubusercontent.com", + "cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io", + "www.androidacy.com")); httpClient = httpclientBuilder.build(); httpclientBuilder.cache(new Cache( new File(mainApplication.getCacheDir(), "http_cache"), @@ -92,6 +94,7 @@ public class Http { httpClientWithCache = httpclientBuilder.build(); Log.i(TAG, "Initialized Http successfully!"); } else { + fallbackDNS = null; httpclientBuilder.dns(dns); httpClientWithCache = httpClient = httpclientBuilder.build(); Log.e(TAG, "Initialized Http too soon!"); @@ -166,6 +169,12 @@ public class Http { return byteArrayOutputStream.toByteArray(); } + public static void cleanDnsCache() { + if (Http.fallbackDNS != null) { + Http.fallbackDNS.cleanDnsCache(); + } + } + /** * Cookie jar that allow CDN cookies, reset on app relaunch * Note: An argument can be made that it allow tracking but @@ -244,36 +253,44 @@ public class Http { @Override public List lookup(@NonNull String s) throws UnknownHostException { if (this.fallbacks.contains(s)) { - List addresses = this.fallbackCache.get(s); - if (addresses != null) - return addresses; - try { - addresses = this.parent.lookup(s); - if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress()) - throw new UnknownHostException(s); - this.fallbackCache.put(s, addresses); - this.sharedPreferences.edit().putString( - s.replace('.', '_'), toString(addresses)).apply(); - } catch (UnknownHostException e) { - String key = this.sharedPreferences.getString( - s.replace('.', '_'), ""); - if (!key.isEmpty()) try { - addresses = fromString(key); - this.fallbackCache.put(s, addresses); + List addresses; + synchronized (this.fallbackCache) { + addresses = this.fallbackCache.get(s); + if (addresses != null) return addresses; - } catch (UnknownHostException e2) { - this.sharedPreferences.edit().remove( - s.replace('.', '_')).apply(); + try { + addresses = this.parent.lookup(s); + if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress()) + throw new UnknownHostException(s); + this.fallbackCache.put(s, addresses); + this.sharedPreferences.edit().putString( + s.replace('.', '_'), toString(addresses)).apply(); + } catch (UnknownHostException e) { + String key = this.sharedPreferences.getString( + s.replace('.', '_'), ""); + if (key.isEmpty()) throw e; + try { + addresses = fromString(key); + this.fallbackCache.put(s, addresses); + } catch (UnknownHostException e2) { + this.sharedPreferences.edit().remove( + s.replace('.', '_')).apply(); + throw e; + } } - throw e; } - return addresses; } else { return this.parent.lookup(s); } } + void cleanDnsCache() { + synchronized (this.fallbackCache) { + this.fallbackCache.clear(); + } + } + @NonNull private static String toString(@NonNull List inetAddresses) { if (inetAddresses.isEmpty()) return ""; 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 8a8362a..d46a69e 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -38,7 +38,7 @@ public class IntentHelper { context.startActivity(myIntent); } catch (ActivityNotFoundException e) { Toast.makeText(context, "No application can handle this request." - + " Please install a webbrowser", Toast.LENGTH_SHORT).show(); + + " Please install a web-browser", Toast.LENGTH_SHORT).show(); e.printStackTrace(); } } diff --git a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java index a6cfcdf..4963762 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java +++ b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java @@ -50,39 +50,83 @@ public class PropUtils { moduleMinApiFallbacks.put("riru-core", RIRU_MIN_API = Build.VERSION_CODES.M); } - public static void readProperties(ModuleInfo moduleInfo, String file) throws IOException { - boolean readId = false, readIdSec = false, readVersionCode = false; + public static void readProperties(ModuleInfo moduleInfo, String file,boolean local) throws IOException { + boolean readId = false, readIdSec = false, readName = false, + readVersionCode = false, readVersion = false, invalid = false; try (BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(SuFileInputStream.open(file), StandardCharsets.UTF_8))) { String line; + int lineNum = 0; while ((line = bufferedReader.readLine()) != null) { + lineNum++; int index = line.indexOf('='); if (index == -1 || line.startsWith("#")) continue; String key = line.substring(0, index); String value = line.substring(index + 1).trim(); + // name and id have their own implementation + if (isInvalidValue(key)) { + if (local) { + invalid = true; + continue; + } else throw new IOException("Invalid key at line " + lineNum); + } else if (isInvalidValue(value) && !key.equals("id") && !key.equals("name")) { + if (local) { + invalid = true; + continue; + } else throw new IOException("Invalid value for key " + key); + } switch (key) { case "id": + if (isInvalidValue(value)) { + if (local) { + invalid = true; + break; + } throw new IOException("Invalid module id!"); + } readId = true; if (!moduleInfo.id.equals(value)) { - throw new IOException(file + " has an non matching module id! "+ - "(Expected \"" + moduleInfo.id + "\" got \"" + value + "\""); + if (local) { + invalid = true; + } else { + throw new IOException(file + " has an non matching module id! " + + "(Expected \"" + moduleInfo.id + "\" got \"" + value + "\""); + } } break; case "name": - if (readIdSec && !moduleInfo.id.equals(value)) - throw new IOException("Duplicate module name!"); + if (readName) { + if (local) { + invalid = true; + break; + } else throw new IOException("Duplicate module name!"); + } + if (isInvalidValue(value)) { + if (local) { + invalid = true; + break; + } throw new IOException("Invalid module name!"); + } + readName = true; moduleInfo.name = value; if (moduleInfo.id.equals(value)) { readIdSec = true; } break; case "version": + readVersion = true; moduleInfo.version = value; break; case "versionCode": readVersionCode = true; - moduleInfo.versionCode = Long.parseLong(value); + try { + moduleInfo.versionCode = Long.parseLong(value); + } catch (RuntimeException e) { + if (local) { + invalid = true; + moduleInfo.versionCode = 0; + } else throw e; + } break; case "author": moduleInfo.author = value; @@ -92,14 +136,14 @@ public class PropUtils { break; case "support": // Do not accept invalid or too broad support links - if (!value.startsWith("https://") || + if (isInvalidURL(value) || "https://forum.xda-developers.com/".equals(value)) break; moduleInfo.support = value; break; case "donate": // Do not accept invalid donate links - if (!value.startsWith("https://")) break; + if (isInvalidURL(value)) break; moduleInfo.donate = value; break; case "config": @@ -136,21 +180,27 @@ public class PropUtils { } } if (!readId) { - if (readIdSec) { + if (readIdSec && local) { // Using the name for module id is not really appropriate, so beautify it a bit - moduleInfo.name = moduleInfo.id.substring(0, 1).toUpperCase(Locale.ROOT) + - moduleInfo.id.substring(1).replace('_', ' '); + moduleInfo.name = makeNameFromId(moduleInfo.id); + } else if (local) { // Allow local module to not declare ids + invalid = true; } else { throw new IOException("Didn't read module id at least once!"); } } if (!readVersionCode) { - throw new IOException("Didn't read module versionCode at least once!"); + if (local) { + invalid = true; + moduleInfo.versionCode = 0; + } else { + throw new IOException("Didn't read module versionCode at least once!"); + } } - if (moduleInfo.name == null) { - moduleInfo.name = moduleInfo.id; + if (moduleInfo.name == null || !readName) { + moduleInfo.name = makeNameFromId(moduleInfo.id); } - if (moduleInfo.version == null) { + if (moduleInfo.version == null || !readVersion) { moduleInfo.version = "v" + moduleInfo.versionCode; } if (moduleInfo.minApi == 0) { @@ -167,6 +217,16 @@ public class PropUtils { if (moduleInfo.config == null) { moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id); } + // All local modules should have an author + // set to "Unknown" if author is missing. + if (local && moduleInfo.author == null) { + moduleInfo.author = "Unknown"; + } + if (invalid) { + moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID; + // This shouldn't happen but just in case + if (!local) throw new IOException("Invalid properties!"); + } } // Some module are really so low quality that it has become very annoying. @@ -179,4 +239,20 @@ public class PropUtils { || description.toLowerCase(Locale.ROOT).equals(moduleInfo.name.toLowerCase(Locale.ROOT)) || description.length() < Math.min(Math.max(moduleInfo.name.length() + 4, 16), 24); } + + private static boolean isInvalidValue(String name) { + return !TextUtils.isGraphic(name) || name.indexOf('\0') != -1; + } + + private static boolean isInvalidURL(String url) { + int i = url.indexOf('/', 8); + int e = url.indexOf('.', 8); + return i == -1 || e == -1 || e >= i || !url.startsWith("https://") + || url.length() <= 12 || url.indexOf('\0') != -1; + } + + private static String makeNameFromId(String moduleId) { + return moduleId.substring(0, 1).toUpperCase(Locale.ROOT) + + moduleId.substring(1).replace('_', ' '); + } } diff --git a/app/src/main/res/values-de/arrays.xml b/app/src/main/res/values-de/arrays.xml index e1079b0..7ec366c 100644 --- a/app/src/main/res/values-de/arrays.xml +++ b/app/src/main/res/values-de/arrays.xml @@ -1,10 +1,5 @@ - - system - dark - light - Systemvorgabe Dunkel