diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2a246a41..12465060 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -69,6 +69,11 @@ along with SwiFTP. If not, see . android:parentActivityName=".gui.MainActivity" android:theme="@style/AppThemeDark" /> + + diff --git a/app/src/main/java/be/ppareit/swiftp/FsSettings.java b/app/src/main/java/be/ppareit/swiftp/FsSettings.java index 61749370..2fd9ba35 100644 --- a/app/src/main/java/be/ppareit/swiftp/FsSettings.java +++ b/app/src/main/java/be/ppareit/swiftp/FsSettings.java @@ -138,6 +138,14 @@ public static int getPortNumber() { return port; } + public static int getAnonMaxConNumber() { + final SharedPreferences sp = getSharedPreferences(); + String s = sp.getString("anon_max", "1"); + int i = Integer.parseInt(s); + Log.v(TAG, "Using anon max connections: " + i); + return i; + } + public static boolean shouldTakeFullWakeLock() { final SharedPreferences sp = getSharedPreferences(); return sp.getBoolean("stayAwake", false); diff --git a/app/src/main/java/be/ppareit/swiftp/gui/ManageAnonActivity.java b/app/src/main/java/be/ppareit/swiftp/gui/ManageAnonActivity.java new file mode 100644 index 00000000..07248821 --- /dev/null +++ b/app/src/main/java/be/ppareit/swiftp/gui/ManageAnonActivity.java @@ -0,0 +1,136 @@ +package be.ppareit.swiftp.gui; + + +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; + +import androidx.core.app.NavUtils; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; + +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; + +import net.vrallev.android.cat.Cat; + +import be.ppareit.swiftp.App; +import be.ppareit.swiftp.FsSettings; +import be.ppareit.swiftp.R; +import be.ppareit.swiftp.utils.ChrootPicker; + +public class ManageAnonActivity extends AppCompatActivity { + + private static final int ACTION_OPEN_DOCUMENT_TREE = 42; + ChrootPicker chrootPicker = null; + + @SuppressLint("ClickableViewAccessibility") + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(FsSettings.getTheme()); + super.onCreate(savedInstanceState); + + setContentView(R.layout.anon_layout); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + } + + chrootPicker = new ChrootPicker(); + TextView chroot = findViewById(R.id.anon_chroot); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext()); + String chrootString = sp.getString("anonChroot", ""); + chroot.setText(chrootString); + chroot.setOnTouchListener((v, event) -> { + sp.edit().remove("anonChroot").apply(); + sp.edit().remove("anonUriString").apply(); + if (event.getAction() == MotionEvent.ACTION_DOWN) { + chrootPicker.showFolderPicker(chroot.getText().toString(), this, null); + } + return true; + }); + chrootPicker.setOnTextEventListener(s -> { + chroot.setText(s); + sp.edit().putString("anonChroot", s).apply(); + }); + chrootPicker.setOnActionTreeEventListener(() -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE); + }); + + EditText anonMaxCon = findViewById(R.id.anon_max); + anonMaxCon.setText(String.valueOf(FsSettings.getAnonMaxConNumber())); + anonMaxCon.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + sp.edit().putString("anon_max", s.toString()).apply(); + } + }); + + CheckBox anonCB = findViewById(R.id.anon_enable); + anonCB.setChecked(sp.getBoolean("allow_anonymous", false)); + anonCB.setOnCheckedChangeListener((buttonView, isChecked) -> { + SharedPreferences sp1 = PreferenceManager.getDefaultSharedPreferences(App.getAppContext()); + if (isChecked ) { + if (sp.getString("anonChroot", "").isEmpty()) { + // Deny as workaround to problems like app crashing with scoped storage. + anonCB.setChecked(false); + final ObjectAnimator loading = ObjectAnimator.ofObject((TextView) chroot, "backgroundColor", + new ArgbEvaluator(), Color.RED, Color.TRANSPARENT).setDuration(1000); + loading.start(); + return; + } + sp1.edit().putBoolean("allow_anonymous", true).apply(); + } + else sp1.edit().putBoolean("allow_anonymous", false).apply(); + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + super.onActivityResult(requestCode, resultCode, resultData); + Cat.d("onActivityResult called"); + if (requestCode == ACTION_OPEN_DOCUMENT_TREE && resultCode == Activity.RESULT_OK) { + if (resultData == null) return; + Uri treeUri = resultData.getData(); + if (treeUri == null) return; + String path = treeUri.getPath(); + Cat.d("Action Open Document Tree on path " + path); + chrootPicker.save(this.getApplicationContext(), treeUri); + String uriString = treeUri.getPath(); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext()); + sp.edit().putString("anonUriString", uriString).apply(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ppareit/swiftp/gui/PreferenceFragment.java b/app/src/main/java/be/ppareit/swiftp/gui/PreferenceFragment.java index 23e37a13..0a88e742 100644 --- a/app/src/main/java/be/ppareit/swiftp/gui/PreferenceFragment.java +++ b/app/src/main/java/be/ppareit/swiftp/gui/PreferenceFragment.java @@ -110,6 +110,12 @@ public void onCreate(Bundle savedInstanceState) { return true; }); + Preference manageAnonPref = findPref("manage_anon"); + manageAnonPref.setOnPreferenceClickListener((preference) -> { + startActivity(new Intent(getActivity(), ManageAnonActivity.class)); + return true; + }); + EditTextPreference portNumberPref = findPref("portNum"); portNumberPref.setSummary(String.valueOf(FsSettings.getPortNumber())); portNumberPref.setOnPreferenceChangeListener((preference, newValue) -> { diff --git a/app/src/main/java/be/ppareit/swiftp/gui/UserEditFragment.java b/app/src/main/java/be/ppareit/swiftp/gui/UserEditFragment.java index 2a4d1018..ea926d23 100644 --- a/app/src/main/java/be/ppareit/swiftp/gui/UserEditFragment.java +++ b/app/src/main/java/be/ppareit/swiftp/gui/UserEditFragment.java @@ -1,12 +1,15 @@ package be.ppareit.swiftp.gui; -import android.app.AlertDialog; +import android.app.Activity; import android.app.Fragment; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; -import android.os.Environment; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,17 +17,22 @@ import android.widget.TextView; import android.widget.Toast; -import java.io.File; +import net.vrallev.android.cat.Cat; import be.ppareit.swiftp.FsSettings; import be.ppareit.swiftp.R; import be.ppareit.swiftp.server.FtpUser; +import be.ppareit.swiftp.utils.ChrootPicker; public class UserEditFragment extends Fragment { + private static final int ACTION_OPEN_DOCUMENT_TREE = 42; + private FtpUser item; private OnEditFinishedListener editFinishedListener; - private boolean isShowingFolderPicker = false; + private TextView chroot = null; + private String uriString = ""; + private ChrootPicker chrootPicker = null; public static UserEditFragment newInstance(@Nullable FtpUser item, @NonNull OnEditFinishedListener listener) { UserEditFragment fragment = new UserEditFragment(); @@ -40,14 +48,21 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, View root = inflater.inflate(R.layout.user_edit_layout, container, false); EditText username = (EditText) root.findViewById(R.id.user_edit_name); EditText password = (EditText) root.findViewById(R.id.user_edit_password); - TextView chroot = (TextView) root.findViewById(R.id.user_edit_chroot); + chrootPicker = new ChrootPicker(); + chroot = (TextView) root.findViewById(R.id.user_edit_chroot); chroot.setText(FsSettings.getDefaultChrootDir().getPath()); chroot.setOnFocusChangeListener((v, hasFocus) -> { - if (!hasFocus) - return; - showFolderPicker(chroot); + if (!hasFocus) return; + chrootPicker.showFolderPicker(chroot.getText().toString(), null, getContext()); + }); + chroot.setOnClickListener(v -> { + chrootPicker.showFolderPicker(chroot.getText().toString(), null, getContext()); + }); + chrootPicker.setOnTextEventListener(s -> chroot.setText(s)); + chrootPicker.setOnActionTreeEventListener(() -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE); }); - chroot.setOnClickListener(view -> showFolderPicker(chroot)); if (item != null) { username.setText(item.getUsername()); @@ -60,7 +75,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, String newPassword = password.getText().toString(); String newChroot = chroot.getText().toString(); if (validateInput(newUsername, newPassword)) { - editFinishedListener.onEditActionFinished(item, new FtpUser(newUsername, newPassword, newChroot)); + editFinishedListener.onEditActionFinished(item, new FtpUser(newUsername, newPassword, newChroot, uriString)); getActivity().onBackPressed(); } }); @@ -68,30 +83,20 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, return root; } - private void showFolderPicker(TextView chrootView) { - if (isShowingFolderPicker) - return; - isShowingFolderPicker = true; - final File startDir; - if (chrootView.getText().toString().isEmpty()) { - startDir = Environment.getExternalStorageDirectory(); - } else { - startDir = new File((chrootView.getText().toString())); + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + Cat.d("onActivityResult called"); + if (requestCode == ACTION_OPEN_DOCUMENT_TREE && resultCode == Activity.RESULT_OK) { + if (resultData == null) return; + Uri treeUri = resultData.getData(); + if (treeUri == null) return; + String path = treeUri.getPath(); + Cat.d("Action Open Document Tree on path " + path); + // ************************************* + // The order following here is critical. They must stay ordered as they are. + chrootPicker.save(this.getContext(), treeUri); + uriString = treeUri.getPath(); } - AlertDialog folderPicker = new FolderPickerDialogBuilder(getActivity(), startDir) - .setSelectedButton(R.string.select, path -> { - final File root = new File(path); - if (!root.canRead()) { - showToast(R.string.notice_cant_read_write); - } else if (!root.canWrite()) { - showToast(R.string.notice_cant_write); - } - chrootView.setText(path); - }) - .setNegativeButton(R.string.cancel, null) - .create(); - folderPicker.setOnDismissListener(dialog -> isShowingFolderPicker = false); - folderPicker.show(); } private boolean validateInput(String username, String password) { diff --git a/app/src/main/java/be/ppareit/swiftp/server/CmdPASS.java b/app/src/main/java/be/ppareit/swiftp/server/CmdPASS.java index 7f2821b1..0a53eb8f 100644 --- a/app/src/main/java/be/ppareit/swiftp/server/CmdPASS.java +++ b/app/src/main/java/be/ppareit/swiftp/server/CmdPASS.java @@ -19,9 +19,15 @@ package be.ppareit.swiftp.server; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.util.Log; + +import be.ppareit.swiftp.App; import be.ppareit.swiftp.FsSettings; import be.ppareit.swiftp.Util; +import be.ppareit.swiftp.utils.AnonymousLimit; +import be.ppareit.swiftp.utils.Logging; public class CmdPASS extends FtpCmd implements Runnable { private static final String TAG = CmdPASS.class.getSimpleName(); @@ -44,8 +50,35 @@ public void run() { return; } if (attemptUsername.equals("anonymous") && FsSettings.allowAnonymous()) { - Log.i(TAG, "Guest logged in with email: " + attemptPassword); - sessionThread.writeString("230 Guest login ok, read only access.\r\n"); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext()); + final int anonMaxCon = Integer.parseInt(sp.getString("anon_max", "1")); + Logging logging = new Logging(); + final int newCount = AnonymousLimit.incrementAndGet(); + logging.appendLog("anon CURRENT client conn count: " + (newCount - 1)); + logging.appendLog("anon MAX conn count: " + anonMaxCon); + if (newCount > anonMaxCon) { + Log.i(TAG, "Failed authentication, too many anonymous users connected."); + Util.sleepIgnoreInterrupt(1000); // sleep to foil brute force attack + sessionThread.writeString("421 too many anonymous users connected.\r\n"); + sessionThread.authAttempt(false); + } else { + Log.i(TAG, "Guest logged in with email: " + attemptPassword); + sessionThread.writeString("230 Guest login ok, read only access.\r\n"); + final String anonChroot = sp.getString("anonChroot", "/storage/emulated/0" /*backwards compat*/); + final String anonUriString = sp.getString("anonUriString", ""); + if (!anonChroot.isEmpty()) { + sessionThread.setChrootDir(anonChroot); + if (!anonUriString.isEmpty()) { + SessionThread.putUriString(Thread.currentThread().getName(), anonUriString); + } else if (Util.useScopedStorage()) { + // Protect against app crashes/problems + Log.i(TAG, "Failed authentication, too many anonymous users connected."); + Util.sleepIgnoreInterrupt(1000); // sleep to foil brute force attack + sessionThread.writeString("421 too many anonymous users connected.\r\n"); + sessionThread.authAttempt(false); + } + } + } return; } FtpUser user = FsSettings.getUser(attemptUsername); @@ -59,6 +92,9 @@ public void run() { sessionThread.writeString("230 Access granted\r\n"); sessionThread.authAttempt(true); sessionThread.setChrootDir(user.getChroot()); + if (Util.useScopedStorage()) { + SessionThread.putUriString(Thread.currentThread().getName(), user.getUriString()); + } } else { Log.i(TAG, "Failed authentication, incorrect password"); Util.sleepIgnoreInterrupt(1000); // sleep to foil brute force attack diff --git a/app/src/main/java/be/ppareit/swiftp/server/SessionThread.java b/app/src/main/java/be/ppareit/swiftp/server/SessionThread.java index 7a485ff8..2b9ede2c 100644 --- a/app/src/main/java/be/ppareit/swiftp/server/SessionThread.java +++ b/app/src/main/java/be/ppareit/swiftp/server/SessionThread.java @@ -35,6 +35,7 @@ import be.ppareit.swiftp.App; import be.ppareit.swiftp.FsSettings; +import be.ppareit.swiftp.utils.AnonymousLimit; public class SessionThread extends Thread { @@ -256,6 +257,7 @@ public void run() { Cat.i("Connection was dropped"); } closeSocket(); + AnonymousLimit.decrement(); } public void closeSocket() { diff --git a/app/src/main/java/be/ppareit/swiftp/utils/AnonymousLimit.java b/app/src/main/java/be/ppareit/swiftp/utils/AnonymousLimit.java new file mode 100644 index 00000000..6dfce0ac --- /dev/null +++ b/app/src/main/java/be/ppareit/swiftp/utils/AnonymousLimit.java @@ -0,0 +1,15 @@ +package be.ppareit.swiftp.utils; + +import java.util.concurrent.atomic.AtomicInteger; + +public class AnonymousLimit { + static AtomicInteger anonUsersConnected = new AtomicInteger(0); + + public static int incrementAndGet() { + return anonUsersConnected.incrementAndGet(); + } + + public static void decrement() { + if (anonUsersConnected.get() >= 1) anonUsersConnected.decrementAndGet(); + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ppareit/swiftp/utils/ChrootPicker.java b/app/src/main/java/be/ppareit/swiftp/utils/ChrootPicker.java new file mode 100644 index 00000000..853c73e6 --- /dev/null +++ b/app/src/main/java/be/ppareit/swiftp/utils/ChrootPicker.java @@ -0,0 +1,124 @@ +package be.ppareit.swiftp.utils; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; + +import java.io.File; + +import be.ppareit.swiftp.FsSettings; +import be.ppareit.swiftp.R; +import be.ppareit.swiftp.Util; +import be.ppareit.swiftp.gui.FolderPickerDialogBuilder; + +public class ChrootPicker { + + public ChrootPicker() { + } + + public interface OnTextEventListener { + void OnEvent(String s); + } + + public interface OnActionEventListener { + void OnEvent(); + } + + public OnTextEventListener onTextEventListener; + public OnActionEventListener onActionEventListener; + + public void setOnTextEventListener(OnTextEventListener onTextEventListener) { + this.onTextEventListener = onTextEventListener; + } + + public void setOnActionTreeEventListener(OnActionEventListener onActionEventListener) { + this.onActionEventListener = onActionEventListener; + } + + private boolean isShowingFolderPicker = false; + + public void save(Context context, Uri treeUri) { + // ************************************* + // The order following here is critical. They must stay ordered as they are. + setPermissionToUseExternalStorage(treeUri, context); + scopedStorageChrootOverride(treeUri); + } + + private void setPermissionToUseExternalStorage(Uri treeUri, Context context) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + FsSettings.setExternalStorageUri(treeUri.toString()); + context.getContentResolver() + .takePersistableUriPermission(treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + } catch (SecurityException e) { + // Harden code against crash: May reach here by adding exact same picker location but + // being removed at same time. + } + } + + private void scopedStorageChrootOverride(Uri treeUri) { + if (Util.useScopedStorage()) { + DocumentFile df = FileUtil.getDocumentFileFromUri(treeUri); + if (df == null) return; + String newPath = "/storage/emulated/0/"; + String treePath = treeUri.getPath(); + if (treePath == null) return; + if (treePath.contains("primary:")) + treePath = treePath.substring(treePath.indexOf(":") + 1); + else if (treePath.contains(":")) { + newPath = "/storage/"; + treePath = treePath.replace("/tree/", ""); + treePath = treePath.replace(":", "/"); + } + newPath += treePath; + if (onTextEventListener != null) onTextEventListener.OnEvent(newPath); + } + } + + public void showFolderPicker(String s, @Nullable Activity a, Context fragment /*Fragment use*/) { + if (Util.useScopedStorage()) { + if (onActionEventListener != null) onActionEventListener.OnEvent(); + return; + } + if (isShowingFolderPicker) + return; + isShowingFolderPicker = true; + final File startDir; + if (s.isEmpty()) { + startDir = Environment.getExternalStorageDirectory(); + } else { + startDir = new File(s); + } + AlertDialog folderPicker = new FolderPickerDialogBuilder(a != null ? a : fragment, startDir) + .setSelectedButton(R.string.select, path -> { + final File root = new File(path); + if (!root.canRead()) { + showToast(R.string.notice_cant_read_write, + a != null ? a : fragment); + } else if (!root.canWrite()) { + showToast(R.string.notice_cant_write, + a != null ? a : fragment); + } + if (onTextEventListener != null) onTextEventListener.OnEvent(path); + }) + .setNegativeButton(R.string.cancel, null) + .create(); + folderPicker.setOnDismissListener(dialog -> isShowingFolderPicker = false); + folderPicker.show(); + } + + private void showToast(int errorResId, Context context) { + Toast.makeText(context, errorResId, Toast.LENGTH_LONG).show(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/anon_layout.xml b/app/src/main/res/layout/anon_layout.xml new file mode 100644 index 00000000..d819cffa --- /dev/null +++ b/app/src/main/res/layout/anon_layout.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 dc66cfee..72f4532d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -89,4 +89,6 @@ Gemeldete Probleme und Verbesserungsvorschläge sind unter https://github.com/pp Gemischtes Thema Thema... + Max. gleichzeitige Verbindungen + Anonym verwalten… diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 8bae480d..7237ca8b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -143,5 +143,7 @@ https://github.com/ppareit/swiftp/issues .\n\n Φωτεινό Θέμα Ανάμεικτο Θέμα + Μέγιστες ταυτόχρονες συνδέσεις + Διαχείριση ανώνυμων… diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e01237ed..185b4100 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -166,5 +166,7 @@ implementarán, mira el registro de incidencias en https://github.com/ppareit/sw En estos dispositivos, toda lo guardado está disponible utilizando la jerarquía de archivos. + Conexiones simultáneas máximas + Administrar anónimo… diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cb8d23d5..9db38183 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -148,4 +148,6 @@ https://github.com/ppareit/swiftp/issues .\n\n Thème mixte Thème... + Nombre maximum de connexions simultanées + Gérer les anonymes… diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 75689352..e12272f6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -93,4 +93,6 @@ https://github.com/ppareit/swiftp/issues .\n\n Tema misto Tema... + Numero massimo di connessioni simultanee + Gestisci anonimo… diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 68299ef8..b44d436b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -127,4 +127,6 @@ https://github.com/ppareit/swiftp/issues を参照してください。\n\n 混合テーマ テーマ... + 最大同時接続数 + 匿名で管理する… diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 624f0a52..6678ea3e 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -95,4 +95,6 @@ van de volgende versie staan op https://github.com/ppareit/swiftp/issues .\n\n Gemengd thema Thema... + Maximaal gelijktijdige verbindingen + Beheer anoniem… diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 622afa3b..291443ee 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -192,4 +192,6 @@ na stronie https://github.com/ppareit/swiftp/issues .\n\n Na tych urządzeniach cały sklep jest dostępny przy użyciu hierarchii plików. + Maksymalna liczba jednoczesnych połączeń + Zarządzaj anonimowo… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e5560b8c..186509a6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -108,4 +108,6 @@ https://github.com/ppareit/swiftp/issues .\n\n Смешанная тема Тема... + Максимальное количество одновременных подключений + Управление анонимностью… diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 80834db1..f05f553c 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -194,4 +194,6 @@ https://github.com/ppareit/swiftp/issues .\n\n Në këto pajisje, e gjithë dyqani është në dispozicion duke përdorur hierarkinë e skedarit. + Lidhjet maksimale të njëkohshme + Menaxho anonim… diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 53fa7851..475cc905 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -127,4 +127,6 @@ https://github.com/ppareit/swiftp/issues .\n\n микед Тема Тема... + Максимално истовремене везе + Управљајте анонимно… diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 3519dae6..7a63e081 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -165,5 +165,6 @@ https://github.com/ppareit/swiftp/issues .\n\n На цих пристроях вся пам\'ять доступна за допомогою ієрархії файлів. - + Максимальна кількість одночасних підключень + Керувати анонімно… diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c3153a07..5aef6ace 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -82,4 +82,6 @@ https://github.com/ppareit/swiftp/issues 提交您的建议。\n\n 警告:当前存储暂不可用,您可能需要取消挂载。 FTP Server Widget + 最大同时连接数 + 管理匿名… diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 08d506a6..21444d23 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -182,4 +182,6 @@ https://github.com/ppareit/swiftp/issues 提交您的建議。\n\n 在這些裝置上,可以使用檔案階層標準來使用所有存儲空間。 + 最大同時連線數 + 管理匿名… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2398f273..8c49f3c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,4 +193,6 @@ https://github.com/ppareit/swiftp/issues .\n\n On these devices, all store is available using the file hierarchy. + Max simultaneous connections + Manage anonymous… diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index b1417a93..de3d41b6 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -38,11 +38,9 @@ along with SwiFTP. If not, see . android:key="manage_users" android:title="@string/manage_users_label" /> - +