@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'expo-module-gradle-plugin'
|
||||
}
|
||||
|
||||
group = 'expo.modules.nativedoh'
|
||||
version = '0.1.0'
|
||||
|
||||
android {
|
||||
namespace "expo.modules.nativedoh"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.facebook.react:react-android'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.2'
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<provider
|
||||
android:name="expo.modules.nativedoh.DohInitializationProvider"
|
||||
android:authorities="${applicationId}.doh-initializer"
|
||||
android:exported="false"
|
||||
android:initOrder="100" />
|
||||
</application>
|
||||
</manifest>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package expo.modules.nativedoh
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
|
||||
class DohInitializationProvider : ContentProvider() {
|
||||
override fun onCreate(): Boolean {
|
||||
context?.let {
|
||||
installDohClientFactory(it, DohPreferences.getProvider(it))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?,
|
||||
): Cursor? = null
|
||||
|
||||
override fun getType(uri: Uri): String? = null
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
): Int = 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package expo.modules.nativedoh
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object DohPreferences {
|
||||
private const val PREFERENCES_NAME = "lnreader_network"
|
||||
private const val PROVIDER_KEY = "doh_provider"
|
||||
|
||||
fun getProvider(context: Context): Int =
|
||||
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
.getInt(PROVIDER_KEY, DohProviders.DISABLED)
|
||||
|
||||
fun setProvider(context: Context, provider: Int) {
|
||||
require(provider in DohProviders.ALL) { "Unknown DNS over HTTPS provider: $provider" }
|
||||
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putInt(PROVIDER_KEY, provider)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package expo.modules.nativedoh
|
||||
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider
|
||||
import java.net.InetAddress
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
|
||||
object DohProviders {
|
||||
const val DISABLED = 0
|
||||
const val CLOUDFLARE = 1
|
||||
const val GOOGLE = 2
|
||||
const val ADGUARD = 3
|
||||
const val QUAD9 = 4
|
||||
const val ALIDNS = 5
|
||||
const val DNSPOD = 6
|
||||
const val DNS_360 = 7
|
||||
const val QUAD_101 = 8
|
||||
const val MULLVAD = 9
|
||||
const val CONTROL_D = 10
|
||||
const val NJALLA = 11
|
||||
const val SHECAN = 12
|
||||
|
||||
val ALL = DISABLED..SHECAN
|
||||
}
|
||||
|
||||
data class DohProvider(
|
||||
val url: String,
|
||||
val bootstrapHosts: List<String>,
|
||||
)
|
||||
|
||||
private val providers = mapOf(
|
||||
DohProviders.CLOUDFLARE to DohProvider(
|
||||
"https://cloudflare-dns.com/dns-query",
|
||||
listOf(
|
||||
"162.159.36.1",
|
||||
"162.159.46.1",
|
||||
"1.1.1.1",
|
||||
"1.0.0.1",
|
||||
"162.159.132.53",
|
||||
"2606:4700:4700::1111",
|
||||
"2606:4700:4700::1001",
|
||||
"2606:4700:4700::0064",
|
||||
"2606:4700:4700::6400",
|
||||
),
|
||||
),
|
||||
DohProviders.GOOGLE to DohProvider(
|
||||
"https://dns.google/dns-query",
|
||||
listOf(
|
||||
"8.8.4.4",
|
||||
"8.8.8.8",
|
||||
"2001:4860:4860::8888",
|
||||
"2001:4860:4860::8844",
|
||||
),
|
||||
),
|
||||
DohProviders.ADGUARD to DohProvider(
|
||||
"https://dns-unfiltered.adguard.com/dns-query",
|
||||
listOf(
|
||||
"94.140.14.140",
|
||||
"94.140.14.141",
|
||||
"2a10:50c0::1:ff",
|
||||
"2a10:50c0::2:ff",
|
||||
),
|
||||
),
|
||||
DohProviders.QUAD9 to DohProvider(
|
||||
"https://dns.quad9.net/dns-query",
|
||||
listOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"),
|
||||
),
|
||||
DohProviders.ALIDNS to DohProvider(
|
||||
"https://dns.alidns.com/dns-query",
|
||||
listOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1"),
|
||||
),
|
||||
DohProviders.DNSPOD to DohProvider(
|
||||
"https://doh.pub/dns-query",
|
||||
listOf("1.12.12.12", "120.53.53.53"),
|
||||
),
|
||||
DohProviders.DNS_360 to DohProvider(
|
||||
"https://doh.360.cn/dns-query",
|
||||
listOf(
|
||||
"101.226.4.6",
|
||||
"218.30.118.6",
|
||||
"123.125.81.6",
|
||||
"140.207.198.6",
|
||||
"180.163.249.75",
|
||||
"101.199.113.208",
|
||||
"36.99.170.86",
|
||||
),
|
||||
),
|
||||
DohProviders.QUAD_101 to DohProvider(
|
||||
"https://dns.twnic.tw/dns-query",
|
||||
listOf("101.101.101.101", "2001:de4::101", "2001:de4::102"),
|
||||
),
|
||||
DohProviders.MULLVAD to DohProvider(
|
||||
"https://dns.mullvad.net/dns-query",
|
||||
listOf("194.242.2.2", "2a07:e340::2"),
|
||||
),
|
||||
DohProviders.CONTROL_D to DohProvider(
|
||||
"https://freedns.controld.com/p0",
|
||||
listOf("76.76.2.0", "76.76.10.0", "2606:1a40::", "2606:1a40:1::"),
|
||||
),
|
||||
DohProviders.NJALLA to DohProvider(
|
||||
"https://dns.njal.la/dns-query",
|
||||
listOf("95.215.19.53", "2001:67c:2354:2::53"),
|
||||
),
|
||||
DohProviders.SHECAN to DohProvider(
|
||||
"https://free.shecan.ir/dns-query",
|
||||
listOf("178.22.122.100", "185.51.200.2"),
|
||||
),
|
||||
)
|
||||
|
||||
fun installDohClientFactory(context: android.content.Context, providerId: Int) {
|
||||
val provider = providers[providerId] ?: return
|
||||
val applicationContext = context.applicationContext
|
||||
|
||||
OkHttpClientProvider.setOkHttpClientFactory {
|
||||
val builder = OkHttpClientProvider.createClientBuilder(applicationContext)
|
||||
builder.dns(
|
||||
DnsOverHttps.Builder()
|
||||
.client(builder.build())
|
||||
.url(provider.url.toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
*provider.bootstrapHosts
|
||||
.map(InetAddress::getByName)
|
||||
.toTypedArray(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package expo.modules.nativedoh
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class NativeDohModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("NativeDoh")
|
||||
|
||||
Function("getProvider") {
|
||||
DohPreferences.getProvider(appContext.reactContext!!)
|
||||
}
|
||||
|
||||
Function("setProvider") { provider: Int ->
|
||||
DohPreferences.setProvider(appContext.reactContext!!, provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["android"],
|
||||
"android": {
|
||||
"modules": ["expo.modules.nativedoh.NativeDohModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { requireOptionalNativeModule } from 'expo-modules-core';
|
||||
|
||||
export type DohProviderId =
|
||||
| 0
|
||||
| 1
|
||||
| 2
|
||||
| 3
|
||||
| 4
|
||||
| 5
|
||||
| 6
|
||||
| 7
|
||||
| 8
|
||||
| 9
|
||||
| 10
|
||||
| 11
|
||||
| 12;
|
||||
|
||||
type NativeDohModule = {
|
||||
getProvider(): DohProviderId;
|
||||
setProvider(provider: DohProviderId): void;
|
||||
};
|
||||
|
||||
export default requireOptionalNativeModule<NativeDohModule>('NativeDoh');
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "native-doh"
|
||||
}
|
||||
@@ -22,17 +22,22 @@
|
||||
"dataManagement": "Data Management",
|
||||
"deleteReadChapters": "Delete read chapters",
|
||||
"deleteReadChaptersDialogTitle": "All chapters marked as read will be deleted.",
|
||||
"disabled": "Disabled",
|
||||
"dnsOverHttps": "DNS over HTTPS (DoH)",
|
||||
"dnsOverHttpsDescription": "Uses the selected resolver for app requests and downloads. WebView traffic is not affected.",
|
||||
"importEpub": "Import EPUB",
|
||||
"importFailed": "Error importing EPUB:",
|
||||
"importNovel": "Import Novel",
|
||||
"importStaticFiles": "Import Static Files",
|
||||
"novelInsertFailed": "novel insert failed",
|
||||
"networking": "Networking",
|
||||
"useFAB": "Use FAB instead of button",
|
||||
"userAgent": "User Agent",
|
||||
"recreateDBIndexes": "Recreate DB indexes",
|
||||
"recreateDBIndexesToast": "Recreated DB indexes",
|
||||
"recreateDBIndexesDialogTitle": "All DB indexes will be recreated.\nThis may take a while.",
|
||||
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices."
|
||||
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
|
||||
"restartRequired": "Restart LNReader to apply this change."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"appearanceScreen": {
|
||||
|
||||
@@ -23,17 +23,22 @@ export interface StringMap {
|
||||
'advancedSettingsScreen.dataManagement': 'string';
|
||||
'advancedSettingsScreen.deleteReadChapters': 'string';
|
||||
'advancedSettingsScreen.deleteReadChaptersDialogTitle': 'string';
|
||||
'advancedSettingsScreen.disabled': 'string';
|
||||
'advancedSettingsScreen.dnsOverHttps': 'string';
|
||||
'advancedSettingsScreen.dnsOverHttpsDescription': 'string';
|
||||
'advancedSettingsScreen.importEpub': 'string';
|
||||
'advancedSettingsScreen.importFailed': 'string';
|
||||
'advancedSettingsScreen.importNovel': 'string';
|
||||
'advancedSettingsScreen.importStaticFiles': 'string';
|
||||
'advancedSettingsScreen.novelInsertFailed': 'string';
|
||||
'advancedSettingsScreen.networking': 'string';
|
||||
'advancedSettingsScreen.useFAB': 'string';
|
||||
'advancedSettingsScreen.userAgent': 'string';
|
||||
'advancedSettingsScreen.recreateDBIndexes': 'string';
|
||||
'advancedSettingsScreen.recreateDBIndexesToast': 'string';
|
||||
'advancedSettingsScreen.recreateDBIndexesDialogTitle': 'string';
|
||||
'advancedSettingsScreen.recreateDBIndexesDesc': 'string';
|
||||
'advancedSettingsScreen.restartRequired': 'string';
|
||||
'appearance': 'string';
|
||||
'appearanceScreen.accentColor': 'string';
|
||||
'appearanceScreen.alwaysShowNavLabels': 'string';
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
|
||||
import NativeDoh, { DohProviderId } from '@modules/native-doh';
|
||||
import { Dialog, RadioButton } from '@components';
|
||||
import { useTheme } from '@hooks/persisted';
|
||||
import { getString } from '@i18n/translations';
|
||||
import { showToast } from '@utils/showToast';
|
||||
|
||||
export const DOH_PROVIDERS: readonly {
|
||||
id: DohProviderId;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ id: 0, label: getString('advancedSettingsScreen.disabled') },
|
||||
{ id: 1, label: 'Cloudflare' },
|
||||
{ id: 2, label: 'Google' },
|
||||
{ id: 3, label: 'AdGuard' },
|
||||
{ id: 4, label: 'Quad9' },
|
||||
{ id: 5, label: 'AliDNS' },
|
||||
{ id: 6, label: 'DNSPod' },
|
||||
{ id: 7, label: '360' },
|
||||
{ id: 8, label: 'Quad 101' },
|
||||
{ id: 9, label: 'Mullvad' },
|
||||
{ id: 10, label: 'Control D' },
|
||||
{ id: 11, label: 'Njalla' },
|
||||
{ id: 12, label: 'Shecan' },
|
||||
];
|
||||
|
||||
interface DohProviderDialogProps {
|
||||
provider: DohProviderId;
|
||||
visible: boolean;
|
||||
onDismiss: () => void;
|
||||
onSelect: (provider: DohProviderId) => void;
|
||||
}
|
||||
|
||||
const DohProviderDialog = ({
|
||||
provider,
|
||||
visible,
|
||||
onDismiss,
|
||||
onSelect,
|
||||
}: DohProviderDialogProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const selectProvider = (nextProvider: DohProviderId) => {
|
||||
NativeDoh?.setProvider(nextProvider);
|
||||
onSelect(nextProvider);
|
||||
onDismiss();
|
||||
showToast(getString('advancedSettingsScreen.restartRequired'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root visible={visible} onDismiss={onDismiss}>
|
||||
<Dialog.Title>
|
||||
{getString('advancedSettingsScreen.dnsOverHttps')}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{getString('advancedSettingsScreen.dnsOverHttpsDescription')}
|
||||
</Dialog.Description>
|
||||
<Dialog.ScrollArea>
|
||||
<FlatList
|
||||
data={DOH_PROVIDERS}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<RadioButton
|
||||
label={item.label}
|
||||
status={provider === item.id}
|
||||
onPress={() => selectProvider(item.id)}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
style={styles.list}
|
||||
/>
|
||||
</Dialog.ScrollArea>
|
||||
<Dialog.Actions>
|
||||
<Dialog.Action onPress={onDismiss}>
|
||||
{getString('common.cancel')}
|
||||
</Dialog.Action>
|
||||
</Dialog.Actions>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default DohProviderDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
list: {
|
||||
maxHeight: 480,
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { TextInput } from 'react-native-paper';
|
||||
import { Platform, ScrollView, StyleSheet } from 'react-native';
|
||||
|
||||
import { deleteCachedNovels, useTheme, useUserAgent } from '@hooks/persisted';
|
||||
import { showToast } from '@utils/showToast';
|
||||
@@ -15,10 +16,11 @@ import {
|
||||
|
||||
import { Appbar, Dialog, List, SafeAreaView } from '@components';
|
||||
import { AdvancedSettingsScreenProps } from '@navigators/types';
|
||||
import { ScrollView, StyleSheet } from 'react-native';
|
||||
import { getUserAgentSync } from 'react-native-device-info';
|
||||
import CookieManager from '@preeternal/react-native-cookie-manager';
|
||||
import { store } from '@plugins/helpers/storage';
|
||||
import NativeDoh, { DohProviderId } from '@modules/native-doh';
|
||||
import DohProviderDialog, { DOH_PROVIDERS } from './DohProviderDialog';
|
||||
|
||||
const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -30,6 +32,9 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
|
||||
|
||||
const { userAgent, setUserAgent } = useUserAgent();
|
||||
const [userAgentInput, setUserAgentInput] = useState(userAgent);
|
||||
const [dohProvider, setDohProvider] = useState<DohProviderId>(
|
||||
NativeDoh?.getProvider() ?? 0,
|
||||
);
|
||||
/**
|
||||
* Confirm Clear Database Dialog
|
||||
*/
|
||||
@@ -53,6 +58,16 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
|
||||
setFalse: hideUserAgentModal,
|
||||
} = useBoolean();
|
||||
|
||||
const {
|
||||
value: dohProviderDialogVisible,
|
||||
setTrue: showDohProviderDialog,
|
||||
setFalse: hideDohProviderDialog,
|
||||
} = useBoolean();
|
||||
|
||||
const dohProviderLabel =
|
||||
DOH_PROVIDERS.find(provider => provider.id === dohProvider)?.label ??
|
||||
DOH_PROVIDERS[0].label;
|
||||
|
||||
return (
|
||||
<SafeAreaView excludeTop>
|
||||
<Appbar
|
||||
@@ -86,11 +101,24 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
|
||||
onPress={showDeleteReadChaptersDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString('advancedSettingsScreen.networking')}
|
||||
</List.SubHeader>
|
||||
<List.Item
|
||||
title={getString('webview.clearCookies')}
|
||||
onPress={clearCookies}
|
||||
theme={theme}
|
||||
/>
|
||||
{Platform.OS === 'android' && NativeDoh ? (
|
||||
<List.Item
|
||||
title={getString('advancedSettingsScreen.dnsOverHttps')}
|
||||
description={dohProviderLabel}
|
||||
onPress={showDohProviderDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
<List.Item
|
||||
title={getString('advancedSettingsScreen.userAgent')}
|
||||
description={userAgent}
|
||||
@@ -130,6 +158,13 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
|
||||
onDismiss={hideClearUpdatesDialog}
|
||||
/>
|
||||
|
||||
<DohProviderDialog
|
||||
provider={dohProvider}
|
||||
visible={dohProviderDialogVisible}
|
||||
onDismiss={hideDohProviderDialog}
|
||||
onSelect={setDohProvider}
|
||||
/>
|
||||
|
||||
<Dialog.Root
|
||||
visible={userAgentModalVisible}
|
||||
onDismiss={hideUserAgentModal}
|
||||
|
||||
Reference in New Issue
Block a user