Refine public proxy

This commit is contained in:
arm64v8a 2021-12-03 21:24:28 +08:00
parent 2432283df7
commit 6d4984b864
9 changed files with 62 additions and 243 deletions

View File

@ -21,7 +21,6 @@ import android.util.Base64;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.pm.ShortcutManagerCompat;
import com.v2ray.ang.V2RayConfig;
@ -29,15 +28,12 @@ import com.v2ray.ang.dto.AngConfig;
import com.v2ray.ang.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.dizitart.no2.objects.filters.ObjectFilters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.telegram.tgnet.ConnectionsManager;
import org.telegram.tgnet.SerializedData;
import org.telegram.ui.Components.SharedMediaLayout;
import org.telegram.ui.Components.SwipeGestureSettingsView;
import org.telegram.tgnet.TLRPC;
@ -65,6 +61,7 @@ import tw.nekomimi.nekogram.utils.AlertUtil;
import tw.nekomimi.nekogram.utils.EnvUtil;
import tw.nekomimi.nekogram.utils.FileUtil;
import tw.nekomimi.nekogram.utils.UIUtil;
import tw.nekomimi.nkmr.NekomuraConfig;
import static com.v2ray.ang.V2RayConfig.SSR_PROTOCOL;
import static com.v2ray.ang.V2RayConfig.SS_PROTOCOL;
@ -1963,224 +1960,111 @@ public class SharedConfig {
int current = MessagesController.getGlobalMainSettings().getInt("current_proxy", 0);
for (SubInfo subInfo : SubManager.getSubList().find()) {
if (!subInfo.enable) continue;
// if (subInfo.id == 1L) {
//
// try {
// RelayBatonProxy publicProxy = (RelayBatonProxy) parseProxyInfo(RelayBatonLoader.publicServer);
// publicProxy.setRemarks(LocaleController.getString("NekoXProxy",R.string.NekoXProxy));
// publicProxy.subId = subInfo.id;
// proxyList.add(publicProxy);
// if (publicProxy.hashCode() == current) {
// currentProxy = publicProxy;
// UIUtil.runOnIoDispatcher(publicProxy::start);
// }
// } catch (InvalidProxyException e) {
// e.printStackTrace();
// }
//
// }
for (String proxy : subInfo.proxies) {
try {
ProxyInfo info = parseProxyInfo(proxy);
info.subId = subInfo.id;
if (info.hashCode() == current) {
currentProxy = info;
if (info instanceof ExternalSocks5Proxy) {
UIUtil.runOnIoDispatcher(() -> {
try {
((ExternalSocks5Proxy) info).start();
} catch (Exception e) {
FileLog.e(e);
AlertUtil.showToast(e);
}
});
}
}
proxyList.add(info);
} catch (Exception e) {
FileLog.d("load sub proxy failed: " + e);
}
}
}
File proxyListFile = new File(ApplicationLoader.applicationContext.getFilesDir().getParentFile(), "nekox/proxy_list.json");
boolean error = false;
if (proxyListFile.isFile()) {
try {
JSONArray proxyArray = new JSONArray(FileUtil.readUtf8String(proxyListFile));
for (int a = 0; a < proxyArray.length(); a++) {
JSONObject proxyObj = proxyArray.getJSONObject(a);
ProxyInfo info;
try {
info = ProxyInfo.fromJson(proxyObj);
} catch (Exception ex) {
FileLog.d("load proxy failed: " + ex);
error = true;
continue;
}
proxyList.add(info);
if (info.hashCode() == current) {
currentProxy = info;
if (info instanceof ExternalSocks5Proxy) {
UIUtil.runOnIoDispatcher(() -> {
try {
((ExternalSocks5Proxy) info).start();
} catch (Exception e) {
FileLog.e(e);
AlertUtil.showToast(e);
}
});
}
}
}
} catch (Exception ex) {
FileLog.d("invalid proxy list json format" + ex);
}
}
if (error) saveProxyList();
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE);
boolean proxyEnabledValue = preferences.getBoolean("proxy_enabled", false);
if (proxyEnabledValue && currentProxy == null) proxyEnabledValue = false;
proxyEnabled = proxyEnabledValue;
}
public static ProxyInfo parseProxyInfo(String url) throws InvalidProxyException {
if (url.startsWith(V2RayConfig.VMESS_PROTOCOL) || url.startsWith(V2RayConfig.VMESS1_PROTOCOL) || url.startsWith(V2RayConfig.TROJAN_PROTOCOL)) {
try {
return new VmessProxy(url);
} catch (Exception ex) {
throw new InvalidProxyException(ex);
}
} else if (url.startsWith(SS_PROTOCOL)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
throw new InvalidProxyException("shadowsocks requires min api 21");
}
try {
return new ShadowsocksProxy(url);
} catch (Exception ex) {
throw new InvalidProxyException(ex);
}
} else if (url.startsWith(SSR_PROTOCOL)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
throw new InvalidProxyException("shadowsocksR requires min api 21");
}
try {
return new ShadowsocksRProxy(url);
} catch (Exception ex) {
throw new InvalidProxyException(ex);
}
} else if (url.startsWith(WS_PROTOCOL) || url.startsWith(WSS_PROTOCOL)) {
try {
return new WsProxy(url);
} catch (Exception ex) {
throw new InvalidProxyException(ex);
}
}/* else if (url.startsWith(RB_PROTOCOL)) {
try {
return new RelayBatonProxy(url);
} catch (Exception ex) {
throw new InvalidProxyException(ex);
}
} */
if (url.startsWith("tg:proxy") ||
@ -2191,9 +2075,7 @@ public class SharedConfig {
url.startsWith("https://t.me/socks")) {
return ProxyInfo.fromUrl(url);
}
throw new InvalidProxyException();
}
public static class InvalidProxyException extends Exception {

View File

@ -916,7 +916,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa
ExternalGcm.checkUpdate(this);
for (SubInfo subInfo : SubManager.getSubList().find()) {
if (NekomuraConfig.autoUpdateSubInfo.Bool()) for (SubInfo subInfo : SubManager.getSubList().find()) {
if (subInfo == null || !subInfo.enable) continue;

View File

@ -105,6 +105,7 @@ import tw.nekomimi.nekogram.utils.AlertUtil;
import tw.nekomimi.nekogram.utils.FileUtil;
import tw.nekomimi.nekogram.utils.ProxyUtil;
import tw.nekomimi.nekogram.utils.UIUtil;
import tw.nekomimi.nkmr.NekomuraConfig;
public class ProxyListActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate {
@ -120,6 +121,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
private int rowCount;
private int useProxyRow;
private int enablePublicProxyRow;
private int useProxyDetailRow;
private int connectionsHeaderRow;
private int proxyStartRow;
@ -682,6 +684,34 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
NotificationCenter.getGlobalInstance().addObserver(ProxyListActivity.this, NotificationCenter.proxySettingsChanged);
updateRows(true);
} else if (position == enablePublicProxyRow) {
final boolean enabled = NekomuraConfig.enablePublicProxy.toggleConfigBool();
TextCheckCell cell = (TextCheckCell) view;
cell.setChecked(enabled);
UIUtil.runOnIoDispatcher(() -> {
SharedPreferences pref = MessagesController.getGlobalMainSettings();
for (SubInfo subInfo : SubManager.getSubList().find()) {
if (subInfo.id != SubManager.publicProxySubID) continue;
subInfo.enable = enabled;
if (enabled) {
try {
subInfo.proxies = subInfo.reloadProxies();
subInfo.lastFetch = System.currentTimeMillis();
} catch (Exception ignored) {
}
}
SubManager.getSubList().update(subInfo, true);
break;
}
// clear proxy id
useProxySettings = false;
SharedConfig.setCurrentProxy(null);
// reload list & UI
AndroidUtilities.runOnUIThread(() -> {
SharedConfig.reloadProxyList();
updateRows(true);
});
});
} else if (position == callsRow) {
useProxyForCalls = !useProxyForCalls;
@ -692,7 +722,6 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
editor.apply();
} else if (position >= proxyStartRow && position < proxyEndRow) {
SharedConfig.ProxyInfo info = proxyList.get(position - proxyStartRow);
useProxySettings = true;
SharedConfig.setCurrentProxy(info);
updateRows(true);
@ -706,9 +735,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
listView.setOnItemLongClickListener((view, position) -> {
if (position >= proxyStartRow && position < proxyEndRow) {
final SharedConfig.ProxyInfo info = SharedConfig.proxyList.get(position - proxyStartRow);
BottomBuilder builder = new BottomBuilder(context);
builder.addItems(new String[]{
info.subId == 1 ? null : LocaleController.getString("EditProxy", R.string.EditProxy),
@ -730,7 +757,6 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
}, (i, text, cell) -> {
if (i == 0) {
if (info instanceof SharedConfig.VmessProxy) {
if (((SharedConfig.VmessProxy) info).bean.getConfigType() == V2RayConfig.EConfigType.Trojan) {
presentFragment(new TrojanSettingsActivity((SharedConfig.VmessProxy) info));
@ -750,21 +776,13 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
} else {
presentFragment(new ProxySettingsActivity(info));
}
} else if (i == 1) {
ProxyUtil.shareProxy(getParentActivity(), info, 0);
} else if (i == 2) {
ProxyUtil.shareProxy(getParentActivity(), info, 2);
} else if (i == 3) {
ProxyUtil.shareProxy(getParentActivity(), info, 1);
} else if (i == 4) {
AlertUtil.showConfirm(getParentActivity(),
LocaleController.getString("DeleteProxy", R.string.DeleteProxy),
R.drawable.baseline_delete_24, LocaleController.getString("Delete", R.string.Delete),
@ -775,38 +793,26 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
SharedConfig.setProxyEnable(false);
}
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged);
});
}
return Unit.INSTANCE;
});
showDialog(builder.create());
return true;
}
return false;
});
if (alert != null) {
AlertUtil.showSimpleAlert(context, alert);
alert = null;
}
return fragmentView;
}
@SuppressLint("NewApi")
private void addProxy() {
BottomBuilder builder = new BottomBuilder(getParentActivity());
builder.addItems(new String[]{
LocaleController.getString("AddProxySocks5", R.string.AddProxySocks5),
@ -822,37 +828,21 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
}, null, (i, t, c) -> {
if (i == 0) {
presentFragment(new ProxySettingsActivity(0));
} else if (i == 1) {
presentFragment(new ProxySettingsActivity(1));
} else if (i == 2) {
presentFragment(new WsSettingsActivity());
} else if (i == 3) {
presentFragment(new VmessSettingsActivity());
} else if (i == 4) {
presentFragment(new TrojanSettingsActivity());
} else if (i == 5) {
presentFragment(new ShadowsocksSettingsActivity());
} else if (i == 6) {
presentFragment(new ShadowsocksRSettingsActivity());
} else if (i == 7) {
ProxyUtil.importFromClipboard(getParentActivity());
} else {
if (Build.VERSION.SDK_INT >= 23) {
@ -879,28 +869,26 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
}
});
}
return Unit.INSTANCE;
});
builder.show();
}
private void updateRows(boolean notify) {
proxyList = SharedConfig.getProxyList();
rowCount = 0;
useProxyRow = rowCount++;
useProxyDetailRow = rowCount++;
connectionsHeaderRow = rowCount++;
enablePublicProxyRow = rowCount++;
if (!proxyList.isEmpty()) {
useProxyDetailRow = rowCount++;
connectionsHeaderRow = rowCount++;
proxyStartRow = rowCount;
rowCount += proxyList.size();
proxyEndRow = rowCount;
} else {
useProxyDetailRow = -1;
connectionsHeaderRow = -1;
proxyStartRow = -1;
proxyEndRow = -1;
}
@ -943,43 +931,26 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
private ExecutorService currentCheck;
private void checkProxyList(boolean force) {
if (currentCheck == null) {
currentCheck = Executors.newFixedThreadPool(3);
}
ProxyChecksKt.checkProxyList(this, force, currentCheck);
}
private void deleteUnavailableProxy() {
for (SharedConfig.ProxyInfo info : SharedConfig.getProxyList()) {
if (info.subId != 0) continue;
checkSingleProxy(info, 1, () -> {
deleteUnavailableProxy(info);
});
}
}
private void deleteUnavailableProxy(SharedConfig.ProxyInfo proxyInfo) {
if (!proxyInfo.available) {
SharedConfig.deleteProxy(proxyInfo);
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxyCheckDone);
}
}
public void checkSingleProxy(SharedConfig.ProxyInfo proxyInfo, int repeat, Runnable callback) {
@ -994,24 +965,15 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
}
UIUtil.runOnIoDispatcher(() -> {
if (proxyInfo instanceof SharedConfig.ExternalSocks5Proxy && !((SharedConfig.ExternalSocks5Proxy) proxyInfo).isStarted()) {
try {
((SharedConfig.ExternalSocks5Proxy) proxyInfo).start();
} catch (Exception e) {
FileLog.e(e);
AlertUtil.showToast(e);
}
ThreadUtil.sleep(233L);
}
proxyInfo.proxyCheckPingId = ConnectionsManager.getInstance(currentAccount).checkProxy(proxyInfo.address, proxyInfo.port, proxyInfo.username, proxyInfo.password, proxyInfo.secret, time -> AndroidUtilities.runOnUIThread(() -> {
if (time == -1) {
if (repeat > 0) {
@ -1041,21 +1003,14 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
}
}
}));
});
}
private void showSubDialog() {
BottomBuilder builder = new BottomBuilder(getParentActivity());
builder.addTitle(LocaleController.getString("ProxySubscription", R.string.ProxySubscription));
HashMap<SubInfo, Boolean> toChange = new HashMap<>();
for (SubInfo sub : SubManager.getSubList().find()) {
TextCheckCell subItem = builder.addCheckItem(sub.name, sub.enable, true, (it, target) -> {
if (target == sub.enable) {
toChange.remove(sub);
@ -1084,7 +1039,6 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
updateStr = StrUtil.upperFirst(updateStr);
builder.addButton(updateStr, (it) -> {
AlertDialog pro = AlertUtil.showProgress(getParentActivity(), LocaleController.getString("SubscriptionUpdating", R.string.SubscriptionUpdating));
AtomicBoolean canceled = new AtomicBoolean();
pro.setOnCancelListener((__) -> {
@ -1093,79 +1047,46 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
pro.show();
UIUtil.runOnIoDispatcher(() -> {
for (SubInfo subInfo : SubManager.getSubList().find()) {
if (!subInfo.enable) continue;
try {
subInfo.proxies = subInfo.reloadProxies();
subInfo.lastFetch = System.currentTimeMillis();
} catch (IOException allTriesFailed) {
if (canceled.get()) return;
AlertUtil.showSimpleAlert(getParentActivity(), "All tries failed: " + allTriesFailed.toString().trim());
continue;
}
SubManager.getSubList().update(subInfo, true);
if (canceled.get()) return;
}
SharedConfig.reloadProxyList();
updateRows(true);
UIUtil.runOnUIThread(pro::dismiss);
});
return Unit.INSTANCE;
});
builder.addButton(LocaleController.getString("OK", R.string.OK), (it) -> {
if (!toChange.isEmpty()) {
AlertDialog pro = AlertUtil.showProgress(getParentActivity());
pro.setCanCacnel(false);
pro.show();
UIUtil.runOnIoDispatcher(() -> {
for (Map.Entry<SubInfo, Boolean> toChangeE : toChange.entrySet()) {
toChangeE.getKey().enable = toChangeE.getValue();
SubManager.getSubList().update(toChangeE.getKey(), true);
}
SharedConfig.reloadProxyList();
UIUtil.runOnUIThread(() -> updateRows(true));
ThreadUtil.sleep(233L);
UIUtil.runOnUIThread(pro::dismiss);
});
}
return Unit.INSTANCE;
});
builder.show();
}
@Override
@ -1256,6 +1177,8 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
checkCell.setTextAndCheck(LocaleController.getString("UseProxySettings", R.string.UseProxySettings), useProxySettings, true);
} else if (position == callsRow) {
checkCell.setTextAndCheck(LocaleController.getString("UseProxyForCalls", R.string.UseProxyForCalls), useProxyForCalls, false);
} else if (position == enablePublicProxyRow) {
checkCell.setTextAndCheck(LocaleController.getString("enablePublicProxy", R.string.enablePublicProxy), NekomuraConfig.enablePublicProxy.Bool(), false);
}
break;
}
@ -1289,6 +1212,8 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
checkCell.setChecked(useProxySettings);
} else if (position == callsRow) {
checkCell.setChecked(useProxyForCalls);
} else if (position == enablePublicProxyRow) {
checkCell.setChecked(NekomuraConfig.enablePublicProxy.Bool());
}
} else {
super.onBindViewHolder(holder, position, payloads);
@ -1305,6 +1230,8 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
checkCell.setChecked(useProxySettings);
} else if (position == callsRow) {
checkCell.setChecked(useProxyForCalls);
} else if (position == enablePublicProxyRow) {
checkCell.setChecked(NekomuraConfig.enablePublicProxy.Bool());
}
}
}
@ -1312,7 +1239,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
@Override
public boolean isEnabled(RecyclerView.ViewHolder holder) {
int position = holder.getAdapterPosition();
return position == useProxyRow || position == callsRow || position >= proxyStartRow && position < proxyEndRow;
return position == useProxyRow || position == callsRow || position == enablePublicProxyRow || position >= proxyStartRow && position < proxyEndRow;
}
@Override
@ -1352,7 +1279,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
public int getItemViewType(int position) {
if (position == useProxyDetailRow || position == proxyDetailRow) {
return 0;
} else if (position == useProxyRow || position == callsRow) {
} else if (position == useProxyRow || position == callsRow || position == enablePublicProxyRow) {
return 3;
} else if (position == connectionsHeaderRow) {
return 2;

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.*
import org.telegram.messenger.FileLog
import tw.nekomimi.nekogram.utils.DnsFactory
import tw.nekomimi.nekogram.utils.ProxyUtil.parseProxies
import tw.nekomimi.nkmr.NekomuraConfig
import tw.nekomimi.nkmr.NekomuraUtil
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@ -15,6 +16,8 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
fun loadProxiesPublic(urls: List<String>, exceptions: MutableMap<String, Exception>): List<String> {
if (!NekomuraConfig.enablePublicProxy.Bool())
return emptyList()
// Try DoH first ( github.com is often blocked
try {
var content = DnsFactory.getTxts("nachonekodayo.sekai.icu").joinToString()

View File

@ -93,6 +93,7 @@ public class NekoGeneralSettingsActivity extends BaseFragment {
private final AbstractCell disableProxyWhenVpnEnabledRow = cellGroup.appendCell(new NekomuraTGTextCheck(NekomuraConfig.disableProxyWhenVpnEnabled));
private final AbstractCell useProxyItemRow = cellGroup.appendCell(new NekomuraTGTextCheck(NekomuraConfig.useProxyItem));
private final AbstractCell hideProxyByDefaultRow = cellGroup.appendCell(new NekomuraTGTextCheck(NekomuraConfig.hideProxyByDefault));
private final AbstractCell autoUpdateSubInfoRow = cellGroup.appendCell(new NekomuraTGTextCheck(NekomuraConfig.autoUpdateSubInfo));
private final AbstractCell useSystemDNSRow = cellGroup.appendCell(new NekomuraTGTextCheck(NekomuraConfig.useSystemDNS));
private final AbstractCell customDoHRow = cellGroup.appendCell(new NekomuraTGTextInput(null, NekomuraConfig.customDoH, "https://1.0.0.1/dns-query", null));
private final AbstractCell customPublicProxyIPRow = cellGroup.appendCell(new NekomuraTGTextDetail(NekomuraConfig.customPublicProxyIP, (view, position) -> {

View File

@ -34,12 +34,12 @@ public class SubInfo implements Mappable {
public String displayName() {
if (id == 1) return LocaleController.getString("PublicPrefix", R.string.PublicPrefix);
if (id == SubManager.publicProxySubID)
return LocaleController.getString("PublicPrefix", R.string.PublicPrefix);
if (name.length() < 10) return name;
return name.substring(0, 10) + "...";
}
public List<String> reloadProxies() throws IOException {
@ -47,7 +47,7 @@ public class SubInfo implements Mappable {
HashMap<String, Exception> exceptions = new HashMap<>();
try {
return internal ? ProxyLoadsKt.loadProxiesPublic(urls, exceptions) : ProxyLoadsKt.loadProxies(urls, exceptions);
return id == SubManager.publicProxySubID ? ProxyLoadsKt.loadProxiesPublic(urls, exceptions) : ProxyLoadsKt.loadProxies(urls, exceptions);
} catch (Exception ignored) {
}
@ -131,5 +131,4 @@ public class SubInfo implements Mappable {
}
}
}

View File

@ -9,6 +9,8 @@ object SubManager {
val database by lazy { mkDatabase("proxy_sub") }
const val publicProxySubID = 1L
@JvmStatic
val count
get() = subList.find().totalCount()
@ -18,7 +20,7 @@ object SubManager {
database.getRepository("proxy_sub", SubInfo::class.java).apply {
val public = find(ObjectFilters.eq("id", 1L)).firstOrDefault()
val public = find(ObjectFilters.eq("id", publicProxySubID)).firstOrDefault()
update(SubInfo().apply {
// SubManager.kt -> SubInfo.java -> ProxyLoads.kt
@ -32,7 +34,7 @@ object SubManager {
"https://api.github.com/repos/NekoX-Dev/ProxyList/contents/proxy_list_pro?ref=master@\"content\": \"@\"",
)
id = 1L
id = publicProxySubID
internal = true
proxies = public?.proxies ?: listOf()

View File

@ -48,6 +48,9 @@ public class NekomuraConfig {
public static ConfigItem disableInstantCamera = addConfig("DisableInstantCamera", configTypeBool, false);
public static ConfigItem showSeconds = addConfig("showSeconds", configTypeBool, false);
public static ConfigItem enablePublicProxy = addConfig("enablePublicProxy", configTypeBool, true);
public static ConfigItem autoUpdateSubInfo = addConfig("autoUpdateSubInfo", configTypeBool, true);
// From NekoConfig
public static ConfigItem useIPv6 = addConfig("IPv6", configTypeBool, false);
public static ConfigItem hidePhone = addConfig("HidePhone", configTypeBool, true);

View File

@ -122,5 +122,7 @@
<string name="disableSwipeToNextChannel">Disable swipe to next channel</string>
<string name="disableChoosingSticker">Send typing instead of choosing sticker</string>
<string name="disableRemoteEmojiInteractions">Disable emoji interactions from remote</string>
<string name="enablePublicProxy">NekoX Public Proxy</string>
<string name="autoUpdateSubInfo">Update proxies automatically</string>
</resources>