Merge branch 'Fox2Code:master' into master

pull/90/head
Jia-Bin 2 years ago committed by GitHub
commit 82d42824ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -10,8 +10,8 @@ android {
applicationId "com.fox2code.mmm"
minSdk 21
targetSdk 32
versionCode 30
versionName "0.3.2"
versionCode 32
versionName "0.4.0-rc1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -64,7 +64,7 @@ dependencies {
// Utils
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
implementation 'com.squareup.okhttp3:okhttp-brotli:4.9.3'
implementation 'com.github.topjohnwu.libsu:io:3.2.1'
implementation 'com.github.topjohnwu.libsu:io:4.0.0'
// Markdown
implementation "io.noties.markwon:core:4.6.2"
@ -74,6 +74,9 @@ dependencies {
annotationProcessor "io.noties:prism4j-bundler:2.0.0"
implementation "com.caverock:androidsvg:1.4"
// Utils for compat
compileOnly "org.robolectric:android-all:11-robolectric-6757853"
// Test
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

@ -2,25 +2,42 @@ package com.fox2code.mmm;
import android.util.Log;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
// See https://docs.github.com/en/rest/reference/repos#releases
public class AppUpdateManager {
public static int FLAG_COMPAT_LOW_QUALITY = 0x01;
public static int FLAG_COMPAT_NO_EXT = 0x02;
public static int FLAG_COMPAT_MAGISK_CMD = 0x04;
public static int FLAG_COMPAT_NEED_32BIT = 0x08;
private static final String TAG = "AppUpdateManager";
private static final AppUpdateManager INSTANCE = new AppUpdateManager();
private static final String RELEASES_API_URL =
"https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/releases";
private static final String COMPAT_API_URL =
"https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/releases";
public static AppUpdateManager getAppUpdateManager() {
return INSTANCE;
}
private final HashMap<String, Integer> compatDataId = new HashMap<>();
private final Object updateLock = new Object();
private final File compatFile;
private String latestRelease;
private String latestPreRelease;
private long lastChecked;
@ -28,12 +45,20 @@ public class AppUpdateManager {
private boolean lastCheckSuccess;
private AppUpdateManager() {
this.compatFile = new File(MainApplication.getINSTANCE().getFilesDir(), "compat.txt");
this.latestRelease = MainApplication.getBootSharedPreferences()
.getString("updater_latest_release", BuildConfig.VERSION_NAME);
this.latestPreRelease = MainApplication.getBootSharedPreferences()
.getString("updater_latest_pre_release", BuildConfig.VERSION_NAME);
this.lastChecked = 0;
this.preReleaseNewer = true;
if (this.compatFile.isFile()) {
try {
this.parseCompatibilityFlags(new FileInputStream(this.compatFile));
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Return true if should show a notification
@ -95,6 +120,31 @@ public class AppUpdateManager {
return this.peekShouldUpdate();
}
public void checkUpdateCompat() {
if (this.compatFile.exists()) {
long lastUpdate = this.compatFile.lastModified();
if (lastUpdate <= System.currentTimeMillis() &&
lastUpdate + 600_000L > System.currentTimeMillis()) {
return; // Skip update
}
}
try {
JSONObject object = new JSONObject(new String(Http.doHttpGet(
COMPAT_API_URL, false), StandardCharsets.UTF_8));
if (object.isNull("body")) {
compatDataId.clear();
Files.write(compatFile, new byte[0]);
return;
}
byte[] rawData = object.getString("body")
.getBytes(StandardCharsets.UTF_8);
this.parseCompatibilityFlags(new ByteArrayInputStream(rawData));
Files.write(compatFile, rawData);
} catch (Exception e) {
Log.e("AppUpdateManager", "Failed to update compat list", e);
}
}
public boolean peekShouldUpdate() {
return !(BuildConfig.VERSION_NAME.equals(this.latestRelease) ||
(this.preReleaseNewer &&
@ -109,4 +159,46 @@ public class AppUpdateManager {
public boolean isLastCheckSuccess() {
return lastCheckSuccess;
}
private void parseCompatibilityFlags(InputStream inputStream) throws IOException {
compatDataId.clear();
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = bufferedReader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
int i = line.indexOf('/');
if (i == -1) continue;
int value = 0;
for (String arg : line.substring(i + 1).split(",")) {
switch (arg) {
default:
break;
case "lowQuality":
value |= FLAG_COMPAT_LOW_QUALITY;
break;
case "noExt":
value |= FLAG_COMPAT_NO_EXT;
break;
case "magiskCmd":
value |= FLAG_COMPAT_MAGISK_CMD;
break;
case "need32bit":
value |= FLAG_COMPAT_NEED_32BIT;
break;
}
}
compatDataId.put(line.substring(0, i), value);
}
}
public int getCompatibilityFlags(String moduleId) {
Integer compatFlags = compatDataId.get(moduleId);
return compatFlags == null ? 0 : compatFlags;
}
public static int getFlagsForModule(String moduleId) {
return INSTANCE.getCompatibilityFlags(moduleId);
}
}

@ -117,6 +117,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
MainActivity.this.searchView.clearFocus();
}
});
this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
this.searchView.setMinimumHeight(CompatDisplay.dpToPixel(16));
this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH |
EditorInfo.IME_FLAG_NO_FULLSCREEN);
@ -179,6 +180,8 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
} else {
if (AppUpdateManager.getAppUpdateManager().checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
if (AppUpdateManager.getAppUpdateManager().isLastCheckSuccess())
AppUpdateManager.getAppUpdateManager().checkUpdateCompat();
if (max != 0) {
int current = 0;
for (LocalModuleInfo localModuleInfo :
@ -241,9 +244,10 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
swipeRefreshLayoutOrigStartOffset + combinedBarsHeight,
swipeRefreshLayoutOrigEndOffset + combinedBarsHeight);
this.moduleViewListBuilder.setHeaderPx(
actionBarHeight + CompatDisplay.dpToPixel(4));
actionBarHeight + CompatDisplay.dpToPixel(8));
this.moduleViewListBuilder.setFooterPx(
bottomInset + this.searchCard.getHeight());
this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
this.moduleViewListBuilder.updateInsets();
this.actionBarBlur.invalidate();
this.overScrollInsetTop = combinedBarsHeight;

@ -104,6 +104,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
}
}
});
this.buttonAction.setClickable(false);
this.switchMaterial.setEnabled(false);
this.switchMaterial.setOnCheckedChangeListener((v, checked) -> {
if (this.initState) return; // Skip if non user

@ -76,6 +76,7 @@ public class ModuleViewListBuilder {
RepoManager repoManager = RepoManager.getINSTANCE();
repoManager.runAfterUpdate(() -> {
Log.i(TAG, "A2: " + repoManager.getModules().size());
boolean no32bitSupport = Build.SUPPORTED_32_BIT_ABIS.length == 0;
for (RepoModule repoModule : repoManager.getModules().values()) {
if (!repoModule.repoData.isEnabled()) continue;
ModuleInfo moduleInfo = repoModule.moduleInfo;
@ -84,9 +85,11 @@ public class ModuleViewListBuilder {
// Only check Magisk compatibility if root is present
(InstallerInitializer.peekMagiskPath() != null &&
repoModule.moduleInfo.minMagisk >
InstallerInitializer.peekMagiskVersion()
)))
continue; // Skip adding incompatible modules
InstallerInitializer.peekMagiskVersion())) ||
// If 64bit only system, skip 32bit only modules
(no32bitSupport && (AppUpdateManager.getFlagsForModule(repoModule.id)
& AppUpdateManager.FLAG_COMPAT_NEED_32BIT) != 0)
) continue; // Skip adding incompatible modules
ModuleHolder moduleHolder = this.mappedModuleHolders.get(repoModule.id);
if (moduleHolder == null) {
this.mappedModuleHolders.put(repoModule.id,

@ -31,13 +31,17 @@ public enum NotificationType implements NotificationTypeCst {
return !MainApplication.isShowcaseMode();
}
},
NO_ROOT(R.string.fail_root_magisk, R.drawable.ic_baseline_numbers_24) {
NO_ROOT(R.string.fail_root_magisk, R.drawable.ic_baseline_numbers_24, v -> {
IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk");
}) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() != null;
}
},
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24) {
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> {
IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk");
}) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() == null ||
@ -128,7 +132,11 @@ public enum NotificationType implements NotificationTypeCst {
public final boolean special;
NotificationType(@StringRes int textId, int iconId) {
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary); //R.attr.colorOnError);
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary);
}
NotificationType(@StringRes int textId, int iconId, View.OnClickListener onClickListener) {
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary, onClickListener);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr) {

@ -81,9 +81,9 @@ public class AndroidacyActivity extends CompatActivity {
setActionBarBackground(null);
this.setDisplayHomeAsUpEnabled(true);
if (title == null || title.isEmpty()) {
this.setTitle(title);
} else {
this.setTitle("Androidacy");
} else {
this.setTitle(title);
}
if (allowInstall || title == null || title.isEmpty()) {
this.hideActionBar();
@ -120,9 +120,9 @@ public class AndroidacyActivity extends CompatActivity {
public boolean shouldOverrideUrlLoading(
@NonNull WebView view, @NonNull WebResourceRequest request) {
// Don't open non Androidacy urls inside WebView
if (request.isForMainFrame() && !(request.getUrl().getScheme().equals("intent") ||
AndroidacyUtil.isAndroidacyLink(request.getUrl()))) {
IntentHelper.openUrl(view.getContext(), request.getUrl().toString());
if (request.isForMainFrame() &&
!AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
IntentHelper.openUri(view.getContext(), request.getUrl().toString());
return true;
}
return false;

@ -10,6 +10,7 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemProperties;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyCharacterMap;
@ -88,8 +89,7 @@ public class CompatActivity extends AppCompatActivity {
if (!this.onCreateCalled) {
this.getLayoutInflater().setFactory2(new LayoutInflaterFactory(this.getDelegate())
.addOnViewCreatedListener(WindowInsetsHelper.Companion.getLISTENER()));
this.hasHardwareNavBar = ViewConfiguration.get(this).hasPermanentMenuKey() ||
KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
this.hasHardwareNavBar = this.hasHardwareNavBar0();
this.onCreateCalledOnce = true;
}
Application application = this.getApplication();
@ -104,6 +104,7 @@ public class CompatActivity extends AppCompatActivity {
@Override
protected void onResume() {
this.hasHardwareNavBar = this.hasHardwareNavBar0();
super.onResume();
this.refreshUI();
}
@ -287,9 +288,13 @@ public class CompatActivity extends AppCompatActivity {
public boolean hasHardwareNavBar() {
// If onCreate has not been called yet, cached value is not valid
return this.onCreateCalledOnce ? this.hasHardwareNavBar :
ViewConfiguration.get(this).hasPermanentMenuKey() ||
KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
return this.onCreateCalledOnce ? this.hasHardwareNavBar : this.hasHardwareNavBar0();
}
private boolean hasHardwareNavBar0() {
return (ViewConfiguration.get(this).hasPermanentMenuKey() ||
KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)) &&
!"0".equals(SystemProperties.get("qemu.hw.mainkeys"));
}
public void setActionBarExtraMenuButton(@DrawableRes int drawableResId,

@ -16,6 +16,7 @@ import android.widget.Toast;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.mmm.ActionButtonType;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
@ -26,6 +27,7 @@ import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.PropUtils;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
@ -261,23 +263,39 @@ public class InstallerActivity extends CompatActivity {
.to(installerController, installerMonitor);
} else {
String arch32 = "true"; // Do nothing by default
boolean needs32bit = false;
String moduleId = null;
try (ZipFile zipFile = new ZipFile(file)) {
if (zipFile.getEntry( // Check if module hard require 32bit support
"common/addon/Volume-Key-Selector/tools/arm64/keycheck") == null &&
zipFile.getEntry(
"common/addon/Volume-Key-Selector/install.sh") != null) {
needs32bit = true;
}
moduleId = PropUtils.readModuleId(zipFile
.getInputStream(zipFile.getEntry("module.prop")));
} catch (IOException ignored) {}
int compatFlags = AppUpdateManager.getFlagsForModule(moduleId);
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NEED_32BIT) != 0)
needs32bit = true;
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NO_EXT) != 0)
noExtensions = true;
if (moduleId != null && (moduleId.isEmpty() ||
moduleId.contains("/") || moduleId.contains("\0") ||
(moduleId.startsWith(".") && moduleId.endsWith(".")))) {
this.setInstallStateFinished(false,
"! This module contain a dangerous moduleId",
null);
return;
}
if (Build.SUPPORTED_32_BIT_ABIS.length == 0) {
boolean needs32bit = false;
try (ZipFile zipFile = new ZipFile(file)) {
if (zipFile.getEntry( // Check if module hard require 32bit support
"common/addon/Volume-Key-Selector/tools/arm64/keycheck") == null &&
zipFile.getEntry(
"common/addon/Volume-Key-Selector/install.sh") != null) {
needs32bit = true;
}
} catch (IOException ignored) {}
if (needs32bit) {
this.setInstallStateFinished(false,
"! This module can't be installed on a 64bit only system",
null);
return;
}
} else {
} else if (needs32bit || (compatFlags & AppUpdateManager.FLAG_COMPAT_NO_EXT) == 0) {
// Restore Magisk legacy stuff for retro compatibility
if (Build.SUPPORTED_32_BIT_ABIS[0].contains("arm"))
arch32 = "export ARCH32=arm";
@ -288,7 +306,8 @@ public class InstallerActivity extends CompatActivity {
File installExecutable;
if (InstallerInitializer.peekMagiskVersion() >=
Constants.MAGISK_VER_CODE_INSTALL_COMMAND &&
(noExtensions || MainApplication.isUsingMagiskCommand())) {
((compatFlags & AppUpdateManager.FLAG_COMPAT_MAGISK_CMD) != 0 ||
noExtensions || MainApplication.isUsingMagiskCommand())) {
installCommand = "magisk --install-module \"" + file.getAbsolutePath() + "\"";
installExecutable = new File(InstallerInitializer.peekMagiskPath()
.equals("/sbin") ? "/sbin/magisk" : "/system/bin/magisk");
@ -296,13 +315,14 @@ public class InstallerActivity extends CompatActivity {
installExecutable = this.extractInstallScript("module_installer_compat.sh");
if (installExecutable == null) {
this.setInstallStateFinished(false,
"! Failed to extract module install script", "");
"! Failed to extract module install script", null);
return;
}
installCommand = "sh \"" + installExecutable.getAbsolutePath() + "\"" +
" /dev/null 1 \"" + file.getAbsolutePath() + "\"";
}
installerMonitor = new InstallerMonitor(installExecutable);
if (moduleId != null) installerMonitor.setForCleanUp(moduleId);
if (noExtensions) {
installJob = Shell.su(arch32, // No Extensions
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
@ -462,6 +482,7 @@ public class InstallerActivity extends CompatActivity {
private static final String DEFAULT_ERR = "! Install failed";
private final String installScriptErr;
public String lastCommand = "";
public String forCleanUp;
public InstallerMonitor(File installScript) {
super(Runnable::run);
@ -476,6 +497,10 @@ public class InstallerActivity extends CompatActivity {
this.lastCommand = s;
}
public void setForCleanUp(String forCleanUp) {
this.forCleanUp = forCleanUp;
}
private String doCleanUp() {
String installScriptErr = this.installScriptErr;
// This block is mainly to help fixing customize.sh syntax errors
@ -490,6 +515,10 @@ public class InstallerActivity extends CompatActivity {
Log.e(TAG, "Failed to delete failed update");
return "Error: " + installScriptErr.substring(i + 1);
}
} else if (this.forCleanUp != null) {
SuFile moduleUpdate = new SuFile("/data/adb/modules_update/" + this.forCleanUp);
if (moduleUpdate.exists() && !moduleUpdate.deleteRecursive())
Log.e(TAG, "Failed to delete failed update");
}
return DEFAULT_ERR;
}

@ -30,8 +30,21 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
public class IntentHelper {
private static final String TAG = "IntentHelper";
public static void openUri(Context context, String uri) {
if (uri.startsWith("intent://")) {
try {
startActivity(context, Intent.parseUri(uri, Intent.URI_INTENT_SCHEME), false);
} catch (URISyntaxException | ActivityNotFoundException e) {
Log.e(TAG, "Failed launch of " + uri, e);
}
} else openUrl(context, uri);
}
public static void openUrl(Context context, String url) {
openUrl(context, url, false);
}
@ -227,7 +240,7 @@ public class IntentHelper {
callback.onReceived(destination, null, RESPONSE_ERROR);
return;
}
Log.d("IntentHelper", "FilePicker returned " + uri);
Log.d(TAG, "FilePicker returned " + uri);
if ("http".equals(uri.getScheme()) ||
"https".equals(uri.getScheme())) {
callback.onReceived(destination, uri, RESPONSE_URL);
@ -259,7 +272,7 @@ public class IntentHelper {
Files.copy(inputStream, outputStream);
success = true;
} catch (Exception e) {
Log.e("IntentHelper", "failed copy of " + uri, e);
Log.e(TAG, "failed copy of " + uri, e);
Toast.makeText(compatActivity,
R.string.file_picker_failure,
Toast.LENGTH_SHORT).show();
@ -267,7 +280,7 @@ public class IntentHelper {
Files.closeSilently(inputStream);
Files.closeSilently(outputStream);
if (!success && destination.exists() && !destination.delete())
Log.e("IntentHelper", "Failed to delete artefact!");
Log.e(TAG, "Failed to delete artefact!");
}
callback.onReceived(destination, uri, success ? RESPONSE_FILE : RESPONSE_ERROR);
});

@ -1,7 +1,11 @@
package com.fox2code.mmm.utils;
import static com.fox2code.mmm.AppUpdateManager.FLAG_COMPAT_LOW_QUALITY;
import static com.fox2code.mmm.AppUpdateManager.getFlagsForModule;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import com.fox2code.mmm.manager.ModuleInfo;
import com.topjohnwu.superuser.io.SuFileInputStream;
@ -274,6 +278,22 @@ public class PropUtils {
}
}
public static String readModuleId(InputStream inputStream) {
String moduleId = null;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.startsWith("id=")) {
moduleId = line.substring(3).trim();
}
}
} catch (IOException e) {
Log.d("PropUtils", "Failed to get moduleId", e);
}
return moduleId;
}
public static void applyFallbacks(ModuleInfo moduleInfo) {
if (moduleInfo.support == null || moduleInfo.support.isEmpty()) {
moduleInfo.support = moduleSupportsFallbacks.get(moduleInfo.id);
@ -299,7 +319,8 @@ public class PropUtils {
|| moduleInfo.author == null || !TextUtils.isGraphic(moduleInfo.author)
|| (description = moduleInfo.description) == null || !TextUtils.isGraphic(description)
|| description.toLowerCase(Locale.ROOT).equals(moduleInfo.name.toLowerCase(Locale.ROOT))
|| description.length() < Math.min(Math.max(moduleInfo.name.length() + 4, 16), 24);
|| description.length() < Math.min(Math.max(moduleInfo.name.length() + 4, 16), 24)
|| (getFlagsForModule(moduleInfo.id) & FLAG_COMPAT_LOW_QUALITY) != 0;
}
private static boolean isInvalidValue(String name) {

@ -0,0 +1,7 @@
<resources>
<string-array name="theme_values_names">
<item>Sistem</item>
<item>Gelap</item>
<item>Terang</item>
</string-array>
</resources>

@ -0,0 +1,96 @@
<resources>
<string name="app_name">Fox\'s Magisk Module Manager</string>
<string name="app_name_short">Fox\'s Mmm</string>
<string name="fail_root_magisk">Tidak dapat mengakses root atau Magisk</string>
<string name="loading">Memuat…</string>
<string name="updatable">Dapat diperbarui</string>
<string name="installed">Terpasang</string>
<string name="online_repo">Repo Online</string>
<string name="showcase_mode">Aplikasi berada dalam mode lockdown</string>
<string name="failed_download">Tidak dapat mengunduh file.</string>
<string name="slow_modules">Modul membutuhkan waktu yang terlalu lama untuk melakukan boot, pertimbangkanglah untuk mematikan beberapa modul</string>
<string name="fail_internet">Tidak dapat terhubung dengan internet</string>
<string name="title_activity_settings">SettingsActivity</string>
<string name="app_update_available">Tersedia pembaruan aplikasi</string>
<string name="app_update">Perbarui</string>
<string name="no_desc_found">Tidak dapat menemukan deskripsi.</string>
<string name="download_module">Unduh modul</string>
<string name="install_module">Instal modul</string>
<string name="update_module">Perbarui modul</string>
<string name="changelog">Changelog</string>
<string name="website">Situs</string>
<string name="support">Dukungan</string>
<string name="donate">Donasi</string>
<string name="submit_modules">Kirim sebuah modul</string>
<string name="require_android_6">Membutuhkan Android 6.0+</string>
<!-- Module section translation -->
<string name="module_last_update">Pembaruan terakhir:</string>
<string name="module_repo">Repo:</string>
<string name="module_by">oleh</string>
<string name="module_downloads">Diunduh:</string>
<string name="module_stars">Bintang:</string>
<!-- Preference Titles -->
<!-- Note: Lockdown mode used to be called showcase mode -->
<string name="manage_repos_pref">Kelola repo</string>
<string name="showcase_mode_pref">Mode lockdown</string>
<string name="showcase_mode_desc">Mode lockdown mencegah manager dari melakukan tindakan terhadap modul</string>
<string name="pref_category_settings">Peraturan</string>
<string name="pref_category_info">Info</string>
<string name="show_licenses">Perlihatkan lisensi</string>
<string name="licenses">Lisensi</string>
<string name="show_incompatible_pref">Tampilkan modul tidak kompatibel</string>
<string name="show_incompatible_desc">Tampilkan modul yang tidak kompatibel dengan perangkat anda berdasarkan metadata mereka</string>
<string name="magisk_outdated">Versi Magisk usang!</string>
<string name="pref_category_repos">Repos</string>
<string name="repo_main_desc">Repository yang menampung modul Magisk</string>
<string name="repo_main_alt">Sebuah alternatif Magisk-Modules-Repo dengan restriksi lebih ringan.</string>
<string name="master_delete">Hapus file modul?</string>
<string name="master_delete_no">Simpan file</string>
<string name="master_delete_yes">Hapus file</string>
<string name="master_delete_fail">Tidak dapat menghapus file modul</string>
<string name="theme_pref">Tema</string>
<string name="theme_mode_pref">Mode tema</string>
<string name="module_id_prefix">Id modul: </string>
<string name="install_from_storage">Instal modul dari penyimpanan</string>
<string name="invalid_format">Modul yang diseleksi mempunyai format yang tidak valid</string>
<string name="local_install_title">Instal lokal</string>
<string name="source_code">Kode sumber</string>
<string name="magisk_builtin_module">Modul bawaan Magisk</string>
<string name="substratum_builtin_module">Modul bawaan Substratum</string>
<string name="force_dark_terminal_title">Paksa mode gelap pada terminal</string>
<string name="file_picker_failure">Pemilih file anda sekarang gagal memberikan akses kepada file.</string>
<string name="remote_install_title">Instal jarak jauh</string>
<string name="file_picker_wierd">Pemilih file anda mengembalikan tanggapan yang tidak standar.</string>
<string name="use_magisk_install_command_pref">Gunakan perintah instal modul magisk</string>
<string name="use_magisk_install_command_desc">
Waktu diuji ini menyebabkan masalah kepada alat diagnosis kesalahan instal modul,
jadi saya menyembunyikan opsi ini dibelakan mode pengembang, aktifkan pada risiko anda sendiri!
</string>
<string name="dev_mode_enabled">Mode pengembang aktif</string>
<string name="force_english_pref">Paksa Bahasa Inggris</string>
<string name="disable_low_quality_module_filter_pref">Nonaktifkan filter modul qualitas rendah</string>
<string name="disable_low_quality_module_filter_desc">
Beberapa modul tidak menyatakan metadata mereka dengan benar,menyebabkan glitch visual,
dan/atau mengindikasikan modul qualitas rendah, nonaktifkan pada risiko anda sendiri!
</string>
<string name="dns_over_https_pref">DNS melalui HTTPS</string>
<string name="dns_over_https_desc">
Mungkin dapat memperbaiki masalah koneksi didalam beberapa kasus.
(Tidak berlaku untuk WebView)
</string>
<string name="disable_extensions_pref">Nonaktifkan ekstensi</string>
<string name="disable_extensions_desc">
Nonaktifkan ekstensi Fox\'s Mmm, ini mencegah modul dari menggunakan
ekstensi terminal, berguna jika sebuah modul menyalahgunakan ekstensi Fox\'s Mmm.
</string>
<string name="wrap_text_pref">Wrap teks</string>
<string name="wrap_text_desc">
Wrap teks ke sebuah garis baru daripada menaruh
semua teks pada garis yang sama pada saat menginstal sebuah modul
</string>
<string name="enable_blur_pref">Aktifkan blur</string>
<string name="repo_enabled">Repo diaktifkan</string>
<string name="repo_disabled">Repo dinonaktifkan</string>
</resources>

@ -22,6 +22,7 @@
<string name="support">支持</string>
<string name="donate">捐赠</string>
<string name="submit_modules">提交一个模块</string>
<string name="require_android_6">需要 Android 6.0+</string>
<!-- Module section translation -->
<string name="module_last_update">最后更新:</string>
@ -50,6 +51,7 @@
<string name="master_delete_yes">删除文件</string>
<string name="master_delete_fail">无法删除模块文件</string>
<string name="theme_pref">主题</string>
<string name="theme_mode_pref">主题模式</string>
<string name="module_id_prefix">模块ID: </string>
<string name="install_from_storage">从存储空间安装模块</string>
<string name="invalid_format">所选模块的格式无效</string>
@ -88,6 +90,7 @@
将文本换行至新行,
而不是将所有文本放在同一行
</string>
<string name="enable_blur_pref">启用模糊</string>
<string name="repo_enabled">启用仓库</string>
<string name="repo_disabled">禁用仓库</string>
</resources>

Loading…
Cancel
Save