diff --git a/app/build.gradle b/app/build.gradle index d230b3cea..8e0677834 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,8 +4,6 @@ plugins { // to download blocklists for the headless variant id "de.undercouch.download" version "5.3.0" id 'kotlin-android' - id 'com.google.gms.google-services' - id 'com.google.firebase.crashlytics' } def keystorePropertiesFile = rootProject.file("keystore.properties") @@ -173,9 +171,8 @@ configurations { } dependencies { - androidTestImplementation 'androidx.test:rules:1.5.0' def room_version = "2.6.1" - def paging_version = "3.2.1" + def paging_version = "3.3.2" implementation 'com.google.guava:guava:32.1.1-android' @@ -184,8 +181,8 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") fullImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.21' - fullImplementation 'androidx.appcompat:appcompat:1.6.1' - fullImplementation 'androidx.core:core-ktx:1.12.0' + fullImplementation 'androidx.appcompat:appcompat:1.7.0' + fullImplementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.preference:preference-ktx:1.2.1' fullImplementation 'androidx.constraintlayout:constraintlayout:2.1.4' fullImplementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' @@ -194,7 +191,7 @@ dependencies { fullImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' // LiveData - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.6' implementation 'com.google.code.gson:gson:2.10.1' @@ -204,14 +201,14 @@ dependencies { implementation "androidx.room:room-paging:$room_version" fullImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - fullImplementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' - fullImplementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + fullImplementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' + fullImplementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6' // Pagers Views implementation "androidx.paging:paging-runtime-ktx:$paging_version" - fullImplementation 'androidx.fragment:fragment-ktx:1.6.2' - implementation 'com.google.android.material:material:1.11.0' - fullImplementation 'androidx.viewpager2:viewpager2:1.0.0' + fullImplementation 'androidx.fragment:fragment-ktx:1.8.3' + implementation 'com.google.android.material:material:1.12.0' + fullImplementation 'androidx.viewpager2:viewpager2:1.1.0' fullImplementation 'com.squareup.okhttp3:okhttp:4.12.0' fullImplementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0' @@ -249,14 +246,14 @@ dependencies { fullImplementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.9' // from: https://jitpack.io/#celzero/firestack - download 'com.github.celzero:firestack:ee0a5ac71f@aar' - websiteImplementation 'com.github.celzero:firestack:ee0a5ac71f@aar' - fdroidImplementation 'com.github.celzero:firestack:ee0a5ac71f@aar' - // debug symbols for crashlytics - playImplementation 'com.github.celzero:firestack:ee0a5ac71f:debug@aar' + download 'com.github.celzero:firestack:1b3c80f71e@aar' + websiteImplementation 'com.github.celzero:firestack:1b3c80f71e@aar' + fdroidImplementation 'com.github.celzero:firestack:1b3c80f71e@aar' + // debug symbols + playImplementation 'com.github.celzero:firestack:1b3c80f71e:debug@aar' // Work manager - implementation('androidx.work:work-runtime-ktx:2.9.0') { + implementation('androidx.work:work-runtime-ktx:2.9.1') { modules { module("com.google.guava:listenablefuture") { replacedBy("com.google.guava:guava", "listenablefuture is part of guava") @@ -270,13 +267,14 @@ dependencies { implementation 'com.github.seancfoley:ipaddress:5.4.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' leakCanaryImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' - fullImplementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' - fullImplementation 'androidx.navigation:navigation-ui-ktx:2.7.7' + fullImplementation 'androidx.navigation:navigation-fragment-ktx:2.8.0' + fullImplementation 'androidx.navigation:navigation-ui-ktx:2.8.0' fullImplementation 'androidx.biometric:biometric:1.1.0' @@ -285,16 +283,12 @@ dependencies { // for encrypting wireguard configuration files implementation("androidx.security:security-crypto:1.1.0-alpha06") - implementation("androidx.security:security-app-authenticator:1.0.0-alpha03") - androidTestImplementation("androidx.security:security-app-authenticator:1.0.0-alpha03") + implementation("androidx.security:security-app-authenticator:1.0.0-beta01") + androidTestImplementation("androidx.security:security-app-authenticator:1.0.0-beta01") // barcode scanner for wireguard fullImplementation 'com.journeyapps:zxing-android-embedded:4.3.0' - - // only using firebase crashlytics experimentally for stability tracking, only in play variant - // not in fdroid or website - playImplementation 'com.google.firebase:firebase-crashlytics:19.0.0' - playImplementation 'com.google.firebase:firebase-crashlytics-ndk:19.0.0' + fullImplementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1' } // github.com/michel-kraemer/gradle-download-task/issues/131#issuecomment-464476903 diff --git a/app/google-services.json b/app/google-services.json deleted file mode 100644 index 8fe85e0a0..000000000 --- a/app/google-services.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "project_info": { - "project_number": "974915159594", - "project_id": "rethink-dns-firewall", - "storage_bucket": "rethink-dns-firewall.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:974915159594:android:ed4f2e6c806fa816bda553", - "android_client_info": { - "package_name": "com.celzero.bravedns" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyBsj6i5hZNgsYopDLZlqV7jFAAp1F0y6JQ" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/app/src/fdroid/java/com/celzero/bravedns/util/Logger.kt b/app/src/fdroid/java/com/celzero/bravedns/util/Logger.kt deleted file mode 100644 index fa043e17c..000000000 --- a/app/src/fdroid/java/com/celzero/bravedns/util/Logger.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2021 RethinkDNS and its authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import android.util.Log -import com.celzero.bravedns.service.PersistentState -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -object Logger : KoinComponent { - private val persistentState by inject() - private var logLevel = persistentState.goLoggerLevel - - const val LOG_TAG_APP_UPDATE = "NonStoreAppUpdater" - const val LOG_TAG_VPN = "VpnLifecycle" - const val LOG_TAG_CONNECTION = "ConnectivityEvents" - const val LOG_TAG_DNS = "DnsManager" - const val LOG_TAG_FIREWALL = "FirewallManager" - const val LOG_BATCH_LOGGER = "BatchLogger" - const val LOG_TAG_APP_DB = "AppDatabase" - const val LOG_TAG_DOWNLOAD = "DownloadManager" - const val LOG_TAG_UI = "ActivityManager" - const val LOG_TAG_SCHEDULER = "JobScheduler" - const val LOG_TAG_BUG_REPORT = "BugReport" - const val LOG_TAG_BACKUP_RESTORE = "BackupRestore" - const val LOG_PROVIDER = "BlocklistProvider" - const val LOG_TAG_PROXY = "ProxyLogs" - const val LOG_QR_CODE = "QrCodeFromFileScanner" - const val LOG_GO_LOGGER = "LibLogger" - - // github.com/celzero/firestack/blob/bce8de917fec5e48a41ed1e96c9d942ee0f7996b/intra/log/logger.go#L76 - enum class LoggerType(val id: Int) { - VERY_VERBOSE(0), - VERBOSE(1), - DEBUG(2), - INFO(3), - WARN(4), - ERROR(5), - STACKTRACE(6), - USR(7), - NONE(8); - - companion object { - fun fromId(id: Int): LoggerType { - return when (id) { - 0 -> VERY_VERBOSE - 1 -> VERBOSE - 2 -> DEBUG - 3 -> INFO - 4 -> WARN - 5 -> ERROR - 6 -> STACKTRACE - 7 -> USR - 8 -> NONE - else -> NONE - } - } - } - - fun stacktrace(): Boolean { - return this == STACKTRACE - } - - fun user(): Boolean { - return this == USR - } - } - - fun vv(tag: String, message: String) { - log(tag, message, LoggerType.VERY_VERBOSE) - } - - fun v(tag: String, message: String) { - log(tag, message, LoggerType.VERBOSE) - } - - fun d(tag: String, message: String) { - log(tag, message, LoggerType.DEBUG) - } - - fun i(tag: String, message: String) { - log(tag, message, LoggerType.INFO) - } - - fun w(tag: String, message: String, e: Exception? = null) { - log(tag, message, LoggerType.WARN, e) - } - - fun e(tag: String, message: String, e: Exception? = null) { - log(tag, message, LoggerType.ERROR, e) - } - - fun crash(tag: String, message: String, e: Exception?= null) { - log(tag, message, LoggerType.ERROR, e) - } - - fun updateConfigLevel(level: Long) { - logLevel = level - } - - fun throwableToException(throwable: Throwable): Exception { - return if (throwable is Exception) { - throwable - } else { - Exception(throwable) - } - } - - private fun log(tag: String, msg: String, type: LoggerType, e: Exception? = null) { - when (type) { - LoggerType.VERY_VERBOSE -> if (logLevel <= LoggerType.VERY_VERBOSE.id) Log.v(tag, msg) - LoggerType.VERBOSE -> if (logLevel <= LoggerType.VERBOSE.id) Log.v(tag, msg) - LoggerType.DEBUG -> if (logLevel <= LoggerType.DEBUG.id) Log.d(tag, msg) - LoggerType.INFO -> if (logLevel <= LoggerType.INFO.id) Log.i(tag, msg) - LoggerType.WARN -> if (logLevel <= LoggerType.WARN.id) Log.w(tag, msg, e) - LoggerType.ERROR -> if (logLevel <= LoggerType.ERROR.id) Log.e(tag, msg, e) - LoggerType.STACKTRACE -> if (logLevel <= LoggerType.ERROR.id) Log.e(tag, msg, e) - LoggerType.USR -> {} // Do nothing - LoggerType.NONE -> {} // Do nothing - } - } -} diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index d5cb6e6bb..3dc2339ae 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -10,6 +10,9 @@ android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" /> + + + + + ().scheduleDatabaseRefreshJob() get().scheduleDataUsageJob() get().schedulePurgeConnectionsLog() + get().schedulePurgeConsoleLogs() } private fun turnOnStrictMode() { diff --git a/app/src/full/java/com/celzero/bravedns/adapter/AppWiseDomainsAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/AppWiseDomainsAdapter.kt index ce5fe747e..aae25c9ed 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/AppWiseDomainsAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/AppWiseDomainsAdapter.kt @@ -40,7 +40,8 @@ import kotlin.math.log2 class AppWiseDomainsAdapter( val context: Context, val lifecycleOwner: LifecycleOwner, - val uid: Int + val uid: Int, + val isRethink: Boolean ) : PagingDataAdapter( DIFF_CALLBACK @@ -68,9 +69,6 @@ class AppWiseDomainsAdapter( private lateinit var adapter: AppWiseDomainsAdapter - // ui component to update/toggle the buttons - data class ToggleBtnUi(val txtColor: Int, val bgColor: Int) - override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -145,6 +143,10 @@ class AppWiseDomainsAdapter( return } + if (isRethink) { + return + } + val bottomSheetFragment = AppDomainRulesBottomSheet() // Fix: free-form window crash // all BottomSheetDialogFragment classes created must have a public, no-arg constructor. diff --git a/app/src/full/java/com/celzero/bravedns/adapter/AppWiseIpsAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/AppWiseIpsAdapter.kt index 95f3c86d8..dbf0bf01e 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/AppWiseIpsAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/AppWiseIpsAdapter.kt @@ -37,7 +37,7 @@ import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.util.Utilities.removeBeginningTrailingCommas import kotlin.math.log2 -class AppWiseIpsAdapter(val context: Context, val lifecycleOwner: LifecycleOwner, val uid: Int) : +class AppWiseIpsAdapter(val context: Context, val lifecycleOwner: LifecycleOwner, val uid: Int, val isRethink: Boolean) : PagingDataAdapter(DIFF_CALLBACK), AppIpRulesBottomSheet.OnBottomSheetDialogFragmentDismiss { @@ -112,6 +112,10 @@ class AppWiseIpsAdapter(val context: Context, val lifecycleOwner: LifecycleOwner return } + if (isRethink) { + return + } + val bottomSheetFragment = AppIpRulesBottomSheet() // Fix: free-form window crash // all BottomSheetDialogFragment classes created must have a public, no-arg constructor. diff --git a/app/src/full/java/com/celzero/bravedns/adapter/ConnectionTrackerAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/ConnectionTrackerAdapter.kt index 8799966b1..167c728de 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/ConnectionTrackerAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/ConnectionTrackerAdapter.kt @@ -45,6 +45,7 @@ import com.celzero.bravedns.util.KnownPorts import com.celzero.bravedns.util.Protocol import com.celzero.bravedns.util.UIUtils.getDurationInHumanReadableFormat import com.celzero.bravedns.util.Utilities +import com.celzero.bravedns.util.Utilities.getDefaultIcon import com.celzero.bravedns.util.Utilities.getIcon import com.google.gson.Gson import kotlinx.coroutines.Dispatchers @@ -69,12 +70,13 @@ class ConnectionTrackerAdapter(private val context: Context) : override fun areContentsTheSame( oldConnection: ConnectionTracker, newConnection: ConnectionTracker - ) = oldConnection.id == newConnection.id + ) = oldConnection == newConnection } private const val MAX_BYTES = 500000 // 500 KB private const val MAX_TIME_TCP = 135 // seconds private const val MAX_TIME_UDP = 135 // seconds + private const val NO_USER_ID = 0 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConnectionTrackerViewHolder { @@ -146,47 +148,43 @@ class ConnectionTrackerAdapter(private val context: Context) : private fun displayAppDetails(ct: ConnectionTracker) { io { uiCtx { - // append the usrId with app name if the usrId is not 0 - // fixme: move the 0 to a constant - if (ct.usrId != 0) { - b.connectionAppName.text = - context.getString( - R.string.about_version_install_source, - ct.appName, - ct.usrId.toString() - ) - } else { - b.connectionAppName.text = ct.appName - } - val apps = FirewallManager.getPackageNamesByUid(ct.uid) + val count = apps.count() - if (apps.isEmpty()) { - loadAppIcon(Utilities.getDefaultIcon(context)) - return@uiCtx - } + val appName = when { + ct.usrId != NO_USER_ID -> context.getString( + R.string.about_version_install_source, + ct.appName, + ct.usrId.toString() + ) - val count = apps.count() - val appName = - if (count > 1) { - context.getString( - R.string.ctbs_app_other_apps, - ct.appName, - (count).minus(1).toString() - ) - } else { - ct.appName - } + count > 1 -> context.getString( + R.string.ctbs_app_other_apps, + ct.appName, + "${count - 1}" + ) + + else -> ct.appName + } b.connectionAppName.text = appName - loadAppIcon(getIcon(context, apps[0], /*No app name */ "")) + if (apps.isEmpty()) { + loadAppIcon(getDefaultIcon(context)) + } else { + loadAppIcon(getIcon(context, apps[0])) + } } } } private fun displayProtocolDetails(port: Int, proto: Int) { - // Instead of showing the port name and protocol, now the ports are resolved with - // known ports(reserved port and protocol identifiers). + // If the protocol is not TCP or UDP, then display the protocol name. + if (Protocol.UDP.protocolType != proto && Protocol.TCP.protocolType != proto) { + b.connLatencyTxt.text = Protocol.getProtocolName(proto).name + return + } + + // Instead of displaying the port number, display the service name if it is known. // https://github.com/celzero/rethink-app/issues/42 - #3 - transport + protocol. val resolvedPort = KnownPorts.resolvePort(port) // case: for UDP/443 label it as HTTP3 instead of HTTPS @@ -339,7 +337,7 @@ class ConnectionTrackerAdapter(private val context: Context) : private fun loadAppIcon(drawable: Drawable?) { Glide.with(context) .load(drawable) - .error(Utilities.getDefaultIcon(context)) + .error(getDefaultIcon(context)) .into(b.connectionAppIcon) } } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/ConsoleLogAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/ConsoleLogAdapter.kt new file mode 100644 index 000000000..c47d42b69 --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/adapter/ConsoleLogAdapter.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.celzero.bravedns.R +import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG +import com.celzero.bravedns.database.ConsoleLog +import com.celzero.bravedns.databinding.ListItemConsoleLogBinding +import com.celzero.bravedns.util.Constants.Companion.TIME_FORMAT_1 +import com.celzero.bravedns.util.UIUtils +import com.celzero.bravedns.util.Utilities + +class ConsoleLogAdapter(private val context: Context) : + PagingDataAdapter(DIFF_CALLBACK) { + + companion object { + private val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(old: ConsoleLog, new: ConsoleLog): Boolean { + return old == new + } + + override fun areContentsTheSame(old: ConsoleLog, new: ConsoleLog): Boolean { + return old == new + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConsoleLogViewHolder { + val itemBinding = + ListItemConsoleLogBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ConsoleLogViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: ConsoleLogViewHolder, position: Int) { + val logInfo = getItem(position) ?: return + holder.update(logInfo) + } + + inner class ConsoleLogViewHolder(private val b: ListItemConsoleLogBinding) : + RecyclerView.ViewHolder(b.root) { + + fun update(log: ConsoleLog) { + // update the textview color with the first letter of the log level + val logLevel = log.message.firstOrNull() ?: 'V' + when (logLevel) { + 'V' -> + b.logDetail.setTextColor( + UIUtils.fetchColor(context, R.attr.primaryLightColorText) + ) + 'D' -> + b.logDetail.setTextColor( + UIUtils.fetchColor(context, R.attr.primaryLightColorText) + ) + 'I' -> + b.logDetail.setTextColor( + UIUtils.fetchColor(context, R.attr.defaultToggleBtnTxt) + ) + 'W' -> + b.logDetail.setTextColor( + UIUtils.fetchColor(context, R.attr.firewallWhiteListToggleBtnTxt) + ) + 'E' -> + b.logDetail.setTextColor( + UIUtils.fetchColor(context, R.attr.firewallBlockToggleBtnTxt) + ) + else -> + b.logDetail.setTextColor( + UIUtils.fetchColor(context, R.attr.firewallNoRuleToggleBtnTxt) + ) + } + b.logDetail.text = log.message + if (DEBUG) { + b.logTimestamp.text = + "${log.id}\n${Utilities.convertLongToTime(log.timestamp, TIME_FORMAT_1)}" + } else { + b.logTimestamp.text = Utilities.convertLongToTime(log.timestamp, TIME_FORMAT_1) + } + } + } +} diff --git a/app/src/full/java/com/celzero/bravedns/adapter/CustomDomainAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/CustomDomainAdapter.kt index a8deb8cde..79c3da235 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/CustomDomainAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/CustomDomainAdapter.kt @@ -18,6 +18,7 @@ package com.celzero.bravedns.adapter import Logger import Logger.LOG_TAG_UI import android.content.Context +import android.content.Intent import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.text.format.DateUtils @@ -58,6 +59,9 @@ import kotlinx.coroutines.withContext class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RULES) : PagingDataAdapter(DIFF_CALLBACK) { + private val selectedItems = mutableSetOf() + private var isSelectionMode = false + companion object { private val DIFF_CALLBACK = @@ -74,8 +78,7 @@ class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RU oldConnection: CustomDomain, newConnection: CustomDomain ): Boolean { - return (oldConnection.domain == newConnection.domain && - oldConnection.status != newConnection.status) + return oldConnection == newConnection } } } @@ -104,16 +107,30 @@ class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RU override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val customDomain: CustomDomain = getItem(position) ?: return - if (holder is CustomDomainViewHolderWithHeader) { - holder.update(customDomain) - } else if (holder is CustomDomainViewHolderWithoutHeader) { - holder.update(customDomain) - } else { - Logger.w(LOG_TAG_UI, "unknown view holder in CustomDomainRulesAdapter") - return + when (holder) { + is CustomDomainViewHolderWithHeader -> { + holder.update(customDomain) + } + + is CustomDomainViewHolderWithoutHeader -> { + holder.update(customDomain) + } + + else -> { + Logger.w(LOG_TAG_UI, "unknown view holder in CustomDomainRulesAdapter") + return + } } } + fun getSelectedItems(): List = selectedItems.toList() + + fun clearSelection() { + selectedItems.clear() + isSelectionMode = false + notifyDataSetChanged() + } + override fun getItemViewType(position: Int): Int { if (rule == CustomRulesActivity.RULES.APP_SPECIFIC_RULES) { return R.layout.list_item_custom_domain @@ -383,6 +400,10 @@ class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RU private lateinit var customDomain: CustomDomain fun update(cd: CustomDomain) { + this.customDomain = cd + + b.customDomainCheckbox.isChecked = selectedItems.contains(cd) + b.customDomainCheckbox.visibility = if (isSelectionMode) View.VISIBLE else View.GONE io { val appInfo = FirewallManager.getAppInfoByUid(cd.uid) val appNames = FirewallManager.getAppNamesByUid(cd.uid) @@ -399,31 +420,67 @@ class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RU b.customDomainAppIconIv ) - this.customDomain = cd + b.customDomainLabelTv.text = customDomain.domain b.customDomainToggleGroup.tag = 1 // update toggle group button based on the status - updateToggleGroup(customDomain.status) + updateToggleGroup(cd.status) // whether to show the toggle group or not toggleActionsUi() // update status in desc and status flag (N/B/W) updateStatusUi( - DomainRulesManager.Status.getStatus(customDomain.status), - customDomain.modifiedTs + DomainRulesManager.Status.getStatus(cd.status), + cd.modifiedTs ) b.customDomainToggleGroup.addOnButtonCheckedListener(domainRulesGroupListener) - b.customDomainEditIcon.setOnClickListener { showEditDomainDialog(customDomain) } + b.customDomainEditIcon.setOnClickListener { showEditDomainDialog(cd) } b.customDomainExpandIcon.setOnClickListener { toggleActionsUi() } - b.customDomainContainer.setOnClickListener { toggleActionsUi() } + b.customDomainContainer.setOnClickListener { + if (isSelectionMode) { + toggleSelection(cd) + } else { + toggleActionsUi() + } + } + + b.customDomainSeeMoreChip.setOnClickListener { openAppWiseRulesActivity(cd.uid) } + + b.customDomainContainer.setOnLongClickListener { + isSelectionMode = true + selectedItems.add(cd) + notifyDataSetChanged() + true + } } } } + private fun toggleSelection(item: CustomDomain) { + if (selectedItems.contains(item)) { + selectedItems.remove(item) + b.customDomainCheckbox.isChecked = false + } else { + selectedItems.add(item) + b.customDomainCheckbox.isChecked = true + } + } + + private fun openAppWiseRulesActivity(uid: Int) { + val intent = Intent(context, CustomRulesActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + intent.putExtra( + Constants.VIEW_PAGER_SCREEN_TO_LOAD, + CustomRulesActivity.Tabs.DOMAIN_RULES.screen + ) + intent.putExtra(Constants.INTENT_UID, uid) + context.startActivity(intent) + } + private fun getAppName(uid: Int, appNames: List): String { if (uid == Constants.UID_EVERYBODY) { return context @@ -581,6 +638,8 @@ class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RU fun update(cd: CustomDomain) { this.customDomain = cd + b.customDomainCheckbox.isChecked = selectedItems.contains(cd) + b.customDomainCheckbox.visibility = if (isSelectionMode) View.VISIBLE else View.GONE b.customDomainLabelTv.text = customDomain.domain b.customDomainToggleGroup.tag = 1 @@ -600,7 +659,30 @@ class CustomDomainAdapter(val context: Context, val rule: CustomRulesActivity.RU b.customDomainExpandIcon.setOnClickListener { toggleActionsUi() } - b.customDomainContainer.setOnClickListener { toggleActionsUi() } + b.customDomainContainer.setOnClickListener { + if (isSelectionMode) { + toggleSelection(cd) + } else { + toggleActionsUi() + } + } + + b.customDomainContainer.setOnLongClickListener { + isSelectionMode = true + selectedItems.add(cd) + notifyDataSetChanged() + true + } + } + + private fun toggleSelection(item: CustomDomain) { + if (selectedItems.contains(item)) { + selectedItems.remove(item) + b.customDomainCheckbox.isChecked = false + } else { + selectedItems.add(item) + b.customDomainCheckbox.isChecked = true + } } private fun updateToggleGroup(id: Int) { diff --git a/app/src/full/java/com/celzero/bravedns/adapter/CustomIpAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/CustomIpAdapter.kt index 8e748bd97..138bd51c3 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/CustomIpAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/CustomIpAdapter.kt @@ -18,6 +18,7 @@ package com.celzero.bravedns.adapter import Logger import Logger.LOG_TAG_UI import android.content.Context +import android.content.Intent import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.text.format.DateUtils @@ -43,6 +44,7 @@ import com.celzero.bravedns.databinding.ListItemCustomIpBinding import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.IpRulesManager import com.celzero.bravedns.ui.activity.CustomRulesActivity +import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Constants.Companion.UID_EVERYBODY import com.celzero.bravedns.util.UIUtils.fetchColor import com.celzero.bravedns.util.UIUtils.fetchToggleBtnColors @@ -61,6 +63,9 @@ import kotlinx.coroutines.withContext class CustomIpAdapter(private val context: Context, private val type: CustomRulesActivity.RULES) : PagingDataAdapter(DIFF_CALLBACK) { + private val selectedItems = mutableSetOf() + private var isSelectionMode = false + companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { @@ -70,8 +75,7 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule oldConnection.status == newConnection.status override fun areContentsTheSame(oldConnection: CustomIp, newConnection: CustomIp) = - oldConnection.ipAddress == newConnection.ipAddress && - oldConnection.status != newConnection.status + oldConnection == newConnection } } @@ -96,9 +100,11 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val customIp: CustomIp = getItem(position) ?: return + when (holder) { is CustomIpAdapter.CustomIpsViewHolderWithHeader -> { holder.update(customIp) + } is CustomIpAdapter.CustomIpsViewHolderWithoutHeader -> { holder.update(customIp) @@ -125,6 +131,14 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule } } + fun getSelectedItems(): List = selectedItems.toList() + + fun clearSelection() { + selectedItems.clear() + isSelectionMode = false + notifyDataSetChanged() + } + private fun displayIcon(drawable: Drawable?, mIconImageView: ImageView) { Glide.with(context) .load(drawable) @@ -233,6 +247,10 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule private lateinit var customIp: CustomIp fun update(ci: CustomIp) { + customIp = ci + + b.customIpCheckbox.isChecked = selectedItems.contains(customIp) + b.customIpCheckbox.visibility = if (isSelectionMode) View.VISIBLE else View.GONE io { val appNames = FirewallManager.getAppNamesByUid(ci.uid) val appName = getAppName(ci.uid, appNames) @@ -250,7 +268,7 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule } } - customIp = ci + b.customIpLabelTv.text = context.getString( R.string.ci_ip_label, @@ -277,7 +295,43 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule b.customIpExpandIcon.setOnClickListener { toggleActionsUi() } - b.customIpContainer.setOnClickListener { toggleActionsUi() } + b.customIpContainer.setOnClickListener { + if (isSelectionMode) { + toggleSelection(customIp) + } else { + toggleActionsUi() + } + } + + b.customIpSeeMoreChip.setOnClickListener { openAppWiseRulesActivity(customIp.uid) } + + b.customIpContainer.setOnLongClickListener { + isSelectionMode = true + selectedItems.add(customIp) + notifyDataSetChanged() + true + } + } + + private fun toggleSelection(item: CustomIp) { + if (selectedItems.contains(item)) { + selectedItems.remove(item) + b.customIpCheckbox.isChecked = false + } else { + selectedItems.add(item) + b.customIpCheckbox.isChecked = true + } + } + + private fun openAppWiseRulesActivity(uid: Int) { + val intent = Intent(context, CustomRulesActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + intent.putExtra( + Constants.VIEW_PAGER_SCREEN_TO_LOAD, + CustomRulesActivity.Tabs.IP_RULES.screen + ) + intent.putExtra(Constants.INTENT_UID, uid) + context.startActivity(intent) } private fun getAppName(uid: Int, appNames: List): String { @@ -500,6 +554,9 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule fun update(ci: CustomIp) { customIp = ci + b.customIpCheckbox.isChecked = selectedItems.contains(customIp) + b.customIpCheckbox.visibility = if (isSelectionMode) View.VISIBLE else View.GONE + b.customIpLabelTv.text = context.getString( R.string.ci_ip_label, @@ -526,7 +583,30 @@ class CustomIpAdapter(private val context: Context, private val type: CustomRule b.customIpExpandIcon.setOnClickListener { toggleActionsUi() } - b.customIpContainer.setOnClickListener { toggleActionsUi() } + b.customIpContainer.setOnClickListener { + if (isSelectionMode) { + toggleSelection(customIp) + } else { + toggleActionsUi() + } + } + + b.customIpContainer.setOnLongClickListener { + isSelectionMode = true + selectedItems.add(customIp) + notifyDataSetChanged() + true + } + } + + private fun toggleSelection(item: CustomIp) { + if (selectedItems.contains(item)) { + selectedItems.remove(item) + b.customIpCheckbox.isChecked = false + } else { + selectedItems.add(item) + b.customIpCheckbox.isChecked = true + } } private fun showBypassUi(uid: Int) { diff --git a/app/src/full/java/com/celzero/bravedns/adapter/DohEndpointAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/DohEndpointAdapter.kt index f2b2f316f..9144ccd1b 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/DohEndpointAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/DohEndpointAdapter.kt @@ -207,12 +207,10 @@ class DohEndpointAdapter(private val context: Context, private val appConfig: Ap builder.setTitle(title) builder.setMessage(url + "\n\n" + getDnsDesc(message)) builder.setCancelable(true) - builder.setPositiveButton(context.getString(R.string.dns_info_positive)) { dialogInterface, - _ -> + builder.setPositiveButton(context.getString(R.string.dns_info_positive)) { dialogInterface, _ -> dialogInterface.dismiss() } - builder.setNeutralButton(context.getString(R.string.dns_info_neutral)) { _: DialogInterface, - _: Int -> + builder.setNeutralButton(context.getString(R.string.dns_info_neutral)) { _: DialogInterface, _: Int -> clipboardCopy(context, url, context.getString(R.string.copy_clipboard_label)) Utilities.showToastUiCentered( context, diff --git a/app/src/full/java/com/celzero/bravedns/adapter/DomainConnectionsAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/DomainConnectionsAdapter.kt new file mode 100644 index 000000000..c8aaf011d --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/adapter/DomainConnectionsAdapter.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.adapter + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.celzero.bravedns.R +import com.celzero.bravedns.data.AppConnection +import com.celzero.bravedns.databinding.ListItemStatisticsSummaryBinding +import com.celzero.bravedns.service.FirewallManager +import com.celzero.bravedns.ui.activity.AppInfoActivity +import com.celzero.bravedns.util.Utilities +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class DomainConnectionsAdapter(private val context: Context) : + PagingDataAdapter( + DIFF_CALLBACK + ) { + + companion object { + private val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldConnection: AppConnection, + newConnection: AppConnection + ): Boolean { + return (oldConnection == newConnection) + } + + override fun areContentsTheSame( + oldConnection: AppConnection, + newConnection: AppConnection + ): Boolean { + return (oldConnection == newConnection) + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): DomainConnectionsViewHolder { + val itemBinding = + ListItemStatisticsSummaryBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return DomainConnectionsViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: DomainConnectionsViewHolder, position: Int) { + val appNetworkActivity = getItem(position) ?: return + holder.bind(appNetworkActivity) + } + + inner class DomainConnectionsViewHolder(private val b: ListItemStatisticsSummaryBinding) : + RecyclerView.ViewHolder(b.root) { + + fun bind(dc: AppConnection) { + b.ssDataUsage.text = dc.appOrDnsName + io { + val appInfo = FirewallManager.getAppInfoByUid(dc.uid) + uiCtx { + b.ssIcon.visibility = View.VISIBLE + b.ssFlag.visibility = View.GONE + loadAppIcon( + Utilities.getIcon( + context, + appInfo?.packageName ?: "", + appInfo?.appName ?: "" + ) + ) + } + } + if (dc.downloadBytes == null || dc.uploadBytes == null) { + return + } + + val download = + context.getString( + R.string.symbol_download, + Utilities.humanReadableByteCount(dc.downloadBytes, true) + ) + val upload = + context.getString( + R.string.symbol_upload, + Utilities.humanReadableByteCount(dc.uploadBytes, true) + ) + val total = context.getString(R.string.two_argument, upload, download) + b.ssName.text = total + b.ssCount.text = dc.count.toString() + + b.ssProgress.visibility = View.GONE + + b.ssContainer.setOnClickListener { + val intent = Intent(context, AppInfoActivity::class.java) + intent.putExtra(AppInfoActivity.INTENT_UID, dc.uid) + context.startActivity(intent) + } + + } + + private fun loadAppIcon(drawable: Drawable?) { + ui { + Glide.with(context) + .load(drawable) + .error(Utilities.getDefaultIcon(context)) + .into(b.ssIcon) + } + } + } + + private fun io(f: suspend () -> Unit) { + (context as LifecycleOwner).lifecycleScope.launch(Dispatchers.IO) { f() } + } + + private fun ui(f: suspend () -> Unit) { + (context as LifecycleOwner).lifecycleScope.launch(Dispatchers.Main) { f() } + } + + private suspend fun uiCtx(f: suspend () -> Unit) { + withContext(Dispatchers.Main) { f() } + } +} \ No newline at end of file diff --git a/app/src/full/java/com/celzero/bravedns/adapter/FirewallAppListAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/FirewallAppListAdapter.kt index 3d748dfc4..4b97f2609 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/FirewallAppListAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/FirewallAppListAdapter.kt @@ -20,13 +20,11 @@ import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ImageView -import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner @@ -40,25 +38,27 @@ import com.celzero.bravedns.database.AppInfo import com.celzero.bravedns.databinding.ListItemFirewallAppBinding import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.FirewallManager.updateFirewallStatus +import com.celzero.bravedns.service.ProxyManager +import com.celzero.bravedns.service.ProxyManager.ID_NONE import com.celzero.bravedns.ui.activity.AppInfoActivity -import com.celzero.bravedns.ui.activity.AppInfoActivity.Companion.UID_INTENT_NAME -import com.celzero.bravedns.util.UIUtils +import com.celzero.bravedns.ui.activity.AppInfoActivity.Companion.INTENT_UID import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.util.Utilities.getIcon import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter +import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.TimeUnit class FirewallAppListAdapter( private val context: Context, private val lifecycleOwner: LifecycleOwner -) : PagingDataAdapter(DIFF_CALLBACK) { +) : PagingDataAdapter(DIFF_CALLBACK), SectionedAdapter { private val packageManager: PackageManager = context.packageManager - private val systemAppColor: Int by lazy { UIUtils.fetchColor(context, R.attr.textColorAccentBad) } - private val userAppColor: Int by lazy { UIUtils.fetchColor(context, R.attr.primaryTextColor) } + // private val systemAppColor: Int by lazy { UIUtils.fetchColor(context, R.attr.textColorAccentBad) } + // private val userAppColor: Int by lazy { UIUtils.fetchColor(context, R.attr.primaryTextColor) } companion object { private val DIFF_CALLBACK = @@ -67,16 +67,14 @@ class FirewallAppListAdapter( oldConnection: AppInfo, newConnection: AppInfo ): Boolean { - return oldConnection == newConnection + return oldConnection.packageName == newConnection.packageName } override fun areContentsTheSame( oldConnection: AppInfo, newConnection: AppInfo ): Boolean { - return (oldConnection.packageName == newConnection.packageName && - oldConnection.firewallStatus == newConnection.firewallStatus && - oldConnection.connectionStatus == newConnection.connectionStatus) + return oldConnection == newConnection } } } @@ -106,21 +104,25 @@ class FirewallAppListAdapter( val connStatus = FirewallManager.connectionStatus(appInfo.uid) uiCtx { b.firewallAppLabelTv.text = appInfo.appName - if (appInfo.isSystemApp) { + // setting the appname with different color for system and user apps + // causes conflict with the firewall status like blocked and isolated + // so removing the color change for now + /* if (appInfo.isSystemApp) { b.firewallAppLabelTv.setTextColor(systemAppColor) } else { b.firewallAppLabelTv.setTextColor(userAppColor) - } + } */ + b.firewallAppToggleOther.text = getFirewallText(appStatus, connStatus) + displayIcon( + getIcon(context, appInfo.packageName, appInfo.appName), b.firewallAppIconIv) + // set the alpha based on internet permission if (appInfo.hasInternetPermission(packageManager)) { b.firewallAppLabelTv.alpha = 1f + b.firewallAppIconIv.alpha = 1f } else { - b.firewallAppLabelTv.alpha = 0.6f + b.firewallAppLabelTv.alpha = 0.4f + b.firewallAppIconIv.alpha = 0.4f } - b.firewallAppToggleOther.text = getFirewallText(appStatus, connStatus) - displayIcon( - getIcon(context, appInfo.packageName, appInfo.appName), - b.firewallAppIconIv - ) if (appInfo.packageName == context.packageName) { b.firewallAppToggleWifi.visibility = View.GONE b.firewallAppToggleMobileData.visibility = View.GONE @@ -132,11 +134,34 @@ class FirewallAppListAdapter( b.firewallAppToggleWifi.visibility = View.VISIBLE b.firewallAppToggleMobileData.visibility = View.VISIBLE displayConnectionStatus(appStatus, connStatus) - showAppHint(b.firewallAppStatusIndicator, appInfo) + displayDataUsage(appInfo) + maybeDisplayProxyStatus(appInfo) } } } + private fun displayDataUsage(appInfo: AppInfo) { + val u = Utilities.humanReadableByteCount(appInfo.uploadBytes, true) + val uploadBytes = context.getString(R.string.symbol_upload, u) + val d = Utilities.humanReadableByteCount(appInfo.downloadBytes, true) + val downloadBytes = context.getString(R.string.symbol_download, d) + b.firewallAppDataUsage.text = + context.getString(R.string.two_argument, uploadBytes, downloadBytes) + } + + private fun maybeDisplayProxyStatus(appInfo: AppInfo) { + if (appInfo.isProxyExcluded) { + return + } + + // show key icon in drawable right of b.firewallAppDataUsage + val proxy = ProxyManager.getProxyIdForApp(appInfo.uid) + if (proxy.isEmpty() || proxy == ID_NONE) { + return + } + b.firewallAppLabelTv.append(context.getString(R.string.symbol_key)) + } + private fun getFirewallText( aStat: FirewallManager.FirewallStatus, cStat: FirewallManager.ConnectionStatus @@ -216,96 +241,32 @@ class FirewallAppListAdapter( private fun showMobileDataDisabled() { b.firewallAppToggleMobileData.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_firewall_data_off) - ) + ContextCompat.getDrawable(context, R.drawable.ic_firewall_data_off)) } private fun showMobileDataEnabled() { b.firewallAppToggleMobileData.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_firewall_data_on) - ) + ContextCompat.getDrawable(context, R.drawable.ic_firewall_data_on)) } private fun showWifiDisabled() { b.firewallAppToggleWifi.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_firewall_wifi_off) - ) + ContextCompat.getDrawable(context, R.drawable.ic_firewall_wifi_off)) } private fun showWifiEnabled() { b.firewallAppToggleWifi.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_firewall_wifi_on) - ) + ContextCompat.getDrawable(context, R.drawable.ic_firewall_wifi_on)) } private fun showMobileDataUnused() { b.firewallAppToggleMobileData.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_firewall_data_on_grey) - ) + ContextCompat.getDrawable(context, R.drawable.ic_firewall_data_on_grey)) } private fun showWifiUnused() { b.firewallAppToggleWifi.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_firewall_wifi_on_grey) - ) - } - - private fun showAppHint(mIconIndicator: TextView, appInfo: AppInfo) { - io { - val connStatus = FirewallManager.connectionStatus(appInfo.uid) - val appStatus = FirewallManager.appStatus(appInfo.uid) - uiCtx { - when (appStatus) { - FirewallManager.FirewallStatus.NONE -> { - when (connStatus) { - FirewallManager.ConnectionStatus.ALLOW -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.colorGreen_900) - ) - } - FirewallManager.ConnectionStatus.METERED -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.colorAmber_900) - ) - } - FirewallManager.ConnectionStatus.UNMETERED -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.colorAmber_900) - ) - } - FirewallManager.ConnectionStatus.BOTH -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.colorAmber_900) - ) - } - } - } - FirewallManager.FirewallStatus.EXCLUDE -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.primaryLightColorText) - ) - } - FirewallManager.FirewallStatus.BYPASS_UNIVERSAL -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.primaryLightColorText) - ) - } - FirewallManager.FirewallStatus.BYPASS_DNS_FIREWALL -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.primaryLightColorText) - ) - } - FirewallManager.FirewallStatus.ISOLATE -> { - mIconIndicator.setBackgroundColor( - context.getColor(R.color.colorAmber_900) - ) - } - FirewallManager.FirewallStatus.UNTRACKED -> { - /* no-op */ - } - } - } - } + ContextCompat.getDrawable(context, R.drawable.ic_firewall_wifi_on_grey)) } private fun displayIcon(drawable: Drawable?, mIconImageView: ImageView) { @@ -377,29 +338,25 @@ class FirewallAppListAdapter( updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.ALLOW - ) + FirewallManager.ConnectionStatus.ALLOW) } FirewallManager.ConnectionStatus.UNMETERED -> { updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.BOTH - ) + FirewallManager.ConnectionStatus.BOTH) } FirewallManager.ConnectionStatus.BOTH -> { updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.UNMETERED - ) + FirewallManager.ConnectionStatus.UNMETERED) } FirewallManager.ConnectionStatus.ALLOW -> { updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.METERED - ) + FirewallManager.ConnectionStatus.METERED) } } } @@ -416,36 +373,32 @@ class FirewallAppListAdapter( updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.BOTH - ) + FirewallManager.ConnectionStatus.BOTH) } FirewallManager.ConnectionStatus.UNMETERED -> { updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.ALLOW - ) + FirewallManager.ConnectionStatus.ALLOW) } FirewallManager.ConnectionStatus.BOTH -> { updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.METERED - ) + FirewallManager.ConnectionStatus.METERED) } FirewallManager.ConnectionStatus.ALLOW -> { updateFirewallStatus( appInfo.uid, FirewallManager.FirewallStatus.NONE, - FirewallManager.ConnectionStatus.UNMETERED - ) + FirewallManager.ConnectionStatus.UNMETERED) } } } private fun openAppDetailActivity(uid: Int) { val intent = Intent(context, AppInfoActivity::class.java) - intent.putExtra(UID_INTENT_NAME, uid) + intent.putExtra(INTENT_UID, uid) context.startActivity(intent) } @@ -461,8 +414,8 @@ class FirewallAppListAdapter( builderSingle.setIcon(R.drawable.ic_firewall_block_grey) val count = packageList.count() builderSingle.setTitle( - context.getString(R.string.ctbs_block_other_apps, appInfo.appName, count.toString()) - ) + context.getString( + R.string.ctbs_block_other_apps, appInfo.appName, count.toString())) val arrayAdapter = ArrayAdapter(context, android.R.layout.simple_list_item_activated_1) @@ -518,4 +471,9 @@ class FirewallAppListAdapter( private suspend fun ioCtx(f: suspend () -> Unit) { withContext(Dispatchers.IO) { f() } } + + override fun getSectionName(position: Int): String { + val appInfo = getItem(position) ?: return "" + return appInfo.appName.substring(0, 1) + } } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/LocalAdvancedViewAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/LocalAdvancedViewAdapter.kt index bf9b51094..4818c3782 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/LocalAdvancedViewAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/LocalAdvancedViewAdapter.kt @@ -54,8 +54,7 @@ class LocalAdvancedViewAdapter(val context: Context) : oldConnection: RethinkLocalFileTag, newConnection: RethinkLocalFileTag ): Boolean { - return (oldConnection.value == newConnection.value && - oldConnection.isSelected == newConnection.isSelected) + return oldConnection == newConnection } } } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/LocalSimpleViewAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/LocalSimpleViewAdapter.kt index e29a8fd63..5d4dea1e7 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/LocalSimpleViewAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/LocalSimpleViewAdapter.kt @@ -55,8 +55,7 @@ class LocalSimpleViewAdapter(val context: Context) : oldConnection: LocalBlocklistPacksMap, newConnection: LocalBlocklistPacksMap ): Boolean { - return (oldConnection.pack == newConnection.pack && - oldConnection.level == newConnection.level) + return oldConnection == newConnection } } } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/OneWgConfigAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/OneWgConfigAdapter.kt index b01e08ab1..5057af961 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/OneWgConfigAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/OneWgConfigAdapter.kt @@ -15,6 +15,7 @@ */ package com.celzero.bravedns.adapter +import Logger.LOG_TAG_PROXY import android.content.Context import android.content.Intent import android.text.format.DateUtils @@ -29,14 +30,18 @@ import androidx.lifecycle.lifecycleScope import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import backend.Backend -import backend.Stats +import backend.RouterStats import com.celzero.bravedns.R import com.celzero.bravedns.database.WgConfigFiles import com.celzero.bravedns.databinding.ListItemWgOneInterfaceBinding +import com.celzero.bravedns.net.doh.Transaction import com.celzero.bravedns.service.ProxyManager import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.service.WireguardManager +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_OTHER_WG_ACTIVE +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_ACTIVE +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_FULL +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_WG_INVALID import com.celzero.bravedns.ui.activity.WgConfigDetailActivity import com.celzero.bravedns.ui.activity.WgConfigDetailActivity.Companion.INTENT_EXTRA_WG_TYPE import com.celzero.bravedns.ui.activity.WgConfigEditorActivity.Companion.INTENT_EXTRA_WG_ID @@ -68,17 +73,14 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns oldConnection: WgConfigFiles, newConnection: WgConfigFiles ): Boolean { - return (oldConnection == newConnection) + return oldConnection == newConnection } override fun areContentsTheSame( oldConnection: WgConfigFiles, newConnection: WgConfigFiles ): Boolean { - return (oldConnection.id == newConnection.id && - oldConnection.name == newConnection.name && - oldConnection.isActive == newConnection.isActive && - oldConnection.oneWireGuard == newConnection.oneWireGuard) + return oldConnection == newConnection } } } @@ -95,36 +97,43 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns parent, false ) - lifecycleOwner = parent.findViewTreeLifecycleOwner() + if (lifecycleOwner == null) { + lifecycleOwner = parent.findViewTreeLifecycleOwner() + } return WgInterfaceViewHolder(itemBinding) } + override fun onViewDetachedFromWindow(holder: WgInterfaceViewHolder) { + super.onViewDetachedFromWindow(holder) + holder.cancelJobIfAny() + } + inner class WgInterfaceViewHolder(private val b: ListItemWgOneInterfaceBinding) : RecyclerView.ViewHolder(b.root) { - private var statusCheckJob: Job? = null + private var job: Job? = null fun update(config: WgConfigFiles) { - b.interfaceNameText.text = config.name - b.oneWgCheck.isChecked = config.isActive - io { - updateStatus(config) - } + b.interfaceNameText.text = config.name.take(12) + b.interfaceIdText.text = context.getString(R.string.single_argument_parenthesis, config.id.toString()) + val isWgActive = config.isActive && VpnController.hasTunnel() + b.oneWgCheck.isChecked = isWgActive setupClickListeners(config) - if (config.oneWireGuard) { + if (isWgActive) { keepStatusUpdated(config) } else { - b.interfaceDetailCard.strokeWidth = 0 - b.interfaceAppsCount.visibility = View.GONE - b.protocolInfoChipGroup.visibility = View.GONE - b.interfaceActiveLayout.visibility = View.GONE - b.oneWgCheck.isChecked = false - b.interfaceStatus.text = - context.getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) + cancelJobIfAny() + disableInterface() + } + } + + fun cancelJobIfAny() { + if (job?.isActive == true) { + job?.cancel() } } private fun keepStatusUpdated(config: WgConfigFiles) { - statusCheckJob = io { + job = io { while (true) { updateStatus(config) delay(ONE_SEC) @@ -168,7 +177,12 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns ?.currentState ?.isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED) == false ) { - statusCheckJob?.cancel() + job?.cancel() + return + } + + if (config.isActive && !VpnController.hasTunnel()) { + disableInterface() return } @@ -177,6 +191,7 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns val pair = VpnController.getSupportedIpVersion(id) val c = WireguardManager.getConfigById(config.id) val stats = VpnController.getProxyStats(id) + val dnsStatusId = VpnController.getDnsStatus(ProxyManager.ID_WG_BASE + config.id) val isSplitTunnel = if (c?.getPeers()?.isNotEmpty() == true) { VpnController.isSplitTunnelProxy(id, pair) @@ -184,70 +199,41 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns false } uiCtx { - updateStatusUi(config, statusId, stats) + updateStatusUi(config, statusId, dnsStatusId, stats) updateProtocolChip(pair) updateSplitTunnelChip(isSplitTunnel) } } - private fun updateStatusUi(config: WgConfigFiles, statusId: Long?, stats: Stats?) { - if (config.isActive) { + private fun isDnsError(statusId: Long?): Boolean { + if (statusId == null) return true + + val s = Transaction.Status.fromId(statusId) + return s == Transaction.Status.BAD_QUERY || s == Transaction.Status.BAD_RESPONSE || s == Transaction.Status.NO_RESPONSE || s == Transaction.Status.SEND_FAIL || s == Transaction.Status.CLIENT_ERROR || s == Transaction.Status.INTERNAL_ERROR || s == Transaction.Status.TRANSPORT_ERROR + } + + private fun updateStatusUi(config: WgConfigFiles, statusId: Long?, dnsStatusId: Long?, stats: RouterStats?) { + if (config.isActive && VpnController.hasTunnel()) { b.interfaceDetailCard.strokeWidth = 2 b.oneWgCheck.isChecked = true b.interfaceAppsCount.visibility = View.VISIBLE b.interfaceAppsCount.text = context.getString(R.string.one_wg_apps_added) - var status: String - val handShakeTime = getHandshakeTime(stats) - if (statusId != null) { - var resId = UIUtils.getProxyStatusStringRes(statusId) - // change the color based on the status - if (statusId == Backend.TOK) { - if (stats?.lastOK == 0L) { - b.interfaceDetailCard.strokeColor = - fetchColor(context, R.attr.chipTextNeutral) - resId = R.string.status_waiting - } else { - b.interfaceDetailCard.strokeColor = - fetchColor(context, R.attr.accentGood) - } - } else if (statusId == Backend.TUP || statusId == Backend.TZZ) { - b.interfaceDetailCard.strokeColor = - fetchColor(context, R.attr.chipTextNeutral) + + if (dnsStatusId != null) { + // check for dns failure cases and update the UI + if (isDnsError(dnsStatusId)) { + b.interfaceDetailCard.strokeColor = fetchColor(context, R.attr.chipTextNegative) + b.interfaceStatus.text = + context.getString(R.string.status_failing).replaceFirstChar(Char::titlecase) } else { - b.interfaceDetailCard.strokeColor = - fetchColor(context, R.attr.chipTextNegative) - } - status = - if (stats?.lastOK == 0L) { - context.getString(resId).replaceFirstChar(Char::titlecase) - } else { - context.getString( - R.string.about_version_install_source, - context.getString(resId).replaceFirstChar(Char::titlecase), - handShakeTime - ) - } - - if ((statusId == Backend.TZZ || statusId == Backend.TNT) && stats != null) { - // for idle state, if lastOk is less than 30 sec, then show as connected - if ( - stats.lastOK != 0L && - System.currentTimeMillis() - stats.lastOK < - 30 * DateUtils.SECOND_IN_MILLIS - ) { - status = - context - .getString(R.string.dns_connected) - .replaceFirstChar(Char::titlecase) - } + // if dns status is not failing, then update the proxy status + updateProxyStatusUi(statusId, stats) } } else { - b.interfaceDetailCard.strokeColor = fetchColor(context, R.attr.chipTextNegative) - b.interfaceDetailCard.strokeWidth = 2 - status = - context.getString(R.string.status_waiting).replaceFirstChar(Char::titlecase) + // in one wg mode, if dns status should be available, this is a fallback case + updateProxyStatusUi(statusId, stats) } - b.interfaceStatus.text = status + b.interfaceActiveLayout.visibility = View.VISIBLE val rxtx = getRxTx(stats) val time = getUpTime(stats) @@ -265,16 +251,63 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns } b.interfaceActiveRxTx.text = rxtx } else { - b.interfaceDetailCard.strokeWidth = 0 - b.interfaceAppsCount.visibility = View.GONE - b.oneWgCheck.isChecked = false - b.interfaceActiveLayout.visibility = View.GONE - b.interfaceStatus.text = - context.getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) + disableInterface() } } - private fun getUpTime(stats: Stats?): CharSequence { + private fun getStrokeColorForStatus(status: UIUtils.ProxyStatus?, stats: RouterStats?): Int{ + return when (status) { + UIUtils.ProxyStatus.TOK -> if (stats?.lastOK == 0L) R.attr.chipTextNeutral else R.attr.accentGood + UIUtils.ProxyStatus.TUP, UIUtils.ProxyStatus.TZZ, UIUtils.ProxyStatus.TNT -> R.attr.chipTextNeutral + else -> R.attr.chipTextNegative + } + } + + private fun getStatusText(status: UIUtils.ProxyStatus?, handshakeTime: String? = null, stats: RouterStats?): String { + if (status == null) return context.getString(R.string.status_waiting).replaceFirstChar(Char::titlecase) + + val baseText = context.getString(UIUtils.getProxyStatusStringRes(status.id)).replaceFirstChar(Char::titlecase) + + return if (stats?.lastOK != 0L && handshakeTime != null) { + context.getString(R.string.about_version_install_source,baseText, handshakeTime) + } else { + baseText + } + } + + private fun getIdleStatusText(status: UIUtils.ProxyStatus?, stats: RouterStats?): String { + if (status != UIUtils.ProxyStatus.TZZ && status != UIUtils.ProxyStatus.TNT) return "" + if (stats == null || stats.lastOK == 0L) return "" + if (System.currentTimeMillis() - stats.lastOK >= 30 * DateUtils.SECOND_IN_MILLIS) return "" + + return context.getString(R.string.dns_connected).replaceFirstChar(Char::titlecase) + } + + private fun updateProxyStatusUi(statusId: Long?, stats: RouterStats?) { + val status = UIUtils.ProxyStatus.entries.find { it.id == statusId } // Convert to enum + + val handshakeTime = getHandshakeTime(stats).toString() + + val strokeColor = getStrokeColorForStatus(status, stats) + b.interfaceDetailCard.strokeColor = fetchColor(context, strokeColor) + + val statusText = getIdleStatusText(status, stats) + .ifEmpty { getStatusText(status, handshakeTime, stats) } + + b.interfaceStatus.text = statusText + } + + private fun disableInterface() { + b.interfaceDetailCard.strokeWidth = 0 + b.protocolInfoChipGroup.visibility = View.GONE + b.interfaceAppsCount.visibility = View.GONE + b.oneWgCheck.isChecked = false + b.interfaceActiveLayout.visibility = View.GONE + b.interfaceStatus.text = + context.getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) + } + + private fun getUpTime(stats: RouterStats?): CharSequence { if (stats == null) { return "" } @@ -288,7 +321,7 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns ) } - private fun getRxTx(stats: Stats?): String { + private fun getRxTx(stats: RouterStats?): String { if (stats == null) return "" val rx = context.getString( @@ -300,10 +333,10 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns R.string.symbol_upload, Utilities.humanReadableByteCount(stats.tx, true) ) - return context.getString(R.string.two_argument_space, rx, tx) + return context.getString(R.string.two_argument_space, tx, rx) } - private fun getHandshakeTime(stats: Stats?): CharSequence { + private fun getHandshakeTime(stats: RouterStats?): CharSequence { if (stats == null) { return "" } @@ -327,35 +360,114 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns val isChecked = b.oneWgCheck.isChecked io { if (isChecked) { - if (WireguardManager.canEnableConfig(config.toImmutable())) { - config.oneWireGuard = true - WireguardManager.updateOneWireGuardConfig(config.id, owg = true) - WireguardManager.enableConfig(config.toImmutable()) - uiCtx { listener.onDnsStatusChanged() } - } else { - uiCtx { - b.oneWgCheck.isChecked = false - Utilities.showToastUiCentered( - context, - context.getString(R.string.wireguard_enabled_failure), - Toast.LENGTH_LONG - ) - } - } + enableWgIfPossible(config) } else { - config.oneWireGuard = false - WireguardManager.updateOneWireGuardConfig(config.id, owg = false) - WireguardManager.disableConfig(config.toImmutable()) - uiCtx { - b.oneWgCheck.isChecked = false - listener.onDnsStatusChanged() - } + disableWgIfPossible(config) } } } } + private suspend fun enableWgIfPossible(config: WgConfigFiles) { + if (!VpnController.hasTunnel()) { + Logger.i(LOG_TAG_PROXY, "VPN not active, cannot enable WireGuard") + uiCtx { + Utilities.showToastUiCentered( + context, + ERR_CODE_VPN_NOT_ACTIVE + + context.getString(R.string.settings_socks5_vpn_disabled_error), + Toast.LENGTH_LONG + ) + // reset the check box + b.oneWgCheck.isChecked = false + } + return + } + + if (!WireguardManager.canEnableProxy()) { + Logger.i(LOG_TAG_PROXY, "not in DNS+Firewall mode, cannot enable WireGuard") + uiCtx { + // reset the check box + b.oneWgCheck.isChecked = false + Utilities.showToastUiCentered( + context, + ERR_CODE_VPN_NOT_FULL + + context.getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) + } + return + } + + if (WireguardManager.isAnyOtherOneWgEnabled(config.id)) { + Logger.i(LOG_TAG_PROXY, "another WireGuard is already enabled") + uiCtx { + // reset the check box + b.oneWgCheck.isChecked = false + Utilities.showToastUiCentered( + context, + ERR_CODE_OTHER_WG_ACTIVE + + context.getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) + } + return + } + + if (!WireguardManager.isValidConfig(config.id)) { + Logger.i(LOG_TAG_PROXY, "invalid WireGuard config") + uiCtx { + // reset the check box + b.oneWgCheck.isChecked = false + Utilities.showToastUiCentered( + context, + ERR_CODE_WG_INVALID + context.getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) + } + return + } + + Logger.i(LOG_TAG_PROXY, "enabling WireGuard, id: ${config.id}") + WireguardManager.updateOneWireGuardConfig(config.id, owg = true) + config.oneWireGuard = true + WireguardManager.enableConfig(config.toImmutable()) + uiCtx { listener.onDnsStatusChanged() } + } + + private suspend fun disableWgIfPossible(config: WgConfigFiles) { + if (!VpnController.hasTunnel()) { + Logger.i(LOG_TAG_PROXY, "VPN not active, cannot disable WireGuard") + uiCtx { + // reset the check box + b.oneWgCheck.isChecked = true + Utilities.showToastUiCentered( + context, + ERR_CODE_VPN_NOT_ACTIVE + + context.getString(R.string.settings_socks5_vpn_disabled_error), + Toast.LENGTH_LONG + ) + } + return + } + + Logger.i(LOG_TAG_PROXY, "disabling WireGuard, id: ${config.id}") + WireguardManager.updateOneWireGuardConfig(config.id, owg = false) + config.oneWireGuard = false + WireguardManager.disableConfig(config.toImmutable()) + uiCtx { listener.onDnsStatusChanged() } + } + private fun launchConfigDetail(id: Int) { + if (!VpnController.hasTunnel()) { + Utilities.showToastUiCentered( + context, + context.getString(R.string.ssv_toast_start_rethink), + Toast.LENGTH_SHORT + ) + return + } + val intent = Intent(context, WgConfigDetailActivity::class.java) intent.putExtra(INTENT_EXTRA_WG_ID, id) intent.putExtra(INTENT_EXTRA_WG_TYPE, WgConfigDetailActivity.WgType.ONE_WG.value) diff --git a/app/src/full/java/com/celzero/bravedns/adapter/RemoteAdvancedViewAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/RemoteAdvancedViewAdapter.kt index a42b2793e..5ae3b1965 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/RemoteAdvancedViewAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/RemoteAdvancedViewAdapter.kt @@ -55,8 +55,7 @@ class RemoteAdvancedViewAdapter(val context: Context) : oldConnection: RethinkRemoteFileTag, newConnection: RethinkRemoteFileTag ): Boolean { - return (oldConnection.value == newConnection.value && - oldConnection.isSelected == newConnection.isSelected) + return oldConnection == newConnection } } } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/RemoteSimpleViewAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/RemoteSimpleViewAdapter.kt index 89cd84d28..c65b0d5e7 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/RemoteSimpleViewAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/RemoteSimpleViewAdapter.kt @@ -55,8 +55,7 @@ class RemoteSimpleViewAdapter(val context: Context) : oldConnection: RemoteBlocklistPacksMap, newConnection: RemoteBlocklistPacksMap ): Boolean { - return (oldConnection.pack == newConnection.pack && - oldConnection.level == newConnection.level) + return oldConnection == newConnection } } } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/RethinkEndpointAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/RethinkEndpointAdapter.kt index 5a397630c..b4cb44d0b 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/RethinkEndpointAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/RethinkEndpointAdapter.kt @@ -205,18 +205,15 @@ class RethinkEndpointAdapter(private val context: Context, private val appConfig builder.setMessage(endpoint.url + "\n\n" + endpoint.desc) builder.setCancelable(true) if (endpoint.isEditable(context)) { - builder.setPositiveButton(context.getString(R.string.rt_edit_dialog_positive)) { _, - _ -> + builder.setPositiveButton(context.getString(R.string.rt_edit_dialog_positive)) { _, _ -> openEditConfiguration(endpoint) } } else { - builder.setPositiveButton(context.getString(R.string.dns_info_positive)) { dialogInterface, - _ -> + builder.setPositiveButton(context.getString(R.string.dns_info_positive)) { dialogInterface, _ -> dialogInterface.dismiss() } } - builder.setNeutralButton(context.getString(R.string.dns_info_neutral)) { _: DialogInterface, - _: Int -> + builder.setNeutralButton(context.getString(R.string.dns_info_neutral)) { _: DialogInterface, _: Int -> clipboardCopy( context, endpoint.url, diff --git a/app/src/full/java/com/celzero/bravedns/adapter/SummaryStatisticsAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/SummaryStatisticsAdapter.kt index 80214ad68..fd3feac2b 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/SummaryStatisticsAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/SummaryStatisticsAdapter.kt @@ -45,6 +45,7 @@ import com.celzero.bravedns.glide.FavIconDownloader import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.ui.activity.AppInfoActivity +import com.celzero.bravedns.ui.activity.DomainConnectionsActivity import com.celzero.bravedns.ui.activity.NetworkLogsActivity import com.celzero.bravedns.ui.fragment.SummaryStatisticsFragment.SummaryStatisticsType import com.celzero.bravedns.util.Constants @@ -52,6 +53,7 @@ import com.celzero.bravedns.util.UIUtils.fetchToggleBtnColors import com.celzero.bravedns.util.UIUtils.getCountryNameFromFlag import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.util.Utilities.isAtleastN +import com.celzero.bravedns.viewmodel.SummaryStatisticsViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -68,6 +70,7 @@ class SummaryStatisticsAdapter( ) { private var maxValue: Int = 0 + private var timeCategory = SummaryStatisticsViewModel.TimeCategory.ONE_HOUR companion object { private val DIFF_CALLBACK = @@ -86,6 +89,7 @@ class SummaryStatisticsAdapter( return (oldConnection == newConnection) } } + } override fun onCreateViewHolder( @@ -119,6 +123,10 @@ class SummaryStatisticsAdapter( } } + fun setTimeCategory(timeCategory: SummaryStatisticsViewModel.TimeCategory) { + this.timeCategory = timeCategory + } + inner class AppNetworkActivityViewHolder( private val itemBinding: ListItemStatisticsSummaryBinding ) : RecyclerView.ViewHolder(itemBinding.root) { @@ -160,7 +168,7 @@ class SummaryStatisticsAdapter( R.string.symbol_upload, Utilities.humanReadableByteCount(appConnection.uploadBytes, true) ) - val total = context.getString(R.string.two_argument, download, upload) + val total = context.getString(R.string.two_argument, upload, download) itemBinding.ssDataUsage.text = total itemBinding.ssCount.text = appConnection.count.toString() } @@ -373,14 +381,7 @@ class SummaryStatisticsAdapter( startAppInfoActivity(appConnection) } SummaryStatisticsType.MOST_CONTACTED_DOMAINS -> { - if (appConfig.getBraveMode().isDnsMode()) { - showDnsLogs(appConnection) - } else { - showNetworkLogs( - appConnection, - SummaryStatisticsType.MOST_CONTACTED_DOMAINS - ) - } + startDomainConnectionsActivity(appConnection, DomainConnectionsActivity.InputType.DOMAIN) } SummaryStatisticsType.MOST_BLOCKED_DOMAINS -> { io { @@ -409,10 +410,7 @@ class SummaryStatisticsAdapter( showNetworkLogs(appConnection, SummaryStatisticsType.MOST_BLOCKED_IPS) } SummaryStatisticsType.MOST_CONTACTED_COUNTRIES -> { - showNetworkLogs( - appConnection, - SummaryStatisticsType.MOST_CONTACTED_COUNTRIES - ) + startDomainConnectionsActivity(appConnection, DomainConnectionsActivity.InputType.FLAG) } SummaryStatisticsType.MOST_BLOCKED_COUNTRIES -> { showNetworkLogs(appConnection, SummaryStatisticsType.MOST_BLOCKED_COUNTRIES) @@ -421,9 +419,21 @@ class SummaryStatisticsAdapter( } } + private fun startDomainConnectionsActivity(appConnection: AppConnection, input: DomainConnectionsActivity.InputType) { + val intent = Intent(context, DomainConnectionsActivity::class.java) + intent.putExtra(DomainConnectionsActivity.INTENT_TYPE, input.type) + if (input == DomainConnectionsActivity.InputType.DOMAIN) { + intent.putExtra(DomainConnectionsActivity.INTENT_DOMAIN, appConnection.appOrDnsName) + } else { + intent.putExtra(DomainConnectionsActivity.INTENT_FLAG, appConnection.flag) + } + intent.putExtra(DomainConnectionsActivity.INTENT_TIME_CATEGORY, timeCategory.value) + context.startActivity(intent) + } + private fun startAppInfoActivity(appConnection: AppConnection) { val intent = Intent(context, AppInfoActivity::class.java) - intent.putExtra(AppInfoActivity.UID_INTENT_NAME, appConnection.uid) + intent.putExtra(AppInfoActivity.INTENT_UID, appConnection.uid) context.startActivity(intent) } diff --git a/app/src/full/java/com/celzero/bravedns/adapter/WgConfigAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/WgConfigAdapter.kt index 48c019a84..a6bae3705 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/WgConfigAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/WgConfigAdapter.kt @@ -15,6 +15,7 @@ */ package com.celzero.bravedns.adapter +import Logger.LOG_TAG_PROXY import android.content.Context import android.content.Intent import android.text.format.DateUtils @@ -28,29 +29,34 @@ import androidx.lifecycle.lifecycleScope import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import backend.Backend -import backend.Stats +import backend.RouterStats import com.celzero.bravedns.R +import com.celzero.bravedns.adapter.OneWgConfigAdapter.DnsStatusListener import com.celzero.bravedns.database.WgConfigFiles +import com.celzero.bravedns.database.WgConfigFilesImmutable import com.celzero.bravedns.databinding.ListItemWgGeneralInterfaceBinding +import com.celzero.bravedns.net.doh.Transaction import com.celzero.bravedns.service.ProxyManager import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.service.WireguardManager +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_OTHER_WG_ACTIVE +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_ACTIVE +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_FULL +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_WG_INVALID import com.celzero.bravedns.ui.activity.WgConfigDetailActivity import com.celzero.bravedns.ui.activity.WgConfigEditorActivity.Companion.INTENT_EXTRA_WG_ID import com.celzero.bravedns.util.UIUtils +import com.celzero.bravedns.util.UIUtils.fetchColor import com.celzero.bravedns.util.Utilities -import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class WgConfigAdapter(private val context: Context) : +class WgConfigAdapter(private val context: Context, private val listener: DnsStatusListener, private val splitDns: Boolean) : PagingDataAdapter(DIFF_CALLBACK) { - private var configs: ConcurrentHashMap = ConcurrentHashMap() private var lifecycleOwner: LifecycleOwner? = null companion object { @@ -62,18 +68,14 @@ class WgConfigAdapter(private val context: Context) : oldConnection: WgConfigFiles, newConnection: WgConfigFiles ): Boolean { - return (oldConnection == newConnection) + return oldConnection == newConnection } override fun areContentsTheSame( oldConnection: WgConfigFiles, newConnection: WgConfigFiles ): Boolean { - return (oldConnection.id == newConnection.id && - oldConnection.name == newConnection.name && - oldConnection.isActive == newConnection.isActive && - oldConnection.isCatchAll == newConnection.isCatchAll && - oldConnection.isLockdown == newConnection.isLockdown) + return oldConnection == newConnection } } } @@ -91,35 +93,34 @@ class WgConfigAdapter(private val context: Context) : parent, false ) - lifecycleOwner = parent.findViewTreeLifecycleOwner() + if (lifecycleOwner == null) { + lifecycleOwner = parent.findViewTreeLifecycleOwner() + } return WgInterfaceViewHolder(itemBinding) } override fun onViewDetachedFromWindow(holder: WgInterfaceViewHolder) { super.onViewDetachedFromWindow(holder) - configs.values.forEach { it.cancel() } - configs.clear() + holder.cancelJobIfAny() } inner class WgInterfaceViewHolder(private val b: ListItemWgGeneralInterfaceBinding) : RecyclerView.ViewHolder(b.root) { + private var job: Job? = null fun update(config: WgConfigFiles) { - b.interfaceNameText.text = config.name - b.interfaceSwitch.isChecked = config.isActive + b.interfaceNameText.text = config.name.take(12) + b.interfaceIdText.text = context.getString(R.string.single_argument_parenthesis, config.id.toString()) + b.interfaceSwitch.isChecked = config.isActive && VpnController.hasTunnel() setupClickListeners(config) updateStatusJob(config) } private fun updateStatusJob(config: WgConfigFiles) { - if (config.isActive) { - val job = updateProxyStatusContinuously(config) - if (job != null) { - // cancel the job if it already exists for the same config - cancelJobIfAny(config.id) - configs[config.id] = job - } + if (config.isActive && VpnController.hasTunnel()) { + job = updateProxyStatusContinuously(config) } else { + cancelJobIfAny() disableInactiveConfig(config) } } @@ -129,25 +130,23 @@ class WgConfigAdapter(private val context: Context) : if (config.isLockdown) { b.protocolInfoChipGroup.visibility = View.GONE b.interfaceActiveLayout.visibility = View.GONE - b.interfaceConfigStatus.text = + b.interfaceStatus.text = context.getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) val id = ProxyManager.ID_WG_BASE + config.id val appsCount = ProxyManager.getAppCountForProxy(id) updateUi(config, appsCount) } else { - b.interfaceStatus.visibility = View.GONE + b.interfaceConfigStatus.visibility = View.GONE b.interfaceAppsCount.visibility = View.GONE b.interfaceActiveLayout.visibility = View.GONE - b.interfaceDetailCard.strokeColor = UIUtils.fetchColor(context, R.attr.background) + b.interfaceDetailCard.strokeColor = fetchColor(context, R.attr.background) b.interfaceDetailCard.strokeWidth = 0 b.interfaceSwitch.isChecked = false b.protocolInfoChipGroup.visibility = View.GONE - b.interfaceConfigStatus.visibility = View.VISIBLE - b.interfaceConfigStatus.text = + b.interfaceStatus.visibility = View.VISIBLE + b.interfaceStatus.text = context.getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) } - // cancel the job if it already exists for the config, as the config is disabled - cancelJobIfAny(config.id) } private fun updateProxyStatusContinuously(config: WgConfigFiles): Job? { @@ -193,15 +192,10 @@ class WgConfigAdapter(private val context: Context) : } } - private fun cancelJobIfAny(id: Int) { - val job = configs[id] - job?.cancel() - configs.remove(id) - } - - private fun cancelAllJobs() { - configs.values.forEach { it.cancel() } - configs.clear() + fun cancelJobIfAny() { + if (job?.isActive == true) { + job?.cancel() + } } private suspend fun updateStatus(config: WgConfigFiles) { @@ -211,6 +205,11 @@ class WgConfigAdapter(private val context: Context) : val pair = VpnController.getSupportedIpVersion(id) val c = WireguardManager.getConfigById(config.id) val stats = VpnController.getProxyStats(id) + val dnsStatusId = if (splitDns) { + VpnController.getDnsStatus(id) + } else { + null + } val isSplitTunnel = if (c?.getPeers()?.isNotEmpty() == true) { VpnController.isSplitTunnelProxy(id, pair) @@ -221,16 +220,16 @@ class WgConfigAdapter(private val context: Context) : // if the view is not active then cancel the job if ( lifecycleOwner != null && - lifecycleOwner - ?.lifecycle - ?.currentState - ?.isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED) == false + lifecycleOwner + ?.lifecycle + ?.currentState + ?.isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED) == false ) { - cancelAllJobs() + cancelJobIfAny() return } uiCtx { - updateStatusUi(config, statusId, stats) + updateStatusUi(config, statusId, dnsStatusId, stats) updateUi(config, appsCount) updateProtocolChip(pair) updateSplitTunnelChip(isSplitTunnel) @@ -243,15 +242,14 @@ class WgConfigAdapter(private val context: Context) : b.interfaceConfigStatus.visibility = View.VISIBLE b.interfaceAppsCount.text = context.getString(R.string.routing_remaining_apps) b.interfaceAppsCount.setTextColor( - UIUtils.fetchColor(context, R.attr.primaryLightColorText) + fetchColor(context, R.attr.primaryLightColorText) ) b.interfaceConfigStatus.text = context.getString(R.string.catch_all_wg_dialog_title) return // no need to update the apps count } else if (config.isLockdown) { if (!config.isActive) { b.interfaceDetailCard.strokeWidth = 2 - b.interfaceDetailCard.strokeColor = - UIUtils.fetchColor(context, R.attr.accentBad) + b.interfaceDetailCard.strokeColor = fetchColor(context, R.attr.accentBad) } b.interfaceConfigStatus.visibility = View.VISIBLE b.interfaceConfigStatus.text = @@ -269,25 +267,21 @@ class WgConfigAdapter(private val context: Context) : b.interfaceAppsCount.text = context.getString(R.string.firewall_card_status_active, appsCount.toString()) if (appsCount == 0) { - b.interfaceAppsCount.setTextColor(UIUtils.fetchColor(context, R.attr.accentBad)) + b.interfaceAppsCount.setTextColor(fetchColor(context, R.attr.accentBad)) } else { - b.interfaceAppsCount.setTextColor( - UIUtils.fetchColor(context, R.attr.primaryLightColorText) - ) + b.interfaceAppsCount.setTextColor(fetchColor(context, R.attr.primaryLightColorText)) } } - private fun updateStatusUi(config: WgConfigFiles, statusId: Long?, stats: Stats?) { + private fun updateStatusUi(config: WgConfigFiles, statusId: Long?, dnsStatusId: Long?, stats: RouterStats?) { if (config.isActive) { b.interfaceSwitch.isChecked = true b.interfaceDetailCard.strokeWidth = 2 b.interfaceStatus.visibility = View.VISIBLE b.interfaceConfigStatus.visibility = View.VISIBLE - var status: String b.interfaceActiveLayout.visibility = View.VISIBLE val time = getUpTime(stats) val rxtx = getRxTx(stats) - val handShakeTime = getHandshakeTime(stats) if (time.isNotEmpty()) { val t = context.getString(R.string.logs_card_duration, time) b.interfaceActiveUptime.text = @@ -300,76 +294,92 @@ class WgConfigAdapter(private val context: Context) : b.interfaceActiveUptime.text = context.getString(R.string.lbl_active) } b.interfaceActiveRxTx.text = rxtx - if (statusId != null) { - var resId = UIUtils.getProxyStatusStringRes(statusId) - // change the color based on the status - if (statusId == Backend.TOK) { - // if the lastOK is 0, then the handshake is not yet completed - // so show the status as waiting - if (stats?.lastOK == 0L) { - b.interfaceDetailCard.strokeColor = - UIUtils.fetchColor(context, R.attr.chipTextNeutral) - resId = R.string.status_waiting - } else { - b.interfaceDetailCard.strokeColor = - UIUtils.fetchColor(context, R.attr.accentGood) - } - } else if ( - statusId == Backend.TUP || - statusId == Backend.TZZ || - statusId == Backend.TNT - ) { + + if (dnsStatusId != null) { + // check for dns failure cases and update the UI + if (isDnsError(dnsStatusId)) { b.interfaceDetailCard.strokeColor = - UIUtils.fetchColor(context, R.attr.chipTextNeutral) + fetchColor(context, R.attr.chipTextNegative) + b.interfaceStatus.text = + context.getString(R.string.status_failing) + .replaceFirstChar(Char::titlecase) } else { - b.interfaceDetailCard.strokeColor = - UIUtils.fetchColor(context, R.attr.accentBad) - } - status = - if (stats?.lastOK == 0L) { - context.getString(resId).replaceFirstChar(Char::titlecase) - } else { - context.getString( - R.string.about_version_install_source, - context.getString(resId).replaceFirstChar(Char::titlecase), - handShakeTime - ) - } - if ((statusId == Backend.TZZ || statusId == Backend.TNT) && stats != null) { - // for idle state, if lastOk is less than 30 sec, then show as connected - if ( - stats.lastOK != 0L && - System.currentTimeMillis() - stats.lastOK < - 30 * DateUtils.SECOND_IN_MILLIS - ) { - status = - context - .getString(R.string.dns_connected) - .replaceFirstChar(Char::titlecase) - } + // if dns status is not failing, then update the proxy status + updateProxyStatusUi(statusId, stats) } } else { - b.interfaceDetailCard.strokeColor = - UIUtils.fetchColor(context, R.attr.accentBad) - status = - context.getString(R.string.status_waiting).replaceFirstChar(Char::titlecase) - b.interfaceActiveLayout.visibility = View.GONE + // in one wg mode, if dns status should be available, this is a fallback case + updateProxyStatusUi(statusId, stats) } - b.interfaceStatus.text = status } else { b.interfaceActiveLayout.visibility = View.GONE - b.interfaceDetailCard.strokeColor = UIUtils.fetchColor(context, R.attr.background) + b.interfaceDetailCard.strokeColor = fetchColor(context, R.attr.background) b.interfaceDetailCard.strokeWidth = 0 b.interfaceSwitch.isChecked = false - b.interfaceStatus.visibility = View.GONE + b.interfaceConfigStatus.visibility = View.GONE b.interfaceAppsCount.visibility = View.GONE - b.interfaceConfigStatus.visibility = View.VISIBLE - b.interfaceConfigStatus.text = + b.interfaceStatus.visibility = View.VISIBLE + b.interfaceStatus.text = context.getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) } } - private fun getRxTx(stats: Stats?): String { + private fun getStrokeColorForStatus(status: UIUtils.ProxyStatus?, stats: RouterStats?): Int { + return when (status) { + UIUtils.ProxyStatus.TOK -> if (stats?.lastOK == 0L) R.attr.chipTextNeutral else R.attr.accentGood + UIUtils.ProxyStatus.TUP, UIUtils.ProxyStatus.TZZ, UIUtils.ProxyStatus.TNT -> R.attr.chipTextNeutral + else -> R.attr.chipTextNegative + } + } + + private fun getStatusText( + status: UIUtils.ProxyStatus?, + handshakeTime: String? = null, + stats: RouterStats? + ): String { + if (status == null) return context.getString(R.string.status_waiting) + .replaceFirstChar(Char::titlecase) + + val baseText = context.getString(UIUtils.getProxyStatusStringRes(status.id)) + .replaceFirstChar(Char::titlecase) + + return if (stats?.lastOK != 0L && handshakeTime != null) { + context.getString(R.string.about_version_install_source, baseText, handshakeTime) + } else { + baseText + } + } + + private fun getIdleStatusText(status: UIUtils.ProxyStatus?, stats: RouterStats?): String { + if (status != UIUtils.ProxyStatus.TZZ && status != UIUtils.ProxyStatus.TNT) return "" + if (stats == null || stats.lastOK == 0L) return "" + if (System.currentTimeMillis() - stats.lastOK >= 30 * DateUtils.SECOND_IN_MILLIS) return "" + + return context.getString(R.string.dns_connected).replaceFirstChar(Char::titlecase) + } + + private fun updateProxyStatusUi(statusId: Long?, stats: RouterStats?) { + val status = UIUtils.ProxyStatus.entries.find { it.id == statusId } // Convert to enum + + val handshakeTime = getHandshakeTime(stats).toString() + + val strokeColor = getStrokeColorForStatus(status, stats) + b.interfaceDetailCard.strokeColor = fetchColor(context, strokeColor) + + val statusText = getIdleStatusText(status, stats) + .ifEmpty { getStatusText(status, handshakeTime, stats) } + + b.interfaceStatus.text = statusText + } + + private fun isDnsError(statusId: Long?): Boolean { + if (statusId == null) return true + + val s = Transaction.Status.fromId(statusId) + return s == Transaction.Status.BAD_QUERY || s == Transaction.Status.BAD_RESPONSE || s == Transaction.Status.NO_RESPONSE || s == Transaction.Status.SEND_FAIL || s == Transaction.Status.CLIENT_ERROR || s == Transaction.Status.INTERNAL_ERROR || s == Transaction.Status.TRANSPORT_ERROR + } + + private fun getRxTx(stats: RouterStats?): String { if (stats == null) return "" val rx = context.getString( @@ -381,10 +391,10 @@ class WgConfigAdapter(private val context: Context) : R.string.symbol_upload, Utilities.humanReadableByteCount(stats.tx, true) ) - return context.getString(R.string.two_argument_space, rx, tx) + return context.getString(R.string.two_argument_space, tx, rx) } - private fun getUpTime(stats: Stats?): CharSequence { + private fun getUpTime(stats: RouterStats?): CharSequence { if (stats == null) { return "" } @@ -398,7 +408,7 @@ class WgConfigAdapter(private val context: Context) : ) } - private fun getHandshakeTime(stats: Stats?): CharSequence { + private fun getHandshakeTime(stats: RouterStats?): CharSequence { if (stats == null) { return "" } @@ -421,33 +431,125 @@ class WgConfigAdapter(private val context: Context) : b.interfaceSwitch.setOnCheckedChangeListener(null) b.interfaceSwitch.setOnClickListener { val cfg = config.toImmutable() - if (b.interfaceSwitch.isChecked) { - if (WireguardManager.canEnableConfig(cfg)) { - WireguardManager.enableConfig(cfg) + io { + if (b.interfaceSwitch.isChecked) { + enableWgIfPossible(cfg) } else { - Utilities.showToastUiCentered( - context, - context.getString(R.string.wireguard_enabled_failure), - Toast.LENGTH_LONG - ) - b.interfaceSwitch.isChecked = false - } - } else { - if (WireguardManager.canDisableConfig(cfg)) { - WireguardManager.disableConfig(cfg) - } else { - Utilities.showToastUiCentered( - context, - context.getString(R.string.wireguard_disable_failure), - Toast.LENGTH_LONG - ) - b.interfaceSwitch.isChecked = true + disableWgIfPossible(cfg) } } } } + private suspend fun disableWgIfPossible(cfg: WgConfigFilesImmutable) { + if (!VpnController.hasTunnel()) { + Logger.i(LOG_TAG_PROXY, "VPN not active, cannot enable WireGuard") + uiCtx { + Utilities.showToastUiCentered( + context, + ERR_CODE_VPN_NOT_ACTIVE + + context.getString(R.string.settings_socks5_vpn_disabled_error), + Toast.LENGTH_LONG + ) + // reset the check box + b.interfaceSwitch.isChecked = true + } + return + } + + if (WireguardManager.canDisableConfig(cfg)) { + WireguardManager.disableConfig(cfg) + } else { + uiCtx { + Utilities.showToastUiCentered( + context, + context.getString(R.string.wireguard_disable_failure), + Toast.LENGTH_LONG + ) + b.interfaceSwitch.isChecked = true + } + } + + WireguardManager.disableConfig(cfg) + uiCtx { listener.onDnsStatusChanged() } + } + + private suspend fun enableWgIfPossible(cfg: WgConfigFilesImmutable) { + + if (!VpnController.hasTunnel()) { + Logger.i(LOG_TAG_PROXY, "VPN not active, cannot enable WireGuard") + uiCtx { + Utilities.showToastUiCentered( + context, + ERR_CODE_VPN_NOT_ACTIVE + + context.getString(R.string.settings_socks5_vpn_disabled_error), + Toast.LENGTH_LONG + ) + // reset the check box + b.interfaceSwitch.isChecked = false + } + return + } + + if (!WireguardManager.canEnableProxy()) { + Logger.i(LOG_TAG_PROXY, "not in DNS+Firewall mode, cannot enable WireGuard") + uiCtx { + // reset the check box + b.interfaceSwitch.isChecked = false + Utilities.showToastUiCentered( + context, + ERR_CODE_VPN_NOT_FULL + + context.getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) + } + return + } + + if (WireguardManager.oneWireGuardEnabled()) { + // this should not happen, ui is disabled if one wireGuard is enabled + Logger.w(LOG_TAG_PROXY, "one wireGuard is already enabled") + uiCtx { + // reset the check box + b.interfaceSwitch.isChecked = false + Utilities.showToastUiCentered( + context, + ERR_CODE_OTHER_WG_ACTIVE + + context.getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) + } + return + } + + if (!WireguardManager.isValidConfig(cfg.id)) { + Logger.i(LOG_TAG_PROXY, "invalid WireGuard config") + uiCtx { + // reset the check box + b.interfaceSwitch.isChecked = false + Utilities.showToastUiCentered( + context, + ERR_CODE_WG_INVALID + context.getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) + } + return + } + + WireguardManager.enableConfig(cfg) + uiCtx { listener.onDnsStatusChanged() } + } + private fun launchConfigDetail(id: Int) { + if (!VpnController.hasTunnel()) { + Utilities.showToastUiCentered( + context, + context.getString(R.string.ssv_toast_start_rethink), + Toast.LENGTH_SHORT + ) + return + } + val intent = Intent(context, WgConfigDetailActivity::class.java) intent.putExtra(INTENT_EXTRA_WG_ID, id) intent.putExtra( diff --git a/app/src/full/java/com/celzero/bravedns/adapter/WgIncludeAppsAdapter.kt b/app/src/full/java/com/celzero/bravedns/adapter/WgIncludeAppsAdapter.kt index ef6e671e4..85bfb5ccb 100644 --- a/app/src/full/java/com/celzero/bravedns/adapter/WgIncludeAppsAdapter.kt +++ b/app/src/full/java/com/celzero/bravedns/adapter/WgIncludeAppsAdapter.kt @@ -19,6 +19,7 @@ import Logger import Logger.LOG_TAG_PROXY import android.content.Context import android.content.DialogInterface +import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View @@ -54,6 +55,7 @@ class WgIncludeAppsAdapter( PagingDataAdapter( DIFF_CALLBACK ) { + private val packageManager: PackageManager = context.packageManager companion object { @@ -74,8 +76,7 @@ class WgIncludeAppsAdapter( oldConnection: ProxyApplicationMapping, newConnection: ProxyApplicationMapping ): Boolean { - return (oldConnection.proxyId == newConnection.proxyId && - oldConnection.uid == newConnection.uid) + return oldConnection == newConnection } } } @@ -139,6 +140,14 @@ class WgIncludeAppsAdapter( val isIncluded = mapping.proxyId == proxyId && mapping.proxyId != "" ui { displayIcon(getIcon(context, mapping.packageName, mapping.appName)) } + // set the alpha based on internet permission + if (mapping.hasInternetPermission(packageManager)) { + b.wgIncludeAppListApkLabelTv.alpha = 1f + b.wgIncludeAppListApkIconIv.alpha = 1f + } else { + b.wgIncludeAppListApkLabelTv.alpha = 0.4f + b.wgIncludeAppListApkIconIv.alpha = 0.4f + } setupClickListeners(mapping, isIncluded) } diff --git a/app/src/full/java/com/celzero/bravedns/download/BlocklistDownloadHelper.kt b/app/src/full/java/com/celzero/bravedns/download/BlocklistDownloadHelper.kt index 14836f859..12888c4c0 100644 --- a/app/src/full/java/com/celzero/bravedns/download/BlocklistDownloadHelper.kt +++ b/app/src/full/java/com/celzero/bravedns/download/BlocklistDownloadHelper.kt @@ -106,14 +106,6 @@ class BlocklistDownloadHelper { } } - fun deleteFromCanonicalPath(context: Context) { - val canonicalPath = - File( - blocklistCanonicalPath(context, Constants.LOCAL_BLOCKLIST_DOWNLOAD_FOLDER_NAME) - ) - deleteRecursive(canonicalPath) - } - fun getExternalFilePath(context: Context, timestamp: String): String { return context.getExternalFilesDir(null).toString() + Constants.ONDEVICE_BLOCKLIST_DOWNLOAD_PATH + diff --git a/app/src/full/java/com/celzero/bravedns/download/FileHandleWorker.kt b/app/src/full/java/com/celzero/bravedns/download/FileHandleWorker.kt index 60fca49a9..919c8cf11 100644 --- a/app/src/full/java/com/celzero/bravedns/download/FileHandleWorker.kt +++ b/app/src/full/java/com/celzero/bravedns/download/FileHandleWorker.kt @@ -21,6 +21,7 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import androidx.work.workDataOf +import com.celzero.bravedns.download.BlocklistDownloadHelper.Companion.deleteBlocklistResidue import com.celzero.bravedns.download.BlocklistDownloadHelper.Companion.deleteOldFiles import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.RethinkBlocklistManager @@ -87,7 +88,6 @@ class FileHandleWorker(val context: Context, workerParameters: WorkerParameters) return false } - BlocklistDownloadHelper.deleteFromCanonicalPath(context) val dir = File(BlocklistDownloadHelper.getExternalFilePath(context, timestamp.toString())) if (!dir.isDirectory) { @@ -139,10 +139,18 @@ class FileHandleWorker(val context: Context, workerParameters: WorkerParameters) val result = updateTagsToDb(timestamp) updatePersistenceOnCopySuccess(timestamp) + // delete the old files in the external directory (downloaded by the download manager) deleteOldFiles(context, timestamp, RethinkBlocklistManager.DownloadType.LOCAL) + // delete the residue files in the app data directory (local_blocklist) + deleteBlocklistResidue( + context, + Constants.REMOTE_BLOCKLIST_DOWNLOAD_FOLDER_NAME, + timestamp + ) + Logger.i(LOG_TAG_DOWNLOAD, "FileHandleWorker, copyFiles success? $result") return true } catch (e: Exception) { - Logger.e(LOG_TAG_DOWNLOAD, "AppDownloadManager Copy exception: ${e.message}", e) + Logger.e(LOG_TAG_DOWNLOAD, "FileHandleWorker Copy exception: ${e.message}", e) } return false } @@ -188,10 +196,10 @@ class FileHandleWorker(val context: Context, workerParameters: WorkerParameters) "tdmd5: $tdmd5, rdmd5: $rdmd5, remotetd: $remoteTdmd5, remoterd: $remoteRdmd5" ) val isDownloadValid = tdmd5 == remoteTdmd5 && rdmd5 == remoteRdmd5 - Logger.i(LOG_TAG_DOWNLOAD, "AppDownloadManager, isDownloadValid? $isDownloadValid") + Logger.i(LOG_TAG_DOWNLOAD, "FileHandleWorker, isDownloadValid? $isDownloadValid") return isDownloadValid } catch (e: Exception) { - Logger.e(LOG_TAG_DOWNLOAD, "AppDownloadManager, isDownloadValid err: ${e.message}", e) + Logger.e(LOG_TAG_DOWNLOAD, "FileHandleWorker, isDownloadValid err: ${e.message}", e) } return false } diff --git a/app/src/full/java/com/celzero/bravedns/receiver/NotificationActionReceiver.kt b/app/src/full/java/com/celzero/bravedns/receiver/NotificationActionReceiver.kt index 97b411b04..feae06b0e 100644 --- a/app/src/full/java/com/celzero/bravedns/receiver/NotificationActionReceiver.kt +++ b/app/src/full/java/com/celzero/bravedns/receiver/NotificationActionReceiver.kt @@ -95,7 +95,7 @@ class NotificationActionReceiver : BroadcastReceiver(), KoinComponent { } private fun stopVpn(context: Context) { - VpnController.stop(context) + VpnController.stop("notif", context) } private fun pauseApp(context: Context) { diff --git a/app/src/full/java/com/celzero/bravedns/scheduler/BugReportZipper.kt b/app/src/full/java/com/celzero/bravedns/scheduler/BugReportZipper.kt index 5e0d6bd88..93133d79c 100644 --- a/app/src/full/java/com/celzero/bravedns/scheduler/BugReportZipper.kt +++ b/app/src/full/java/com/celzero/bravedns/scheduler/BugReportZipper.kt @@ -27,7 +27,6 @@ import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Utilities import com.google.common.io.Files -import intra.Intra import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException @@ -222,14 +221,20 @@ object BugReportZipper { file.appendText(prefsDetails.toString()) val separator = "--------------------------------------------\n" file.appendText(separator) - val build = VpnController.goBuildVersion() + val build = VpnController.goBuildVersion(true) file.appendText(build) file.appendText(separator) } private fun copy(input: InputStream, output: OutputStream) { - while (input.read() != -1) { - output.write(input.readBytes()) + try { + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + } + } catch (e: Exception) { + Logger.w(LOG_TAG_BUG_REPORT, "Exception while copying the file", e) } } } diff --git a/app/src/full/java/com/celzero/bravedns/scheduler/EnhancedBugReport.kt b/app/src/full/java/com/celzero/bravedns/scheduler/EnhancedBugReport.kt index 678e5312d..c4f63199b 100644 --- a/app/src/full/java/com/celzero/bravedns/scheduler/EnhancedBugReport.kt +++ b/app/src/full/java/com/celzero/bravedns/scheduler/EnhancedBugReport.kt @@ -15,11 +15,13 @@ */ package com.celzero.bravedns.scheduler -import Logger import Logger.LOG_TAG_BUG_REPORT import android.content.Context import android.os.Build +import android.util.Log import androidx.annotation.RequiresApi +import com.celzero.bravedns.util.Constants +import com.celzero.bravedns.util.Utilities import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException @@ -45,11 +47,11 @@ object EnhancedBugReport { // add the logs to the zip file // close the zip file val zipFilePath = File(context.filesDir.canonicalPath + File.separator + ZIP_FILE_NAME) - Logger.d(LOG_TAG_BUG_REPORT, "zip file path: $zipFilePath") + Log.d(LOG_TAG_BUG_REPORT, "zip file path: $zipFilePath") val zipOutputStream = ZipOutputStream(FileOutputStream(zipFilePath)) val folder = getFolderPath(context.filesDir) ?: return val files = File(folder).listFiles() - Logger.d(LOG_TAG_BUG_REPORT, "files to add to zip: ${files?.size}") + Log.d(LOG_TAG_BUG_REPORT, "files to add to zip: ${files?.size}") files?.forEach { file -> val inputStream = FileInputStream(file) val zipEntry = ZipEntry(file.name) @@ -59,32 +61,33 @@ object EnhancedBugReport { } zipOutputStream.close() } catch (e: FileNotFoundException) { - Logger.e(LOG_TAG_BUG_REPORT, "err adding logs to zip file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err adding logs to zip file: ${e.message}", e) } catch (e: ZipException) { - Logger.e(LOG_TAG_BUG_REPORT, "err adding logs to zip file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err adding logs to zip file: ${e.message}", e) } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err adding logs to zip file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err adding logs to zip file: ${e.message}", e) } - Logger.i(LOG_TAG_BUG_REPORT, "logs added to zip file") + Log.i(LOG_TAG_BUG_REPORT, "logs added to zip file") } fun writeLogsToFile(context: Context, logs: String) { try { val file = getFileToWrite(context) if (file == null) { - Logger.e(LOG_TAG_BUG_REPORT, "file name is null, cannot write logs to file") + Log.e(LOG_TAG_BUG_REPORT, "file name is null, cannot write logs to file") return } - val l = logs + "\n" // append a new line character + val time = Utilities.convertLongToTime(System.currentTimeMillis(), Constants.TIME_FORMAT_3) + val l = "$time: $logs\n" // append a new line character file.appendText(l, Charset.defaultCharset()) } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err writing logs to file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err writing logs to file: ${e.message}", e) } } - private fun getFileToWrite(context: Context): File? { + fun getFileToWrite(context: Context): File? { val file = getTombstoneFile(context) - Logger.d(LOG_TAG_BUG_REPORT, "file to write logs: ${file?.name}") + Log.d(LOG_TAG_BUG_REPORT, "file to write logs: ${file?.name}") return file } @@ -92,20 +95,20 @@ object EnhancedBugReport { try { val folderPath = getFolderPath(context.filesDir) if (folderPath == null) { - Logger.e(LOG_TAG_BUG_REPORT, "folder path is null, cannot get tombstone file") + Log.e(LOG_TAG_BUG_REPORT, "folder path is null, cannot get tombstone file") return null } val folder = File(folderPath) val files = folder.listFiles() if (files.isNullOrEmpty()) { - Logger.d(LOG_TAG_BUG_REPORT, "no files found in the tombstone folder") + Log.d(LOG_TAG_BUG_REPORT, "no files found in the tombstone folder") return createTombstoneFile(folderPath) } else { - Logger.d(LOG_TAG_BUG_REPORT, "files found in the tombstone folder") + Log.d(LOG_TAG_BUG_REPORT, "files found in the tombstone folder") return getLatestFile(files) ?: createTombstoneFile(folderPath) } } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err getting tombstone file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err getting tombstone file: ${e.message}", e) return null } } @@ -117,13 +120,13 @@ object EnhancedBugReport { files.sortByDescending { it.name } var latestFile = files[0] var latestTimestamp = latestFile.lastModified() - Logger.vv(LOG_TAG_BUG_REPORT, "latest timestamp: $latestTimestamp, ${latestFile.name}") + Log.v(LOG_TAG_BUG_REPORT, "latest timestamp: $latestTimestamp, ${latestFile.name}") for (file in files) { - Logger.vv(LOG_TAG_BUG_REPORT, "file timestamp: ${file.lastModified()}, ${file.name}") + Log.v(LOG_TAG_BUG_REPORT, "file timestamp: ${file.lastModified()}, ${file.name}") if (file.lastModified() > latestTimestamp) { latestFile = file latestTimestamp = file.lastModified() - Logger.vv(LOG_TAG_BUG_REPORT, "updated timestamp: $latestTimestamp, ${latestFile.name}") + Log.v(LOG_TAG_BUG_REPORT, "updated timestamp: $latestTimestamp, ${latestFile.name}") } } @@ -137,13 +140,13 @@ object EnhancedBugReport { files.sortBy { it.name } var oldestFile = files[0] var oldestTimestamp = oldestFile.lastModified() - Logger.vv(LOG_TAG_BUG_REPORT, "oldest timestamp: $oldestTimestamp, ${oldestFile.name}") + Log.v(LOG_TAG_BUG_REPORT, "oldest timestamp: $oldestTimestamp, ${oldestFile.name}") for (file in files) { - Logger.vv(LOG_TAG_BUG_REPORT, "file timestamp: ${file.lastModified()}, ${file.name}") + Log.v(LOG_TAG_BUG_REPORT, "file timestamp: ${file.lastModified()}, ${file.name}") if (file.lastModified() < oldestTimestamp) { oldestFile = file oldestTimestamp = file.lastModified() - Logger.vv(LOG_TAG_BUG_REPORT, "updated timestamp: $oldestTimestamp, ${oldestFile.name}") + Log.v(LOG_TAG_BUG_REPORT, "updated timestamp: $oldestTimestamp, ${oldestFile.name}") } } @@ -154,12 +157,12 @@ object EnhancedBugReport { try { val zipFile = File(context.filesDir.canonicalPath + File.separator + ZIP_FILE_NAME) if (!zipFile.exists()) { - Logger.w(LOG_TAG_BUG_REPORT, "zip file is null, cannot add logs to zip file") + Log.w(LOG_TAG_BUG_REPORT, "zip file is null, cannot add logs to zip file") return null } return zipFile } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err getting tombstone zip file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err getting tombstone zip file: ${e.message}", e) } return null } @@ -167,12 +170,12 @@ object EnhancedBugReport { private fun getLatestFile(files: Array): File? { try { if (files.isEmpty()) { - Logger.w(LOG_TAG_BUG_REPORT, "no files found in the tombstone folder") + Log.w(LOG_TAG_BUG_REPORT, "no files found in the tombstone folder") return null } val latestFile = findLatestFile(files) ?: return null if (latestFile.length() > MAX_FILE_SIZE) { - Logger.d(LOG_TAG_BUG_REPORT, "file size is more than 1MB, ${latestFile.name}") + Log.d(LOG_TAG_BUG_REPORT, "file size is more than 1MB, ${latestFile.name}") // create a new file val parent = latestFile.parent ?: return null return createTombstoneFile(parent) @@ -180,12 +183,12 @@ object EnhancedBugReport { // if the file count is more than MAX_TOMBSTONE_FILES, delete the oldest file if (files.size > MAX_TOMBSTONE_FILES) { val fileToDelete = findOldestFile(files) ?: return null - Logger.i(LOG_TAG_BUG_REPORT, "deleted the oldest file ${fileToDelete.name}, file count: ${files.size}") + Log.i(LOG_TAG_BUG_REPORT, "deleted the oldest file ${fileToDelete.name}, file count: ${files.size}") fileToDelete.delete() } return latestFile } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err getting latest file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err getting latest file: ${e.message}", e) } return null } @@ -197,10 +200,10 @@ object EnhancedBugReport { val file = File(folderPath + File.separator + TOMBSTONE_FILE_NAME + ts + FILE_EXTENSION) file.createNewFile() - Logger.d(LOG_TAG_BUG_REPORT, "created tombstone file: ${file.name}") + Log.d(LOG_TAG_BUG_REPORT, "created tombstone file: ${file.name}") return file } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err creating tombstone file: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err creating tombstone file: ${e.message}", e) } return null } @@ -212,14 +215,14 @@ object EnhancedBugReport { val path = file.canonicalPath + File.separator + TOMBSTONE_DIR_NAME val folder = File(path) if (folder.exists()) { - Logger.vv(LOG_TAG_BUG_REPORT, "folder exists: $path") + Log.v(LOG_TAG_BUG_REPORT, "folder exists: $path") } else { folder.mkdir() - Logger.vv(LOG_TAG_BUG_REPORT, "folder created: $path") + Log.v(LOG_TAG_BUG_REPORT, "folder created: $path") } return path } catch (e: Exception) { - Logger.e(LOG_TAG_BUG_REPORT, "err getting folder path: ${e.message}", e) + Log.e(LOG_TAG_BUG_REPORT, "err getting folder path: ${e.message}", e) } return null } diff --git a/app/src/full/java/com/celzero/bravedns/scheduler/LogExportWorker.kt b/app/src/full/java/com/celzero/bravedns/scheduler/LogExportWorker.kt new file mode 100644 index 000000000..8fe7065ca --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/scheduler/LogExportWorker.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.scheduler + +import Logger +import Logger.LOG_TAG_BUG_REPORT +import android.content.Context +import android.database.Cursor +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.celzero.bravedns.database.ConsoleLogDAO +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.BufferedOutputStream + +class LogExportWorker(context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams), KoinComponent { + + private val consoleLogDao by inject() + + companion object { + private const val QUERY = "SELECT * FROM ConsoleLog order by id" + } + + override suspend fun doWork(): Result { + return try { + val filePath = inputData.getString("filePath") ?: return Result.failure() + Logger.i(LOG_TAG_BUG_REPORT, "Exporting logs to $filePath") + exportLogsToCsvStream(filePath) + Result.success() + } catch (e: Exception) { + Result.failure() + } + } + + private fun exportLogsToCsvStream(filePath: String): Boolean { + var cursor: Cursor? = null + try { + val query = SimpleSQLiteQuery(QUERY) + cursor = consoleLogDao.getLogsCursor(query) + + val file = File(filePath) + if (file.exists()) { + Logger.v(LOG_TAG_BUG_REPORT, "Deleting existing zip file, ${file.absolutePath}") + file.delete() + } + + val stringBuilder = StringBuilder() + cursor.let { + if (it.moveToFirst()) { + do { + val timestamp = it.getLong(it.getColumnIndexOrThrow("timestamp")) + val message = it.getString(it.getColumnIndexOrThrow("message")) + stringBuilder.append("$timestamp,$message\n") + } while (it.moveToNext()) + } + } + + ZipOutputStream(BufferedOutputStream(FileOutputStream(filePath))).use { zos -> + val zipEntry = ZipEntry("log_${System.currentTimeMillis()}.txt") + zos.putNextEntry(zipEntry) + zos.write(stringBuilder.toString().toByteArray()) + zos.closeEntry() + } + + Logger.i(LOG_TAG_BUG_REPORT, "Logs exported to ${file.absolutePath}") + return true + } catch (e: Exception) { + Logger.e(LOG_TAG_BUG_REPORT, "Error exporting logs", e) + } finally { + cursor?.close() + } + return false + } +} diff --git a/app/src/full/java/com/celzero/bravedns/scheduler/PurgeConsoleLogs.kt b/app/src/full/java/com/celzero/bravedns/scheduler/PurgeConsoleLogs.kt new file mode 100644 index 000000000..f3bd9ea10 --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/scheduler/PurgeConsoleLogs.kt @@ -0,0 +1,39 @@ +package com.celzero.bravedns.scheduler + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.celzero.bravedns.database.ConsoleLogRepository +import com.celzero.bravedns.service.PersistentState +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.util.concurrent.TimeUnit + +class PurgeConsoleLogs(val context: Context, workerParameters: WorkerParameters) : + CoroutineWorker(context, workerParameters), KoinComponent { + + private val consoleLogRepository by inject() + private val persistentState by inject() + + companion object { + const val MAX_TIME: Long = 3 + } + override suspend fun doWork(): Result { + // delete logs which are older than MAX_TIME hrs + val threshold = TimeUnit.HOURS.toMillis(MAX_TIME) + val currTime = System.currentTimeMillis() + val time = currTime - threshold + + consoleLogRepository.deleteOldLogs(time) + if (persistentState.consoleLogEnabled) { + val startTime = consoleLogRepository.consoleLogStartTimestamp + // stop the console log if it exceeds max time + if (currTime - startTime > TimeUnit.HOURS.toMillis(MAX_TIME)) { + consoleLogRepository.consoleLogStartTimestamp = 0 + persistentState.consoleLogEnabled = false + } + } + return Result.success() + } + +} diff --git a/app/src/full/java/com/celzero/bravedns/scheduler/WorkScheduler.kt b/app/src/full/java/com/celzero/bravedns/scheduler/WorkScheduler.kt index 624682d3a..51a9bed09 100644 --- a/app/src/full/java/com/celzero/bravedns/scheduler/WorkScheduler.kt +++ b/app/src/full/java/com/celzero/bravedns/scheduler/WorkScheduler.kt @@ -20,6 +20,7 @@ import Logger import Logger.LOG_TAG_SCHEDULER import android.content.Context import androidx.work.BackoffPolicy +import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder @@ -38,18 +39,21 @@ class WorkScheduler(val context: Context) { const val APP_EXIT_INFO_ONE_TIME_JOB_TAG = "OnDemandCollectAppExitInfoJob" const val APP_EXIT_INFO_JOB_TAG = "ScheduledCollectAppExitInfoJob" const val PURGE_CONNECTION_LOGS_JOB_TAG = "ScheduledPurgeConnectionLogsJob" + const val PURGE_CONSOLE_LOGS_JOB_TAG = "ScheduledPurgeConsoleLogsJob" const val BLOCKLIST_UPDATE_CHECK_JOB_TAG = "ScheduledBlocklistUpdateCheckJob" const val DATA_USAGE_JOB_TAG = "ScheduledDataUsageJob" + const val CONSOLE_LOG_SAVE_JOB_TAG = "ConsoleLogSaveJob" const val APP_EXIT_INFO_JOB_TIME_INTERVAL_DAYS: Long = 7 const val PURGE_LOGS_TIME_INTERVAL_HOURS: Long = 4 + const val PURGE_CONSOLE_LOGS_TIME_INTERVAL_HOURS: Long = 3 const val BLOCKLIST_UPDATE_CHECK_INTERVAL_DAYS: Long = 3 const val DATA_USAGE_TIME_INTERVAL_MINS: Long = 20 fun isWorkRunning(context: Context, tag: String): Boolean { val instance = WorkManager.getInstance(context) val statuses: ListenableFuture> = instance.getWorkInfosByTag(tag) - Logger.d(LOG_TAG_SCHEDULER, "Job $tag already running check") + Logger.i(LOG_TAG_SCHEDULER, "Job $tag already running check") return try { var running = false val workInfos = statuses.get() @@ -75,7 +79,7 @@ class WorkScheduler(val context: Context) { fun isWorkScheduled(context: Context, tag: String): Boolean { val instance = WorkManager.getInstance(context) val statuses: ListenableFuture> = instance.getWorkInfosByTag(tag) - Logger.d(LOG_TAG_SCHEDULER, "Job $tag already scheduled check") + Logger.i(LOG_TAG_SCHEDULER, "Job $tag already scheduled check") return try { var running = false val workInfos = statuses.get() @@ -104,7 +108,7 @@ class WorkScheduler(val context: Context) { // app exit info is supported from R+ if (!Utilities.isAtleastR()) return - Logger.d(LOG_TAG_SCHEDULER, "App exit info job scheduled") + Logger.i(LOG_TAG_SCHEDULER, "App exit info job scheduled") val bugReportCollector = PeriodicWorkRequest.Builder( BugReportCollector::class.java, @@ -131,7 +135,7 @@ class WorkScheduler(val context: Context) { .addTag(PURGE_CONNECTION_LOGS_JOB_TAG) .build() - Logger.d(LOG_TAG_SCHEDULER, "purge connection logs job scheduled") + Logger.i(LOG_TAG_SCHEDULER, "purge connection logs job scheduled") WorkManager.getInstance(context.applicationContext) .enqueueUniquePeriodicWork( PURGE_CONNECTION_LOGS_JOB_TAG, @@ -140,6 +144,25 @@ class WorkScheduler(val context: Context) { ) } + fun schedulePurgeConsoleLogs() { + val purgeLogs = + PeriodicWorkRequest.Builder( + PurgeConsoleLogs::class.java, + PURGE_CONSOLE_LOGS_TIME_INTERVAL_HOURS, + TimeUnit.HOURS + ) + .addTag(PURGE_CONSOLE_LOGS_JOB_TAG) + .build() + + Logger.i(LOG_TAG_SCHEDULER, "purge console logs job scheduled") + WorkManager.getInstance(context.applicationContext) + .enqueueUniquePeriodicWork( + PURGE_CONSOLE_LOGS_JOB_TAG, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + purgeLogs + ) + } + // Schedule AppExitInfo on demand fun scheduleOneTimeWorkForAppExitInfo() { val bugReportCollector = @@ -199,4 +222,21 @@ class WorkScheduler(val context: Context) { workRequest ) } + + fun scheduleConsoleLogSaveJob(filePath: String) { + Logger.i(LOG_TAG_SCHEDULER, "Console log save job scheduled") + val inputData = Data.Builder().putString("filePath", filePath).build() + val workRequest = + OneTimeWorkRequestBuilder() + .addTag(CONSOLE_LOG_SAVE_JOB_TAG) + .setInputData(inputData) + .build() + + WorkManager.getInstance(context.applicationContext) + .enqueueUniqueWork( + CONSOLE_LOG_SAVE_JOB_TAG, + ExistingWorkPolicy.REPLACE, + workRequest + ) + } } diff --git a/app/src/full/java/com/celzero/bravedns/service/TcpProxyHelper.kt b/app/src/full/java/com/celzero/bravedns/service/TcpProxyHelper.kt index 05ff43b8f..8a727b6b9 100644 --- a/app/src/full/java/com/celzero/bravedns/service/TcpProxyHelper.kt +++ b/app/src/full/java/com/celzero/bravedns/service/TcpProxyHelper.kt @@ -202,7 +202,7 @@ object TcpProxyHelper : KoinComponent { Logger.w(LOG_TAG_PROXY, "getTcpProxyPaymentStatus: tcpProxy not found") return PaymentStatus.NOT_PAID } - return PaymentStatus.values().find { it.value == tcpProxy.paymentStatus } + return PaymentStatus.entries.find { it.value == tcpProxy.paymentStatus } ?: PaymentStatus.NOT_PAID } diff --git a/app/src/full/java/com/celzero/bravedns/service/WireguardManager.kt b/app/src/full/java/com/celzero/bravedns/service/WireguardManager.kt index 442a080b1..69c2c943e 100644 --- a/app/src/full/java/com/celzero/bravedns/service/WireguardManager.kt +++ b/app/src/full/java/com/celzero/bravedns/service/WireguardManager.kt @@ -31,6 +31,7 @@ import com.celzero.bravedns.wireguard.BadConfigException import com.celzero.bravedns.wireguard.Config import com.celzero.bravedns.wireguard.Peer import com.celzero.bravedns.wireguard.WgInterface +import inet.ipaddr.IPAddressString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -78,6 +79,12 @@ object WireguardManager : KoinComponent { const val WARP_ID = 1 const val WARP_FILE_NAME = "wg1.conf" + // let the error code be a string, so that it can be concatenated with the error message + const val ERR_CODE_VPN_NOT_ACTIVE = "1" + const val ERR_CODE_VPN_NOT_FULL = "2" + const val ERR_CODE_OTHER_WG_ACTIVE = "3" + const val ERR_CODE_WG_INVALID = "4" + // invalid config id const val INVALID_CONF_ID = -1 @@ -101,7 +108,6 @@ object WireguardManager : KoinComponent { EncryptedFileManager.readWireguardConfig(applicationContext, path) if (config == null) { Logger.e(LOG_TAG_PROXY, "error loading wg config: $path, deleting...") - db.deleteConfig(it.id) return@forEach } if (configs.none { i -> i.getId() == it.id }) { @@ -144,6 +150,10 @@ object WireguardManager : KoinComponent { return mappings.any { it.isActive } } + fun isAdvancedWgActive(): Boolean { + return mappings.any { it.isActive && !it.oneWireGuard } + } + fun getEnabledConfigs(): List { val m = mappings.filter { it.isActive } val l = mutableListOf() @@ -229,23 +239,31 @@ object WireguardManager : KoinComponent { return } - fun canEnableConfig(map: WgConfigFilesImmutable): Boolean { - val canEnable = appConfig.canEnableProxy() && appConfig.canEnableWireguardProxy() - if (!canEnable) { - return false - } - // if one wireguard is enabled, don't allow to enable another - if (oneWireGuardEnabled()) { - return false - } - val config = configs.find { it.getId() == map.id } + fun canEnableProxy(): Boolean { + return appConfig.canEnableProxy() + } + + fun isValidConfig(id: Int): Boolean { + val config = configs.find { it.getId() == id } if (config == null) { - Logger.e(LOG_TAG_PROXY, "canEnableConfig: wg not found, id: ${map.id}, ${configs.size}") + Logger.e(LOG_TAG_PROXY, "canEnableConfig: wg not found, id: ${id}, ${configs.size}") return false } return true } + fun isAnyOtherOneWgEnabled(id: Int): Boolean { + return mappings.any { it.oneWireGuard && it.isActive && it.id != id } + } + + fun canEnableWg(): Boolean { + val canEnableProxy = appConfig.canEnableProxy() + val canEnableWireGuardProxy = appConfig.canEnableWireguardProxy() + val canEnable = canEnableProxy && canEnableWireGuardProxy + Logger.i(LOG_TAG_PROXY, "canEnableConfig? $canEnableProxy && $canEnableWireGuardProxy") + return canEnable + } + fun canDisableConfig(map: WgConfigFilesImmutable): Boolean { // do not allow to disable the proxy if it is catch-all return !map.isCatchAll @@ -427,30 +445,63 @@ object WireguardManager : KoinComponent { // if the app is added to config, return the config if it is active or lockdown if (config != null && (config.isActive || config.isLockdown)) { - return config + val isValidConfig = if (config.isLockdown) { + // if lockdown is enabled, canRoute checks peer configuration and if it returns + // "false", then the connection will be sent to base and not dropped + // if lockdown is disabled, then canRoute returns default (true) which + // will have the effect of blocking all connections + // ie, if lockdown is enabled, split-tunneling happens as expected but if + // lockdown is disabled, it has the effect of blocking all connections + isValidWgConnForIp(id, ip, true) + } else { + isValidWgConnForIp(id, ip, false) + } + if (isValidConfig) { + Logger.d(LOG_TAG_PROXY, "app config mapping found for uid: $uid, $configId") + return config + } else { + Logger.d(LOG_TAG_PROXY, "app config mapping found for uid: $uid, $configId, but not valid") + // pass-through and check if any catch-all config is enabled + } } + // check if any catch-all config is enabled - if (configId == "" || !configId.contains(ProxyManager.ID_WG_BASE) || config == null) { - Logger.d(LOG_TAG_PROXY, "app config mapping not found for uid: $uid") - // there maybe catch-all config enabled, so return the active catch-all config - val catchAllConfig = mappings.find { it.isActive && it.isCatchAll } - return if (catchAllConfig == null) { - Logger.d(LOG_TAG_PROXY, "catch all config not found for uid: $uid") + Logger.d(LOG_TAG_PROXY, "app config mapping not found for uid: $uid") + val catchAllConfig = mappings.find { it.isActive && it.isCatchAll } + return if (catchAllConfig == null) { + Logger.d(LOG_TAG_PROXY, "catch all config not found for uid: $uid") + null + } else { + // if catch-all config is enabled, check for the validity of the connection + val optimalId = fetchOptimalCatchAllConfig(uid, ip) + if (optimalId == null) { + Logger.d(LOG_TAG_PROXY, "no catch all config found for uid: $uid") null } else { - val optimalId = fetchOptimalCatchAllConfig(uid, ip) - if (optimalId == null) { - Logger.d(LOG_TAG_PROXY, "no catch all config found for uid: $uid") - null - } else { - Logger.d(LOG_TAG_PROXY, "catch all config found for uid: $uid, $optimalId") - mappings.find { it.id == optimalId } - } + Logger.d(LOG_TAG_PROXY, "catch all config found for uid: $uid, $optimalId") + mappings.find { it.id == optimalId } } } + } + + suspend fun getConfigIdForApp(uid: Int): WgConfigFilesImmutable? { + val configId = ProxyManager.getProxyIdForApp(uid) + + val id = if (configId.isNotEmpty()) convertStringIdToId(configId) else INVALID_CONF_ID + val config = if (id == INVALID_CONF_ID) null else mappings.find { it.id == id } + if (config != null && (config.isActive || config.isLockdown)) { + Logger.d(LOG_TAG_PROXY, "app config mapping found for uid: $uid, $configId") + return config + } - // if the app is not added to any config, and no catch-all config is enabled - return null + val catchAllConfig = getOptimalCatchAllConfigId() + return if (catchAllConfig == null) { + Logger.d(LOG_TAG_PROXY, "catch all config not found for uid: $uid") + null + } else { + Logger.d(LOG_TAG_PROXY, "catch all config found for uid: $uid, $catchAllConfig") + mappings.find { it.id == catchAllConfig } + } } private fun convertStringIdToId(id: String): Int { @@ -486,7 +537,7 @@ object WireguardManager : KoinComponent { if (available) { val wgId = catchAllAppConfigCache.value[uid] if (wgId != null) { - if (isProxyConnectionValid(wgId, ip)) { + if (isValidWgConnForIp(wgId, ip)) { Logger.d(LOG_TAG_PROXY, "optimalCatchAllConfig: returning cached wgId: $wgId") return wgId // return the already mapped wgId which is active } @@ -495,11 +546,9 @@ object WireguardManager : KoinComponent { } } Logger.d(LOG_TAG_PROXY, "optimalCatchAllConfig: fetching new wgId for uid: $uid") - val catchAllList = mappings.filter { - val id = ProxyManager.ID_WG_BASE + it.id - it.isActive && it.isCatchAll && VpnController.canRouteIp(id, ip, false) } + val catchAllList = mappings.filter { it.isActive && it.isCatchAll } catchAllList.forEach { - if (isProxyConnectionValid(it.id, ip)) { + if (isValidWgConnForIp(it.id, ip)) { // note the uid and wgid in a cache, so that we can use it for further requests catchAllAppConfigCache.value += uid to it.id Logger.d(LOG_TAG_PROXY, "optimalCatchAllConfig: returning new wgId: ${it.id}") @@ -512,6 +561,13 @@ object WireguardManager : KoinComponent { return catchAllList.randomOrNull()?.id } + private suspend fun isValidWgConnForIp(wgId: Int, ip: String, default: Boolean = false): Boolean { + val isSupported = isSupportedIpVersion(wgId, ip) + val isValid = isProxyConnectionValid(wgId, ip, default) + Logger.d(LOG_TAG_PROXY, "isValidWgConnForIp: $wgId? isSupported: $isSupported, isValid: $isValid") + return isSupported && isValid + } + private fun pingCatchAllConfigs(catchAllConfigs: List) { io { // ping the catch-all config @@ -522,7 +578,7 @@ object WireguardManager : KoinComponent { } } - private suspend fun isProxyConnectionValid(wgId: Int, ip: String, default: Boolean = false): Boolean { + private suspend fun isProxyConnectionValid(wgId: Int, ip: String, default: Boolean): Boolean { // check if the handshake is less than 3 minutes (VALID_LAST_OK_SEC) // and if the ip can be routed val id = ProxyManager.ID_WG_BASE + wgId @@ -531,6 +587,20 @@ object WireguardManager : KoinComponent { return isValidLastOk(wgId) && canRoute } + private suspend fun isSupportedIpVersion(wgId: Int, ip: String): Boolean { + val ipVersion = IPAddressString(ip).toAddress().ipVersion + val id = ProxyManager.ID_WG_BASE + wgId + val supportedVersion = VpnController.getSupportedIpVersion(id) + Logger.d(LOG_TAG_PROXY, "isSupportedIpVersion: $wgId? ipVersion: $ipVersion, supportedVersion: $supportedVersion") + if (ipVersion.isIPv4 && supportedVersion.first) { + return true + } + if (ipVersion.isIPv6 && supportedVersion.second) { + return true + } + return false + } + private suspend fun isValidLastOk(wgId: Int): Boolean { val id = ProxyManager.ID_WG_BASE + wgId val stat = VpnController.getProxyStats(id) ?: return false @@ -936,21 +1006,33 @@ object WireguardManager : KoinComponent { return mappings.find { it.oneWireGuard && it.isActive }?.id } - suspend fun getOptimalCatchAllConfigId(ip: String?): Int? { - val configs = mappings.filter { - val id = ProxyManager.ID_WG_BASE + it.id - it.isCatchAll && it.isActive && ((ip == null) || VpnController.canRouteIp(id, ip, false)) } - configs.forEach { - if (isValidLastOk(it.id)) { - Logger.d(LOG_TAG_PROXY, "found optimal catch all config: ${it.id}") - return it.id + suspend fun getOptimalCatchAllConfigId(): Int? { + val configs = mappings.filter { it.isCatchAll && it.isActive } + // get the config which doesn't have split tunnel. If none, return any catch-all config + // TODO: instead get the dns ip from go-lib and check if it can be routed + configs.forEach { config -> + if (isValidLastOk(config.id) && !isSplitTunnelProxy(config.id)) { + Logger.d(LOG_TAG_PROXY, "optimal catch all config found: ${config.id}") + return config.id } } Logger.d(LOG_TAG_PROXY, "no optimal catch all config found, returning any catchall") - // if no catch-all config is active, return any catch-all config + // return any catch-all config return configs.randomOrNull()?.id } + private suspend fun isSplitTunnelProxy(configId: Int): Boolean { + val id = ProxyManager.ID_WG_BASE + configId + val pair = VpnController.getSupportedIpVersion(id) + return VpnController.isSplitTunnelProxy(id, pair) + } + + fun invalidateCatchAllCache() { + if (catchAllAppConfigCache.value.isNotEmpty()) { + catchAllAppConfigCache.value = emptyMap() + } + } + private fun io(f: suspend () -> Unit) { CoroutineScope(Dispatchers.IO).launch { f() } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt index aa2d79071..60f4359bd 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt @@ -29,8 +29,8 @@ import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.net.Uri import android.os.Bundle -import android.os.PersistableBundle import android.os.SystemClock +import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager @@ -54,7 +54,7 @@ import com.celzero.bravedns.backup.BackupHelper.Companion.BACKUP_FILE_EXTN import com.celzero.bravedns.backup.BackupHelper.Companion.INTENT_RESTART_APP import com.celzero.bravedns.backup.BackupHelper.Companion.INTENT_SCHEME import com.celzero.bravedns.backup.RestoreAgent -import com.celzero.bravedns.data.AppConfig +import com.celzero.bravedns.database.AppInfoRepository import com.celzero.bravedns.database.RefreshDatabase import com.celzero.bravedns.databinding.ActivityHomeScreenBinding import com.celzero.bravedns.service.AppUpdater @@ -62,6 +62,7 @@ import com.celzero.bravedns.service.BraveVPNService import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.RethinkBlocklistManager import com.celzero.bravedns.service.VpnController +import com.celzero.bravedns.ui.activity.MiscSettingsActivity import com.celzero.bravedns.ui.activity.PauseActivity import com.celzero.bravedns.ui.activity.WelcomeActivity import com.celzero.bravedns.util.Constants @@ -76,19 +77,21 @@ import com.celzero.bravedns.util.Utilities.showToastUiCentered import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import java.util.Calendar -import java.util.concurrent.Executor -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.get import org.koin.android.ext.android.inject +import java.util.Calendar +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit +import kotlin.math.abs + class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { private val b by viewBinding(ActivityHomeScreenBinding::bind) private val persistentState by inject() - private val appConfig by inject() + private val appInfoDb by inject() private val appUpdateManager by inject() private val rdb by inject() @@ -97,12 +100,7 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { private lateinit var biometricPrompt: BiometricPrompt private lateinit var promptInfo: BiometricPrompt.PromptInfo - // private var biometricPromptRetryCount = 1 - private var onResumeCalledAlready = false - - companion object { - private const val ON_RESUME_CALLED_PREFERENCE_KEY = "onResumeCalled" - } + private var isActivityStarted = false // TODO - #324 - Usage of isDarkTheme() in all activities. private fun Context.isDarkThemeOn(): Boolean { @@ -114,15 +112,9 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { setTheme(getCurrentTheme(isDarkThemeOn(), persistentState.theme)) super.onCreate(savedInstanceState) - // stackoverflow.com/questions/44221195/multiple-onstop-onresume-calls-in-android-activity - // Restore value of members from saved state - onResumeCalledAlready = - savedInstanceState?.getBoolean(ON_RESUME_CALLED_PREFERENCE_KEY) ?: false - // do not launch on board activity when app is running on TV if (persistentState.firstTimeLaunch && !isAppRunningOnTv()) { launchOnboardActivity() - rdnsRemote() return } updateNewVersion() @@ -135,20 +127,31 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { initUpdateCheck() observeAppState() - } - - override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) { - outState.putBoolean(ON_RESUME_CALLED_PREFERENCE_KEY, onResumeCalledAlready) - super.onSaveInstanceState(outState, outPersistentState) + isActivityStarted = false } override fun onResume() { super.onResume() - if (persistentState.biometricAuth && !isAppRunningOnTv() && !onResumeCalledAlready) { - biometricPrompt() + Logger.v(LOG_TAG_UI, "isActivityStarted: $isActivityStarted, intent: $intent, action: ${intent?.action}") + if (!isActivityStarted) { + isActivityStarted = true + Logger.vv(LOG_TAG_UI, "HomeScreenActivity is resumed") + if (intent != null) { // intent is not null means the activity is started from intent + Logger.vv(LOG_TAG_UI, "HomeScreenActivity is started from intent") + Logger.vv(LOG_TAG_UI, "isBiometricEnabled: ${isBiometricEnabled()}, isAppRunningOnTv: ${isAppRunningOnTv()}") + if (isBiometricEnabled() && !isAppRunningOnTv()) { + biometricPrompt() + } + } } } + private fun isBiometricEnabled(): Boolean { + val type = MiscSettingsActivity.BioMetricType.fromValue(persistentState.biometricAuthType) + // use the biometricAuth flag for backward compatibility with older versions + return persistentState.biometricAuth || type.enabled() + } + // check if app running on TV private fun isAppRunningOnTv(): Boolean { return try { @@ -161,11 +164,18 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { private fun biometricPrompt() { // if the biometric authentication is already done in the last 15 minutes, then skip - // fixme - #324 - move the 15 minutes to a configurable value - if ( - SystemClock.elapsedRealtime() - persistentState.biometricAuthTime < - TimeUnit.MINUTES.toMillis(15) - ) { + val minutes = MiscSettingsActivity.BioMetricType.fromValue(persistentState.biometricAuthType).mins + + val timeoutMinutes = if (minutes == -1L) { // this is for backward compatibility with older versions + MiscSettingsActivity.BioMetricType.FIFTEEN_MIN.mins + } else { + minutes + } + + Logger.d(LOG_TAG_UI, "Biometric timeout: $timeoutMinutes, biometricAuthTime: ${persistentState.biometricAuthTime}") + val timeSinceLastAuth = abs(SystemClock.elapsedRealtime() - persistentState.biometricAuthTime) + if (timeSinceLastAuth < TimeUnit.MINUTES.toMillis(timeoutMinutes)) { + Logger.i(LOG_TAG_UI, "Biometric auth skipped, time since last auth: $timeSinceLastAuth") return } @@ -228,7 +238,7 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { super.onAuthenticationSucceeded(result) // biometricPromptRetryCount = 1 persistentState.biometricAuthTime = SystemClock.elapsedRealtime() - Logger.i(LOG_TAG_UI, "Biometric success @ ${System.currentTimeMillis()}") + Logger.i(LOG_TAG_UI, "Biometric success @ ${SystemClock.elapsedRealtime()}") } override fun onAuthenticationFailed() { @@ -381,6 +391,12 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { } private fun removeThisMethod() { + // set allowBypass to false for all versions, overriding the user's preference. + // the default was true for Play Store and website versions, and false for F-Droid. + // when allowBypass is true, some OEMs bypass the VPN service, causing connections + // to fail due to the "Block connections without VPN" option. + persistentState.allowBypass = false + // change the persistent state for defaultDnsUrl, if its google.com (only for v055d) // fixme: remove this post v054. // this is to fix the default dns url, as the default dns url is changed from @@ -393,17 +409,11 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { // reset the bio metric auth time, as now the value is changed from System.currentTimeMillis // to SystemClock.elapsedRealtime persistentState.biometricAuthTime = SystemClock.elapsedRealtime() - } - - private fun rdnsRemote() { - // enforce the dns to sky for play store build, and max for website and f-droid build - // on first time launch - io { - if (isPlayStoreFlavour()) { - appConfig.switchRethinkDnsToSky() - } else { - appConfig.switchRethinkDnsToMax() - } + // set the rethink app in firewall mode as allowed by default + io { appInfoDb.resetRethinkAppFirewallMode() } + // if biometric auth is enabled, then set the biometric auth type to 3 (15 minutes) + if (persistentState.biometricAuth) { + persistentState.biometricAuthType = MiscSettingsActivity.BioMetricType.FIFTEEN_MIN.action } } @@ -603,17 +613,22 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { } private fun showUpdateCompleteSnackbar() { - val snack = - Snackbar.make( - b.container, - getString(R.string.update_complete_snack_message), - Snackbar.LENGTH_INDEFINITE - ) - snack.setAction(getString(R.string.update_complete_action_snack)) { - appUpdateManager.completeUpdate() + try { + val container: View = findViewById(R.id.container) + val snack = + Snackbar.make( + container, + getString(R.string.update_complete_snack_message), + Snackbar.LENGTH_INDEFINITE + ) + snack.setAction(getString(R.string.update_complete_action_snack)) { + appUpdateManager.completeUpdate() + } + snack.setActionTextColor(ContextCompat.getColor(this, R.color.primaryLightColorText)) + snack.show() + } catch (e: Exception) { + Logger.e(LOG_TAG_UI, "Error showing update complete snackbar: ${e.message}", e) } - snack.setActionTextColor(ContextCompat.getColor(this, R.color.primaryLightColorText)) - snack.show() } private fun showDownloadDialog( @@ -686,6 +701,7 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) { } catch (e: IllegalArgumentException) { Logger.w(LOG_TAG_DOWNLOAD, "Unregister receiver exception") } + Logger.d(LOG_TAG_UI, "HomeScreenActivity is stopped") } private fun setupNavigationItemSelectedListener() { diff --git a/app/src/full/java/com/celzero/bravedns/ui/NotificationHandlerDialog.kt b/app/src/full/java/com/celzero/bravedns/ui/NotificationHandlerDialog.kt index 5c370981d..d566aa550 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/NotificationHandlerDialog.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/NotificationHandlerDialog.kt @@ -26,6 +26,8 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.celzero.bravedns.R import com.celzero.bravedns.service.VpnController +import com.celzero.bravedns.ui.activity.AppInfoActivity +import com.celzero.bravedns.ui.activity.AppInfoActivity.Companion.INTENT_UID import com.celzero.bravedns.ui.activity.AppListActivity import com.celzero.bravedns.ui.activity.PauseActivity import com.celzero.bravedns.util.Constants @@ -34,8 +36,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder class NotificationHandlerDialog : AppCompatActivity() { enum class TrampolineType { ACCESSIBILITY_SERVICE_FAILURE_DIALOG, - NEW_APP_INSTAL_DIALOG, - HOMESCREEN_ACTIVITY, + NEW_APP_INSTALL_DIALOG, + HOME_SCREEN_ACTIVITY, PAUSE_ACTIVITY, NONE } @@ -55,12 +57,12 @@ class NotificationHandlerDialog : AppCompatActivity() { private fun handleNotificationIntent(intent: Intent) { // app not started launch home screen if (!VpnController.isOn()) { - trampoline(TrampolineType.NONE) + trampoline(TrampolineType.NONE, intent) return } if (VpnController.isAppPaused()) { - trampoline(TrampolineType.PAUSE_ACTIVITY) + trampoline(TrampolineType.PAUSE_ACTIVITY, intent) return } @@ -68,28 +70,28 @@ class NotificationHandlerDialog : AppCompatActivity() { if (isAccessibilityIntent(intent)) { TrampolineType.ACCESSIBILITY_SERVICE_FAILURE_DIALOG } else if (isNewAppInstalledIntent(intent)) { - TrampolineType.NEW_APP_INSTAL_DIALOG + TrampolineType.NEW_APP_INSTALL_DIALOG } else { TrampolineType.NONE } - trampoline(t) + trampoline(t, intent) } - private fun trampoline(trampolineType: TrampolineType) { + private fun trampoline(trampolineType: TrampolineType, intent: Intent) { Logger.i(LOG_TAG_VPN, "act on notification, notification type: $trampolineType") when (trampolineType) { TrampolineType.ACCESSIBILITY_SERVICE_FAILURE_DIALOG -> { handleAccessibilitySettings() } - TrampolineType.NEW_APP_INSTAL_DIALOG -> { + TrampolineType.NEW_APP_INSTALL_DIALOG -> { // navigate to all apps screen - launchFirewallActivityAndFinish() + launchFirewallActivityAndFinish(intent) } - TrampolineType.HOMESCREEN_ACTIVITY -> { + TrampolineType.HOME_SCREEN_ACTIVITY -> { launchHomeScreenAndFinish() } TrampolineType.PAUSE_ACTIVITY -> { - showAppPauseDialog(trampolineType) + showAppPauseDialog(trampolineType, intent) } TrampolineType.NONE -> { launchHomeScreenAndFinish() @@ -102,11 +104,19 @@ class NotificationHandlerDialog : AppCompatActivity() { finish() } - private fun launchFirewallActivityAndFinish() { - val intent = Intent(this, AppListActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED - startActivity(intent) - finish() + private fun launchFirewallActivityAndFinish(recvIntent: Intent) { + val uid = recvIntent.getIntExtra(Constants.NOTIF_INTENT_EXTRA_APP_UID, Int.MIN_VALUE) + Logger.d(LOG_TAG_VPN, "notification intent - new app installed, uid: $uid") + if (uid > 0) { + val intent = Intent(this, AppInfoActivity::class.java) + intent.putExtra(INTENT_UID, uid) + startActivity(intent) + finish() + } else { + val intent = Intent(this, AppListActivity::class.java) + startActivity(intent) + finish() + } } private fun handleAccessibilitySettings() { @@ -131,7 +141,7 @@ class NotificationHandlerDialog : AppCompatActivity() { ContextCompat.startActivity(context, intent, null) } - private fun showAppPauseDialog(trampolineType: TrampolineType) { + private fun showAppPauseDialog(trampolineType: TrampolineType, intent: Intent) { val builder = MaterialAlertDialogBuilder(this) builder.setTitle(R.string.notif_dialog_pause_dialog_title) @@ -141,7 +151,7 @@ class NotificationHandlerDialog : AppCompatActivity() { builder.setPositiveButton(R.string.notif_dialog_pause_dialog_positive) { _, _ -> VpnController.resumeApp() - trampoline(trampolineType) + trampoline(trampolineType, intent) } builder.setNegativeButton(R.string.notif_dialog_pause_dialog_negative) { _, _ -> finish() } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/AdvancedSettingActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/AdvancedSettingActivity.kt new file mode 100644 index 000000000..d353657c3 --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/AdvancedSettingActivity.kt @@ -0,0 +1,418 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.ui.activity + +import Logger.LOG_TAG_UI +import Logger.LOG_TAG_VPN +import Logger.updateConfigLevel +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import android.view.View +import android.widget.CompoundButton +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import by.kirich1409.viewbindingdelegate.viewBinding +import com.celzero.bravedns.R +import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG +import com.celzero.bravedns.backup.BackupHelper +import com.celzero.bravedns.data.AppConfig +import com.celzero.bravedns.databinding.ActivityAdvancedSettingBinding +import com.celzero.bravedns.net.go.GoVpnAdapter +import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.util.Constants +import com.celzero.bravedns.util.PcapMode +import com.celzero.bravedns.util.Themes +import com.celzero.bravedns.util.Utilities +import com.celzero.bravedns.util.Utilities.isAtleastR +import com.celzero.bravedns.util.Utilities.isAtleastS +import com.celzero.bravedns.util.Utilities.showToastUiCentered +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit +import org.koin.android.ext.android.inject + +class AdvancedSettingActivity : AppCompatActivity(R.layout.activity_advanced_setting) { + private val persistentState by inject() + private val appConfig by inject() + private val b by viewBinding(ActivityAdvancedSettingBinding::bind) + + private fun Context.isDarkThemeOn(): Boolean { + return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES + } + + companion object { + private const val SCHEME_PACKAGE = "package" + private const val STORAGE_PERMISSION_CODE = 23 + } + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) + super.onCreate(savedInstanceState) + initView() + setupClickListeners() + } + + private fun initView() { + b.dvWgListenPortSwitch.isChecked = !persistentState.randomizeListenPort + // Auto start app after reboot + b.settingsActivityAutoStartSwitch.isChecked = persistentState.prefAutoStartBootUp + // check if the device is running on Android 12 or above for EIMF + if (isAtleastS()) { + // endpoint independent mapping (eim) / endpoint independent filtering (eif) + b.dvEimfRl.visibility = View.VISIBLE + b.dvEimfSwitch.isChecked = persistentState.endpointIndependence + } else { + b.dvEimfRl.visibility = View.GONE + } + if (DEBUG) { + b.dvTcpKeepAliveRl.visibility = View.VISIBLE + b.dvTcpKeepAliveSwitch.isChecked = persistentState.tcpKeepAlive + } else { + b.dvTcpKeepAliveRl.visibility = View.GONE + } + if (DEBUG) { + b.settingsActivitySlowdownRl.visibility = View.VISIBLE + b.settingsActivitySlowdownSwitch.isChecked = persistentState.slowdownMode + } else { + b.settingsActivitySlowdownRl.visibility = View.GONE + } + displayPcapUi() + } + + private fun setupClickListeners() { + + b.dvWgListenPortSwitch.setOnCheckedChangeListener { _, isChecked -> + persistentState.randomizeListenPort = !isChecked + } + + b.dvWgListenPortRl.setOnClickListener { + b.dvWgListenPortSwitch.isChecked = !b.dvWgListenPortSwitch.isChecked + } + + b.dvEimfSwitch.setOnCheckedChangeListener { _, isChecked -> + if (!isAtleastS()) { + return@setOnCheckedChangeListener + } + + persistentState.endpointIndependence = isChecked + } + + b.dvEimfRl.setOnClickListener { b.dvEimfSwitch.isChecked = !b.dvEimfSwitch.isChecked } + + b.settingsAntiCensorshipRl.setOnClickListener { + val intent = Intent(this, AntiCensorshipActivity::class.java) + startActivity(intent) + } + + b.settingsGoLogRl.setOnClickListener { + enableAfterDelay(500, b.settingsGoLogRl) + showGoLoggerDialog() + } + + b.settingsConsoleLogRl.setOnClickListener { openConsoleLogActivity() } + + b.settingsActivityAutoStartRl.setOnClickListener { + b.settingsActivityAutoStartSwitch.isChecked = + !b.settingsActivityAutoStartSwitch.isChecked + } + + b.settingsActivityAutoStartSwitch.setOnCheckedChangeListener { _: CompoundButton, b: Boolean + -> + persistentState.prefAutoStartBootUp = b + } + + b.settingsActivityPcapRl.setOnClickListener { + enableAfterDelay(TimeUnit.SECONDS.toMillis(1L), b.settingsActivityPcapRl) + showPcapOptionsDialog() + } + + b.dvTcpKeepAliveSwitch.setOnCheckedChangeListener { _, isChecked -> + persistentState.tcpKeepAlive = isChecked + } + + b.dvTcpKeepAliveRl.setOnClickListener { b.dvTcpKeepAliveSwitch.isChecked = !b.dvTcpKeepAliveSwitch.isChecked } + + b.settingsActivitySlowdownRl.setOnClickListener { + b.settingsActivitySlowdownSwitch.isChecked = !b.settingsActivitySlowdownSwitch.isChecked + } + + b.settingsActivitySlowdownSwitch.setOnCheckedChangeListener { _, isChecked -> + persistentState.slowdownMode = isChecked + } + } + + private fun displayPcapUi() { + b.settingsActivityPcapRl.isEnabled = true + when (PcapMode.getPcapType(persistentState.pcapMode)) { + PcapMode.NONE -> { + b.settingsActivityPcapDesc.text = getString(R.string.settings_pcap_dialog_option_1) + } + + PcapMode.LOGCAT -> { + b.settingsActivityPcapDesc.text = getString(R.string.settings_pcap_dialog_option_2) + } + + PcapMode.EXTERNAL_FILE -> { + b.settingsActivityPcapDesc.text = getString(R.string.settings_pcap_dialog_option_3) + } + } + } + + private fun showPcapOptionsDialog() { + val alertBuilder = MaterialAlertDialogBuilder(this) + alertBuilder.setTitle(getString(R.string.settings_pcap_dialog_title)) + val items = + arrayOf( + getString(R.string.settings_pcap_dialog_option_1), + getString(R.string.settings_pcap_dialog_option_2), + getString(R.string.settings_pcap_dialog_option_3), + ) + val checkedItem = persistentState.pcapMode + alertBuilder.setSingleChoiceItems(items, checkedItem) { dialog, which -> + dialog.dismiss() + if (persistentState.pcapMode == which) { + return@setSingleChoiceItems + } + + when (PcapMode.getPcapType(which)) { + PcapMode.NONE -> { + b.settingsActivityPcapDesc.text = + getString(R.string.settings_pcap_dialog_option_1) + appConfig.setPcap(PcapMode.NONE.id) + } + + PcapMode.LOGCAT -> { + b.settingsActivityPcapDesc.text = + getString(R.string.settings_pcap_dialog_option_2) + appConfig.setPcap(PcapMode.LOGCAT.id, PcapMode.ENABLE_PCAP_LOGCAT) + } + + PcapMode.EXTERNAL_FILE -> { + b.settingsActivityPcapDesc.text = + getString(R.string.settings_pcap_dialog_option_3) + createAndSetPcapFile() + } + } + } + alertBuilder.create().show() + } + + private fun createAndSetPcapFile() { + // check for storage permissions + if (!checkStoragePermissions()) { + // request for storage permissions + Logger.i(LOG_TAG_VPN, "requesting for storage permissions") + requestForStoragePermissions() + return + } + + Logger.i(LOG_TAG_VPN, "storage permission granted, creating pcap file") + try { + val file = makePcapFile() + if (file == null) { + showFileCreationErrorToast() + return + } + // set the file descriptor instead of fd, need to close the file descriptor + // after tunnel creation + appConfig.setPcap(PcapMode.EXTERNAL_FILE.id, file.absolutePath) + } catch (e: Exception) { + showFileCreationErrorToast() + } + } + + private val storageActivityResultLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (isAtleastR()) { + // version 11 (R) or above + if (Environment.isExternalStorageManager()) { + createAndSetPcapFile() + } else { + showFileCreationErrorToast() + } + } else { + // below ver 11 (R), the permission is handled via onRequestPermissionsResult + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == STORAGE_PERMISSION_CODE) { + if (grantResults.isNotEmpty()) { + val write = grantResults[0] == PackageManager.PERMISSION_GRANTED + val read = grantResults[1] == PackageManager.PERMISSION_GRANTED + if (read && write) { + createAndSetPcapFile() + } else { + showFileCreationErrorToast() + } + } + } + } + + private fun requestForStoragePermissions() { + // version 11 (R) or above + if (isAtleastR()) { + try { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION + val uri = Uri.fromParts(SCHEME_PACKAGE, this.packageName, null) + intent.data = uri + storageActivityResultLauncher.launch(intent) + } catch (e: Exception) { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION + storageActivityResultLauncher.launch(intent) + } + } else { + // below version 11 + ActivityCompat.requestPermissions( + this, + arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE, + ), + STORAGE_PERMISSION_CODE, + ) + } + } + + private fun showFileCreationErrorToast() { + showToastUiCentered(this, getString(R.string.pcap_failure_toast), Toast.LENGTH_SHORT) + // reset the pcap mode to NONE + persistentState.pcapMode = PcapMode.NONE.id + displayPcapUi() + } + + private fun makePcapFile(): File? { + return try { + val sdf = SimpleDateFormat(BackupHelper.BACKUP_FILE_NAME_DATETIME, Locale.ROOT) + // create folder in DOWNLOADS + val dir = + if (isAtleastR()) { + val downloadsDir = + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + ) + // create folder in DOWNLOADS/Rethink + File(downloadsDir, Constants.PCAP_FOLDER_NAME) + } else { + val downloadsDir = Environment.getExternalStorageDirectory() + // create folder in DOWNLOADS/Rethink + File(downloadsDir, Constants.PCAP_FOLDER_NAME) + } + if (!dir.exists()) { + dir.mkdirs() + } + // filename format (rethink_pcap_.pcap) + val pcapFileName: String = + Constants.PCAP_FILE_NAME_PART + sdf.format(Date()) + Constants.PCAP_FILE_EXTENSION + val file = File(dir, pcapFileName) + // just in case, create the parent dir if it doesn't exist + if (file.parentFile?.exists() != true) file.parentFile?.mkdirs() + // create the file if it doesn't exist + if (!file.exists()) { + file.createNewFile() + } + file + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "error creating pcap file ${e.message}", e) + null + } + } + + private fun checkStoragePermissions(): Boolean { + return if (isAtleastR()) { + // version 11 (R) or above + Environment.isExternalStorageManager() + } else { + // below version 11 + val write = + ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + val read = + ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + read == PackageManager.PERMISSION_GRANTED && write == PackageManager.PERMISSION_GRANTED + } + } + + private fun openConsoleLogActivity() { + try { + val intent = Intent(this, ConsoleLogActivity::class.java) + startActivity(intent) + } catch (e: Exception) { + Logger.e(LOG_TAG_UI, "error opening console log activity ${e.message}", e) + } + } + + private fun showGoLoggerDialog() { + // show dialog with logger options, change log level in GoVpnAdapter based on selection + val alertBuilder = MaterialAlertDialogBuilder(this) + alertBuilder.setTitle(getString(R.string.settings_go_log_heading)) + val items = + arrayOf( + getString(R.string.settings_gologger_dialog_option_0), + getString(R.string.settings_gologger_dialog_option_1), + getString(R.string.settings_gologger_dialog_option_2), + getString(R.string.settings_gologger_dialog_option_3), + getString(R.string.settings_gologger_dialog_option_4), + getString(R.string.settings_gologger_dialog_option_5), + getString(R.string.settings_gologger_dialog_option_6), + getString(R.string.settings_gologger_dialog_option_7), + ) + val checkedItem = persistentState.goLoggerLevel.toInt() + alertBuilder.setSingleChoiceItems(items, checkedItem) { dialog, which -> + dialog.dismiss() + if (checkedItem == which) { + return@setSingleChoiceItems + } + + persistentState.goLoggerLevel = which.toLong() + GoVpnAdapter.setLogLevel(persistentState.goLoggerLevel.toInt()) + updateConfigLevel(persistentState.goLoggerLevel) + } + alertBuilder.create().show() + } + + private fun enableAfterDelay(ms: Long, vararg views: View) { + for (v in views) v.isEnabled = false + + Utilities.delay(ms, lifecycleScope) { + if (isFinishing) return@delay + + for (v in views) v.isEnabled = true + } + } +} diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/AntiCensorshipActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/AntiCensorshipActivity.kt new file mode 100644 index 000000000..c3221e7da --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/AntiCensorshipActivity.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.ui.activity + +import Logger.LOG_TAG_FIREWALL +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.view.View +import android.widget.CompoundButton +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import by.kirich1409.viewbindingdelegate.viewBinding +import com.celzero.bravedns.R +import com.celzero.bravedns.databinding.ActivityAntiCensorshipBinding +import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.util.Themes +import com.celzero.bravedns.util.Utilities +import com.celzero.bravedns.util.Utilities.isAtleastS +import org.koin.android.ext.android.inject +import settings.Settings + +class AntiCensorshipActivity : AppCompatActivity(R.layout.activity_anti_censorship) { + val b by viewBinding(ActivityAntiCensorshipBinding::bind) + + private val persistentState by inject() + + private fun Context.isDarkThemeOn(): Boolean { + return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES + } + + enum class DialStrategies(val mode: Int) { + SPLIT_AUTO(Settings.SplitAuto), + SPLIT_TCP(Settings.SplitTCP), + SPLIT_TCP_TLS(Settings.SplitTCPOrTLS), + DESYNC(Settings.SplitDesync), + NEVER_SPLIT(Settings.SplitNever) + } + + enum class RetryStrategies(val mode: Int) { + RETRY_WITH_SPLIT(Settings.RetryWithSplit), + RETRY_NEVER(Settings.RetryNever), + RETRY_AFTER_SPLIT(Settings.RetryAfterSplit) + } + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) + super.onCreate(savedInstanceState) + initView() + setupClickListeners() + } + + private fun initView() { + updateDialStrategy(persistentState.dialStrategy) + updateRetryStrategy(persistentState.retryStrategy) + } + + private fun updateDialStrategy(selectedState: Int) { + if (!isAtleastS()) { + // desync is not supported in Android 11 and below versions + // so reset the dial strategy to split auto if desync is selected + if (selectedState == DialStrategies.DESYNC.mode) { + persistentState.dialStrategy = DialStrategies.SPLIT_AUTO.mode + b.acRadioDesync.isChecked = false + b.acDesyncRl.visibility = View.GONE + b.acRadioSplitAuto.isChecked = true + Logger.i(LOG_TAG_FIREWALL, "Desync mode is not supported in Android 11 and below") + return + } else { + b.acRadioDesync.isEnabled = false + b.acDesyncRl.visibility = View.GONE + } + } + when (selectedState) { + DialStrategies.NEVER_SPLIT.mode -> { + b.acRadioNeverSplit.isChecked = true + } + DialStrategies.SPLIT_AUTO.mode -> { + b.acRadioSplitAuto.isChecked = true + } + DialStrategies.SPLIT_TCP.mode -> { + b.acRadioSplitTcp.isChecked = true + } + DialStrategies.SPLIT_TCP_TLS.mode -> { + b.acRadioSplitTls.isChecked = true + } + DialStrategies.DESYNC.mode -> { + b.acRadioDesync.isChecked = true + } + } + } + + private fun updateRetryStrategy(selectedState: Int) { + when (selectedState) { + RetryStrategies.RETRY_WITH_SPLIT.mode -> { + b.acRadioRetryWithSplit.isChecked = true + } + RetryStrategies.RETRY_NEVER.mode -> { + b.acRadioNeverRetry.isChecked = true + } + RetryStrategies.RETRY_AFTER_SPLIT.mode -> { + b.acRadioRetryAfterSplit.isChecked = true + } + } + } + + private fun setupClickListeners() { + b.acRadioNeverSplit.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleAcMode(isSelected, DialStrategies.NEVER_SPLIT.mode) + } + + b.acNeverSplitRl.setOnClickListener { + b.acRadioNeverSplit.isChecked = !b.acRadioNeverSplit.isChecked + } + + b.acRadioSplitAuto.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleAcMode(isSelected, DialStrategies.SPLIT_AUTO.mode) + } + + b.acRadioSplitTcp.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleAcMode(isSelected, DialStrategies.SPLIT_TCP.mode) + } + + b.acRadioSplitTls.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleAcMode(isSelected, DialStrategies.SPLIT_TCP_TLS.mode) + } + + b.acRadioDesync.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleAcMode(isSelected, DialStrategies.DESYNC.mode) + } + + b.acSplitAutoRl.setOnClickListener { + b.acRadioSplitAuto.isChecked = !b.acRadioSplitAuto.isChecked + } + + b.acSplitTcpRl.setOnClickListener { + b.acRadioSplitTcp.isChecked = !b.acRadioSplitTcp.isChecked + } + + b.acSplitTlsRl.setOnClickListener { + b.acRadioSplitTls.isChecked = !b.acRadioSplitTls.isChecked + } + + b.acDesyncRl.setOnClickListener { + b.acRadioDesync.isChecked = !b.acRadioDesync.isChecked + } + + b.acRadioRetryWithSplit.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleRetryMode(isSelected, RetryStrategies.RETRY_WITH_SPLIT.mode) + } + + b.acRadioNeverRetry.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleRetryMode(isSelected, RetryStrategies.RETRY_NEVER.mode) + } + + b.acRadioRetryAfterSplit.setOnCheckedChangeListener { _: CompoundButton, isSelected: Boolean -> + handleRetryMode(isSelected, RetryStrategies.RETRY_AFTER_SPLIT.mode) + } + + b.acRetryWithSplitRl.setOnClickListener { + b.acRadioRetryWithSplit.isChecked = !b.acRadioRetryWithSplit.isChecked + } + + b.acRetryNeverRl.setOnClickListener { + b.acRadioNeverRetry.isChecked = !b.acRadioNeverRetry.isChecked + } + + b.acRetryAfterSplitRl.setOnClickListener { + b.acRadioRetryAfterSplit.isChecked = !b.acRadioRetryAfterSplit.isChecked + } + } + + private fun handleAcMode(isSelected: Boolean, mode: Int) { + if (isSelected) { + persistentState.dialStrategy = mode + disableRadioButtons(mode) + if (mode == DialStrategies.NEVER_SPLIT.mode) { + // disable retry radio buttons for never split + handleRetryMode(true, RetryStrategies.RETRY_NEVER.mode) + } + } else { + // no-op + } + } + + private fun handleRetryMode(isSelected: Boolean, mode: Int) { + var m = mode + if (DialStrategies.NEVER_SPLIT.mode == persistentState.dialStrategy) { + m = RetryStrategies.RETRY_NEVER.mode + Utilities.showToastUiCentered(this, getString(R.string.ac_toast_retry_disabled), Toast.LENGTH_LONG) + } + + if (isSelected) { + persistentState.retryStrategy = m + disableRetryRadioButtons(m) + } else { + // no-op + } + } + + private fun disableRadioButtons(mode: Int) { + when (mode) { + DialStrategies.NEVER_SPLIT.mode -> { + b.acRadioSplitAuto.isChecked = false + b.acRadioSplitTcp.isChecked = false + b.acRadioSplitTls.isChecked = false + b.acRadioDesync.isChecked = false + } + DialStrategies.SPLIT_AUTO.mode -> { + b.acRadioNeverSplit.isChecked = false + b.acRadioSplitTcp.isChecked = false + b.acRadioSplitTls.isChecked = false + b.acRadioDesync.isChecked = false + } + DialStrategies.SPLIT_TCP.mode -> { + b.acRadioNeverSplit.isChecked = false + b.acRadioSplitAuto.isChecked = false + b.acRadioSplitTls.isChecked = false + b.acRadioDesync.isChecked = false + } + DialStrategies.SPLIT_TCP_TLS.mode -> { + b.acRadioNeverSplit.isChecked = false + b.acRadioSplitAuto.isChecked = false + b.acRadioSplitTcp.isChecked = false + b.acRadioDesync.isChecked = false + } + DialStrategies.DESYNC.mode -> { + b.acRadioNeverSplit.isChecked = false + b.acRadioSplitAuto.isChecked = false + b.acRadioSplitTcp.isChecked = false + b.acRadioSplitTls.isChecked = false + } + } + } + + private fun disableRetryRadioButtons(mode: Int) { + when (mode) { + RetryStrategies.RETRY_WITH_SPLIT.mode -> { + b.acRadioNeverRetry.isChecked = false + b.acRadioRetryAfterSplit.isChecked = false + } + RetryStrategies.RETRY_NEVER.mode -> { + b.acRadioRetryWithSplit.isChecked = false + b.acRadioRetryAfterSplit.isChecked = false + } + RetryStrategies.RETRY_AFTER_SPLIT.mode -> { + b.acRadioRetryWithSplit.isChecked = false + b.acRadioNeverRetry.isChecked = false + } + } + } +} diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/AppInfoActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/AppInfoActivity.kt index f57a03d34..93fe0c1fc 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/AppInfoActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/AppInfoActivity.kt @@ -38,11 +38,12 @@ import com.celzero.bravedns.R import com.celzero.bravedns.adapter.AppWiseDomainsAdapter import com.celzero.bravedns.adapter.AppWiseIpsAdapter import com.celzero.bravedns.database.AppInfo -import com.celzero.bravedns.database.ConnectionTrackerRepository import com.celzero.bravedns.databinding.ActivityAppDetailsBinding import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.FirewallManager.updateFirewallStatus import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.service.ProxyManager +import com.celzero.bravedns.service.ProxyManager.ID_NONE import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Constants.Companion.INVALID_UID @@ -80,14 +81,17 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { private var showBypassToolTip: Boolean = true + private var isRethinkApp: Boolean = false + companion object { - const val UID_INTENT_NAME = "UID" + const val INTENT_UID = "UID" } override fun onCreate(savedInstanceState: Bundle?) { setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) super.onCreate(savedInstanceState) - uid = intent.getIntExtra(UID_INTENT_NAME, INVALID_UID) + uid = intent.getIntExtra(INTENT_UID, INVALID_UID) + Logger.d(Logger.LOG_TAG_UI, "AppInfoActivity, intent uid: $uid") ipRulesViewModel.setUid(uid) domainRulesViewModel.setUid(uid) networkLogsViewModel.setUid(uid) @@ -121,8 +125,10 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { this.appInfo = appInfo b.aadAppDetailName.text = appName(packages.count()) + b.aadPkgName.text = appInfo.packageName b.excludeProxySwitch.isChecked = appInfo.isProxyExcluded - updateDataUsage() + displayDataUsage() + displayProxyStatus() displayIcon( Utilities.getIcon(this, appInfo.packageName, appInfo.appName), b.aadAppDetailIcon @@ -130,23 +136,41 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { // do not show the firewall status if the app is Rethink if (appInfo.packageName == rethinkPkgName) { + isRethinkApp = true b.aadFirewallStatus.visibility = View.GONE hideFirewallStatusUi() hideDomainBlockUi() hideIpBlockUi() - return@uiCtx + hideBypassProxyUi() + setRethinkDomainLogsAdapter() + setRethinkIpLogsAdapter() + } else { + updateFirewallStatusUi(appStatus, connStatus) + setDomainsAdapter() + setIpAdapter() } - updateFirewallStatusUi(appStatus, connStatus) - setDomainsAdapter() - setIpAdapter() } } } + private fun displayProxyStatus() { + val proxy = ProxyManager.getProxyIdForApp(appInfo.uid) + if (proxy.isEmpty() || proxy == ID_NONE) { + b.aadProxyDetails.visibility = View.GONE + return + } + b.aadProxyDetails.visibility = View.VISIBLE + b.aadProxyDetails.text = getString(R.string.wireguard_apps_proxy_map_desc, proxy) + } + private fun hideFirewallStatusUi() { b.aadAppSettingsCard.visibility = View.GONE } + private fun hideBypassProxyUi() { + b.excludeProxyRl.visibility = View.GONE + } + private fun hideDomainBlockUi() { b.aadDomainBlockCard.visibility = View.GONE } @@ -174,7 +198,7 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { startActivity(intent) } - private fun updateDataUsage() { + private fun displayDataUsage() { val u = Utilities.humanReadableByteCount(appInfo.uploadBytes, true) val uploadBytes = getString(R.string.symbol_upload, u) val d = Utilities.humanReadableByteCount(appInfo.downloadBytes, true) @@ -359,20 +383,20 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { private fun openAppWiseDomainLogsActivity() { val intent = Intent(this, AppWiseDomainLogsActivity::class.java) - intent.putExtra(UID_INTENT_NAME, uid) + intent.putExtra(INTENT_UID, uid) startActivity(intent) } private fun openAppWiseIpLogsActivity() { val intent = Intent(this, AppWiseIpLogsActivity::class.java) - intent.putExtra(UID_INTENT_NAME, uid) + intent.putExtra(INTENT_UID, uid) startActivity(intent) } private fun setDomainsAdapter() { val layoutManager = LinearLayoutManager(this) b.aadMostContactedDomainRv.layoutManager = layoutManager - val adapter = AppWiseDomainsAdapter(this, this, uid) + val adapter = AppWiseDomainsAdapter(this, this, uid, isRethinkApp) networkLogsViewModel.getDomainLogsLimited(uid).observe(this) { adapter.submitData(this.lifecycle, it) } @@ -384,8 +408,53 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { b.aadMostContactedDomainRl.visibility = View.GONE } else { b.aadMostContactedDomainRl.visibility = View.VISIBLE + } + } else { + b.aadMostContactedDomainRl.visibility = View.VISIBLE + } + } + } + private fun setRethinkDomainLogsAdapter() { + val layoutManager = LinearLayoutManager(this) + b.aadMostContactedDomainRv.layoutManager = layoutManager + val adapter = AppWiseDomainsAdapter(this, this, uid, isRethinkApp) + networkLogsViewModel.getRethinkDomainLogsLimited().observe(this) { + adapter.submitData(this.lifecycle, it) + } + b.aadMostContactedDomainRv.adapter = adapter + + adapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (adapter.itemCount < 1) { + b.aadMostContactedDomainRl.visibility = View.GONE + } else { + b.aadMostContactedDomainRl.visibility = View.VISIBLE + } + } else { + b.aadMostContactedDomainRl.visibility = View.VISIBLE + } + } + } + + private fun setRethinkIpLogsAdapter() { + val layoutManager = LinearLayoutManager(this) + b.aadMostContactedIpsRv.layoutManager = layoutManager + val adapter = AppWiseIpsAdapter(this, this, uid, isRethinkApp) + networkLogsViewModel.getRethinkIpLogsLimited().observe(this) { + adapter.submitData(this.lifecycle, it) + } + b.aadMostContactedIpsRv.adapter = adapter + + adapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (adapter.itemCount < 1) { + b.aadMostContactedIpsRl.visibility = View.GONE + } else { + b.aadMostContactedIpsRl.visibility = View.VISIBLE } + } else { + b.aadMostContactedIpsRl.visibility = View.VISIBLE } } } @@ -394,7 +463,7 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { b.aadMostContactedIpsRv.setHasFixedSize(true) val layoutManager = LinearLayoutManager(this) b.aadMostContactedIpsRv.layoutManager = layoutManager - val adapter = AppWiseIpsAdapter(this, this, uid) + val adapter = AppWiseIpsAdapter(this, this, uid, isRethinkApp) networkLogsViewModel.getIpLogsLimited(uid).observe(this) { adapter.submitData(this.lifecycle, it) } @@ -407,6 +476,8 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) { } else { b.aadMostContactedIpsRl.visibility = View.VISIBLE } + } else { + b.aadMostContactedIpsRl.visibility = View.VISIBLE } } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/AppListActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/AppListActivity.kt index 37414bcdb..84f27fdbb 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/AppListActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/AppListActivity.kt @@ -19,9 +19,12 @@ import android.content.Context import android.content.res.Configuration import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter +import android.graphics.Rect import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View +import android.view.ViewTreeObserver import android.view.animation.Animation import android.view.animation.RotateAnimation import android.widget.CompoundButton @@ -33,6 +36,7 @@ import androidx.appcompat.widget.TooltipCompat import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R @@ -41,7 +45,6 @@ import com.celzero.bravedns.database.RefreshDatabase import com.celzero.bravedns.databinding.ActivityAppListBinding import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.ui.bottomsheet.FirewallAppFilterBottomSheet -import com.celzero.bravedns.util.CustomLinearLayoutManager import com.celzero.bravedns.util.Themes import com.celzero.bravedns.util.UIUtils import com.celzero.bravedns.util.Utilities @@ -77,7 +80,7 @@ class AppListActivity : private const val ANIMATION_END_DEGREE = 360.0f private const val REFRESH_TIMEOUT: Long = 4000 - private const val QUERY_TEXT_TIMEOUT: Long = 600 + private const val QUERY_TEXT_TIMEOUT: Long = 1000 } // enum class for bulk ui update @@ -116,14 +119,18 @@ class AppListActivity : ALL(0), ALLOWED(1), BLOCKED(2), - BYPASS(3), - EXCLUDED(4), - LOCKDOWN(5); + BLOCKED_WIFI(3), + BLOCKED_MOBILE_DATA(4), + BYPASS(5), + EXCLUDED(6), + LOCKDOWN(7); fun getFilter(): Set { return when (this) { ALL -> setOf(0, 1, 2, 3, 4, 5, 7) ALLOWED -> setOf(5) + BLOCKED_WIFI -> setOf(5) + BLOCKED_MOBILE_DATA -> setOf(5) BLOCKED -> setOf(5) BYPASS -> setOf(2, 7) EXCLUDED -> setOf(3) @@ -135,7 +142,9 @@ class AppListActivity : return when (this) { ALL -> setOf(0, 1, 2, 3) ALLOWED -> setOf(3) - BLOCKED -> setOf(0, 1, 2) + BLOCKED_WIFI -> setOf(1) + BLOCKED_MOBILE_DATA -> setOf(2) + BLOCKED -> setOf(0) BYPASS -> setOf(0, 1, 2, 3) EXCLUDED -> setOf(0, 1, 2, 3) LOCKDOWN -> setOf(0, 1, 2, 3) @@ -146,6 +155,8 @@ class AppListActivity : return when (this) { ALL -> context.getString(R.string.lbl_all) ALLOWED -> context.getString(R.string.lbl_allowed) + BLOCKED_WIFI -> context.getString(R.string.two_argument_colon, context.getString(R.string.lbl_blocked), context.getString(R.string.firewall_rule_block_unmetered)) + BLOCKED_MOBILE_DATA -> context.getString(R.string.two_argument_colon, context.getString(R.string.lbl_blocked), context.getString(R.string.firewall_rule_block_metered)) BLOCKED -> context.getString(R.string.lbl_blocked) BYPASS -> context.getString(R.string.fapps_firewall_filter_bypass_universal) EXCLUDED -> context.getString(R.string.fapps_firewall_filter_excluded) @@ -158,6 +169,8 @@ class AppListActivity : return when (id) { ALL.id -> ALL ALLOWED.id -> ALLOWED + BLOCKED_WIFI.id -> BLOCKED_WIFI + BLOCKED_MOBILE_DATA.id -> BLOCKED_MOBILE_DATA BLOCKED.id -> BLOCKED BYPASS.id -> BYPASS EXCLUDED.id -> EXCLUDED @@ -191,6 +204,7 @@ class AppListActivity : override fun onResume() { super.onResume() setFirewallFilter(filters.value?.firewallFilter) + b.ffaAppList.requestFocus() } private fun initObserver() { @@ -200,11 +214,8 @@ class AppListActivity : if (it == null) return@observe - ui { - appInfoViewModel.setFilter(it) - b.ffaAppList.smoothScrollToPosition(0) - updateFilterText(it) - } + appInfoViewModel.setFilter(it) + updateFilterText(it) } } @@ -217,9 +228,7 @@ class AppListActivity : getString( R.string.fapps_firewall_filter_desc, firewallLabel.lowercase(), - filterLabel - ) - ) + filterLabel)) } else { b.firewallAppLabelTv.text = UIUtils.updateHtmlEncodedText( @@ -227,20 +236,21 @@ class AppListActivity : R.string.fapps_firewall_filter_desc_category, firewallLabel.lowercase(), filterLabel, - filter.categoryFilters - ) - ) + filter.categoryFilters)) } b.firewallAppLabelTv.isSelected = true } override fun onPause() { filters.postValue(Filters()) + b.ffaSearch.clearFocus() + b.ffaAppList.requestFocus() super.onPause() } override fun onQueryTextSubmit(query: String): Boolean { addQueryToFilters(query) + b.ffaSearch.clearFocus() return true } @@ -278,10 +288,7 @@ class AppListActivity : b.ffaRefreshList.isEnabled = true b.ffaRefreshList.clearAnimation() Utilities.showToastUiCentered( - this, - getString(R.string.refresh_complete), - Toast.LENGTH_SHORT - ) + this, getString(R.string.refresh_complete), Toast.LENGTH_SHORT) } } } @@ -290,30 +297,27 @@ class AppListActivity : showBulkRulesUpdateDialog( getBulkActionDialogTitle(BlockType.UNMETER), getBulkActionDialogMessage(BlockType.UNMETER), - BlockType.UNMETER - ) + BlockType.UNMETER) } b.ffaToggleAllMobileData.setOnClickListener { showBulkRulesUpdateDialog( getBulkActionDialogTitle(BlockType.METER), getBulkActionDialogMessage(BlockType.METER), - BlockType.METER - ) + BlockType.METER) } b.ffaToggleAllLockdown.setOnClickListener { showBulkRulesUpdateDialog( getBulkActionDialogTitle(BlockType.LOCKDOWN), getBulkActionDialogMessage(BlockType.LOCKDOWN), - BlockType.LOCKDOWN - ) + BlockType.LOCKDOWN) } TooltipCompat.setTooltipText( b.ffaToggleAllBypassDnsFirewall, - getString(R.string.bypass_dns_firewall_tooltip, getString(R.string.bypass_dns_firewall)) - ) + getString( + R.string.bypass_dns_firewall_tooltip, getString(R.string.bypass_dns_firewall))) b.ffaToggleAllBypassDnsFirewall.setOnClickListener { // show tooltip once the user clicks on the button @@ -326,24 +330,21 @@ class AppListActivity : showBulkRulesUpdateDialog( getBulkActionDialogTitle(BlockType.BYPASS_DNS_FIREWALL), getBulkActionDialogMessage(BlockType.BYPASS_DNS_FIREWALL), - BlockType.BYPASS_DNS_FIREWALL - ) + BlockType.BYPASS_DNS_FIREWALL) } b.ffaToggleAllBypass.setOnClickListener { showBulkRulesUpdateDialog( getBulkActionDialogTitle(BlockType.BYPASS), getBulkActionDialogMessage(BlockType.BYPASS), - BlockType.BYPASS - ) + BlockType.BYPASS) } b.ffaToggleAllExclude.setOnClickListener { showBulkRulesUpdateDialog( getBulkActionDialogTitle(BlockType.EXCLUDE), getBulkActionDialogMessage(BlockType.EXCLUDE), - BlockType.EXCLUDE - ) + BlockType.EXCLUDE) } b.ffaAppInfoIcon.setOnClickListener { showInfoDialog() } @@ -505,28 +506,42 @@ class AppListActivity : makeFirewallChip(FirewallFilter.ALLOWED.id, getString(R.string.lbl_allowed), false) val blocked = makeFirewallChip(FirewallFilter.BLOCKED.id, getString(R.string.lbl_blocked), false) + val blockedWifiTxt = getString( + R.string.two_argument_colon, + getString(R.string.lbl_blocked), + getString(R.string.firewall_rule_block_unmetered) + ) + val blockedWifi = + makeFirewallChip(FirewallFilter.BLOCKED_WIFI.id, blockedWifiTxt, false) + val blockedMobileDataTxt = getString( + R.string.two_argument_colon, + getString(R.string.lbl_blocked), + getString(R.string.firewall_rule_block_metered) + ) + val blockedMobileData = + makeFirewallChip(FirewallFilter.BLOCKED_MOBILE_DATA.id, blockedMobileDataTxt, false) + val bypassUniversal = makeFirewallChip( FirewallFilter.BYPASS.id, getString(R.string.fapps_firewall_filter_bypass_universal), - false - ) + false) val excluded = makeFirewallChip( FirewallFilter.EXCLUDED.id, getString(R.string.fapps_firewall_filter_excluded), - false - ) + false) val lockdown = makeFirewallChip( FirewallFilter.LOCKDOWN.id, getString(R.string.fapps_firewall_filter_isolate), - false - ) + false) b.ffaFirewallChipGroup.addView(none) b.ffaFirewallChipGroup.addView(allowed) b.ffaFirewallChipGroup.addView(blocked) + b.ffaFirewallChipGroup.addView(blockedWifi) + b.ffaFirewallChipGroup.addView(blockedMobileData) b.ffaFirewallChipGroup.addView(bypassUniversal) b.ffaFirewallChipGroup.addView(excluded) b.ffaFirewallChipGroup.addView(lockdown) @@ -567,9 +582,7 @@ class AppListActivity : private fun colorUpChipIcon(chip: Chip) { val colorFilter = PorterDuffColorFilter( - ContextCompat.getColor(this, R.color.primaryText), - PorterDuff.Mode.SRC_IN - ) + ContextCompat.getColor(this, R.color.primaryText), PorterDuff.Mode.SRC_IN) chip.checkedIcon?.colorFilter = colorFilter chip.chipIcon?.colorFilter = colorFilter } @@ -583,8 +596,7 @@ class AppListActivity : b.ffaToggleAllBypass.setImageResource(R.drawable.ic_firewall_bypass_off) b.ffaToggleAllLockdown.setImageResource(R.drawable.ic_firewall_lockdown_off) b.ffaToggleAllBypassDnsFirewall.setImageResource( - R.drawable.ic_bypass_dns_firewall_off - ) + R.drawable.ic_bypass_dns_firewall_off) } BlockType.METER -> { b.ffaToggleAllWifi.setImageResource(R.drawable.ic_firewall_wifi_on_grey) @@ -592,8 +604,7 @@ class AppListActivity : b.ffaToggleAllBypass.setImageResource(R.drawable.ic_firewall_bypass_off) b.ffaToggleAllLockdown.setImageResource(R.drawable.ic_firewall_lockdown_off) b.ffaToggleAllBypassDnsFirewall.setImageResource( - R.drawable.ic_bypass_dns_firewall_off - ) + R.drawable.ic_bypass_dns_firewall_off) } BlockType.LOCKDOWN -> { b.ffaToggleAllMobileData.setImageResource(R.drawable.ic_firewall_data_on_grey) @@ -601,8 +612,7 @@ class AppListActivity : b.ffaToggleAllExclude.setImageResource(R.drawable.ic_firewall_exclude_off) b.ffaToggleAllBypass.setImageResource(R.drawable.ic_firewall_bypass_off) b.ffaToggleAllBypassDnsFirewall.setImageResource( - R.drawable.ic_bypass_dns_firewall_off - ) + R.drawable.ic_bypass_dns_firewall_off) } BlockType.BYPASS -> { b.ffaToggleAllMobileData.setImageResource(R.drawable.ic_firewall_data_on_grey) @@ -610,8 +620,7 @@ class AppListActivity : b.ffaToggleAllExclude.setImageResource(R.drawable.ic_firewall_exclude_off) b.ffaToggleAllLockdown.setImageResource(R.drawable.ic_firewall_lockdown_off) b.ffaToggleAllBypassDnsFirewall.setImageResource( - R.drawable.ic_bypass_dns_firewall_off - ) + R.drawable.ic_bypass_dns_firewall_off) } BlockType.BYPASS_DNS_FIREWALL -> { b.ffaToggleAllMobileData.setImageResource(R.drawable.ic_firewall_data_on_grey) @@ -626,8 +635,7 @@ class AppListActivity : b.ffaToggleAllBypass.setImageResource(R.drawable.ic_firewall_bypass_off) b.ffaToggleAllLockdown.setImageResource(R.drawable.ic_firewall_lockdown_off) b.ffaToggleAllBypassDnsFirewall.setImageResource( - R.drawable.ic_bypass_dns_firewall_off - ) + R.drawable.ic_bypass_dns_firewall_off) } } } @@ -719,14 +727,58 @@ class AppListActivity : b.ffaSearch.setOnQueryTextListener(this) addAnimation() remakeFirewallChipsUi() + handleKeyboardEvent() + } + + private fun handleKeyboardEvent() { + // ref: stackoverflow.com/a/36259261 + val rootView = findViewById(android.R.id.content) + + rootView.viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + private var alreadyOpen = false + private val defaultKeyboardHeightDP = 100 + private val EstimatedKeyboardDP = defaultKeyboardHeightDP + 48 + private val rect = Rect() + + override fun onGlobalLayout() { + val estimatedKeyboardHeight = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + EstimatedKeyboardDP.toFloat(), + rootView.resources.displayMetrics + ).toInt() + rootView.getWindowVisibleDisplayFrame(rect) + val heightDiff = rootView.rootView.height - (rect.bottom - rect.top) + val isShown = heightDiff >= estimatedKeyboardHeight + + if (isShown == alreadyOpen) { + return // nothing to do + } + + alreadyOpen = isShown + + if (!isShown) { + if (b.ffaSearch.hasFocus()) { + // clear focus from search view when keyboard is closed + b.ffaSearch.clearFocus() + } + } + } + }) } private fun initListAdapter() { + val recyclerAdapter = FirewallAppListAdapter(this, this) b.ffaAppList.setHasFixedSize(true) - layoutManager = CustomLinearLayoutManager(this) + layoutManager = LinearLayoutManager(this) b.ffaAppList.layoutManager = layoutManager - val recyclerAdapter = FirewallAppListAdapter(this, this) - appInfoViewModel.appInfo.observe(this) { recyclerAdapter.submitData(this.lifecycle, it) } + recyclerAdapter.stateRestorationPolicy = + RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + + appInfoViewModel.appInfo.observe(this) { + b.ffaAppList.post { recyclerAdapter.submitData(lifecycle, it) } + } + b.ffaAppList.adapter = recyclerAdapter } @@ -743,8 +795,7 @@ class AppListActivity : Animation.RELATIVE_TO_SELF, ANIMATION_PIVOT_VALUE, Animation.RELATIVE_TO_SELF, - ANIMATION_PIVOT_VALUE - ) + ANIMATION_PIVOT_VALUE) animation.repeatCount = ANIMATION_REPEAT_COUNT animation.duration = ANIMATION_DURATION } @@ -756,8 +807,4 @@ class AppListActivity : private fun io(f: suspend () -> Unit) { lifecycleScope.launch(Dispatchers.IO) { f() } } - - private fun ui(f: () -> Unit) { - lifecycleScope.launch(Dispatchers.Main) { f() } - } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseDomainLogsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseDomainLogsActivity.kt index c63382b97..19a1faefe 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseDomainLogsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseDomainLogsActivity.kt @@ -20,6 +20,7 @@ import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.drawable.Drawable import android.os.Bundle +import android.view.View import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView @@ -31,7 +32,6 @@ import com.bumptech.glide.Glide import com.celzero.bravedns.R import com.celzero.bravedns.adapter.AppWiseDomainsAdapter import com.celzero.bravedns.database.AppInfo -import com.celzero.bravedns.database.ConnectionTrackerRepository import com.celzero.bravedns.databinding.ActivityAppWiseDomainLogsBinding import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.PersistentState @@ -57,10 +57,10 @@ class AppWiseDomainLogsActivity : private val persistentState by inject() private val networkLogsViewModel: AppConnectionsViewModel by viewModel() - private val connectionTrackerRepository by inject() private var uid: Int = INVALID_UID private var layoutManager: RecyclerView.LayoutManager? = null private lateinit var appInfo: AppInfo + private var isRethink = false private fun Context.isDarkThemeOn(): Boolean { return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == @@ -70,14 +70,20 @@ class AppWiseDomainLogsActivity : override fun onCreate(savedInstanceState: Bundle?) { setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) super.onCreate(savedInstanceState) - uid = intent.getIntExtra(AppInfoActivity.UID_INTENT_NAME, INVALID_UID) + uid = intent.getIntExtra(AppInfoActivity.INTENT_UID, INVALID_UID) if (uid == INVALID_UID) { finish() } - init() - setAdapter() - observeNetworkLogSize() - setClickListener() + if (Utilities.getApplicationInfo(this, this.packageName)?.uid == uid) { + isRethink = true + init() + setRethinkAdapter() + b.toggleGroup.addOnButtonCheckedListener(listViewToggleListener) + } else { + init() + setAdapter() + setClickListener() + } } private fun setTabbedViewTxt() { @@ -147,6 +153,7 @@ class AppWiseDomainLogsActivity : val appNameTruncated = appName.substring(0, appName.length.coerceAtMost(10)) val hint = getString(R.string.two_argument_colon, appNameTruncated, getString(R.string.search_custom_domains)) b.awlSearch.queryHint = hint + b.awlSearch.findViewById(androidx.appcompat.R.id.search_src_text).textSize = 14f return } @@ -185,44 +192,71 @@ class AppWiseDomainLogsActivity : b.awlRecyclerConnection.setHasFixedSize(true) layoutManager = LinearLayoutManager(this) b.awlRecyclerConnection.layoutManager = layoutManager - val recyclerAdapter = AppWiseDomainsAdapter(this, this, uid) + val recyclerAdapter = AppWiseDomainsAdapter(this, this, uid, isRethink) networkLogsViewModel.appDomainLogs.observe(this) { recyclerAdapter.submitData(this.lifecycle, it) } b.awlRecyclerConnection.adapter = recyclerAdapter + + recyclerAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (recyclerAdapter.itemCount < 1) { + showNoRulesUi() + hideRulesUi() + } else { + hideNoRulesUi() + showRulesUi() + } + } else { + hideNoRulesUi() + showRulesUi() + } + } } - private fun observeNetworkLogSize() { - networkLogsViewModel.getConnectionsCount(uid).observe(this) { - if (it == null) return@observe + private fun setRethinkAdapter() { + networkLogsViewModel.setUid(uid) + b.awlRecyclerConnection.setHasFixedSize(true) + layoutManager = LinearLayoutManager(this) + b.awlRecyclerConnection.layoutManager = layoutManager + val recyclerAdapter = AppWiseDomainsAdapter(this, this, uid, isRethink) + networkLogsViewModel.rinrDomainLogs.observe(this) { + recyclerAdapter.submitData(this.lifecycle, it) + } + b.awlRecyclerConnection.adapter = recyclerAdapter - if (it <= 0) { - showNoRulesUi() - hideRulesUi() - return@observe + recyclerAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (recyclerAdapter.itemCount < 1) { + showNoRulesUi() + hideRulesUi() + } else { + hideNoRulesUi() + showRulesUi() + } + } else { + hideNoRulesUi() + showRulesUi() } - - hideNoRulesUi() - showRulesUi() } } private fun showNoRulesUi() { - b.awlNoRulesRl.visibility = android.view.View.VISIBLE + b.awlNoRulesRl.visibility = View.VISIBLE } private fun hideRulesUi() { - b.awlCardViewTop.visibility = android.view.View.GONE - b.awlRecyclerConnection.visibility = android.view.View.GONE + b.awlCardViewTop.visibility = View.GONE + b.awlRecyclerConnection.visibility = View.GONE } private fun hideNoRulesUi() { - b.awlNoRulesRl.visibility = android.view.View.GONE + b.awlNoRulesRl.visibility = View.GONE } private fun showRulesUi() { - b.awlCardViewTop.visibility = android.view.View.VISIBLE - b.awlRecyclerConnection.visibility = android.view.View.VISIBLE + b.awlCardViewTop.visibility = View.VISIBLE + b.awlRecyclerConnection.visibility = View.VISIBLE } override fun onQueryTextSubmit(query: String): Boolean { @@ -251,7 +285,7 @@ class AppWiseDomainLogsActivity : } private fun deleteAppLogs() { - io { connectionTrackerRepository.clearLogsByUid(uid) } + io { networkLogsViewModel.deleteLogs(uid) } } private fun io(f: suspend () -> Unit): Job { diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseIpLogsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseIpLogsActivity.kt index 3d3e0c9a5..9b16e2e09 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseIpLogsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/AppWiseIpLogsActivity.kt @@ -20,6 +20,7 @@ import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.drawable.Drawable import android.os.Bundle +import android.view.View import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView @@ -31,7 +32,6 @@ import com.bumptech.glide.Glide import com.celzero.bravedns.R import com.celzero.bravedns.adapter.AppWiseIpsAdapter import com.celzero.bravedns.database.AppInfo -import com.celzero.bravedns.database.ConnectionTrackerRepository import com.celzero.bravedns.databinding.ActivityAppWiseIpLogsBinding import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.PersistentState @@ -56,10 +56,10 @@ class AppWiseIpLogsActivity : private val persistentState by inject() private val networkLogsViewModel: AppConnectionsViewModel by viewModel() - private val connectionTrackerRepository by inject() private var uid: Int = INVALID_UID private var layoutManager: RecyclerView.LayoutManager? = null private lateinit var appInfo: AppInfo + private var isRethink = false private fun Context.isDarkThemeOn(): Boolean { return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == @@ -69,14 +69,20 @@ class AppWiseIpLogsActivity : override fun onCreate(savedInstanceState: Bundle?) { setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) super.onCreate(savedInstanceState) - uid = intent.getIntExtra(AppInfoActivity.UID_INTENT_NAME, INVALID_UID) + uid = intent.getIntExtra(AppInfoActivity.INTENT_UID, INVALID_UID) if (uid == INVALID_UID) { finish() } - init() - setAdapter() - observeNetworkLogSize() - setClickListener() + if (Utilities.getApplicationInfo(this, this.packageName)?.uid == uid) { + isRethink = true + init() + setRethinkAdapter() + b.toggleGroup.addOnButtonCheckedListener(listViewToggleListener) + } else { + init() + setAdapter() + setClickListener() + } } private fun init() { @@ -94,10 +100,11 @@ class AppWiseIpLogsActivity : uiCtx { this.appInfo = appInfo - b.awlAppDetailName.text = appName(packages.count()) + val appName = appName(packages.count()) + updateAppNameInSearchHint(appName) displayIcon( Utilities.getIcon(this, appInfo.packageName, appInfo.appName), - b.awlAppDetailIcon + b.awlAppDetailIcon1 ) } } @@ -171,46 +178,84 @@ class AppWiseIpLogsActivity : b.awlRecyclerConnection.setHasFixedSize(true) layoutManager = LinearLayoutManager(this) b.awlRecyclerConnection.layoutManager = layoutManager - val recyclerAdapter = AppWiseIpsAdapter(this, this, uid) + val recyclerAdapter = AppWiseIpsAdapter(this, this, uid, isRethink) networkLogsViewModel.appIpLogs.observe(this) { recyclerAdapter.submitData(this.lifecycle, it) } b.awlRecyclerConnection.adapter = recyclerAdapter + + recyclerAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (recyclerAdapter.itemCount < 1) { + showNoRulesUi() + hideRulesUi() + } else { + hideNoRulesUi() + showRulesUi() + } + } else { + hideNoRulesUi() + showRulesUi() + } + } } - private fun observeNetworkLogSize() { - networkLogsViewModel.getConnectionsCount(uid).observe(this) { - if (it == null) return@observe + private fun setRethinkAdapter() { + networkLogsViewModel.setUid(uid) + b.awlRecyclerConnection.setHasFixedSize(true) + layoutManager = LinearLayoutManager(this) + b.awlRecyclerConnection.layoutManager = layoutManager + val recyclerAdapter = AppWiseIpsAdapter(this, this, uid, isRethink) + networkLogsViewModel.rinrIpLogs.observe(this) { + recyclerAdapter.submitData(this.lifecycle, it) + } + b.awlRecyclerConnection.adapter = recyclerAdapter - if (it <= 0) { - showNoRulesUi() - hideRulesUi() - return@observe + recyclerAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (recyclerAdapter.itemCount < 1) { + showNoRulesUi() + hideRulesUi() + } else { + hideNoRulesUi() + showRulesUi() + } + } else { + hideNoRulesUi() + showRulesUi() } - - hideNoRulesUi() - showRulesUi() } } + private fun updateAppNameInSearchHint(appName: String) { + val appNameTruncated = appName.substring(0, appName.length.coerceAtMost(10)) + val hint = getString( + R.string.two_argument_colon, + appNameTruncated, + getString(R.string.search_universal_ips) + ) + b.awlSearch.queryHint = hint + b.awlSearch.findViewById(androidx.appcompat.R.id.search_src_text).textSize = + 14f + return + } + private fun showNoRulesUi() { - b.awlNoRulesRl.visibility = android.view.View.VISIBLE + b.awlNoRulesRl.visibility = View.VISIBLE } private fun hideRulesUi() { - b.awlCardViewTop.visibility = android.view.View.GONE - b.awlAppDetailRl.visibility = android.view.View.GONE - b.awlRecyclerConnection.visibility = android.view.View.GONE + b.awlCardViewTop.visibility = View.GONE + b.awlRecyclerConnection.visibility = View.GONE } private fun hideNoRulesUi() { - b.awlNoRulesRl.visibility = android.view.View.GONE + b.awlNoRulesRl.visibility = View.GONE } private fun showRulesUi() { - b.awlCardViewTop.visibility = android.view.View.VISIBLE - b.awlAppDetailRl.visibility = android.view.View.VISIBLE - b.awlRecyclerConnection.visibility = android.view.View.VISIBLE + b.awlCardViewTop.visibility = View.VISIBLE + b.awlRecyclerConnection.visibility = View.VISIBLE } override fun onQueryTextSubmit(query: String): Boolean { @@ -239,7 +284,9 @@ class AppWiseIpLogsActivity : } private fun deleteAppLogs() { - io { connectionTrackerRepository.clearLogsByUid(uid) } + io { + networkLogsViewModel.deleteLogs(uid) + } } private fun io(f: suspend () -> Unit): Job { diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/ConsoleLogActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/ConsoleLogActivity.kt new file mode 100644 index 000000000..9c07be061 --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/ConsoleLogActivity.kt @@ -0,0 +1,589 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.ui.activity + +import Logger.LOG_TAG_BUG_REPORT +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.text.method.LinkMovementMethod +import android.view.View +import android.view.WindowManager +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.work.WorkInfo +import androidx.work.WorkManager +import by.kirich1409.viewbindingdelegate.viewBinding +import com.celzero.bravedns.R +import com.celzero.bravedns.adapter.ConsoleLogAdapter +import com.celzero.bravedns.database.ConsoleLogRepository +import com.celzero.bravedns.databinding.ActivityConsoleLogBinding +import com.celzero.bravedns.databinding.DialogInfoRulesLayoutBinding +import com.celzero.bravedns.scheduler.WorkScheduler +import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.service.VpnController +import com.celzero.bravedns.util.Constants +import com.celzero.bravedns.util.Themes +import com.celzero.bravedns.util.UIUtils +import com.celzero.bravedns.util.Utilities +import com.celzero.bravedns.util.Utilities.isAtleastR +import com.celzero.bravedns.util.Utilities.showToastUiCentered +import com.celzero.bravedns.viewmodel.ConsoleLogViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject + +class ConsoleLogActivity : AppCompatActivity(R.layout.activity_console_log) { + + private val b by viewBinding(ActivityConsoleLogBinding::bind) + private var layoutManager: RecyclerView.LayoutManager? = null + private val persistentState by inject() + + private val consoleLogViewModel by inject() + private val consoleLogRepository by inject() + private val workScheduler by inject() + + companion object { + private const val FOLDER_NAME = "Rethink" + private const val SCHEME_PACKAGE = "package" + private const val FILE_NAME = "rethink_app_logs_" + private const val FILE_EXTENSION = ".zip" + private const val STORAGE_PERMISSION_CODE = 231 // request code for storage permission + } + + enum class SaveType { + SAVE, + SHARE; + + fun isSave(): Boolean { + return this == SAVE + } + + fun isShare(): Boolean { + return this == SHARE + } + } + + private fun Context.isDarkThemeOn(): Boolean { + return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES + } + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) + super.onCreate(savedInstanceState) + initView() + setupClickListener() + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (persistentState.consoleLogEnabled) { + showStopDialog() + } else { + finish() + } + return + } + } + ) + } + + private fun initView() { + setAdapter() + // update the text view with the time since logs are available + io { + val sinceTime = consoleLogViewModel.sinceTime() + if (sinceTime == 0L) return@io + + val since = Utilities.convertLongToTime(sinceTime, Constants.TIME_FORMAT_3) + uiCtx { + val desc = getString(R.string.console_log_desc) + val sinceTxt = getString(R.string.logs_card_duration, since) + val descWithTime = getString(R.string.two_argument_space, desc, sinceTxt) + b.consoleLogInfoText.text = descWithTime + } + } + if (persistentState.consoleLogEnabled) { + b.consoleLogStartStop.text = getString(R.string.hsf_stop_btn_state) + } else { + b.consoleLogStartStop.text = getString(R.string.hsf_start_btn_state) + showStartDialog() // show dialog to user to start logging + } + } + + var recyclerAdapter: ConsoleLogAdapter? = null + + private fun setAdapter() { + b.consoleLogList.setHasFixedSize(true) + layoutManager = LinearLayoutManager(this@ConsoleLogActivity) + b.consoleLogList.layoutManager = layoutManager + recyclerAdapter = ConsoleLogAdapter(this) + b.consoleLogList.adapter = recyclerAdapter + observeLog() + + /*lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + consoleLogViewModel.logs.collectLatest { pagingData -> + recyclerAdapter.submitData(pagingData) + } + } + }*/ + } + + private fun observeLog() { + consoleLogViewModel.logs.observe(this) { l -> + lifecycleScope.launch { + recyclerAdapter?.submitData(l) + } + } + } + + private fun unobserveLog1() { + consoleLogViewModel.logs.removeObservers(this) + } + + private fun setupClickListener() { + b.consoleLogSave.setOnClickListener { + val filePath = createFile(SaveType.SAVE) + if (filePath == null) { + showFileCreationErrorToast() + return@setOnClickListener + } + handleSaveOrShareLogs(filePath, SaveType.SAVE) + } + + b.consoleLogShare.setOnClickListener { + val filePath = createFile(SaveType.SHARE) + if (filePath == null) { + showFileCreationErrorToast() + return@setOnClickListener + } + handleSaveOrShareLogs(filePath, SaveType.SHARE) + } + b.fabShareLog.setOnClickListener { + val filePath = createFile(SaveType.SHARE) + if (filePath == null) { + showFileCreationErrorToast() + return@setOnClickListener + } + handleSaveOrShareLogs(filePath, SaveType.SHARE) + } + b.consoleLogStartStop.setOnClickListener { + persistentState.consoleLogEnabled = !persistentState.consoleLogEnabled + if (persistentState.consoleLogEnabled) { + b.consoleLogStartStop.text = getString(R.string.hsf_stop_btn_state) + consoleLogRepository.consoleLogStartTimestamp = System.currentTimeMillis() + } else { + b.consoleLogStartStop.text = getString(R.string.hsf_start_btn_state) + consoleLogRepository.consoleLogStartTimestamp = 0L + } + } + + b.consoleStatInfo.setOnClickListener { showStatsDialog() } + + b.consoleLogDelete.setOnClickListener { + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.console_log_delete_title)) + .setMessage(getString(R.string.console_log_delete_desc)) + .setPositiveButton(getString(R.string.lbl_delete)) { _, _ -> + io { + consoleLogRepository.deleteAllLogs() + uiCtx { + showToastUiCentered( + this, + getString(R.string.console_log_delete_toast), + Toast.LENGTH_SHORT + ) + finish() + } + } + } + .setNegativeButton(getString(R.string.lbl_cancel)) { dialog, _ -> + dialog.dismiss() + } + .show() + } + } + + private fun showStartDialog() { + //unobserveLog() + val handler = Handler(Looper.getMainLooper()) + val builder = MaterialAlertDialogBuilder(this) + val title = getString(R.string.console_log_title) + builder.setTitle(title) + builder.setCancelable(true) + builder.setPositiveButton(getString(R.string.hsf_start_btn_state)) { dialogInterface, _ -> + handler.post { + persistentState.consoleLogEnabled = true + consoleLogRepository.consoleLogStartTimestamp = System.currentTimeMillis() + b.consoleLogStartStop.text = getString(R.string.hsf_stop_btn_state) + showToastUiCentered( + this, + getString(R.string.config_add_success_toast), + Toast.LENGTH_SHORT + ) + observeLog() + } + dialogInterface.dismiss() + } + + builder.setNeutralButton(getString(R.string.lbl_cancel)) { dialogInterface, _ -> + handler.post { + b.consoleLogStartStop.text = getString(R.string.hsf_start_btn_state) + observeLog() + } + dialogInterface.dismiss() + } + builder.setOnCancelListener { + handler.post { + observeLog() + } + } + val alertDialog: AlertDialog = builder.create() + alertDialog.setCancelable(true) + alertDialog.show() + } + + private fun showStopDialog() { + // unobserveLog() + val handler = Handler(Looper.getMainLooper()) + val builder = MaterialAlertDialogBuilder(this) + val title = getString(R.string.console_log_title) + builder.setTitle(title) + builder.setCancelable(true) + builder.setPositiveButton(getString(R.string.hsf_stop_btn_state)) { dialogInterface, _ -> + handler.post { + persistentState.consoleLogEnabled = false + consoleLogRepository.consoleLogStartTimestamp = 0L + b.consoleLogStartStop.text = getString(R.string.hsf_start_btn_state) + showToastUiCentered( + this, + getString(R.string.config_add_success_toast), + Toast.LENGTH_SHORT + ) + } + dialogInterface.dismiss() + handler.post { finish() } + } + + builder.setNeutralButton(getString(R.string.lbl_cancel)) { dialogInterface, _ -> + dialogInterface.dismiss() + handler.post { finish() } + } + builder.setOnCancelListener { + observeLog() + } + val alertDialog: AlertDialog = builder.create() + alertDialog.setCancelable(true) + alertDialog.show() + } + + private fun showStatsDialog() { + //unobserveLog() + io { + val stat = VpnController.getNetStat() + val formatedStat = UIUtils.formatNetStat(stat) + uiCtx { + val dialogBinding = DialogInfoRulesLayoutBinding.inflate(layoutInflater) + val builder = MaterialAlertDialogBuilder(this).setView(dialogBinding.root) + val lp = WindowManager.LayoutParams() + val dialog = builder.create() + dialog.show() + lp.copyFrom(dialog.window?.attributes) + lp.width = WindowManager.LayoutParams.MATCH_PARENT + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + dialog.setCancelable(true) + dialog.window?.attributes = lp + + val heading = dialogBinding.infoRulesDialogRulesTitle + val okBtn = dialogBinding.infoRulesDialogCancelImg + val descText = dialogBinding.infoRulesDialogRulesDesc + dialogBinding.infoRulesDialogRulesIcon.visibility = View.GONE + + heading.text = "Network Stats" + heading.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(this, R.drawable.ic_info_white), + null, + null, + null + ) + + descText.movementMethod = LinkMovementMethod.getInstance() + descText.text = formatedStat + + okBtn.setOnClickListener { + dialog.dismiss() + observeLog() + } + + dialog.setOnCancelListener { + observeLog() + } + + dialog.show() + } + } + } + + private fun handleSaveOrShareLogs(filePath: String, type: SaveType) { + if (WorkScheduler.isWorkRunning(this, WorkScheduler.CONSOLE_LOG_SAVE_JOB_TAG)) return + + workScheduler.scheduleConsoleLogSaveJob(filePath) + showLogGenerationProgressUi() + + val workManager = WorkManager.getInstance(this.applicationContext) + workManager.getWorkInfosByTagLiveData(WorkScheduler.CONSOLE_LOG_SAVE_JOB_TAG).observe( + this + ) { workInfoList -> + val workInfo = workInfoList?.getOrNull(0) ?: return@observe + Logger.i( + Logger.LOG_TAG_SCHEDULER, + "WorkManager state: ${workInfo.state} for ${WorkScheduler.CONSOLE_LOG_SAVE_JOB_TAG}" + ) + if (WorkInfo.State.SUCCEEDED == workInfo.state) { + onSuccess() + if (type.isShare()) { + shareZipFileViaEmail(filePath) + } + workManager.pruneWork() + } else if ( + WorkInfo.State.CANCELLED == workInfo.state || + WorkInfo.State.FAILED == workInfo.state + ) { + onFailure() + workManager.pruneWork() + workManager.cancelAllWorkByTag(WorkScheduler.CONSOLE_LOG_SAVE_JOB_TAG) + } else { // state == blocked, queued, or running + // no-op + } + } + } + + private fun onSuccess() { + // show success message + Logger.i(LOG_TAG_BUG_REPORT, "Logs saved successfully") + b.consoleLogProgressBar.visibility = View.GONE + Toast.makeText(this, getString(R.string.config_add_success_toast), Toast.LENGTH_LONG).show() + } + + private fun onFailure() { + // show failure message + Logger.i(LOG_TAG_BUG_REPORT, "Logs save failed") + b.consoleLogProgressBar.visibility = View.GONE + Toast.makeText( + this, + getString(R.string.download_update_dialog_failure_title), + Toast.LENGTH_LONG + ) + .show() + } + + private fun showLogGenerationProgressUi() { + // show progress dialog or progress bar + Logger.i(LOG_TAG_BUG_REPORT, "Logs generation in progress") + b.consoleLogProgressBar.visibility = View.VISIBLE + } + + private fun createFile(type: SaveType): String? { + if (type.isShare()) { + // create file in localdir and share, no need to check for permissions + return makeConsoleLogFile(type) + } + + // check for storage permissions + if (!checkStoragePermissions()) { + // request for storage permissions + Logger.i(LOG_TAG_BUG_REPORT, "requesting for storage permissions") + requestForStoragePermissions() + return null + } + + Logger.i(LOG_TAG_BUG_REPORT, "storage permission granted, creating pcap file") + try { + val filePath = makeConsoleLogFile(type) + if (filePath == null) { + showFileCreationErrorToast() + return null + } + return filePath + } catch (e: Exception) { + showFileCreationErrorToast() + } + return null + } + + private fun shareZipFileViaEmail(filePath: String) { + val file = File(filePath) + // Get the URI of the file using FileProvider + val uri: Uri = FileProvider.getUriForFile(this, "${this.packageName}.provider", file) + + // Create the intent + val intent = + Intent(Intent.ACTION_SEND).apply { + type = "application/zip" + putExtra(Intent.EXTRA_SUBJECT, "Log File") + putExtra(Intent.EXTRA_TEXT, "Please find the attached log file.") + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + // Start the email app + startActivity(Intent.createChooser(intent, "Send email...")) + } + + private fun makeConsoleLogFile(type: SaveType): String? { + return try { + val appVersion = getVersionName() + "_" + System.currentTimeMillis() + return if (type.isShare()) { + // create file in filesdir, no need to check for permissions + val dir = filesDir.canonicalPath + File.separator + val fileName: String = FILE_NAME + appVersion + FILE_EXTENSION + val file = File(dir, fileName) + if (!file.exists()) { + file.createNewFile() + } + file.absolutePath + } else { + // create folder in DOWNLOADS + val dir = + if (isAtleastR()) { + val downloadsDir = + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + ) + // create folder in DOWNLOADS/Rethink + File(downloadsDir, FOLDER_NAME) + } else { + val downloadsDir = Environment.getExternalStorageDirectory() + // create folder in DOWNLOADS/Rethink + File(downloadsDir, FOLDER_NAME) + } + if (!dir.exists()) { + dir.mkdirs() + } + // filename format (rethink_app_logs_.txt) + val logFileName: String = FILE_NAME + appVersion + FILE_EXTENSION + val file = File(dir, logFileName) + // just in case, create the parent dir if it doesn't exist + if (file.parentFile?.exists() != true) file.parentFile?.mkdirs() + // create the file if it doesn't exist + if (!file.exists()) { + file.createNewFile() + } + file.absolutePath + } + } catch (e: Exception) { + Logger.e(LOG_TAG_BUG_REPORT, "error creating log file", e) + null + } + } + + private fun getVersionName(): String { + val pInfo: PackageInfo? = + Utilities.getPackageMetadata(this.packageManager, this.packageName) + return pInfo?.versionName ?: "" + } + + private fun showFileCreationErrorToast() { + // show toast message + } + + private fun requestForStoragePermissions() { + // version 11 (R) or above + if (isAtleastR()) { + try { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION + val uri = Uri.fromParts(SCHEME_PACKAGE, this.packageName, null) + intent.data = uri + storageActivityResultLauncher.launch(intent) + } catch (e: Exception) { + val intent = Intent() + intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION + storageActivityResultLauncher.launch(intent) + } + } else { + // below version 11 + ActivityCompat.requestPermissions( + this, + arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ), + STORAGE_PERMISSION_CODE + ) + } + } + + private val storageActivityResultLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (isAtleastR()) { + // version 11 (R) or above + if (Environment.isExternalStorageManager()) { + createFile(SaveType.SAVE) + } else { + showFileCreationErrorToast() + } + } else { + // below ver 11 (R), the permission is handled via onRequestPermissionsResult + } + } + + private fun checkStoragePermissions(): Boolean { + return if (isAtleastR()) { + // version 11 (R) or above + Environment.isExternalStorageManager() + } else { + // below version 11 + val write = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + val read = + ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + read == PackageManager.PERMISSION_GRANTED && write == PackageManager.PERMISSION_GRANTED + } + } + + private fun io(f: suspend () -> Unit) { + lifecycleScope.launch(Dispatchers.IO) { f() } + } + + private suspend fun uiCtx(f: () -> Unit) { + withContext(Dispatchers.Main) { f() } + } +} diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/DetailedStatisticsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/DetailedStatisticsActivity.kt index 9bf8f7c8b..e7e9c462c 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/DetailedStatisticsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/DetailedStatisticsActivity.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.celzero.bravedns.ui.activity import android.content.Context @@ -103,6 +118,7 @@ class DetailedStatisticsActivity : AppCompatActivity(R.layout.activity_detailed_ b.dsaRecycler.layoutManager = layoutManager val recyclerAdapter = SummaryStatisticsAdapter(this, persistentState, appConfig, type) + recyclerAdapter.setTimeCategory(timeCategory) viewModel.timeCategoryChanged(timeCategory) handleStatType(type).observe(this) { recyclerAdapter.submitData(this.lifecycle, it) } @@ -113,7 +129,13 @@ class DetailedStatisticsActivity : AppCompatActivity(R.layout.activity_detailed_ if (recyclerAdapter.itemCount < 1) { b.dsaRecycler.visibility = View.GONE b.dsaNoDataRl.visibility = View.VISIBLE + } else { + b.dsaRecycler.visibility = View.VISIBLE + b.dsaNoDataRl.visibility = View.GONE } + } else { + b.dsaRecycler.visibility = View.VISIBLE + b.dsaNoDataRl.visibility = View.GONE } } b.dsaRecycler.adapter = recyclerAdapter diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/DnsDetailActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/DnsDetailActivity.kt index 812d24709..ff5137f4c 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/DnsDetailActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/DnsDetailActivity.kt @@ -40,7 +40,7 @@ class DnsDetailActivity : AppCompatActivity(R.layout.activity_dns_detail) { companion object { fun getCount(): Int { - return values().count() + return entries.toTypedArray().count() } } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/DomainConnectionsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/DomainConnectionsActivity.kt new file mode 100644 index 000000000..8ada30c0e --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/DomainConnectionsActivity.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.ui.activity + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import by.kirich1409.viewbindingdelegate.viewBinding +import com.celzero.bravedns.R +import com.celzero.bravedns.adapter.DomainConnectionsAdapter +import com.celzero.bravedns.databinding.ActivityDomainConnectionsBinding +import com.celzero.bravedns.net.doh.CountryMap +import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.util.CustomLinearLayoutManager +import com.celzero.bravedns.util.Themes.Companion.getCurrentTheme +import com.celzero.bravedns.util.UIUtils.getCountryNameFromFlag +import com.celzero.bravedns.viewmodel.DomainConnectionsViewModel +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import java.nio.charset.StandardCharsets + +class DomainConnectionsActivity : AppCompatActivity(R.layout.activity_domain_connections){ + private val b by viewBinding(ActivityDomainConnectionsBinding::bind) + private val persistentState by inject() + private val viewModel by viewModel() + + private var type: InputType = InputType.DOMAIN + + private fun Context.isDarkThemeOn(): Boolean { + return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + UI_MODE_NIGHT_YES + } + + companion object { + const val INTENT_TYPE = "TYPE" + const val INTENT_FLAG = "FLAG" + const val INTENT_DOMAIN = "DOMAIN" + const val INTENT_TIME_CATEGORY = "TIME_CATEGORY" + } + + enum class InputType(val type: Int) { + DOMAIN(0), FLAG(1) + } + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(getCurrentTheme(isDarkThemeOn(), persistentState.theme)) + super.onCreate(savedInstanceState) + val t = intent.getIntExtra(INTENT_TYPE, 0) + type = InputType.entries.toTypedArray()[t] + if (type == InputType.DOMAIN) { + val domain = intent.getStringExtra(INTENT_DOMAIN) ?: "" + viewModel.setDomain(domain) + b.dcTitle.text = domain + } else { + val flag = intent.getStringExtra(INTENT_FLAG) ?: "" + viewModel.setFlag(flag) + b.dcTitle.text = getString(R.string.two_argument_space, flag, getCountryNameFromFlag(flag)) + } + val tc = intent.getIntExtra(INTENT_TIME_CATEGORY, 0) + val timeCategory = + DomainConnectionsViewModel.TimeCategory.fromValue(tc) + ?: DomainConnectionsViewModel.TimeCategory.ONE_HOUR + setSubTitle(timeCategory) + + viewModel.timeCategoryChanged(timeCategory) + setRecyclerView() + } + + private fun setSubTitle(timeCategory: DomainConnectionsViewModel.TimeCategory) { + b.dcSubtitle.text = + when (timeCategory) { + DomainConnectionsViewModel.TimeCategory.ONE_HOUR -> { + getString( + R.string.three_argument, + getString(R.string.lbl_last), + getString(R.string.numeric_one), + getString(R.string.lbl_hour) + ) + } + + DomainConnectionsViewModel.TimeCategory.TWENTY_FOUR_HOUR -> { + getString( + R.string.three_argument, + getString(R.string.lbl_last), + getString(R.string.numeric_twenty_four), + getString(R.string.lbl_hour) + ) + } + + DomainConnectionsViewModel.TimeCategory.SEVEN_DAYS -> { + getString( + R.string.three_argument, + getString(R.string.lbl_last), + getString(R.string.numeric_seven), + getString(R.string.lbl_day) + ) + } + } + } + + private fun setRecyclerView() { + b.dcRecycler.setHasFixedSize(true) + val layoutManager = CustomLinearLayoutManager(this) + b.dcRecycler.layoutManager = layoutManager + + val recyclerAdapter = DomainConnectionsAdapter(this) + + if (type == InputType.DOMAIN) { + viewModel.domainConnectionList.observe(this) { recyclerAdapter.submitData(this.lifecycle, it) } + } else { + viewModel.flagConnectionList.observe(this) { recyclerAdapter.submitData(this.lifecycle, it) } + } + + // remove the view if there is no data + recyclerAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (recyclerAdapter.itemCount < 1) { + b.dcRecycler.visibility = View.GONE + b.dcNoDataRl.visibility = View.VISIBLE + } else { + b.dcRecycler.visibility = View.VISIBLE + b.dcNoDataRl.visibility = View.GONE + } + } else { + b.dcRecycler.visibility = View.VISIBLE + b.dcNoDataRl.visibility = View.GONE + } + } + b.dcRecycler.adapter = recyclerAdapter + } +} \ No newline at end of file diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/FirewallActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/FirewallActivity.kt index 6660856fb..f7d6aa583 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/FirewallActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/FirewallActivity.kt @@ -43,7 +43,7 @@ class FirewallActivity : AppCompatActivity(R.layout.activity_firewall) { companion object { fun getCount(): Int { - return values().count() + return entries.count() } } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/MiscSettingsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/MiscSettingsActivity.kt index 69d4f7585..2b86de12c 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/MiscSettingsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/MiscSettingsActivity.kt @@ -16,9 +16,9 @@ package com.celzero.bravedns.ui.activity import Logger +import Logger.LOG_TAG_APP_OPS import Logger.LOG_TAG_UI import Logger.LOG_TAG_VPN -import Logger.updateConfigLevel import android.Manifest import android.app.LocaleManager import android.content.ActivityNotFoundException @@ -29,7 +29,6 @@ import android.content.res.Configuration import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Environment import android.os.LocaleList import android.provider.Settings import android.view.View @@ -41,27 +40,22 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.biometric.BiometricManager -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.core.os.LocaleListCompat import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R -import com.celzero.bravedns.backup.BackupHelper -import com.celzero.bravedns.data.AppConfig import com.celzero.bravedns.databinding.ActivityMiscSettingsBinding -import com.celzero.bravedns.net.go.GoVpnAdapter import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.ui.bottomsheet.BackupRestoreBottomSheet +import com.celzero.bravedns.util.BackgroundAccessibilityService import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.NotificationActionType -import com.celzero.bravedns.util.PcapMode import com.celzero.bravedns.util.Themes import com.celzero.bravedns.util.Themes.Companion.getCurrentTheme import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.util.Utilities.delay -import com.celzero.bravedns.util.Utilities.isAtleastR import com.celzero.bravedns.util.Utilities.isAtleastT import com.celzero.bravedns.util.Utilities.isFdroidFlavour import com.celzero.bravedns.util.Utilities.showToastUiCentered @@ -69,24 +63,32 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koin.android.ext.android.inject import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException -import java.io.File import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale import java.util.concurrent.TimeUnit + class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) { private val b by viewBinding(ActivityMiscSettingsBinding::bind) private val persistentState by inject() - private val appConfig by inject() private lateinit var notificationPermissionResult: ActivityResultLauncher - companion object { - private const val SCHEME_PACKAGE = "package" - private const val STORAGE_PERMISSION_CODE = 23 + enum class BioMetricType(val action: Int, val mins: Long) { + OFF(0, -1L), + IMMEDIATE(1, 0L), + FIVE_MIN(2, 5L), + FIFTEEN_MIN(3, 15L); + + companion object { + fun fromValue(action: Int): BioMetricType { + return entries.firstOrNull { it.action == action } ?: OFF + } + } + + fun enabled(): Boolean { + return this != OFF + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -105,10 +107,10 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) // enable logs b.settingsActivityEnableLogsSwitch.isChecked = persistentState.logsEnabled - // Auto start app after reboot - b.settingsActivityAutoStartSwitch.isChecked = persistentState.prefAutoStartBootUp // check for app updates b.settingsActivityCheckUpdateSwitch.isChecked = persistentState.checkForAppUpdate + // camera and microphone access + b.settingsMicCamAccessSwitch.isChecked = persistentState.micCamAccess // for app locale (default system/user selected locale) if (isAtleastT()) { @@ -127,14 +129,14 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS ) { - b.settingsBiometricSwitch.isChecked = persistentState.biometricAuth + b.settingsBiometricDesc.text = getString(R.string.settings_biometric_desc) + "; " + + BioMetricType.fromValue(persistentState.biometricAuthType).name } else { b.settingsBiometricRl.visibility = View.GONE } displayAppThemeUi() displayNotificationActionUi() - displayPcapUi() } private fun displayNotificationActionUi() { @@ -168,22 +170,7 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) } } - private fun displayPcapUi() { - b.settingsActivityPcapRl.isEnabled = true - when (PcapMode.getPcapType(persistentState.pcapMode)) { - PcapMode.NONE -> { - b.settingsActivityPcapDesc.text = getString(R.string.settings_pcap_dialog_option_1) - } - PcapMode.LOGCAT -> { - b.settingsActivityPcapDesc.text = getString(R.string.settings_pcap_dialog_option_2) - } - - PcapMode.EXTERNAL_FILE -> { - b.settingsActivityPcapDesc.text = getString(R.string.settings_pcap_dialog_option_3) - } - } - } private fun displayAppThemeUi() { b.settingsActivityThemeRl.isEnabled = true @@ -233,16 +220,6 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) persistentState.logsEnabled = b } - b.settingsActivityAutoStartRl.setOnClickListener { - b.settingsActivityAutoStartSwitch.isChecked = - !b.settingsActivityAutoStartSwitch.isChecked - } - - b.settingsActivityAutoStartSwitch.setOnCheckedChangeListener { _: CompoundButton, b: Boolean - -> - persistentState.prefAutoStartBootUp = b - } - b.settingsActivityCheckUpdateRl.setOnClickListener { b.settingsActivityCheckUpdateSwitch.isChecked = !b.settingsActivityCheckUpdateSwitch.isChecked @@ -258,11 +235,6 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) showThemeDialog() } - b.settingsGoLogRl.setOnClickListener { - enableAfterDelay(500, b.settingsGoLogRl) - showGoLoggerDialog() - } - // Ideally this property should be part of VPN category / section. // As of now the VPN section will be disabled when the // VPN is in lockdown mode. @@ -282,11 +254,6 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) persistentState.persistentNotification = b } - b.settingsActivityPcapRl.setOnClickListener { - enableAfterDelay(TimeUnit.SECONDS.toMillis(1L), b.settingsActivityPcapRl) - showPcapOptionsDialog() - } - b.settingsActivityImportExportRl.setOnClickListener { invokeImportExport() } b.settingsActivityAppNotificationSwitch.setOnClickListener { @@ -298,14 +265,167 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) b.settingsLocaleRl.setOnClickListener { invokeChangeLocaleDialog() } b.settingsBiometricRl.setOnClickListener { - b.settingsBiometricSwitch.isChecked = !b.settingsBiometricSwitch.isChecked + showBiometricDialog() } - b.settingsBiometricSwitch.setOnCheckedChangeListener { _: CompoundButton, checked: Boolean + b.settingsBiometricImg.setOnClickListener { + showBiometricDialog() + } + + + b.settingsMicCamAccessRl.setOnClickListener { + b.settingsMicCamAccessSwitch.isChecked = !b.settingsMicCamAccessSwitch.isChecked + } + + b.settingsMicCamAccessSwitch.setOnCheckedChangeListener { _: CompoundButton, checked: Boolean -> - persistentState.biometricAuth = checked - // Reset the biometric auth time - persistentState.biometricAuthTime = Constants.INIT_TIME_MS + if (!checked) { + b.settingsMicCamAccessSwitch.isChecked = false + persistentState.micCamAccess = false + return@setOnCheckedChangeListener + } + + // check for the permission and enable the switch + handleAccessibilityPermission() + } + } + + + private fun showBiometricDialog() { + val alertBuilder = MaterialAlertDialogBuilder(this) + alertBuilder.setTitle(getString(R.string.settings_biometric_dialog_heading)) + // show an list of options disable, enable immediate, ask after 5 min, ask after 15 min + val item0 = getString(R.string.settings_biometric_dialog_option_0) + val item1 = getString(R.string.settings_biometric_dialog_option_1) + val item2 = getString(R.string.settings_biometric_dialog_option_2) + val item3 = getString(R.string.settings_biometric_dialog_option_3) + val items = arrayOf(item0, item1, item2, item3) + + val checkedItem = persistentState.biometricAuthType + alertBuilder.setSingleChoiceItems(items, checkedItem) { dialog, which -> + dialog.dismiss() + if (persistentState.biometricAuthType == which) { + return@setSingleChoiceItems + } + + when (BioMetricType.fromValue(which)) { + BioMetricType.OFF -> { + b.settingsBiometricDesc.text = b.settingsBiometricDesc.text.toString() + "; Off" + persistentState.biometricAuthType = BioMetricType.OFF.action + persistentState.biometricAuthTime = Constants.INIT_TIME_MS + } + + BioMetricType.IMMEDIATE -> { + b.settingsBiometricDesc.text = b.settingsBiometricDesc.text.toString() + "; Immediate" + persistentState.biometricAuthType = BioMetricType.IMMEDIATE.action + persistentState.biometricAuthTime = Constants.INIT_TIME_MS + } + + BioMetricType.FIVE_MIN -> { + b.settingsBiometricDesc.text = b.settingsBiometricDesc.text.toString() + "; 5 min" + persistentState.biometricAuthType = BioMetricType.FIVE_MIN.action + persistentState.biometricAuthTime = System.currentTimeMillis() + } + + BioMetricType.FIFTEEN_MIN -> { + b.settingsBiometricDesc.text = b.settingsBiometricDesc.text.toString() + "; 15 min" + persistentState.biometricAuthType = BioMetricType.FIFTEEN_MIN.action + persistentState.biometricAuthTime = System.currentTimeMillis() + } + } + } + alertBuilder.create().show() + } + + private fun handleAccessibilityPermission() { + try { + val isAccessibilityServiceRunning = + Utilities.isAccessibilityServiceEnabled( + this, + BackgroundAccessibilityService::class.java + ) + val isAccessibilityServiceEnabled = + Utilities.isAccessibilityServiceEnabledViaSettingsSecure( + this, + BackgroundAccessibilityService::class.java + ) + val isAccessibilityServiceFunctional = + isAccessibilityServiceRunning && isAccessibilityServiceEnabled + + if (isAccessibilityServiceFunctional) { + persistentState.micCamAccess = true + b.settingsMicCamAccessSwitch.isChecked = true + return + } + + showPermissionAlert() + b.settingsMicCamAccessSwitch.isChecked = false + persistentState.micCamAccess = false + } catch (e: PackageManager.NameNotFoundException) { + Logger.e(LOG_TAG_APP_OPS, "error checking usage stats permission ${e.message}", e) + return + } + } + + private fun checkMicCamAccessRule() { + if (!persistentState.micCamAccess) return + + val running = + Utilities.isAccessibilityServiceEnabled( + this, + BackgroundAccessibilityService::class.java + ) + val enabled = + Utilities.isAccessibilityServiceEnabledViaSettingsSecure( + this, + BackgroundAccessibilityService::class.java + ) + + Logger.d(LOG_TAG_APP_OPS, "cam/mic access - running: $running, enabled: $enabled") + + val isAccessibilityServiceFunctional = running && enabled + + if (!isAccessibilityServiceFunctional) { + persistentState.micCamAccess = false + b.settingsMicCamAccessSwitch.isChecked = false + showToastUiCentered( + this, + getString(R.string.accessibility_failure_toast), + Toast.LENGTH_SHORT + ) + return + } + + if (running) { + b.settingsMicCamAccessSwitch.isChecked = persistentState.micCamAccess + return + } + } + + private fun showPermissionAlert() { + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(R.string.alert_permission_accessibility) + builder.setMessage(R.string.alert_firewall_accessibility_explanation) + builder.setPositiveButton(getString(R.string.univ_accessibility_dialog_positive)) { _, _ -> + openAccessibilitySettings() + } + builder.setNegativeButton(getString(R.string.univ_accessibility_dialog_negative)) { _, _ -> + } + builder.setCancelable(false) + builder.create().show() + } + + private fun openAccessibilitySettings() { + try { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + startActivity(intent) + } catch (e: ActivityNotFoundException) { + showToastUiCentered( + this, + getString(R.string.alert_firewall_accessibility_exception), + Toast.LENGTH_SHORT + ) + Logger.e(LOG_TAG_APP_OPS, "Failure accessing accessibility settings: ${e.message}", e) } } @@ -440,79 +560,11 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) alertBuilder.create().show() } - private fun showGoLoggerDialog() { - // show dialog with logger options, change log level in GoVpnAdapter based on selection - val alertBuilder = MaterialAlertDialogBuilder(this) - alertBuilder.setTitle(getString(R.string.settings_go_log_heading)) - val items = - arrayOf( - getString(R.string.settings_gologger_dialog_option_0), - getString(R.string.settings_gologger_dialog_option_1), - getString(R.string.settings_gologger_dialog_option_2), - getString(R.string.settings_gologger_dialog_option_3), - getString(R.string.settings_gologger_dialog_option_4), - getString(R.string.settings_gologger_dialog_option_5), - getString(R.string.settings_gologger_dialog_option_6), - getString(R.string.settings_gologger_dialog_option_7) - ) - val checkedItem = persistentState.goLoggerLevel.toInt() - alertBuilder.setSingleChoiceItems(items, checkedItem) { dialog, which -> - dialog.dismiss() - if (checkedItem == which) { - return@setSingleChoiceItems - } - - persistentState.goLoggerLevel = which.toLong() - GoVpnAdapter.setLogLevel(persistentState.goLoggerLevel.toInt()) - updateConfigLevel(persistentState.goLoggerLevel) - } - alertBuilder.create().show() - } - private fun setThemeRecreate(theme: Int) { setTheme(theme) recreate() } - private fun showPcapOptionsDialog() { - val alertBuilder = MaterialAlertDialogBuilder(this) - alertBuilder.setTitle(getString(R.string.settings_pcap_dialog_title)) - val items = - arrayOf( - getString(R.string.settings_pcap_dialog_option_1), - getString(R.string.settings_pcap_dialog_option_2), - getString(R.string.settings_pcap_dialog_option_3) - ) - val checkedItem = persistentState.pcapMode - alertBuilder.setSingleChoiceItems(items, checkedItem) { dialog, which -> - dialog.dismiss() - if (persistentState.pcapMode == which) { - return@setSingleChoiceItems - } - - when (PcapMode.getPcapType(which)) { - PcapMode.NONE -> { - b.settingsActivityPcapDesc.text = - getString(R.string.settings_pcap_dialog_option_1) - appConfig.setPcap(PcapMode.NONE.id) - } - - PcapMode.LOGCAT -> { - b.settingsActivityPcapDesc.text = - getString(R.string.settings_pcap_dialog_option_2) - appConfig.setPcap(PcapMode.LOGCAT.id, PcapMode.ENABLE_PCAP_LOGCAT) - } - - PcapMode.EXTERNAL_FILE -> { - b.settingsActivityPcapDesc.text = - getString(R.string.settings_pcap_dialog_option_3) - createAndSetPcapFile() - } - } - } - alertBuilder.create().show() - } - private fun showNotificationActionDialog() { val alertBuilder = MaterialAlertDialogBuilder(this) alertBuilder.setTitle(getString(R.string.settings_notification_dialog_title)) @@ -567,6 +619,7 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) super.onResume() // app notification permission android 13 showEnableNotificationSettingIfNeeded() + checkMicCamAccessRule() } private fun registerForActivityResult() { @@ -590,148 +643,6 @@ class MiscSettingsActivity : AppCompatActivity(R.layout.activity_misc_settings) } } - private val storageActivityResultLauncher: ActivityResultLauncher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (isAtleastR()) { - // version 11 (R) or above - if (Environment.isExternalStorageManager()) { - createAndSetPcapFile() - } else { - showFileCreationErrorToast() - } - } else { - // below ver 11 (R), the permission is handled via onRequestPermissionsResult - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == STORAGE_PERMISSION_CODE) { - if (grantResults.isNotEmpty()) { - val write = grantResults[0] == PackageManager.PERMISSION_GRANTED - val read = grantResults[1] == PackageManager.PERMISSION_GRANTED - if (read && write) { - createAndSetPcapFile() - } else { - showFileCreationErrorToast() - } - } - } - } - - private fun requestForStoragePermissions() { - // version 11 (R) or above - if (isAtleastR()) { - try { - val intent = Intent() - intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION - val uri = Uri.fromParts(SCHEME_PACKAGE, this.packageName, null) - intent.data = uri - storageActivityResultLauncher.launch(intent) - } catch (e: Exception) { - val intent = Intent() - intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION - storageActivityResultLauncher.launch(intent) - } - } else { - // below version 11 - ActivityCompat.requestPermissions( - this, - arrayOf( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE - ), - STORAGE_PERMISSION_CODE - ) - } - } - - private fun showFileCreationErrorToast() { - showToastUiCentered(this, getString(R.string.pcap_failure_toast), Toast.LENGTH_SHORT) - // reset the pcap mode to NONE - persistentState.pcapMode = PcapMode.NONE.id - displayPcapUi() - } - - private fun createAndSetPcapFile() { - // check for storage permissions - if (!checkStoragePermissions()) { - // request for storage permissions - Logger.i(LOG_TAG_VPN, "requesting for storage permissions") - requestForStoragePermissions() - return - } - - Logger.i(LOG_TAG_VPN, "storage permission granted, creating pcap file") - try { - val file = makePcapFile() - if (file == null) { - showFileCreationErrorToast() - return - } - // set the file descriptor instead of fd, need to close the file descriptor - // after tunnel creation - appConfig.setPcap(PcapMode.EXTERNAL_FILE.id, file.absolutePath) - } catch (e: Exception) { - showFileCreationErrorToast() - } - } - - private fun makePcapFile(): File? { - return try { - val sdf = SimpleDateFormat(BackupHelper.BACKUP_FILE_NAME_DATETIME, Locale.ROOT) - // create folder in DOWNLOADS - val dir = - if (isAtleastR()) { - val downloadsDir = - Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS - ) - // create folder in DOWNLOADS/Rethink - File(downloadsDir, Constants.PCAP_FOLDER_NAME) - } else { - val downloadsDir = Environment.getExternalStorageDirectory() - // create folder in DOWNLOADS/Rethink - File(downloadsDir, Constants.PCAP_FOLDER_NAME) - } - if (!dir.exists()) { - dir.mkdirs() - } - // filename format (rethink_pcap_.pcap) - val pcapFileName: String = - Constants.PCAP_FILE_NAME_PART + sdf.format(Date()) + Constants.PCAP_FILE_EXTENSION - val file = File(dir, pcapFileName) - // just in case, create the parent dir if it doesn't exist - if (file.parentFile?.exists() != true) file.parentFile?.mkdirs() - // create the file if it doesn't exist - if (!file.exists()) { - file.createNewFile() - } - file - } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "error creating pcap file ${e.message}", e) - null - } - } - - private fun checkStoragePermissions(): Boolean { - return if (isAtleastR()) { - // version 11 (R) or above - Environment.isExternalStorageManager() - } else { - // below version 11 - val write = - ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - val read = - ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - read == PackageManager.PERMISSION_GRANTED && write == PackageManager.PERMISSION_GRANTED - } - } - private fun invokeNotificationPermission() { if (!isAtleastT()) { // notification permission is needed for version 13 or above diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/NetworkLogsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/NetworkLogsActivity.kt index 929ddb68f..a6629f303 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/NetworkLogsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/NetworkLogsActivity.kt @@ -30,6 +30,7 @@ import com.celzero.bravedns.databinding.ActivityNetworkLogsBinding import com.celzero.bravedns.service.BraveVPNService import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.VpnController +import com.celzero.bravedns.ui.activity.UniversalFirewallSettingsActivity.Companion.RULES_SEARCH_ID import com.celzero.bravedns.ui.fragment.ConnectionTrackerFragment import com.celzero.bravedns.ui.fragment.DnsLogFragment import com.celzero.bravedns.ui.fragment.RethinkLogFragment @@ -42,6 +43,11 @@ class NetworkLogsActivity : AppCompatActivity(R.layout.activity_network_logs) { private val b by viewBinding(ActivityNetworkLogsBinding::bind) private var fragmentIndex = 0 private var searchParam = "" + // to handle search navigation from universal firewall, to show only the search results + // of the selected universal rule, show only network logs tab + private var isUnivNavigated = false + // to handle the wireguard connections + private var isWireGuardLogs = false private val persistentState by inject() private val appConfig by inject() @@ -51,12 +57,21 @@ class NetworkLogsActivity : AppCompatActivity(R.layout.activity_network_logs) { DNS_LOGS(1), RETHINK_LOGS(2) } + + companion object { + const val RULES_SEARCH_ID_WIREGUARD = "W:" + } override fun onCreate(savedInstanceState: Bundle?) { setTheme(getCurrentTheme(isDarkThemeOn(), persistentState.theme)) super.onCreate(savedInstanceState) fragmentIndex = intent.getIntExtra(Constants.VIEW_PAGER_SCREEN_TO_LOAD, 0) searchParam = intent.getStringExtra(Constants.SEARCH_QUERY) ?: "" + if (searchParam.contains(RULES_SEARCH_ID)) { + isUnivNavigated = true + } else if(searchParam.contains(RULES_SEARCH_ID_WIREGUARD)) { + isWireGuardLogs = true + } init() } @@ -87,9 +102,22 @@ class NetworkLogsActivity : AppCompatActivity(R.layout.activity_network_logs) { b.logsActViewpager.setCurrentItem(fragmentIndex, false) observeAppState() + + b.appLogs.setOnClickListener { + openConsoleLogActivity() + } + } + + private fun openConsoleLogActivity() { + val intent = Intent(this, ConsoleLogActivity::class.java) + startActivity(intent) } private fun getCount(): Int { + if (isUnivNavigated || isWireGuardLogs) { + return 1 + } + var count = 0 if (persistentState.routeRethinkInRethink) { count = 1 @@ -102,6 +130,9 @@ class NetworkLogsActivity : AppCompatActivity(R.layout.activity_network_logs) { } private fun getFragment(position: Int): Fragment { + if (isUnivNavigated || isWireGuardLogs) { + return ConnectionTrackerFragment.newInstance(searchParam) + } return when (position) { 0 -> { if (appConfig.getBraveMode().isDnsMode()) { diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/ProxySettingsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/ProxySettingsActivity.kt index 744a24491..8b9da17dd 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/ProxySettingsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/ProxySettingsActivity.kt @@ -415,10 +415,6 @@ class ProxySettingsActivity : AppCompatActivity(R.layout.fragment_proxy_configur } private fun displayHttpProxyUi() { - if (!isAtleastQ()) { - b.settingsActivityHttpProxyContainer.visibility = View.GONE - return - } val isCustomHttpProxyEnabled = appConfig.isCustomHttpProxyEnabled() b.settingsActivityHttpProxyContainer.visibility = View.VISIBLE b.settingsActivityHttpProxySwitch.isChecked = isCustomHttpProxyEnabled @@ -745,7 +741,7 @@ class ProxySettingsActivity : AppCompatActivity(R.layout.fragment_proxy_configur if (Utilities.isLanIpv4(ip)) { Utilities.isValidLocalPort(port) } else { - Utilities.isValidPort(port) + isValidPort(port) } if (!isValid) { errorTxt.text = getString(R.string.settings_http_proxy_error_text1) diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/TunnelSettingsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/TunnelSettingsActivity.kt index 6c3c466a8..f34b382c1 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/TunnelSettingsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/TunnelSettingsActivity.kt @@ -15,18 +15,29 @@ */ package com.celzero.bravedns.ui.activity +import Logger +import Logger.LOG_TAG_UI import android.content.Context import android.content.res.Configuration +import android.graphics.drawable.Drawable +import android.net.NetworkCapabilities import android.os.Bundle +import android.view.LayoutInflater import android.view.View import android.widget.CompoundButton +import android.widget.ProgressBar import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R import com.celzero.bravedns.data.AppConfig import com.celzero.bravedns.databinding.ActivityTunnelSettingsBinding +import com.celzero.bravedns.service.ConnectionMonitor import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.util.Constants @@ -34,7 +45,14 @@ import com.celzero.bravedns.util.InternetProtocol import com.celzero.bravedns.util.Themes import com.celzero.bravedns.util.UIUtils import com.celzero.bravedns.util.Utilities +import com.google.android.material.chip.Chip import com.google.android.material.dialog.MaterialAlertDialogBuilder +import inet.ipaddr.IPAddress.IPVersion +import inet.ipaddr.IPAddressString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import java.util.concurrent.TimeUnit @@ -71,6 +89,8 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin b.settingsActivityLanTrafficSwitch.isChecked = persistentState.privateIps // connectivity check b.settingsActivityConnectivityChecksSwitch.isChecked = persistentState.connectivityChecks + // show ping ips + b.settingsActivityPingIpsBtn.visibility = if (persistentState.connectivityChecks) View.VISIBLE else View.GONE // exclude apps in proxy b.settingsActivityExcludeProxyAppsSwitch.isChecked = !persistentState.excludeAppsInProxy // for protocol translation, enable only on DNS/DNS+Firewall mode @@ -126,6 +146,7 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin getString(R.string.settings_network_all_networks) ) alertBuilder.setMessage(msg) + alertBuilder.setCancelable(false) alertBuilder.setPositiveButton(getString(R.string.lbl_proceed)) { dialog, _ -> dialog.dismiss() b.settingsActivityAllNetworkSwitch.isChecked = true @@ -213,6 +234,15 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin b.settingsActivityConnectivityChecksSwitch.setOnCheckedChangeListener { _, isChecked -> persistentState.connectivityChecks = isChecked + if (isChecked) { + b.settingsActivityPingIpsBtn.visibility = View.VISIBLE + } else { + b.settingsActivityPingIpsBtn.visibility = View.GONE + } + } + + b.settingsActivityPingIpsBtn.setOnClickListener { + showPingIpsDialog() } } @@ -233,6 +263,256 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin alertBuilder.create().show() } + private fun showPingIpsDialog() { + val alertBuilder = MaterialAlertDialogBuilder(this) + val inflater = LayoutInflater.from(this) + val dialogView = inflater.inflate(R.layout.dialog_input_ips, null) + alertBuilder.setView(dialogView) + alertBuilder.setCancelable(false) + + val protocols = VpnController.protocols() + + val proto4 = dialogView.findViewById(R.id.protocol_v4) + val proto6 = dialogView.findViewById(R.id.protocol_v6) + + val ip41 = dialogView.findViewById(R.id.ipv4_address_1) + val progress41 = dialogView.findViewById(R.id.progress_ipv4_1) + val status41 = dialogView.findViewById(R.id.status_ipv4_1) + + // Repeat for other IP address fields + val ip42 = dialogView.findViewById(R.id.ipv4_address_2) + val progress42 = dialogView.findViewById(R.id.progress_ipv4_2) + val status42 = dialogView.findViewById(R.id.status_ipv4_2) + + val ip43 = dialogView.findViewById(R.id.ipv4_address_3) + val progress43 = dialogView.findViewById(R.id.progress_ipv4_3) + val status43 = dialogView.findViewById(R.id.status_ipv4_3) + + val ip61 = dialogView.findViewById(R.id.ipv6_address_1) + val progress61 = dialogView.findViewById(R.id.progress_ipv6_1) + val status61 = dialogView.findViewById(R.id.status_ipv6_1) + + val ip62 = dialogView.findViewById(R.id.ipv6_address_2) + val progress62 = dialogView.findViewById(R.id.progress_ipv6_2) + val status62 = dialogView.findViewById(R.id.status_ipv6_2) + + val ip63 = dialogView.findViewById(R.id.ipv6_address_3) + val progress63 = dialogView.findViewById(R.id.progress_ipv6_3) + val status63 = dialogView.findViewById(R.id.status_ipv6_3) + + val defaultDrawable = ContextCompat.getDrawable(this, R.drawable.edittext_default) + val errorDrawable = ContextCompat.getDrawable(this, R.drawable.edittext_error) + + val saveBtn: AppCompatTextView = dialogView.findViewById(R.id.save_button) + val testBtn: AppCompatImageView = dialogView.findViewById(R.id.test_button) + val cancelBtn: AppCompatTextView = dialogView.findViewById(R.id.cancel_button) + val resetChip: Chip = dialogView.findViewById(R.id.reset_chip) + + saveBtn.text = getString(R.string.lbl_save).uppercase() + cancelBtn.text = getString(R.string.lbl_cancel).uppercase() + + val errorMsg: AppCompatTextView = dialogView.findViewById(R.id.error_message) + + val items4 = persistentState.pingv4Ips.split(",").toTypedArray() + val items6 = persistentState.pingv6Ips.split(",").toTypedArray() + + if (protocols.contains("IPv4")) { + proto4.setImageResource(R.drawable.ic_tick) + } else { + proto4.setImageResource(R.drawable.ic_cross) + } + + if (protocols.contains("IPv6")) { + proto6.setImageResource(R.drawable.ic_tick) + } else { + proto6.setImageResource(R.drawable.ic_cross) + } + + ip41.setText(items4.getOrNull(0) ?: "") + ip42.setText(items4.getOrNull(1) ?: "") + ip43.setText(items4.getOrNull(2) ?: "") + + ip61.setText(items6.getOrNull(0) ?: "") + ip62.setText(items6.getOrNull(1) ?: "") + ip63.setText(items6.getOrNull(2) ?: "") + + val dialog = alertBuilder.create() + + resetChip.setOnClickListener { + // reset to default values + ip41.setText(Constants.ip4probes[0]) + ip42.setText(Constants.ip4probes[1]) + ip43.setText(Constants.ip4probes[2]) + ip61.setText(Constants.ip6probes[0]) + ip62.setText(Constants.ip6probes[1]) + ip63.setText(Constants.ip6probes[2]) + } + + testBtn.setOnClickListener { + try { + progress41.visibility = View.VISIBLE + progress42.visibility = View.VISIBLE + progress43.visibility = View.VISIBLE + progress61.visibility = View.VISIBLE + progress62.visibility = View.VISIBLE + progress63.visibility = View.VISIBLE + + io { + val valid41 = isReachable(ip41.text.toString()) + val valid42 = isReachable(ip42.text.toString()) + val valid43 = isReachable(ip43.text.toString()) + + val valid61 = isReachable(ip61.text.toString()) + val valid62 = isReachable(ip62.text.toString()) + val valid63 = isReachable(ip63.text.toString()) + + uiCtx { + if (!dialogView.isShown) return@uiCtx + + progress41.visibility = View.GONE + progress42.visibility = View.GONE + progress43.visibility = View.GONE + progress61.visibility = View.GONE + progress62.visibility = View.GONE + progress63.visibility = View.GONE + + status41.visibility = View.VISIBLE + status42.visibility = View.VISIBLE + status43.visibility = View.VISIBLE + status61.visibility = View.VISIBLE + status62.visibility = View.VISIBLE + status63.visibility = View.VISIBLE + + status41.setImageDrawable(getImgRes(valid41)) + status42.setImageDrawable(getImgRes(valid42)) + status43.setImageDrawable(getImgRes(valid43)) + status61.setImageDrawable(getImgRes(valid61)) + status62.setImageDrawable(getImgRes(valid62)) + status63.setImageDrawable(getImgRes(valid63)) + } + } + } catch (e: Exception) { + Logger.e(LOG_TAG_UI, "err on ip ping: ${e.message}", e) + } + } + + cancelBtn.setOnClickListener { + dialog.dismiss() + } + + saveBtn.setOnClickListener { + try { + val valid41 = isValidIp(ip41.text.toString(), IPVersion.IPV4) + val valid42 = isValidIp(ip42.text.toString(), IPVersion.IPV4) + val valid43 = isValidIp(ip43.text.toString(), IPVersion.IPV4) + + val valid61 = isValidIp(ip61.text.toString(), IPVersion.IPV6) + val valid62 = isValidIp(ip62.text.toString(), IPVersion.IPV6) + val valid63 = isValidIp(ip63.text.toString(), IPVersion.IPV6) + + // mark the edit text background as red if the ip is invalid + ip41.background = if (valid41) defaultDrawable else errorDrawable + ip42.background = if (valid42) defaultDrawable else errorDrawable + ip43.background = if (valid43) defaultDrawable else errorDrawable + ip61.background = if (valid61) defaultDrawable else errorDrawable + ip62.background = if (valid62) defaultDrawable else errorDrawable + ip63.background = if (valid63) defaultDrawable else errorDrawable + + if (!valid41 || !valid42 || !valid43 || !valid61 || !valid62 || !valid63) { + errorMsg.visibility = View.VISIBLE + errorMsg.text = getString(R.string.cd_dns_proxy_error_text_1) + return@setOnClickListener + } else { + errorMsg.visibility = View.VISIBLE + errorMsg.text = "" + } + + val ip4 = listOf(ip41.text.toString(), ip42.text.toString(), ip43.text.toString()) + val ip6 = listOf(ip61.text.toString(), ip62.text.toString(), ip63.text.toString()) + + val isSame = persistentState.pingv4Ips == ip4.joinToString(",") && + persistentState.pingv6Ips == ip6.joinToString(",") + + if (isSame) { + dialog.dismiss() + return@setOnClickListener + } + + persistentState.pingv4Ips = ip4.joinToString(",") + persistentState.pingv6Ips = ip6.joinToString(",") + Utilities.showToastUiCentered( + this, + getString(R.string.config_add_success_toast), + Toast.LENGTH_LONG + ) + notifyConnectionMonitor() + + Logger.i(LOG_TAG_UI, "ping ips: ${persistentState.pingv4Ips}, ${persistentState.pingv6Ips}") + dialog.dismiss() + } catch (e: Exception) { + Logger.e(LOG_TAG_UI, "err on ip save: ${e.message}", e) + // reset persistent state to the previous value + persistentState.pingv4Ips = Constants.ip4probes.joinToString(",") + persistentState.pingv6Ips = Constants.ip6probes.joinToString(",") + } + } + + dialog.show() + } + + private fun notifyConnectionMonitor() { + // change in ips, inform connection monitor to recheck the connectivity + io { VpnController.notifyConnectionMonitor() } + } + + private fun getImgRes(probeResult: ConnectionMonitor.ProbeResult?): Drawable? { + val failureDrawable = ContextCompat.getDrawable(this, R.drawable.ic_cross) + + if (probeResult == null) return failureDrawable + + if (!probeResult.ok) return failureDrawable + + val cap = probeResult.capabilities ?: return failureDrawable + + val a = if (cap.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + R.drawable.ic_firewall_wifi_on // wifi + } else if (cap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + R.drawable.ic_firewall_data_on + } else { + R.drawable.ic_tick + } + + val successDrawable = ContextCompat.getDrawable(this, R.drawable.ic_tick) + + return ContextCompat.getDrawable(this, a) ?: successDrawable + } + + private suspend fun isReachable(ip: String): ConnectionMonitor.ProbeResult? { + delay(500) + return try { + val res = VpnController.probeIp(ip) + Logger.d(LOG_TAG_UI, "probe res: ${res?.ok}, ${res?.ip}, ${res?.capabilities}") + res + } catch (e: Exception) { + Logger.d(LOG_TAG_UI, "err on ip ping(isReachable): ${e.message}") + null + } + } + + private fun isValidIp(ipString: String, type: IPVersion): Boolean { + try { + if (type.isIPv4) { + return IPAddressString(ipString).toAddress().isIPv4 + } + if (type.isIPv6) { + return IPAddressString(ipString).toAddress().isIPv6 + } + } catch (e: Exception) { + Logger.i(LOG_TAG_UI, "err on ip validation: ${e.message}") + } + return false + } + private fun displayInternetProtocolUi() { b.settingsActivityIpRl.isEnabled = true when (persistentState.internetProtocolType) { @@ -244,6 +524,7 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin ) b.settingsActivityPtransRl.visibility = View.GONE b.settingsActivityConnectivityChecksRl.visibility = View.GONE + b.settingsActivityPingIpsBtn.visibility = View.GONE } InternetProtocol.IPv6.id -> { b.genSettingsIpDesc.text = @@ -253,6 +534,7 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin ) b.settingsActivityPtransRl.visibility = View.VISIBLE b.settingsActivityConnectivityChecksRl.visibility = View.GONE + b.settingsActivityPingIpsBtn.visibility = View.GONE } InternetProtocol.IPv46.id -> { b.genSettingsIpDesc.text = @@ -262,6 +544,7 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin ) b.settingsActivityPtransRl.visibility = View.GONE b.settingsActivityConnectivityChecksRl.visibility = View.VISIBLE + b.settingsActivityPingIpsBtn.visibility = View.VISIBLE } else -> { b.genSettingsIpDesc.text = @@ -314,17 +597,14 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin if (isLockdown) { b.settingsActivityVpnLockdownDesc.visibility = View.VISIBLE b.settingsActivityAllowBypassRl.alpha = 0.5f - b.settingsActivityLanTrafficRl.alpha = 0.5f b.settingsActivityExcludeProxyAppsRl.alpha = 0.5f } else { b.settingsActivityVpnLockdownDesc.visibility = View.GONE b.settingsActivityAllowBypassRl.alpha = 1f - b.settingsActivityLanTrafficRl.alpha = 1f b.settingsActivityExcludeProxyAppsRl.alpha = 1f } b.settingsActivityAllowBypassSwitch.isEnabled = !isLockdown b.settingsActivityAllowBypassRl.isEnabled = !isLockdown - b.settingsActivityLanTrafficSwitch.isEnabled = !isLockdown b.settingsActivityLanTrafficRl.isEnabled = !isLockdown b.settingsActivityExcludeProxyAppsSwitch.isEnabled = !isLockdown b.settingsActivityExcludeProxyAppsRl.isEnabled = !isLockdown @@ -335,4 +615,14 @@ class TunnelSettingsActivity : AppCompatActivity(R.layout.activity_tunnel_settin Utilities.delay(ms, lifecycleScope) { for (v in views) v.isEnabled = true } } + + private fun io(fn: suspend () -> Unit) { + lifecycleScope.launch(Dispatchers.IO) { fn() } + } + + private suspend fun uiCtx(f: suspend () -> Unit) { + withContext(Dispatchers.Main) { f() } + } + + } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/UniversalFirewallSettingsActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/UniversalFirewallSettingsActivity.kt index d900ca169..2542882b0 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/UniversalFirewallSettingsActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/UniversalFirewallSettingsActivity.kt @@ -23,23 +23,41 @@ import android.content.Intent import android.content.res.Configuration import android.os.Bundle import android.provider.Settings +import android.view.View import android.widget.CompoundButton import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R +import com.celzero.bravedns.database.ConnectionTracker +import com.celzero.bravedns.database.ConnectionTrackerRepository import com.celzero.bravedns.databinding.ActivityUniversalFirewallSettingsBinding +import com.celzero.bravedns.service.FirewallRuleset import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.util.BackgroundAccessibilityService +import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Themes import com.celzero.bravedns.util.Utilities import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject class UniversalFirewallSettingsActivity : AppCompatActivity(R.layout.activity_universal_firewall_settings) { private val b by viewBinding(ActivityUniversalFirewallSettingsBinding::bind) private val persistentState by inject() + private val connTrackerRepository by inject() + + private lateinit var blockedUniversalRules : List + + companion object { + const val RULES_SEARCH_ID = "R:" + } override fun onCreate(savedInstanceState: Bundle?) { setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) @@ -69,6 +87,7 @@ class UniversalFirewallSettingsActivity : b.firewallUnivLockdownCheck.isChecked = persistentState.getUniversalLockdown() setupClickListeners() + updateStats() } private fun setupClickListeners() { @@ -164,6 +183,25 @@ class UniversalFirewallSettingsActivity : b.firewallUnivLockdownTxt.setOnClickListener { b.firewallUnivLockdownCheck.isChecked = !b.firewallUnivLockdownCheck.isChecked } + + // click listener for the stats + b.firewallDeviceLockedRl.setOnClickListener { startActivity(FirewallRuleset.RULE3.id) } + + b.firewallNotInUseRl.setOnClickListener { startActivity(FirewallRuleset.RULE4.id) } + + b.firewallUnknownRl.setOnClickListener { startActivity(FirewallRuleset.RULE5.id) } + + b.firewallUdpRl.setOnClickListener { startActivity(FirewallRuleset.RULE6.id) } + + b.firewallDnsBypassRl.setOnClickListener { startActivity(FirewallRuleset.RULE7.id) } + + b.firewallNewAppRl.setOnClickListener { startActivity(FirewallRuleset.RULE8.id) } + + b.firewallMeteredRl.setOnClickListener { startActivity(FirewallRuleset.RULE1F.id) } + + b.firewallHttpRl.setOnClickListener { startActivity(FirewallRuleset.RULE10.id) } + + b.firewallLockdownRl.setOnClickListener { startActivity(FirewallRuleset.RULE11.id) } } private fun recheckFirewallBackgroundMode(isChecked: Boolean) { @@ -249,6 +287,18 @@ class UniversalFirewallSettingsActivity : } } + private var maxValue: Double = 0.0 + + private fun calculatePercentage(c: Double): Int { + if (maxValue == 0.0) return 0 + if (c > maxValue) { + maxValue = c + return 100 + } + val percentage = (c / maxValue) * 100 + return percentage.toInt() + } + private fun showPermissionAlert() { val builder = MaterialAlertDialogBuilder(this) builder.setTitle(R.string.alert_permission_accessibility) @@ -275,4 +325,156 @@ class UniversalFirewallSettingsActivity : Logger.e(LOG_TAG_FIREWALL, "Failure accessing accessibility settings: ${e.message}", e) } } + + private fun updateStats() { + io { + // get stats for all the firewall rules + // update the UI with the stats + // 1. device locked - 2. background mode - 3. unknown 4. udp 5. dns bypass 6. new app 7. + // metered 8. http 9. universal lockdown + // instead get all the stats in one go and update the UI + blockedUniversalRules = connTrackerRepository.getBlockedUniversalRulesCount() + val deviceLocked = + blockedUniversalRules.filter { it.blockedByRule.contains(FirewallRuleset.RULE3.id) } + val backgroundMode = + blockedUniversalRules.filter { it.blockedByRule.contains(FirewallRuleset.RULE4.id) } + val unknown = + blockedUniversalRules.filter { it.blockedByRule.contains(FirewallRuleset.RULE5.id) } + val udp = + blockedUniversalRules.filter { it.blockedByRule.contains(FirewallRuleset.RULE6.id) } + val dnsBypass = + blockedUniversalRules.filter { it.blockedByRule.contains(FirewallRuleset.RULE7.id) } + val newApp = + blockedUniversalRules.filter { it.blockedByRule.contains(FirewallRuleset.RULE8.id) } + val metered = + blockedUniversalRules.filter { + it.blockedByRule.contains(FirewallRuleset.RULE1F.id) + } + val http = + blockedUniversalRules.filter { + it.blockedByRule.contains(FirewallRuleset.RULE10.id) + } + val universalLockdown = + blockedUniversalRules.filter { + it.blockedByRule.contains(FirewallRuleset.RULE11.id) + } + + val blockedCountList = + listOf( + deviceLocked.size, + backgroundMode.size, + unknown.size, + udp.size, + dnsBypass.size, + newApp.size, + metered.size, + http.size, + universalLockdown.size + ) + + maxValue = blockedCountList.maxOrNull()?.toDouble() ?: 0.0 + + uiCtx { + b.firewallDeviceLockedShimmerLayout.postDelayed( + { + if (!canPerformUiAction()) return@postDelayed + + stopShimmer() + hideShimmer() + + b.deviceLockedProgress.progress = + calculatePercentage(blockedCountList[0].toDouble()) + b.notInUseProgress.progress = + calculatePercentage(blockedCountList[1].toDouble()) + b.unknownProgress.progress = + calculatePercentage(blockedCountList[2].toDouble()) + b.udpProgress.progress = calculatePercentage(blockedCountList[3].toDouble()) + b.dnsBypassProgress.progress = + calculatePercentage(blockedCountList[4].toDouble()) + b.newAppProgress.progress = + calculatePercentage(blockedCountList[5].toDouble()) + b.meteredProgress.progress = + calculatePercentage(blockedCountList[6].toDouble()) + b.httpProgress.progress = + calculatePercentage(blockedCountList[7].toDouble()) + b.lockdownProgress.progress = + calculatePercentage(blockedCountList[8].toDouble()) + + b.firewallDeviceLockedStats.text = deviceLocked.size.toString() + b.firewallNotInUseStats.text = backgroundMode.size.toString() + b.firewallUnknownStats.text = unknown.size.toString() + b.firewallUdpStats.text = udp.size.toString() + b.firewallDnsBypassStats.text = dnsBypass.size.toString() + b.firewallNewAppStats.text = newApp.size.toString() + b.firewallMeteredStats.text = metered.size.toString() + b.firewallHttpStats.text = http.size.toString() + b.firewallLockdownStats.text = universalLockdown.size.toString() + }, + 500 + ) + } + } + } + + private fun canPerformUiAction(): Boolean { + return !isFinishing && + !isDestroyed && + lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED) && + !isChangingConfigurations + } + + override fun onPause() { + super.onPause() + stopShimmer() + } + + private fun stopShimmer() { + if (!canPerformUiAction()) return + + b.firewallUdpShimmerLayout.stopShimmer() + b.firewallDeviceLockedShimmerLayout.stopShimmer() + b.firewallNotInUseShimmerLayout.stopShimmer() + b.firewallUnknownShimmerLayout.stopShimmer() + b.firewallDnsBypassShimmerLayout.stopShimmer() + b.firewallNewAppShimmerLayout.stopShimmer() + b.firewallMeteredShimmerLayout.stopShimmer() + b.firewallHttpShimmerLayout.stopShimmer() + b.firewallLockdownShimmerLayout.stopShimmer() + } + + private fun hideShimmer() { + if (!canPerformUiAction()) return + + b.firewallUdpShimmerLayout.visibility = View.GONE + b.firewallDeviceLockedShimmerLayout.visibility = View.GONE + b.firewallNotInUseShimmerLayout.visibility = View.GONE + b.firewallUnknownShimmerLayout.visibility = View.GONE + b.firewallDnsBypassShimmerLayout.visibility = View.GONE + b.firewallNewAppShimmerLayout.visibility = View.GONE + b.firewallMeteredShimmerLayout.visibility = View.GONE + b.firewallHttpShimmerLayout.visibility = View.GONE + b.firewallLockdownShimmerLayout.visibility = View.GONE + } + + private fun startActivity(rule: String?) { + if (rule.isNullOrEmpty()) return + + // if the rules are not blocked, then no need to start the activity + val size = blockedUniversalRules.filter { it.blockedByRule.contains(rule) }.size + if (size == 0) return + + val intent = Intent(this, NetworkLogsActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + val searchParam = RULES_SEARCH_ID + rule + intent.putExtra(Constants.SEARCH_QUERY, searchParam) + startActivity(intent) + } + + private fun io(f: suspend () -> Unit): Job { + return lifecycleScope.launch(Dispatchers.IO) { f() } + } + + private suspend fun uiCtx(f: suspend () -> Unit) { + withContext(Dispatchers.Main) { f() } + } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigDetailActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigDetailActivity.kt index ee88283a4..25a08a910 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigDetailActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigDetailActivity.kt @@ -21,11 +21,13 @@ import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Bundle +import android.text.format.DateUtils import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import backend.RouterStats import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R import com.celzero.bravedns.adapter.WgIncludeAppsAdapter @@ -33,12 +35,20 @@ import com.celzero.bravedns.adapter.WgPeersAdapter import com.celzero.bravedns.databinding.ActivityWgDetailBinding import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.ProxyManager +import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.service.WireguardManager +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_OTHER_WG_ACTIVE +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_ACTIVE +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_FULL +import com.celzero.bravedns.service.WireguardManager.ERR_CODE_WG_INVALID import com.celzero.bravedns.service.WireguardManager.INVALID_CONF_ID +import com.celzero.bravedns.ui.activity.NetworkLogsActivity.Companion.RULES_SEARCH_ID_WIREGUARD import com.celzero.bravedns.ui.dialog.WgAddPeerDialog import com.celzero.bravedns.ui.dialog.WgIncludeAppsDialog +import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Themes import com.celzero.bravedns.util.UIUtils +import com.celzero.bravedns.util.UIUtils.fetchColor import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.viewmodel.ProxyAppsMappingViewModel import com.celzero.bravedns.wireguard.Config @@ -79,7 +89,7 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { fun isDefault() = this == DEFAULT companion object { - fun fromInt(value: Int) = entries.first { it.value == value } + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } ?: DEFAULT } } @@ -102,6 +112,19 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { } private fun init() { + if (!VpnController.hasTunnel()) { + Logger.i(LOG_TAG_PROXY, "VPN not active, config may not be available") + Utilities.showToastUiCentered( + this, + ERR_CODE_VPN_NOT_ACTIVE + + getString(R.string.settings_socks5_vpn_disabled_error), + Toast.LENGTH_LONG + ) + finish() + return + } + + b.editBtn.text = getString(R.string.rt_edit_dialog_positive).lowercase() b.globalLockdownTitleTv.text = getString( R.string.two_argument_space, @@ -136,7 +159,7 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { val mapping = WireguardManager.getConfigFilesById(configId) if (config == null) { - finish() + showInvalidConfigDialog() return } @@ -164,9 +187,106 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { return@uiCtx }*/ + io { updateStatusUi(config.getId()) } prefillConfig(config) } + private suspend fun updateStatusUi(id: Int) { + val config = WireguardManager.getConfigFilesById(id) + val cid = ProxyManager.ID_WG_BASE + id + if (config?.isActive == true) { + val statusId = VpnController.getProxyStatusById(cid) + val stats = VpnController.getProxyStats(cid) + val ps = UIUtils.ProxyStatus.entries.find { it.id == statusId } + uiCtx { + if (statusId != null) { + + val handshakeTime = getHandshakeTime(stats).toString() + val statusText = getIdleStatusText(ps, stats) + .ifEmpty { getStatusText(ps, handshakeTime, stats) } + b.statusText.text = statusText + } else { + b.statusText.text = + getString(R.string.status_waiting).replaceFirstChar(Char::titlecase) + } + val strokeColor = getStrokeColorForStatus(ps, stats) + b.interfaceDetailCard.strokeWidth = 2 + b.interfaceDetailCard.strokeColor = fetchColor(this, strokeColor) + } + } else { + uiCtx { + b.interfaceDetailCard.strokeWidth = 0 + b.statusText.text = + getString(R.string.lbl_disabled).replaceFirstChar(Char::titlecase) + } + } + } + + private fun getStatusText( + status: UIUtils.ProxyStatus?, + handshakeTime: String? = null, + stats: RouterStats? + ): String { + if (status == null) return getString(R.string.status_waiting) + .replaceFirstChar(Char::titlecase) + + val baseText = getString(UIUtils.getProxyStatusStringRes(status.id)) + .replaceFirstChar(Char::titlecase) + + return if (stats?.lastOK != 0L && handshakeTime != null) { + getString(R.string.about_version_install_source, baseText, handshakeTime) + } else { + baseText + } + } + + private fun getIdleStatusText(status: UIUtils.ProxyStatus?, stats: RouterStats?): String { + if (status != UIUtils.ProxyStatus.TZZ && status != UIUtils.ProxyStatus.TNT) return "" + if (stats == null || stats.lastOK == 0L) return "" + if (System.currentTimeMillis() - stats.lastOK >= 30 * DateUtils.SECOND_IN_MILLIS) return "" + + return getString(R.string.dns_connected).replaceFirstChar(Char::titlecase) + } + + private fun getHandshakeTime(stats: RouterStats?): CharSequence { + if (stats == null) { + return "" + } + if (stats.lastOK == 0L) { + return "" + } + val now = System.currentTimeMillis() + // returns a string describing 'time' as a time relative to 'now' + return DateUtils.getRelativeTimeSpanString( + stats.lastOK, + now, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + } + + private fun getStrokeColorForStatus(status: UIUtils.ProxyStatus?, stats: RouterStats?): Int { + return when (status) { + UIUtils.ProxyStatus.TOK -> if (stats?.lastOK == 0L) R.attr.chipTextNeutral else R.attr.accentGood + UIUtils.ProxyStatus.TUP, UIUtils.ProxyStatus.TZZ, UIUtils.ProxyStatus.TNT -> R.attr.chipTextNeutral + else -> R.attr.chipTextNegative + } + } + + private fun showInvalidConfigDialog() { + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(getString(R.string.lbl_wireguard)) + builder.setMessage(getString(R.string.config_invalid_desc)) + builder.setCancelable(false) + builder.setPositiveButton(getString(R.string.fapps_info_dialog_positive_btn)) { _, _ -> + finish() + } + builder.setNeutralButton(getString(R.string.lbl_delete)) { _, _ -> + WireguardManager.deleteConfig(configId) + } + builder.create().show() + } + private fun shouldObserveAppsCount(): Boolean { return !wgType.isOneWg() && !b.catchAllCheck.isChecked } @@ -178,15 +298,10 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { if (wgInterface == null) { return } + b.configNameText.visibility = View.VISIBLE b.configNameText.text = config.getName() - b.publicKeyText.text = wgInterface?.getKeyPair()?.getPublicKey()?.base64() + b.configIdText.text = getString(R.string.single_argument_parenthesis, config.getId().toString()) - if (wgInterface?.getAddresses()?.isEmpty() == true) { - b.addressesLabel.visibility = View.GONE - b.addressesText.visibility = View.GONE - } else { - b.addressesText.text = wgInterface?.getAddresses()?.joinToString { it.toString() } - } setPeersAdapter() // show dns servers if in one-wg mode if (wgType.isOneWg()) { @@ -202,11 +317,23 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { } b.dnsServersText.text = dns } else { + b.publicKeyLabel.visibility = View.VISIBLE + b.publicKeyText.visibility = View.VISIBLE + b.publicKeyText.text = wgInterface?.getKeyPair()?.getPublicKey()?.base64() b.dnsServersLabel.visibility = View.GONE b.dnsServersText.visibility = View.GONE } - // uncomment this if we want to show the dns servers, listen port and mtu + // uncomment this if we want to show the public key, addresses, listen port and mtu + /*b.publicKeyText.text = wgInterface?.getKeyPair()?.getPublicKey()?.base64() + + if (wgInterface?.getAddresses()?.isEmpty() == true) { + b.addressesLabel.visibility = View.GONE + b.addressesText.visibility = View.GONE + } else { + b.addressesText.text = wgInterface?.getAddresses()?.joinToString { it.toString() } + }*/ + /*if (wgInterface?.dnsServers?.isEmpty() == true) { b.dnsServersText.visibility = View.GONE b.dnsServersLabel.visibility = View.GONE @@ -300,21 +427,22 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { b.peersList.visibility = View.VISIBLE } */ + private fun handleAppsCount() { val id = ProxyManager.ID_WG_BASE + configId b.applicationsBtn.isEnabled = true mappingViewModel.getAppCountById(id).observe(this) { if (it == 0) { - b.applicationsBtn.setTextColor(UIUtils.fetchColor(this, R.attr.accentBad)) + b.applicationsBtn.setTextColor(fetchColor(this, R.attr.accentBad)) } else { - b.applicationsBtn.setTextColor(UIUtils.fetchColor(this, R.attr.accentGood)) + b.applicationsBtn.setTextColor(fetchColor(this, R.attr.accentGood)) } b.applicationsBtn.text = getString(R.string.add_remove_apps, it.toString()) } } private fun setupClickListeners() { - b.interfaceEdit.setOnClickListener { + b.editBtn.setOnClickListener { val intent = Intent(this, WgConfigEditorActivity::class.java) intent.putExtra(WgConfigEditorActivity.INTENT_EXTRA_WG_ID, configId) intent.putExtra(INTENT_EXTRA_WG_TYPE, wgType.value) @@ -328,7 +456,7 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { openAppsDialog(proxyName) } - b.interfaceDelete.setOnClickListener { showDeleteInterfaceDialog() } + b.deleteBtn.setOnClickListener { showDeleteInterfaceDialog() } /*b.newConfLayout.setOnClickListener { b.newConfProgressBar.visibility = View.VISIBLE @@ -356,6 +484,18 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { b.lockdownCheck.setOnClickListener { updateLockdown(b.lockdownCheck.isChecked) } b.catchAllCheck.setOnClickListener { updateCatchAll(b.catchAllCheck.isChecked) } + + b.logsBtn.setOnClickListener { + startActivity(ProxyManager.ID_WG_BASE + configId) + } + } + + private fun startActivity(searchParam: String?) { + val intent = Intent(this, NetworkLogsActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + val query = RULES_SEARCH_ID_WIREGUARD + searchParam + intent.putExtra(Constants.SEARCH_QUERY, query) + startActivity(intent) } private fun updateLockdown(enabled: Boolean) { @@ -377,33 +517,78 @@ class WgConfigDetailActivity : AppCompatActivity(R.layout.activity_wg_detail) { private fun updateCatchAll(enabled: Boolean) { io { - val config = WireguardManager.getConfigFilesById(configId) - if (config == null) { - Logger.e(LOG_TAG_PROXY, "updateCatchAll: config not found for $configId") + if (!VpnController.hasTunnel()) { + uiCtx { + Utilities.showToastUiCentered( + this, + ERR_CODE_VPN_NOT_ACTIVE + getString(R.string.settings_socks5_vpn_disabled_error), + Toast.LENGTH_LONG + ) + b.catchAllCheck.isChecked = !enabled + } return@io } - if (WireguardManager.canEnableConfig(config)) { - WireguardManager.updateCatchAllConfig(configId, enabled) + + if (!WireguardManager.canEnableProxy()) { + Logger.i( + LOG_TAG_PROXY, + "not in DNS+Firewall mode, cannot enable WireGuard" + ) uiCtx { - b.lockdownCheck.isEnabled = !enabled - b.applicationsBtn.isEnabled = !enabled - if (enabled) { - b.applicationsBtn.text = getString(R.string.routing_remaining_apps) - } else { - handleAppsCount() - } + // reset the check box + b.catchAllCheck.isChecked = false + Utilities.showToastUiCentered( + this, + ERR_CODE_VPN_NOT_FULL + getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) } - } else { + return@io + } + + if (WireguardManager.oneWireGuardEnabled()) { + // this should not happen, ui is disabled if one wireGuard is enabled + Logger.w(LOG_TAG_PROXY, "one wireGuard is already enabled") uiCtx { + // reset the check box + b.catchAllCheck.isChecked = false Utilities.showToastUiCentered( this, - getString(R.string.wireguard_enabled_failure), + ERR_CODE_OTHER_WG_ACTIVE + getString( + R.string.wireguard_enabled_failure + ), Toast.LENGTH_LONG ) + } + return@io + } + + + val config = WireguardManager.getConfigFilesById(configId) + if (config == null) { + Logger.e(LOG_TAG_PROXY, "updateCatchAll: config not found for $configId") + uiCtx { + // reset the check box b.catchAllCheck.isChecked = false + Utilities.showToastUiCentered( + this, + ERR_CODE_WG_INVALID + getString(R.string.wireguard_enabled_failure), + Toast.LENGTH_LONG + ) } return@io } + + WireguardManager.updateCatchAllConfig(configId, enabled) + uiCtx { + b.lockdownCheck.isEnabled = !enabled + b.applicationsBtn.isEnabled = !enabled + if (enabled) { + b.applicationsBtn.text = getString(R.string.routing_remaining_apps) + } else { + handleAppsCount() + } + } } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigEditorActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigEditorActivity.kt index e040ede3f..3d00081c9 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigEditorActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/WgConfigEditorActivity.kt @@ -109,10 +109,7 @@ class WgConfigEditorActivity : AppCompatActivity(R.layout.activity_wg_config_edi wgInterface?.getAddresses()?.joinToString { it.toString() } ) } - if ( - wgInterface?.listenPort?.isPresent == true && - wgInterface?.listenPort?.get() != 1 && wgType.isOneWg() - ) { + if (showListenPort()) { b.listenPortText.setText(wgInterface?.listenPort?.get().toString()) } if (wgInterface?.mtu?.isPresent == true) { @@ -122,6 +119,12 @@ class WgConfigEditorActivity : AppCompatActivity(R.layout.activity_wg_config_edi } } + private fun showListenPort(): Boolean { + val isPresent = wgInterface?.listenPort?.isPresent == true && wgInterface?.listenPort?.get() != 1 + val byType = wgType.isOneWg() || (!persistentState.randomizeListenPort && wgType.isDefault()) + return isPresent && byType + } + private fun setupClickListeners() { b.privateKeyTextLayout.setEndIconOnClickListener { val key = Backend.newWgPrivateKey() diff --git a/app/src/full/java/com/celzero/bravedns/ui/activity/WgMainActivity.kt b/app/src/full/java/com/celzero/bravedns/ui/activity/WgMainActivity.kt index ff2f72d76..df5f10dd2 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/activity/WgMainActivity.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/activity/WgMainActivity.kt @@ -24,7 +24,7 @@ import android.content.res.Configuration import android.os.Bundle import android.view.View import android.widget.Toast -import androidx.activity.addCallback +import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope @@ -152,6 +152,19 @@ class WgMainActivity : setTheme(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme)) super.onCreate(savedInstanceState) init() + onBackPressedDispatcher.addCallback( + this /* lifecycle owner */, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (b.createFab.visibility == View.VISIBLE) { + collapseFab() + } else { + finish() + } + return + } + } + ) } private fun init() { @@ -160,14 +173,6 @@ class WgMainActivity : observeConfig() observeDnsName() setupClickListeners() - - onBackPressedDispatcher.addCallback(this /* lifecycle owner */) { - if (b.createFab.visibility == View.VISIBLE) { - collapseFab() - } else { - finish() - } - } } private fun setAdapter() { @@ -207,7 +212,7 @@ class WgMainActivity : val layoutManager = LinearLayoutManager(this) b.wgGeneralInterfaceList.layoutManager = layoutManager - wgConfigAdapter = WgConfigAdapter(this) + wgConfigAdapter = WgConfigAdapter(this, this, persistentState.splitDns) wgConfigViewModel.interfaces.observe(this) { wgConfigAdapter?.submitData(lifecycle, it) } b.wgGeneralInterfaceList.adapter = wgConfigAdapter } @@ -277,18 +282,25 @@ class WgMainActivity : } private fun observeDnsName() { + val activeConfigs = WireguardManager.getEnabledConfigs() if (WireguardManager.oneWireGuardEnabled()) { - val activeConfigs = WireguardManager.getEnabledConfigs() - val isAnyConfigActive = activeConfigs.isNotEmpty() - if (isAnyConfigActive) { - val dnsName = activeConfigs.firstOrNull()?.getName() ?: return - b.wgWireguardDisclaimer.text = getString(R.string.wireguard_disclaimer, dnsName) - } + val dnsName = activeConfigs.firstOrNull()?.getName() ?: return + b.wgWireguardDisclaimer.text = getString(R.string.wireguard_disclaimer, dnsName) // remove the observer if any config is active appConfig.getConnectedDnsObservable().removeObservers(this) } else { - appConfig.getConnectedDnsObservable().observe(this) { - b.wgWireguardDisclaimer.text = getString(R.string.wireguard_disclaimer, it) + appConfig.getConnectedDnsObservable().observe(this) { dns -> + var dnsNames: String = dns.ifEmpty { "" } + if (persistentState.splitDns) { + if (activeConfigs.isNotEmpty()) { + dnsNames += ", " + } + dnsNames += activeConfigs.joinToString(", ") { it.getName() } + b.wgWireguardDisclaimer.text = + getString(R.string.wireguard_disclaimer, dnsNames) + } else { + b.wgWireguardDisclaimer.text = getString(R.string.wireguard_disclaimer, dnsNames) + } } } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/BackupRestoreBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/BackupRestoreBottomSheet.kt index 525f14ec2..5174a4dbf 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/BackupRestoreBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/BackupRestoreBottomSheet.kt @@ -160,18 +160,19 @@ class BackupRestoreBottomSheet : BottomSheetDialogFragment() { LOG_TAG_BACKUP_RESTORE, "WorkManager state: ${workInfo.state} for ${BackupAgent.TAG}" ) - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - showBackupSuccessUi() - workManager.pruneWork() - } else if ( - workInfo.state == WorkInfo.State.CANCELLED || - workInfo.state == WorkInfo.State.FAILED - ) { - showBackupFailureDialog() - workManager.pruneWork() - workManager.cancelAllWorkByTag(BackupAgent.TAG) - } else { - // no-op + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + showBackupSuccessUi() + workManager.pruneWork() + } + WorkInfo.State.CANCELLED, WorkInfo.State.FAILED -> { + showBackupFailureDialog() + workManager.pruneWork() + workManager.cancelAllWorkByTag(BackupAgent.TAG) + } + else -> { + // no-op + } } } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/ConnTrackerBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/ConnTrackerBottomSheet.kt index 4c55004e4..b8048b17e 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/ConnTrackerBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/ConnTrackerBottomSheet.kt @@ -35,6 +35,7 @@ import android.widget.Toast import androidx.appcompat.widget.AppCompatImageView import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope +import backend.Backend import com.celzero.bravedns.R import com.celzero.bravedns.adapter.FirewallStatusSpinnerAdapter import com.celzero.bravedns.data.ConnectionRules @@ -226,11 +227,15 @@ class ConnTrackerBottomSheet : BottomSheetDialogFragment(), KoinComponent { } val rule = info!!.blockedByRule + val skipProxyList = listOf(Backend.Base, Backend.Exit, Backend.Block) // TODO: below code is not required, remove it in future (20/03/2023) if (rule.contains(FirewallRuleset.RULE2G.id)) { b.bsConnTrackAppInfo.text = getFirewallRule(FirewallRuleset.RULE2G.id)?.title?.let { getString(it) } return + } else if (!info?.proxyDetails.isNullOrEmpty() && !skipProxyList.contains(info?.proxyDetails)) { + // add the proxy id to the chip text if available + b.bsConnTrackAppInfo.text = getString(R.string.two_argument_colon, getFirewallRule(rule)?.title?.let { getString(it) }, info?.proxyDetails) } else { b.bsConnTrackAppInfo.text = getFirewallRule(rule)?.title?.let { getString(it) } } @@ -276,7 +281,15 @@ class ConnTrackerBottomSheet : BottomSheetDialogFragment(), KoinComponent { private fun displaySummaryDetails() { b.bsConnConnTypeSecondary.visibility = View.GONE - b.connectionMessage.text = info?.message + // show connId and message if the log level is less than DEBUG + if (Logger.LoggerType.fromId(persistentState.goLoggerLevel.toInt()) + .isLessThan(Logger.LoggerType.DEBUG) + ) { + b.connectionMessage.text = + requireContext().getString(R.string.two_argument_colon, info?.connId, info?.message) + } else { + b.connectionMessage.text = info?.message + } if (VpnController.hasCid(info!!.connId, info!!.uid)) { b.connectionMessageLl.visibility = View.VISIBLE @@ -525,7 +538,7 @@ class ConnTrackerBottomSheet : BottomSheetDialogFragment(), KoinComponent { private fun openAppDetailActivity(uid: Int) { this.dismiss() val intent = Intent(requireContext(), AppInfoActivity::class.java) - intent.putExtra(AppInfoActivity.UID_INTENT_NAME, uid) + intent.putExtra(AppInfoActivity.INTENT_UID, uid) requireContext().startActivity(intent) } diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/DnsBlocklistBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/DnsBlocklistBottomSheet.kt index 40c12109b..41febb513 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/DnsBlocklistBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/DnsBlocklistBottomSheet.kt @@ -134,6 +134,13 @@ class DnsBlocklistBottomSheet : BottomSheetDialogFragment() { displayRecordTypeChip() setupClickListeners() updateRulesUi(log!!.queryStr) + + if (log!!.region.isNotEmpty()) { + b.dnsRegion.visibility = View.VISIBLE + b.dnsRegion.text = log!!.region + } else { + b.dnsRegion.visibility = View.GONE + } } private fun getResponseIp(): String { diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/FirewallAppFilterBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/FirewallAppFilterBottomSheet.kt index 65d5352af..2e8d4eb4f 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/FirewallAppFilterBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/FirewallAppFilterBottomSheet.kt @@ -47,7 +47,7 @@ class FirewallAppFilterBottomSheet : BottomSheetDialogFragment() { get() = _binding!! private val persistentState by inject() - private val sortValues = AppListActivity.Filters() + private val filters = AppListActivity.Filters() override fun getTheme(): Int = Themes.getBottomsheetCurrentTheme(isDarkThemeOn(), persistentState.theme) @@ -68,23 +68,23 @@ class FirewallAppFilterBottomSheet : BottomSheetDialogFragment() { } private fun initView() { - val filters = AppListActivity.filters.value + val f = AppListActivity.filters.value remakeParentFilterChipsUi() - if (filters == null) { + if (f == null) { applyParentFilter(AppListActivity.TopLevelFilter.ALL.id) return } else { - sortValues.firewallFilter = filters.firewallFilter + this.filters.firewallFilter = f.firewallFilter } - applyParentFilter(filters.topLevelFilter.id) - setFilter(filters.topLevelFilter, filters.categoryFilters) + applyParentFilter(f.topLevelFilter.id) + setFilter(f.topLevelFilter, f.categoryFilters) } private fun initClickListeners() { b.fsApply.setOnClickListener { - AppListActivity.filters.postValue(sortValues) + AppListActivity.filters.postValue(filters) this.dismiss() } @@ -173,24 +173,24 @@ class FirewallAppFilterBottomSheet : BottomSheetDialogFragment() { private fun applyParentFilter(tag: Any) { when (tag) { AppListActivity.TopLevelFilter.ALL.id -> { - sortValues.topLevelFilter = AppListActivity.TopLevelFilter.ALL - sortValues.categoryFilters.clear() + filters.topLevelFilter = AppListActivity.TopLevelFilter.ALL + filters.categoryFilters.clear() io { val categories = FirewallManager.getAllCategories() uiCtx { remakeChildFilterChipsUi(categories) } } } AppListActivity.TopLevelFilter.INSTALLED.id -> { - sortValues.topLevelFilter = AppListActivity.TopLevelFilter.INSTALLED - sortValues.categoryFilters.clear() + filters.topLevelFilter = AppListActivity.TopLevelFilter.INSTALLED + filters.categoryFilters.clear() io { val categories = FirewallManager.getCategoriesForInstalledApps() uiCtx { remakeChildFilterChipsUi(categories) } } } AppListActivity.TopLevelFilter.SYSTEM.id -> { - sortValues.topLevelFilter = AppListActivity.TopLevelFilter.SYSTEM - sortValues.categoryFilters.clear() + filters.topLevelFilter = AppListActivity.TopLevelFilter.SYSTEM + filters.categoryFilters.clear() io { val categories = FirewallManager.getCategoriesForSystemApps() uiCtx { remakeChildFilterChipsUi(categories) } @@ -220,9 +220,9 @@ class FirewallAppFilterBottomSheet : BottomSheetDialogFragment() { private fun applyChildFilter(tag: Any, show: Boolean) { if (show) { - sortValues.categoryFilters.add(tag.toString()) + filters.categoryFilters.add(tag.toString()) } else { - sortValues.categoryFilters.remove(tag.toString()) + filters.categoryFilters.remove(tag.toString()) } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/HomeScreenSettingBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/HomeScreenSettingBottomSheet.kt index 0f1002e43..5e76ca4f1 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/HomeScreenSettingBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/HomeScreenSettingBottomSheet.kt @@ -19,6 +19,7 @@ import Logger import Logger.LOG_TAG_VPN import android.content.res.Configuration import android.os.Bundle +import android.os.SystemClock import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View @@ -26,6 +27,7 @@ import android.view.ViewGroup import android.widget.CompoundButton import androidx.lifecycle.lifecycleScope import com.celzero.bravedns.R +import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.data.AppConfig import com.celzero.bravedns.databinding.BottomSheetHomeScreenBinding import com.celzero.bravedns.service.PersistentState @@ -37,6 +39,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.inject +import kotlin.math.abs class HomeScreenSettingBottomSheet : BottomSheetDialogFragment() { private var _binding: BottomSheetHomeScreenBinding? = null @@ -83,6 +86,15 @@ class HomeScreenSettingBottomSheet : BottomSheetDialogFragment() { val selectedIndex = appConfig.getBraveMode().mode Logger.d(LOG_TAG_VPN, "Home screen bottom sheet selectedIndex: $selectedIndex") + if (DEBUG) { + val timeSinceLastAuth = abs(SystemClock.elapsedRealtime() - persistentState.biometricAuthTime) + b.bsHomeScreenAppLockTime.visibility = View.VISIBLE + b.bsHomeScreenAppLockTime.text = java.util.concurrent.TimeUnit.MILLISECONDS.toMinutes(timeSinceLastAuth).toString() + " minutes" + Logger.d(LOG_TAG_VPN, "last auth time: ${persistentState.biometricAuthTime}") + } else { + b.bsHomeScreenAppLockTime.visibility = View.GONE + } + updateStatus(selectedIndex) } diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/LocalBlocklistsBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/LocalBlocklistsBottomSheet.kt index 8bfda3062..6bc84c5be 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/LocalBlocklistsBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/LocalBlocklistsBottomSheet.kt @@ -124,7 +124,7 @@ class LocalBlocklistsBottomSheet : BottomSheetDialogFragment() { b.lbbsVersion.text = getString( R.string.settings_local_blocklist_version, - Utilities.convertLongToTime( + convertLongToTime( persistentState.localBlocklistTimestamp, Constants.TIME_FORMAT_2 ) diff --git a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/RethinkLogBottomSheet.kt b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/RethinkLogBottomSheet.kt index 4b0a71e8a..f2a6fa48c 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/RethinkLogBottomSheet.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/bottomsheet/RethinkLogBottomSheet.kt @@ -382,7 +382,7 @@ class RethinkLogBottomSheet : BottomSheetDialogFragment(), KoinComponent { private fun openAppDetailActivity(uid: Int) { this.dismiss() val intent = Intent(requireContext(), AppInfoActivity::class.java) - intent.putExtra(AppInfoActivity.UID_INTENT_NAME, uid) + intent.putExtra(AppInfoActivity.INTENT_UID, uid) requireContext().startActivity(intent) } diff --git a/app/src/full/java/com/celzero/bravedns/ui/dialog/WgAddPeerDialog.kt b/app/src/full/java/com/celzero/bravedns/ui/dialog/WgAddPeerDialog.kt index 3f91c291b..c0baed0e3 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/dialog/WgAddPeerDialog.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/dialog/WgAddPeerDialog.kt @@ -22,8 +22,6 @@ import android.os.Bundle import android.view.View import android.view.Window import android.view.WindowManager -import android.view.inputmethod.EditorInfo -import android.widget.TextView.OnEditorActionListener import android.widget.Toast import androidx.core.widget.doOnTextChanged import androidx.lifecycle.LifecycleOwner diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt index bcb3c3f89..b91318805 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/AboutFragment.kt @@ -22,10 +22,8 @@ import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageManager -import android.icu.lang.UCharacter.GraphemeClusterBreak.T import android.net.Uri import android.os.Bundle -import android.os.Parcelable import android.os.SystemClock import android.provider.Settings import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS @@ -53,10 +51,12 @@ import com.celzero.bravedns.scheduler.BugReportZipper.getZipFileName import com.celzero.bravedns.scheduler.EnhancedBugReport import com.celzero.bravedns.scheduler.WorkScheduler import com.celzero.bravedns.service.AppUpdater +import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.ui.HomeScreenActivity import com.celzero.bravedns.util.Constants.Companion.INIT_TIME_MS import com.celzero.bravedns.util.Constants.Companion.RETHINKDNS_SPONSOR_LINK +import com.celzero.bravedns.util.UIUtils import com.celzero.bravedns.util.UIUtils.openAppInfo import com.celzero.bravedns.util.UIUtils.openVpnProfile import com.celzero.bravedns.util.UIUtils.sendEmailIntent @@ -82,6 +82,7 @@ import java.util.zip.ZipInputStream class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, KoinComponent { private val b by viewBinding(FragmentAboutBinding::bind) + private val persistentState by inject() private var lastAppExitInfoDialogInvokeTime = INIT_TIME_MS private val workScheduler by inject() @@ -91,11 +92,14 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K } private fun initView() { - if (isFdroidFlavour()) { b.aboutAppUpdate.visibility = View.GONE } + updateVersionInfo() + + b.sponsorInfoUsage.text = getSponsorInfo() + b.aboutSponsor.setOnClickListener(this) b.aboutWebsite.setOnClickListener(this) b.aboutTwitter.setOnClickListener(this) @@ -104,10 +108,12 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K b.aboutPrivacyPolicy.setOnClickListener(this) b.aboutMail.setOnClickListener(this) b.aboutTelegram.setOnClickListener(this) + b.aboutReddit.setOnClickListener(this) + b.aboutMastodon.setOnClickListener(this) + b.aboutElement.setOnClickListener(this) b.aboutFaq.setOnClickListener(this) b.mozillaImg.setOnClickListener(this) b.fossImg.setOnClickListener(this) - b.osomImg.setOnClickListener(this) b.aboutAppUpdate.setOnClickListener(this) b.aboutWhatsNew.setOnClickListener(this) b.aboutAppInfo.setOnClickListener(this) @@ -117,16 +123,30 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K b.aboutAppVersion.setOnClickListener(this) b.aboutAppContributors.setOnClickListener(this) b.aboutAppTranslate.setOnClickListener(this) + b.aboutStats.setOnClickListener(this) + } + private fun updateVersionInfo() { try { - val version = getVersionName() ?: "" + val version = getVersionName() // take first 7 characters of the version name, as the version has build number // appended to it, which is not required for the user to see. - val slicedVersion = version.slice(0..6) ?: "" + val slicedVersion = version.slice(0..6) b.aboutWhatsNew.text = getString(R.string.about_whats_new, slicedVersion) - // show the complete version name along with the source of installation - b.aboutAppVersion.text = - getString(R.string.about_version_install_source, version, getDownloadSource()) + + // complete version name along with the source of installation + val v = getString(R.string.about_version_install_source, version, getDownloadSource()) + + // show the go version if the log level is less than INFO, ie, DEBUG or VERBOSE + if (Logger.LoggerType.fromId(persistentState.goLoggerLevel.toInt()) + .isLessThan(Logger.LoggerType.INFO) + ) { + val build = VpnController.goBuildVersion(false) + b.aboutAppVersion.text = "$v\n$build" + } else { + b.aboutAppVersion.text = v + + } } catch (e: PackageManager.NameNotFoundException) { Logger.w(LOG_TAG_UI, "package name not found: ${e.message}", e) } @@ -141,6 +161,19 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K return pInfo?.versionName ?: "" } + private fun getSponsorInfo(): String { + val installTime = requireContext().packageManager.getPackageInfo( + requireContext().packageName, + 0 + ).firstInstallTime + val timeDiff = System.currentTimeMillis() - installTime + val days = (timeDiff / (1000 * 60 * 60 * 24)).toDouble() + val month = days / 30 + val amount = month * (0.60 + 0.20) + val msg = "You’ve been using Rethink for ${days.toInt()} days, which translates to a usage cost of ${"%.2f".format(amount)}$." + return msg + } + private fun getDownloadSource(): String { if (isFdroidFlavour()) return getString(R.string.build__flavor_fdroid) @@ -188,9 +221,6 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K b.fossImg -> { openActionViewIntent(getString(R.string.about_foss_link).toUri()) } - b.osomImg -> { - openActionViewIntent(getString(R.string.about_osom_link).toUri()) - } b.aboutAppUpdate -> { (requireContext() as HomeScreenActivity).checkForUpdate( AppUpdater.UserPresent.INTERACTIVE @@ -217,6 +247,59 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K b.aboutPrivacyPolicy -> { openActionViewIntent(getString(R.string.about_privacy_policy_link).toUri()) } + b.aboutReddit -> { + openActionViewIntent(getString(R.string.about_reddit_handle).toUri()) + } + b.aboutMastodon -> { + openActionViewIntent(getString(R.string.about_mastodom_handle).toUri()) + } + b.aboutElement -> { + openActionViewIntent(getString(R.string.about_matrix_handle).toUri()) + } + b.aboutStats -> { + openStatsDialog() + } + } + } + + private fun openStatsDialog() { + io { + val stat = VpnController.getNetStat() + val formatedStat = UIUtils.formatNetStat(stat) + uiCtx { + val dialogBinding = DialogInfoRulesLayoutBinding.inflate(layoutInflater) + val builder = + MaterialAlertDialogBuilder(requireContext()).setView(dialogBinding.root) + val lp = WindowManager.LayoutParams() + val dialog = builder.create() + dialog.show() + lp.copyFrom(dialog.window?.attributes) + lp.width = WindowManager.LayoutParams.MATCH_PARENT + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + dialog.setCancelable(true) + dialog.window?.attributes = lp + + val heading = dialogBinding.infoRulesDialogRulesTitle + val okBtn = dialogBinding.infoRulesDialogCancelImg + val descText = dialogBinding.infoRulesDialogRulesDesc + dialogBinding.infoRulesDialogRulesIcon.visibility = View.GONE + + heading.text = "Network Stats" + heading.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_log_level), + null, + null, + null + ) + + descText.movementMethod = LinkMovementMethod.getInstance() + descText.text = formatedStat + + okBtn.setOnClickListener { dialog.dismiss() } + + dialog.show() + } } } @@ -299,12 +382,14 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K private fun emailBugReport() { try { // get the rethink.tombstone file - val tombstoneFile = EnhancedBugReport.getTombstoneZipFile(requireContext()) - // Get the bug_report.zip file + val tombstoneFile:File? = EnhancedBugReport.getTombstoneZipFile(requireContext()) + + // get the bug_report.zip file val dir = requireContext().filesDir val file = File(getZipFileName(dir)) - val uri = getFileUri(file) + val uri = getFileUri(file) ?: throw Exception("file uri is null") + // create an intent for sending email with or without multiple attachments val emailIntent = if (tombstoneFile != null) { Intent(Intent.ACTION_SEND_MULTIPLE) } else { @@ -316,14 +401,19 @@ class AboutFragment : Fragment(R.layout.fragment_about), View.OnClickListener, K Intent.EXTRA_SUBJECT, getString(R.string.about_mail_bugreport_subject) ) - emailIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.about_mail_bugreport_text)) - // attach extra as list or single file based on the availability + // attach extra files (either as a list or single file based on availability) if (tombstoneFile != null) { - val tombstoneUri = getFileUri(tombstoneFile) + val tombstoneUri = + getFileUri(tombstoneFile) ?: throw Exception("tombstoneUri is null") + val uriList = arrayListOf(uri, tombstoneUri) // send multiple attachments - emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, arrayListOf(uri, tombstoneUri)) + emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) } else { + // ensure EXTRA_TEXT is passed correctly as an ArrayList + val bugReportText = getString(R.string.about_mail_bugreport_text) + val bugReportTextList = arrayListOf(bugReportText) + emailIntent.putCharSequenceArrayListExtra(Intent.EXTRA_TEXT, bugReportTextList) emailIntent.putExtra(Intent.EXTRA_STREAM, uri) } Logger.i(LOG_TAG_UI, "email with attachment: $uri, ${tombstoneFile?.path}") diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/ConfigureFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/ConfigureFragment.kt index 5592dcd34..c4c26bb02 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/ConfigureFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/ConfigureFragment.kt @@ -23,6 +23,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R import com.celzero.bravedns.databinding.FragmentConfigureBinding import com.celzero.bravedns.ui.activity.AppListActivity +import com.celzero.bravedns.ui.activity.AdvancedSettingActivity import com.celzero.bravedns.ui.activity.DnsDetailActivity import com.celzero.bravedns.ui.activity.FirewallActivity import com.celzero.bravedns.ui.activity.MiscSettingsActivity @@ -41,18 +42,23 @@ class ConfigureFragment : Fragment(R.layout.fragment_configure) { PROXY, VPN, OTHERS, - LOGS + LOGS, + ADVANCED } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initView() + setupClickListeners() } private fun initView() { b.fsNetworkTv.text = getString(R.string.lbl_network).replaceFirstChar(Char::titlecase) b.fsLogsTv.text = getString(R.string.lbl_logs).replaceFirstChar(Char::titlecase) + b.fsAdvancedTv.text = getString(R.string.lbl_advanced).replaceFirstChar(Char::titlecase) + } + private fun setupClickListeners() { b.fsAppsCard.setOnClickListener { // open apps configuration startActivity(ScreenType.APPS) @@ -87,6 +93,11 @@ class ConfigureFragment : Fragment(R.layout.fragment_configure) { // open logs configuration startActivity(ScreenType.LOGS) } + + b.fsAdvancedCard.setOnClickListener { + // open developer options configuration + startActivity(ScreenType.ADVANCED) + } } private fun startActivity(type: ScreenType) { @@ -99,8 +110,8 @@ class ConfigureFragment : Fragment(R.layout.fragment_configure) { ScreenType.VPN -> Intent(requireContext(), TunnelSettingsActivity::class.java) ScreenType.OTHERS -> Intent(requireContext(), MiscSettingsActivity::class.java) ScreenType.LOGS -> Intent(requireContext(), NetworkLogsActivity::class.java) + ScreenType.ADVANCED -> Intent(requireContext(), AdvancedSettingActivity::class.java) } - intent.flags = Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED startActivity(intent) } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/ConnectionTrackerFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/ConnectionTrackerFragment.kt index 3d1d234b7..e0581e4e5 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/ConnectionTrackerFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/ConnectionTrackerFragment.kt @@ -31,9 +31,11 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R import com.celzero.bravedns.adapter.ConnectionTrackerAdapter import com.celzero.bravedns.database.ConnectionTrackerRepository -import com.celzero.bravedns.databinding.ActivityConnectionTrackerBinding +import com.celzero.bravedns.databinding.FragmentConnectionTrackerBinding import com.celzero.bravedns.service.FirewallRuleset import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.ui.activity.NetworkLogsActivity +import com.celzero.bravedns.ui.activity.UniversalFirewallSettingsActivity import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.UIUtils.formatToRelativeTime import com.celzero.bravedns.util.Utilities @@ -48,8 +50,8 @@ import org.koin.androidx.viewmodel.ext.android.viewModel /** Captures network logs and stores in ConnectionTracker, a room database. */ class ConnectionTrackerFragment : - Fragment(R.layout.activity_connection_tracker), SearchView.OnQueryTextListener { - private val b by viewBinding(ActivityConnectionTrackerBinding::bind) + Fragment(R.layout.fragment_connection_tracker), SearchView.OnQueryTextListener { + private val b by viewBinding(FragmentConnectionTrackerBinding::bind) private var layoutManager: RecyclerView.LayoutManager? = null private val viewModel: ConnectionTrackerViewModel by viewModel() @@ -60,8 +62,12 @@ class ConnectionTrackerFragment : private val connectionTrackerRepository by inject() private val persistentState by inject() + private var fromWireGuardScreen: Boolean = false + private var fromUniversalFirewallScreen: Boolean = false + companion object { const val PROTOCOL_FILTER_PREFIX = "P:" + private const val QUERY_TEXT_TIMEOUT: Long = 600 fun newInstance(param: String): ConnectionTrackerFragment { val args = Bundle() @@ -77,7 +83,23 @@ class ConnectionTrackerFragment : initView() if (arguments != null) { val query = arguments?.getString(Constants.SEARCH_QUERY) ?: return - b.connectionSearch.setQuery(query, true) + fromUniversalFirewallScreen = query.contains(UniversalFirewallSettingsActivity.RULES_SEARCH_ID) + fromWireGuardScreen = query.contains(NetworkLogsActivity.RULES_SEARCH_ID_WIREGUARD) + if (fromUniversalFirewallScreen) { + val rule = query.split(UniversalFirewallSettingsActivity.RULES_SEARCH_ID)[1] + filterCategories.add(rule) + filterType = TopLevelFilter.BLOCKED + viewModel.setFilter(filterQuery, filterCategories, filterType) + hideSearchLayout() + } else if (fromWireGuardScreen) { + val rule = query.split(NetworkLogsActivity.RULES_SEARCH_ID_WIREGUARD)[1] + filterQuery = rule + filterType = TopLevelFilter.ALL + viewModel.setFilter(filterQuery, filterCategories, filterType) + hideSearchLayout() + } else { + b.connectionSearch.setQuery(query, true) + } } } @@ -92,20 +114,7 @@ class ConnectionTrackerFragment : b.connectionListLogsDisabledTv.visibility = View.GONE b.connectionCardViewTop.visibility = View.VISIBLE - b.recyclerConnection.setHasFixedSize(true) - layoutManager = LinearLayoutManager(requireContext()) - b.recyclerConnection.layoutManager = layoutManager - val recyclerAdapter = ConnectionTrackerAdapter(requireContext()) - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.connectionTrackerList.observe(viewLifecycleOwner) { it -> - recyclerAdapter.submitData(lifecycle, it) - } - } - } - b.recyclerConnection.adapter = recyclerAdapter - - setupRecyclerScrollListener() + setupRecyclerView() b.connectionSearch.setOnQueryTextListener(this) b.connectionSearch.setOnClickListener { @@ -123,6 +132,49 @@ class ConnectionTrackerFragment : remakeChildFilterChipsUi(FirewallRuleset.getBlockedRules()) } + private fun setupRecyclerView() { + b.recyclerConnection.setHasFixedSize(true) + layoutManager = LinearLayoutManager(requireContext()) + b.recyclerConnection.layoutManager = layoutManager + val recyclerAdapter = ConnectionTrackerAdapter(requireContext()) + recyclerAdapter.stateRestorationPolicy = + RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.connectionTrackerList.observe(viewLifecycleOwner) { pagingData -> + recyclerAdapter.submitData(lifecycle, pagingData) + } + } + } + recyclerAdapter.addLoadStateListener { + if (it.append.endOfPaginationReached) { + if (recyclerAdapter.itemCount < 1) { + if (fromUniversalFirewallScreen || fromWireGuardScreen) { + b.connectionListLogsDisabledTv.text = getString(R.string.ada_ip_no_connection) + b.connectionListLogsDisabledTv.visibility = View.VISIBLE + b.connectionCardViewTop.visibility = View.GONE + } else { + b.connectionListLogsDisabledTv.visibility = View.GONE + b.connectionCardViewTop.visibility = View.VISIBLE + } + } else { + b.connectionListLogsDisabledTv.visibility = View.GONE + b.connectionCardViewTop.visibility = View.VISIBLE + } + } else { + b.connectionListLogsDisabledTv.visibility = View.GONE + b.connectionCardViewTop.visibility = View.VISIBLE + } + } + b.recyclerConnection.adapter = recyclerAdapter + + setupRecyclerScrollListener() + } + + private fun hideSearchLayout() { + b.connectionCardViewTop.visibility = View.GONE + } + override fun onResume() { super.onResume() b.connectionListRl.requestFocus() @@ -253,7 +305,7 @@ class ConnectionTrackerFragment : } override fun onQueryTextChange(query: String): Boolean { - Utilities.delay(500, lifecycleScope) { + Utilities.delay(QUERY_TEXT_TIMEOUT, lifecycleScope) { if (this.isAdded) { this.filterQuery = query viewModel.setFilter(query, filterCategories, filterType) diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomDomainFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomDomainFragment.kt index 5576ed3b0..d014f6001 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomDomainFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomDomainFragment.kt @@ -34,6 +34,7 @@ import com.celzero.bravedns.databinding.FragmentCustomDomainBinding import com.celzero.bravedns.service.DomainRulesManager import com.celzero.bravedns.service.DomainRulesManager.isValidDomain import com.celzero.bravedns.service.DomainRulesManager.isWildCardEntry +import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.ui.activity.CustomRulesActivity import com.celzero.bravedns.util.Constants.Companion.INTENT_UID import com.celzero.bravedns.util.Constants.Companion.UID_EVERYBODY @@ -50,6 +51,7 @@ class CustomDomainFragment : private val b by viewBinding(FragmentCustomDomainBinding::bind) private var layoutManager: RecyclerView.LayoutManager? = null + private lateinit var adapter: CustomDomainAdapter private val viewModel by inject() @@ -101,17 +103,36 @@ class CustomDomainFragment : private fun setupAppSpecificRules(rule: CustomRulesActivity.RULES) { observeCustomRules() - val adapter = CustomDomainAdapter(requireContext(), rule) + adapter = CustomDomainAdapter(requireContext(), rule) b.cdaRecycler.adapter = adapter viewModel.setUid(uid) viewModel.customDomains.observe(this as LifecycleOwner) { adapter.submitData(this.lifecycle, it) } + io { + val appName = FirewallManager.getAppNameByUid(uid) + if (appName != null) { + uiCtx { updateAppNameInSearchHint(appName) } + } + } + } + + private fun updateAppNameInSearchHint(appName: String) { + val appNameTruncated = appName.substring(0, appName.length.coerceAtMost(10)) + val hint = getString( + R.string.two_argument_colon, + appNameTruncated, + getString(R.string.search_custom_domains) + ) + b.cdaSearchView.queryHint = hint + b.cdaSearchView.findViewById(androidx.appcompat.R.id.search_src_text).textSize = + 14f + return } private fun setupAllRules(rule: CustomRulesActivity.RULES) { observeAllRules() - val adapter = CustomDomainAdapter(requireContext(), rule) + adapter = CustomDomainAdapter(requireContext(), rule) b.cdaRecycler.adapter = adapter viewModel.allDomainRules.observe(this as LifecycleOwner) { adapter.submitData(this.lifecycle, it) @@ -300,11 +321,18 @@ class CustomDomainFragment : builder.setTitle(R.string.univ_delete_firewall_dialog_title) builder.setMessage(R.string.univ_delete_firewall_dialog_message) builder.setPositiveButton(getString(R.string.univ_ip_delete_dialog_positive)) { _, _ -> + io { - if (rule == CustomRulesActivity.RULES.APP_SPECIFIC_RULES) { - DomainRulesManager.deleteRulesByUid(uid) + val selectedItems = adapter.getSelectedItems() + if (selectedItems.isNotEmpty()) { + uiCtx { adapter.clearSelection() } + DomainRulesManager.deleteRules(selectedItems) } else { - DomainRulesManager.deleteAllRules() + if (rule == CustomRulesActivity.RULES.APP_SPECIFIC_RULES) { + DomainRulesManager.deleteRulesByUid(uid) + } else { + DomainRulesManager.deleteAllRules() + } } } Utilities.showToastUiCentered( @@ -315,7 +343,7 @@ class CustomDomainFragment : } builder.setNegativeButton(getString(R.string.lbl_cancel)) { _, _ -> - // no-op + adapter.clearSelection() } builder.setCancelable(true) @@ -325,4 +353,8 @@ class CustomDomainFragment : private fun io(f: suspend () -> Unit) { lifecycleScope.launch(Dispatchers.IO) { f() } } + + private fun uiCtx(f: suspend () -> Unit) { + lifecycleScope.launch(Dispatchers.Main) { f() } + } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomIpFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomIpFragment.kt index d65c3f1f7..993a11fad 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomIpFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/CustomIpFragment.kt @@ -30,6 +30,7 @@ import com.celzero.bravedns.R import com.celzero.bravedns.adapter.CustomIpAdapter import com.celzero.bravedns.databinding.DialogAddCustomIpBinding import com.celzero.bravedns.databinding.FragmentCustomIpBinding +import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.IpRulesManager import com.celzero.bravedns.ui.activity.CustomRulesActivity import com.celzero.bravedns.util.Constants.Companion.INTENT_UID @@ -51,6 +52,7 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue private val viewModel: CustomIpViewModel by viewModel() private var uid = UID_EVERYBODY private var rules = CustomRulesActivity.RULES.APP_SPECIFIC_RULES + private lateinit var adapter: CustomIpAdapter companion object { fun newInstance(uid: Int, rules: CustomRulesActivity.RULES): CustomIpFragment { @@ -140,16 +142,34 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue if (rules == CustomRulesActivity.RULES.APP_SPECIFIC_RULES) { b.cipAddFab.visibility = View.VISIBLE setupAdapterForApp() + io { + val appName = FirewallManager.getAppNameByUid(uid) + if (!appName.isNullOrEmpty()) { + uiCtx { updateAppNameInSearchHint(appName) } + } + } } else { b.cipAddFab.visibility = View.GONE setupAdapterForAllApps() } } + private fun updateAppNameInSearchHint(appName: String) { + val appNameTruncated = appName.substring(0, appName.length.coerceAtMost(10)) + val hint = getString( + R.string.two_argument_colon, + appNameTruncated, + getString(R.string.search_universal_ips) + ) + b.cipSearchView.queryHint = hint + b.cipSearchView.findViewById(androidx.appcompat.R.id.search_src_text).textSize = + 14f + return + } + private fun setupAdapterForApp() { observeAppSpecificRules() - val adapter = - CustomIpAdapter(requireContext(), CustomRulesActivity.RULES.APP_SPECIFIC_RULES) + adapter = CustomIpAdapter(requireContext(), CustomRulesActivity.RULES.APP_SPECIFIC_RULES) viewModel.setUid(uid) viewModel.customIpDetails.observe(viewLifecycleOwner) { adapter.submitData(this.lifecycle, it) @@ -159,7 +179,7 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue private fun setupAdapterForAllApps() { observeAllAppsRules() - val adapter = CustomIpAdapter(requireContext(), CustomRulesActivity.RULES.ALL_RULES) + adapter = CustomIpAdapter(requireContext(), CustomRulesActivity.RULES.ALL_RULES) viewModel.allIpRules.observe(viewLifecycleOwner) { adapter.submitData(this.lifecycle, it) } b.cipRecycler.adapter = adapter } @@ -228,7 +248,7 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue val input = dBind.daciIpEditText.text.toString() val ipString = Utilities.removeLeadingAndTrailingDots(input) var ip: IPAddress? = null - var port: Int = 0 + var port = 0 // chances of creating NetworkOnMainThread exception, handling with io operation ioCtx { @@ -265,10 +285,16 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue builder.setMessage(R.string.univ_delete_firewall_dialog_message) builder.setPositiveButton(getString(R.string.univ_ip_delete_dialog_positive)) { _, _ -> io { - if (rules == CustomRulesActivity.RULES.APP_SPECIFIC_RULES) { - IpRulesManager.deleteRulesByUid(uid) + val selectedItems = adapter.getSelectedItems() + if (selectedItems.isNotEmpty()) { + IpRulesManager.deleteRules(selectedItems) + uiCtx { adapter.clearSelection() } } else { - IpRulesManager.deleteAllAppsRules() + if (rules == CustomRulesActivity.RULES.APP_SPECIFIC_RULES) { + IpRulesManager.deleteRulesByUid(uid) + } else { + IpRulesManager.deleteAllAppsRules() + } } } Utilities.showToastUiCentered( @@ -279,7 +305,7 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue } builder.setNegativeButton(getString(R.string.lbl_cancel)) { _, _ -> - // no-op + adapter.clearSelection() } builder.setCancelable(true) @@ -290,6 +316,10 @@ class CustomIpFragment : Fragment(R.layout.fragment_custom_ip), SearchView.OnQue withContext(Dispatchers.IO) { f() } } + private suspend fun uiCtx(f: suspend () -> Unit) { + withContext(Dispatchers.Main) { f() } + } + private fun io(f: suspend () -> Unit) { lifecycleScope.launch(Dispatchers.IO) { f() } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsCryptListFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsCryptListFragment.kt index 8f817d43d..b76e70f73 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsCryptListFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsCryptListFragment.kt @@ -195,7 +195,7 @@ class DnsCryptListFragment : Fragment(R.layout.fragment_dns_crypt_list) { // Do the DNS Crypt setting there if (mode == 0) { insertDNSCryptServer(name, urlStamp, desc) - } else if (mode == 1) { + } else { insertDNSCryptRelay(name, urlStamp, desc) } dialog.dismiss() diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsLogFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsLogFragment.kt index bef66da2b..76e08ee60 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsLogFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsLogFragment.kt @@ -33,6 +33,7 @@ import com.celzero.bravedns.adapter.DnsQueryAdapter import com.celzero.bravedns.database.DnsLogRepository import com.celzero.bravedns.databinding.FragmentDnsLogsBinding import com.celzero.bravedns.service.PersistentState +import com.celzero.bravedns.ui.activity.UniversalFirewallSettingsActivity import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.UIUtils.formatToRelativeTime import com.celzero.bravedns.util.Utilities @@ -80,6 +81,10 @@ class DnsLogFragment : Fragment(R.layout.fragment_dns_logs), SearchView.OnQueryT initView() if (arguments != null) { val query = arguments?.getString(Constants.SEARCH_QUERY) ?: return + if (query.contains(UniversalFirewallSettingsActivity.RULES_SEARCH_ID)) { + // do nothing, as the search is for the firewall rules and not for the dns + return + } b.queryListSearch.setQuery(query, true) } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsSettingsFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsSettingsFragment.kt index 14f4c58b6..e43b4ea25 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsSettingsFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/DnsSettingsFragment.kt @@ -16,6 +16,7 @@ package com.celzero.bravedns.ui.fragment import Logger +import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.view.View @@ -40,17 +41,19 @@ import com.celzero.bravedns.ui.activity.ConfigureRethinkBasicActivity import com.celzero.bravedns.ui.activity.DnsListActivity import com.celzero.bravedns.ui.activity.PauseActivity import com.celzero.bravedns.ui.bottomsheet.LocalBlocklistsBottomSheet +import com.celzero.bravedns.util.UIUtils import com.celzero.bravedns.util.UIUtils.fetchColor import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.util.Utilities.isPlayStoreFlavour +import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import java.util.concurrent.TimeUnit -class DnsSettingsFragment : - Fragment(R.layout.fragment_dns_configure), +class DnsSettingsFragment : Fragment(R.layout.fragment_dns_configure), LocalBlocklistsBottomSheet.OnBottomSheetDialogFragmentDismiss { private val b by viewBinding(FragmentDnsConfigureBinding::bind) @@ -93,18 +96,21 @@ class DnsSettingsFragment : b.dcFaviconSwitch.isChecked = persistentState.fetchFavIcon // prevent dns leaks b.dcPreventDnsLeaksSwitch.isChecked = persistentState.preventDnsLeaks + // enable per-app domain rules (dns alg) + b.dcAlgSwitch.isChecked = persistentState.enableDnsAlg // periodically check for blocklist update b.dcCheckUpdateSwitch.isChecked = persistentState.periodicallyCheckBlocklistUpdate // use custom download manager b.dcDownloaderSwitch.isChecked = persistentState.useCustomDownloadManager - // enable per-app domain rules (dns alg) - b.dcAlgSwitch.isChecked = persistentState.enableDnsAlg // enable dns caching in tunnel b.dcEnableCacheSwitch.isChecked = persistentState.enableDnsCache // proxy dns b.dcProxyDnsSwitch.isChecked = !persistentState.proxyDns - + // use system dns for undelegated domains + b.dcUndelegatedDomainsSwitch.isChecked = persistentState.useSystemDnsForUndelegatedDomains b.connectedStatusTitle.text = getConnectedDnsType() + b.dvBypassDnsBlockSwitch.isChecked = persistentState.bypassBlockInDns + updateSpiltDnsUi() } private fun updateLocalBlocklistUi() { @@ -131,11 +137,6 @@ class DnsSettingsFragment : } private fun initObservers() { - observeBraveMode() - observeAppState() - } - - private fun observeAppState() { VpnController.connectionStatus.observe(viewLifecycleOwner) { if (it == BraveVPNService.State.PAUSED) { val intent = Intent(requireContext(), PauseActivity::class.java) @@ -145,6 +146,17 @@ class DnsSettingsFragment : appConfig.getConnectedDnsObservable().observe(viewLifecycleOwner) { updateConnectedStatus(it) + updateSelectedDns() + } + } + + private fun updateSpiltDnsUi() { + if (persistentState.enableDnsAlg) { + b.dcSplitDnsRl.visibility = View.VISIBLE + b.dcSplitDnsSwitch.isChecked = persistentState.splitDns + } else { + b.dcSplitDnsRl.visibility = View.GONE + b.dcSplitDnsSwitch.isChecked = false } } @@ -153,44 +165,47 @@ class DnsSettingsFragment : b.connectedStatusTitleUrl.text = resources.getString(R.string.configure_dns_connected_dns_proxy_status) b.connectedStatusTitle.text = resources.getString(R.string.lbl_wireguard) - disableAllDns() - b.wireguardRb.isEnabled = true return } + var dns = connectedDns + if (persistentState.splitDns && WireguardManager.isAdvancedWgActive()) { + dns += ", " + resources.getString(R.string.lbl_wireguard) + } + when (appConfig.getDnsType()) { AppConfig.DnsType.DOH -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.configure_dns_connected_doh_status) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } AppConfig.DnsType.DOT -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.lbl_dot) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } AppConfig.DnsType.DNSCRYPT -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.configure_dns_connected_dns_crypt_status) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } AppConfig.DnsType.DNS_PROXY -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.configure_dns_connected_dns_proxy_status) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } AppConfig.DnsType.RETHINK_REMOTE -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.configure_dns_connected_doh_status) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } AppConfig.DnsType.SYSTEM_DNS -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.configure_dns_connected_dns_proxy_status) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } AppConfig.DnsType.ODOH -> { b.connectedStatusTitleUrl.text = resources.getString(R.string.lbl_odoh) - b.connectedStatusTitle.text = connectedDns + b.connectedStatusTitle.text = dns } } } @@ -207,25 +222,30 @@ class DnsSettingsFragment : if (WireguardManager.oneWireGuardEnabled()) { b.wireguardRb.visibility = View.VISIBLE b.wireguardRb.isChecked = true + b.wireguardRb.setChecked(true) b.wireguardRb.isEnabled = true disableAllDns() return - } else { - b.wireguardRb.visibility = View.GONE } + b.wireguardRb.visibility = View.GONE if (isSystemDns()) { b.networkDnsRb.isChecked = true - return - } - - if (isRethinkDns()) { + b.rethinkPlusDnsRb.isChecked = false + b.customDnsRb.isChecked = false + b.networkDnsRb.setChecked(true) + } else if (isRethinkDns()) { b.rethinkPlusDnsRb.isChecked = true - return + b.customDnsRb.isChecked = false + b.networkDnsRb.isChecked = false + b.rethinkPlusDnsRb.setChecked(true) + } else { + // connected to custom dns, update the dns details + b.customDnsRb.isChecked = true + b.rethinkPlusDnsRb.isChecked = false + b.networkDnsRb.isChecked = false + b.customDnsRb.setChecked(true) } - - // connected to custom dns, update the dns details - b.customDnsRb.isChecked = true } private fun disableAllDns() { @@ -266,10 +286,6 @@ class DnsSettingsFragment : } } - private fun observeBraveMode() { - appConfig.getConnectedDnsObservable().observe(viewLifecycleOwner) { updateSelectedDns() } - } - private fun initClickListeners() { b.dcLocalBlocklistRl.setOnClickListener { openLocalBlocklist() } @@ -280,13 +296,6 @@ class DnsSettingsFragment : b.dcCheckUpdateSwitch.isChecked = !b.dcCheckUpdateSwitch.isChecked } - b.dcAlgSwitch.setOnCheckedChangeListener { _: CompoundButton, enabled: Boolean -> - enableAfterDelay(TimeUnit.SECONDS.toMillis(1), b.dcAlgSwitch) - persistentState.enableDnsAlg = enabled - } - - b.dcAlgRl.setOnClickListener { b.dcAlgSwitch.isChecked = !b.dcAlgSwitch.isChecked } - b.dcCheckUpdateSwitch.setOnCheckedChangeListener { _: CompoundButton, enabled: Boolean -> persistentState.periodicallyCheckBlocklistUpdate = enabled if (enabled) { @@ -301,6 +310,14 @@ class DnsSettingsFragment : } } + b.dcAlgSwitch.setOnCheckedChangeListener { _: CompoundButton, enabled: Boolean -> + enableAfterDelay(TimeUnit.SECONDS.toMillis(1), b.dcAlgSwitch) + persistentState.enableDnsAlg = enabled + updateSpiltDnsUi() + } + + b.dcAlgRl.setOnClickListener { b.dcAlgSwitch.isChecked = !b.dcAlgSwitch.isChecked } + b.dcFaviconRl.setOnClickListener { b.dcFaviconSwitch.isChecked = !b.dcFaviconSwitch.isChecked } @@ -320,17 +337,27 @@ class DnsSettingsFragment : persistentState.preventDnsLeaks = enabled } + b.rethinkPlusDnsRb.setOnCheckedChangeListener(null) b.rethinkPlusDnsRb.setOnClickListener { // rethink dns plus invokeRethinkActivity(ConfigureRethinkBasicActivity.FragmentLoader.DB_LIST) } + b.customDnsRb.setOnCheckedChangeListener(null) b.customDnsRb.setOnClickListener { // custom dns showCustomDns() } + b.networkDnsRb.setOnCheckedChangeListener(null) b.networkDnsRb.setOnClickListener { + if (isSystemDns()) { + io { + val sysDns = VpnController.getSystemDns() + uiCtx { showSystemDnsDialog(sysDns) } + } + return@setOnClickListener + } // network dns proxy setNetworkDns() } @@ -374,6 +401,61 @@ class DnsSettingsFragment : } } } + + b.dvBypassDnsBlockSwitch.setOnCheckedChangeListener { _, isChecked -> + persistentState.bypassBlockInDns = isChecked + } + + b.dvBypassDnsBlockRl.setOnClickListener { + b.dvBypassDnsBlockSwitch.isChecked = !b.dvBypassDnsBlockSwitch.isChecked + } + + b.dcSplitDnsSwitch.setOnCheckedChangeListener { _, isChecked -> + persistentState.splitDns = isChecked + } + + b.dcSplitDnsRl.setOnClickListener { + b.dcSplitDnsSwitch.isChecked = !b.dcSplitDnsSwitch.isChecked + } + + b.networkDnsInfo.setOnClickListener { + io { + val sysDns = VpnController.getSystemDns() + uiCtx { showSystemDnsDialog(sysDns) } + } + } + + b.dcUndelegatedDomainsRl.setOnClickListener { + b.dcUndelegatedDomainsSwitch.isChecked = !b.dcUndelegatedDomainsSwitch.isChecked + } + + b.dcUndelegatedDomainsSwitch.setOnCheckedChangeListener { _, isChecked -> + persistentState.useSystemDnsForUndelegatedDomains = isChecked + } + } + + private fun showSystemDnsDialog(dns: String) { + val builder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.network_dns) + .setMessage(dns) + .setCancelable(true) + .setPositiveButton(R.string.ada_noapp_dialog_positive) { di, _ -> + di.dismiss() + } + .setNeutralButton(requireContext().getString(R.string.dns_info_neutral)) { _: DialogInterface, _: Int -> + UIUtils.clipboardCopy( + requireContext(), + dns, + requireContext().getString(R.string.copy_clipboard_label) + ) + Utilities.showToastUiCentered( + requireContext(), + requireContext().getString(R.string.info_dialog_url_copy_toast_msg), + Toast.LENGTH_SHORT + ) + } + val dialog = builder.create() + dialog.show() } private fun initAnimation() { @@ -428,6 +510,10 @@ class DnsSettingsFragment : lifecycleScope.launch(Dispatchers.IO) { f() } } + private suspend fun uiCtx(f: suspend () -> Unit) { + withContext(Dispatchers.Main) { f() } + } + override fun onBtmSheetDismiss() { if (!isAdded) return diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/HomeScreenFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/HomeScreenFragment.kt index 909a047b2..6fe9f3913 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/HomeScreenFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/HomeScreenFragment.kt @@ -38,11 +38,13 @@ import android.os.SystemClock import android.provider.Settings import android.text.format.DateUtils import android.util.TypedValue +import android.view.LayoutInflater import android.view.View import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.fragment.app.Fragment @@ -125,6 +127,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + initializeValues() initializeClickListeners() observeVpnState() @@ -193,19 +196,57 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { } b.fhsSponsor.setOnClickListener { - val intent = Intent(Intent.ACTION_VIEW, RETHINKDNS_SPONSOR_LINK.toUri()) - startActivity(intent) + promptForAppSponsorship() + } + + b.fhsSponsorBottom.setOnClickListener { + promptForAppSponsorship() } b.fhsTitleRethink.setOnClickListener { - val intent = Intent(Intent.ACTION_VIEW, RETHINKDNS_SPONSOR_LINK.toUri()) - startActivity(intent) + promptForAppSponsorship() } // comment out the below code to disable the alerts card (v0.5.5b) // b.fhsCardAlertsLl.setOnClickListener { startActivity(ScreenType.ALERTS) } } + private fun promptForAppSponsorship() { + val installTime = requireContext().packageManager.getPackageInfo( + requireContext().packageName, + 0 + ).firstInstallTime + val timeDiff = System.currentTimeMillis() - installTime + // convert it to month + val days = (timeDiff / (1000 * 60 * 60 * 24)).toDouble() + val month = days / 30 + // multiply the month with 0.60$ + 0.20$ for every month + val amount = month * (0.60 + 0.20) + Logger.d(LOG_TAG_UI, "Sponsor: $installTime, days/month: $days/$month, amount: $amount") + val alertBuilder = MaterialAlertDialogBuilder(requireContext()) + val inflater = LayoutInflater.from(requireContext()) + val dialogView = inflater.inflate(R.layout.dialog_sponsor_info, null) + alertBuilder.setView(dialogView) + alertBuilder.setCancelable(true) + + val amountTxt = dialogView.findViewById(R.id.dialog_sponsor_info_amount) + val usageTxt = dialogView.findViewById(R.id.dialog_sponsor_info_usage) + val sponsorBtn = dialogView.findViewById(R.id.dialog_sponsor_info_sponsor) + + val dialog = alertBuilder.create() + + val msg = getString(R.string.sponser_dialog_usage_msg, days.toInt().toString(), "%.2f".format(amount)) + amountTxt.text = getString(R.string.two_argument_no_space, getString(R.string.symbol_dollar), "%.2f".format(amount)) + usageTxt.text = msg + + sponsorBtn.setOnClickListener { + val intent = Intent(Intent.ACTION_VIEW, RETHINKDNS_SPONSOR_LINK.toUri()) + startActivity(intent) + } + + dialog.show() + } + private fun handlePause() { if (!VpnController.hasTunnel()) { showToastUiCentered( @@ -406,8 +447,15 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { val status = VpnController.getProxyStatusById(proxyId) if (status != null) { // consider starting and up as active - if (status == Backend.TOK || status == Backend.TUP || status == Backend.TZZ) { - active++ + if (status == Backend.TOK) { + val stats = VpnController.getProxyStats(proxyId) + val lastOk = stats?.lastOK ?: 0 + val isUp = System.currentTimeMillis() - lastOk < 30 * DateUtils.SECOND_IN_MILLIS + if (isUp) { + active++ + } else { + failing++ + } } else { failing++ } @@ -494,30 +542,35 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { private fun observeDnsStates() { persistentState.median.observe(viewLifecycleOwner) { // show status as very fast, fast, slow, and very slow based on the latency - if (it in 0L..19L) { - val string = - getString( - R.string.ci_desc, - getString(R.string.lbl_very), - getString(R.string.lbl_fast) - ) - .replaceFirstChar(Char::titlecase) - b.fhsCardDnsLatency.text = string - } else if (it in 20L..50L) { - b.fhsCardDnsLatency.text = - getString(R.string.lbl_fast).replaceFirstChar(Char::titlecase) - } else if (it in 50L..100L) { - b.fhsCardDnsLatency.text = - getString(R.string.lbl_slow).replaceFirstChar(Char::titlecase) - } else { - val string = - getString( - R.string.ci_desc, - getString(R.string.lbl_very), - getString(R.string.lbl_slow) - ) - .replaceFirstChar(Char::titlecase) - b.fhsCardDnsLatency.text = string + when (it) { + in 0L..19L -> { + val string = + getString( + R.string.ci_desc, + getString(R.string.lbl_very), + getString(R.string.lbl_fast) + ) + .replaceFirstChar(Char::titlecase) + b.fhsCardDnsLatency.text = string + } + in 20L..50L -> { + b.fhsCardDnsLatency.text = + getString(R.string.lbl_fast).replaceFirstChar(Char::titlecase) + } + in 50L..100L -> { + b.fhsCardDnsLatency.text = + getString(R.string.lbl_slow).replaceFirstChar(Char::titlecase) + } + else -> { + val string = + getString( + R.string.ci_desc, + getString(R.string.lbl_very), + getString(R.string.lbl_slow) + ) + .replaceFirstChar(Char::titlecase) + b.fhsCardDnsLatency.text = string + } } b.fhsCardDnsLatency.isSelected = true @@ -526,6 +579,12 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { appConfig.getConnectedDnsObservable().observe(viewLifecycleOwner) { updateUiWithDnsStates(it) } + + VpnController.getRegionLiveData().observe(viewLifecycleOwner) { + if (it != null) { + b.fhsCardRegion.text = it + } + } } private fun updateUiWithDnsStates(dnsName: String) { @@ -542,6 +601,10 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { "${ProxyManager.ID_WG_BASE}${id}" } } else { + if (persistentState.splitDns && WireguardManager.isAdvancedWgActive()) { + dns += ", " + resources.getString(R.string.lbl_wireguard) + } + preferredId } @@ -554,7 +617,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { failing = false if (isAdded) { b.fhsCardDnsLatency.visibility = View.VISIBLE - b.fhsCardDnsFailure.visibility = View.GONE + b.fhsCardDnsFailure.visibility = View.INVISIBLE } return@ui } @@ -563,7 +626,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { failing = true } if (failing && isAdded) { - b.fhsCardDnsLatency.visibility = View.GONE + b.fhsCardDnsLatency.visibility = View.INVISIBLE b.fhsCardDnsFailure.visibility = View.VISIBLE b.fhsCardDnsFailure.text = getString(R.string.failed_using_default) } @@ -621,6 +684,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { private fun unobserveDnsStates() { persistentState.median.removeObservers(viewLifecycleOwner) appConfig.getConnectedDnsObservable().removeObservers(viewLifecycleOwner) + VpnController.getRegionLiveData().removeObservers(viewLifecycleOwner) } private fun observeUniversalStates() { @@ -919,6 +983,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { syncDnsStatus() handleLockdownModeIfNeeded() startTrafficStats() + b.fhsSponsorBottom.bringToFront() } private lateinit var trafficStatsTicker: Job @@ -926,15 +991,60 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { private fun startTrafficStats() { trafficStatsTicker = ui("trafficStatsTicker") { + var counter = 0 while (true) { - fetchTrafficStats() - kotlinx.coroutines.delay(1500L) + // make it as 3 options and add the protos + if (!isAdded) return@ui + + if (counter % 3 == 0) { + displayTrafficStatsRate() + } else if (counter % 3 == 1) { + displayTrafficStatsBW() + } else { + displayProtos() + } + // show protos + kotlinx.coroutines.delay(2500L) + counter++ } } } + private fun displayProtos() { + b.fhsInternetSpeed.visibility = View.VISIBLE + b.fhsInternetSpeedUnit.visibility = View.VISIBLE + b.fhsInternetSpeed.text = VpnController.protocols() + b.fhsInternetSpeedUnit.text = getString(R.string.lbl_protos) + } + + private fun displayTrafficStatsBW() { + val txRx = convertToCommonUnit(txRx.tx, txRx.rx) + + b.fhsInternetSpeed.visibility = View.VISIBLE + b.fhsInternetSpeedUnit.visibility = View.VISIBLE + b.fhsInternetSpeed.text = + getString( + R.string.two_argument_space, + getString( + R.string.two_argument_space, + txRx.first, + getString(R.string.symbol_black_up) + ), + getString( + R.string.two_argument_space, + txRx.second, + getString(R.string.symbol_black_down) + ) + ) + b.fhsInternetSpeedUnit.text = getCommonUnit(this.txRx.tx, this.txRx.rx) + } + private fun stopTrafficStats() { - trafficStatsTicker.cancel() + try { + trafficStatsTicker.cancel() + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "error stopping traffic stats ticker", e) + } } data class TxRx( @@ -945,7 +1055,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { private var txRx = TxRx() - private fun fetchTrafficStats() { + private fun displayTrafficStatsRate() { val curr = TxRx() if (txRx.time <= 0L) { txRx = curr @@ -960,12 +1070,10 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { b.fhsInternetSpeedUnit.visibility = View.GONE return } - val tx = curr.tx - txRx.tx val rx = curr.rx - txRx.rx txRx = curr - val txBytes = String.format("%.2f", ((tx / dur) / 1000.0)) - val rxBytes = String.format("%.2f", ((rx / dur) / 1000.0)) + val txRx = convertToCommonUnit(tx/dur, rx/dur) b.fhsInternetSpeed.visibility = View.VISIBLE b.fhsInternetSpeedUnit.visibility = View.VISIBLE b.fhsInternetSpeed.text = @@ -973,17 +1081,47 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { R.string.two_argument_space, getString( R.string.two_argument_space, - txBytes, + txRx.first, getString(R.string.symbol_black_up) ), getString( R.string.two_argument_space, - rxBytes, + txRx.second, getString(R.string.symbol_black_down) ) ) + b.fhsInternetSpeedUnit.text = getString(R.string.symbol_ps, getCommonUnit(tx/dur, rx/dur)) + } + + // TODO: Move this to a common utility class + private fun getCommonUnit(bytes1: Long, bytes2: Long): String { + val maxBytes = maxOf(bytes1, bytes2) + return when { + maxBytes >= 1024L * 1024L * 1024L * 1024L -> "TB" + maxBytes >= 1024L * 1024L * 1024L -> "GB" + maxBytes >= 1024L * 1024L -> "MB" + maxBytes >= 1024L -> "KB" + else -> "B" + } } + private fun convertToCommonUnit(bytes1: Long, bytes2: Long): Pair { + val unit = getCommonUnit(bytes1, bytes2) + val v = when (unit) { + "TB" -> Pair(bytesToTB(bytes1), bytesToTB(bytes2)) + "GB" -> Pair(bytesToGB(bytes1), bytesToGB(bytes2)) + "MB" -> Pair(bytesToMB(bytes1), bytesToMB(bytes2)) + "KB" -> Pair(bytesToKB(bytes1), bytesToKB(bytes2)) + else -> Pair(bytes1.toDouble(), bytes2.toDouble()) + } + return Pair(String.format(Locale.ROOT, "%.2f", v.first), String.format(Locale.ROOT, "%.2f", v.second)) + } + + private fun bytesToKB(bytes: Long): Double = bytes / 1024.0 + private fun bytesToMB(bytes: Long): Double = bytes / (1024.0 * 1024.0) + private fun bytesToGB(bytes: Long): Double = bytes / (1024.0 * 1024.0 * 1024.0) + private fun bytesToTB(bytes: Long): Double = bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0) + /** * Issue fix - https://github.com/celzero/rethink-app/issues/57 When the application * crashes/updates it goes into red waiting state. This causes confusion to the users also @@ -1138,7 +1276,7 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { } private fun stopVpnService() { - VpnController.stop(requireContext()) + VpnController.stop("home", requireContext()) } private fun startVpnService() { @@ -1412,20 +1550,28 @@ class HomeScreenFragment : Fragment(R.layout.fragment_home_screen) { private fun fetchTextColor(attr: Int): Int { val attributeFetch = - if (attr == R.color.accentGood) { - R.attr.accentGood - } else if (attr == R.color.accentBad) { - R.attr.accentBad - } else if (attr == R.color.primaryLightColorText) { - R.attr.primaryLightColorText - } else if (attr == R.color.secondaryText) { - R.attr.invertedPrimaryTextColor - } else if (attr == R.color.primaryText) { - R.attr.primaryTextColor - } else if (attr == R.color.primaryTextLight) { - R.attr.primaryTextColor - } else { - R.attr.accentGood + when (attr) { + R.color.accentGood -> { + R.attr.accentGood + } + R.color.accentBad -> { + R.attr.accentBad + } + R.color.primaryLightColorText -> { + R.attr.primaryLightColorText + } + R.color.secondaryText -> { + R.attr.invertedPrimaryTextColor + } + R.color.primaryText -> { + R.attr.primaryTextColor + } + R.color.primaryTextLight -> { + R.attr.primaryTextColor + } + else -> { + R.attr.accentGood + } } val typedValue = TypedValue() val a: TypedArray = diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/RethinkLogFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/RethinkLogFragment.kt index 43d232109..127c81492 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/RethinkLogFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/RethinkLogFragment.kt @@ -28,7 +28,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.celzero.bravedns.R import com.celzero.bravedns.adapter.RethinkLogAdapter import com.celzero.bravedns.database.RethinkLogRepository -import com.celzero.bravedns.databinding.ActivityConnectionTrackerBinding +import com.celzero.bravedns.databinding.FragmentConnectionTrackerBinding import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.UIUtils.formatToRelativeTime @@ -41,8 +41,8 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel class RethinkLogFragment : - Fragment(R.layout.activity_connection_tracker), SearchView.OnQueryTextListener { - private val b by viewBinding(ActivityConnectionTrackerBinding::bind) + Fragment(R.layout.fragment_connection_tracker), SearchView.OnQueryTextListener { + private val b by viewBinding(FragmentConnectionTrackerBinding::bind) private var layoutManager: RecyclerView.LayoutManager? = null private val viewModel: RethinkLogViewModel by viewModel() @@ -89,7 +89,7 @@ class RethinkLogFragment : val recyclerAdapter = RethinkLogAdapter(requireContext()) viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.rlogList.observe(viewLifecycleOwner) { it -> + viewModel.rlogList.observe(viewLifecycleOwner) { recyclerAdapter.submitData(lifecycle, it) } } diff --git a/app/src/full/java/com/celzero/bravedns/ui/fragment/SummaryStatisticsFragment.kt b/app/src/full/java/com/celzero/bravedns/ui/fragment/SummaryStatisticsFragment.kt index ea6ea3588..788d576d1 100644 --- a/app/src/full/java/com/celzero/bravedns/ui/fragment/SummaryStatisticsFragment.kt +++ b/app/src/full/java/com/celzero/bravedns/ui/fragment/SummaryStatisticsFragment.kt @@ -51,6 +51,9 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) private var isVpnActive: Boolean = false + private var contactedDomainsAdapter: SummaryStatisticsAdapter? = null + private var contactedCountriesAdapter: SummaryStatisticsAdapter? = null + enum class SummaryStatisticsType(val tid: Int) { MOST_CONNECTED_APPS(0), MOST_BLOCKED_APPS(1), @@ -114,8 +117,8 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) // get the tabbed view from the view model and set the toggle button // to the selected one. in case of fragment resume, the recycler view // and the toggle button to be in sync - val timeCategory = viewModel.getTimeCategory().value.toString() - val btn = b.toggleGroup.findViewWithTag(timeCategory) + val tc = viewModel.getTimeCategory().value.toString() + val btn = b.toggleGroup.findViewWithTag(tc) btn.isChecked = true handleTotalUsagesUi() } @@ -230,6 +233,8 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) uiCtx { viewModel.timeCategoryChanged(timeCategory, isAppBypassed) } handleTotalUsagesUi() } + contactedDomainsAdapter?.setTimeCategory(timeCategory) + contactedCountriesAdapter?.setTimeCategory(timeCategory) return@OnButtonCheckedListener } @@ -282,8 +287,7 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) requireContext(), persistentState, appConfig, - SummaryStatisticsType.MOST_CONNECTED_APPS - ) + SummaryStatisticsType.MOST_CONNECTED_APPS) viewModel.getAllowedAppNetworkActivity.observe(viewLifecycleOwner) { recyclerAdapter.submitData(viewLifecycleOwner.lifecycle, it) @@ -294,7 +298,11 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) if (it.append.endOfPaginationReached) { if (recyclerAdapter.itemCount < 1) { b.fssAppAllowedLl.visibility = View.GONE + } else { + b.fssAppAllowedLl.visibility = View.VISIBLE } + } else { + b.fssAppAllowedLl.visibility = View.VISIBLE } } @@ -325,7 +333,11 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) if (it.append.endOfPaginationReached) { if (recyclerAdapter.itemCount < 1) { b.fssAppBlockedLl.visibility = View.GONE + } else { + b.fssAppBlockedLl.visibility = View.VISIBLE } + } else { + b.fssAppBlockedLl.visibility = View.VISIBLE } } @@ -346,7 +358,7 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) val layoutManager = CustomLinearLayoutManager(requireContext()) b.fssContactedDomainRecyclerView.layoutManager = layoutManager - val recyclerAdapter = + contactedDomainsAdapter = SummaryStatisticsAdapter( requireContext(), persistentState, @@ -354,21 +366,29 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) SummaryStatisticsType.MOST_CONTACTED_DOMAINS ) + + val timeCategory = viewModel.getTimeCategory() + contactedDomainsAdapter?.setTimeCategory(timeCategory) + viewModel.getMostContactedDomains.observe(viewLifecycleOwner) { - recyclerAdapter.submitData(viewLifecycleOwner.lifecycle, it) + contactedDomainsAdapter?.submitData(viewLifecycleOwner.lifecycle, it) } - recyclerAdapter.addLoadStateListener { + contactedDomainsAdapter?.addLoadStateListener { if (it.append.endOfPaginationReached) { - if (recyclerAdapter.itemCount < 1) { + if (contactedDomainsAdapter!!.itemCount < 1) { b.fssDomainAllowedLl.visibility = View.GONE + } else { + b.fssDomainAllowedLl.visibility = View.VISIBLE } + } else { + b.fssDomainAllowedLl.visibility = View.VISIBLE } } val scale = resources.displayMetrics.density val pixels = ((RECYCLER_ITEM_VIEW_HEIGHT - 60) * scale + 0.5f) b.fssContactedDomainRecyclerView.minimumHeight = pixels.toInt() - b.fssContactedDomainRecyclerView.adapter = recyclerAdapter + b.fssContactedDomainRecyclerView.adapter = contactedDomainsAdapter } private fun showMostBlockedDomains() { @@ -397,7 +417,11 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) if (it.append.endOfPaginationReached) { if (recyclerAdapter.itemCount < 1) { b.fssDomainBlockedLl.visibility = View.GONE + } else { + b.fssDomainBlockedLl.visibility = View.VISIBLE } + } else { + b.fssDomainBlockedLl.visibility = View.VISIBLE } } val scale = resources.displayMetrics.density @@ -432,7 +456,11 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) if (it.append.endOfPaginationReached) { if (recyclerAdapter.itemCount < 1) { b.fssIpAllowedLl.visibility = View.GONE + } else { + b.fssIpAllowedLl.visibility = View.VISIBLE } + } else { + b.fssIpAllowedLl.visibility = View.VISIBLE } } val scale = resources.displayMetrics.density @@ -452,7 +480,7 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) val layoutManager = CustomLinearLayoutManager(requireContext()) b.fssContactedCountriesRecyclerView.layoutManager = layoutManager - val recyclerAdapter = + contactedCountriesAdapter = SummaryStatisticsAdapter( requireContext(), persistentState, @@ -460,20 +488,24 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) SummaryStatisticsType.MOST_CONTACTED_COUNTRIES ) viewModel.getMostContactedCountries.observe(viewLifecycleOwner) { - recyclerAdapter.submitData(viewLifecycleOwner.lifecycle, it) + contactedCountriesAdapter?.submitData(viewLifecycleOwner.lifecycle, it) } - recyclerAdapter.addLoadStateListener { + contactedCountriesAdapter?.addLoadStateListener { if (it.append.endOfPaginationReached) { - if (recyclerAdapter.itemCount < 1) { + if (contactedCountriesAdapter!!.itemCount < 1) { b.fssCountriesAllowedLl.visibility = View.GONE + } else { + b.fssCountriesAllowedLl.visibility = View.VISIBLE } + } else { + b.fssCountriesAllowedLl.visibility = View.VISIBLE } } val scale = resources.displayMetrics.density val pixels = ((RECYCLER_ITEM_VIEW_HEIGHT - 60) * scale + 0.5f) b.fssContactedCountriesRecyclerView.minimumHeight = pixels.toInt() - b.fssContactedCountriesRecyclerView.adapter = recyclerAdapter + b.fssContactedCountriesRecyclerView.adapter = contactedCountriesAdapter } private fun showMostBlockedIps() { @@ -502,7 +534,11 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) if (it.append.endOfPaginationReached) { if (recyclerAdapter.itemCount < 1) { b.fssIpBlockedLl.visibility = View.GONE + } else { + b.fssIpBlockedLl.visibility = View.VISIBLE } + } else { + b.fssIpBlockedLl.visibility = View.VISIBLE } } val scale = resources.displayMetrics.density @@ -518,7 +554,7 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) return } - b.fssContactedCountriesRecyclerView.setHasFixedSize(true) + b.fssCountriesBlockedRecyclerView.setHasFixedSize(true) val layoutManager = CustomLinearLayoutManager(requireContext()) b.fssCountriesBlockedRecyclerView.layoutManager = layoutManager @@ -527,7 +563,7 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) requireContext(), persistentState, appConfig, - SummaryStatisticsType.MOST_CONTACTED_COUNTRIES + SummaryStatisticsType.MOST_BLOCKED_COUNTRIES ) viewModel.getMostBlockedCountries.observe(viewLifecycleOwner) { recyclerAdapter.submitData(viewLifecycleOwner.lifecycle, it) @@ -537,7 +573,11 @@ class SummaryStatisticsFragment : Fragment(R.layout.fragment_summary_statistics) if (it.append.endOfPaginationReached) { if (recyclerAdapter.itemCount < 1) { b.fssCountriesBlockedLl.visibility = View.GONE + } else { + b.fssCountriesBlockedLl.visibility = View.VISIBLE } + } else { + b.fssCountriesBlockedLl.visibility = View.VISIBLE } } val scale = resources.displayMetrics.density diff --git a/app/src/full/java/com/celzero/bravedns/util/BackgroundAccessibilityService.kt b/app/src/full/java/com/celzero/bravedns/util/BackgroundAccessibilityService.kt index ca83ac169..70c6dbe53 100644 --- a/app/src/full/java/com/celzero/bravedns/util/BackgroundAccessibilityService.kt +++ b/app/src/full/java/com/celzero/bravedns/util/BackgroundAccessibilityService.kt @@ -16,29 +16,284 @@ package com.celzero.bravedns.util import Logger +import Logger.LOG_TAG_APP_OPS import Logger.LOG_TAG_FIREWALL +import android.Manifest import android.accessibilityservice.AccessibilityService +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.PixelFormat +import android.hardware.camera2.CameraManager +import android.media.AudioManager +import android.media.AudioRecordingConfiguration +import android.os.Build import android.text.TextUtils +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager import android.view.accessibility.AccessibilityEvent +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.celzero.bravedns.R +import com.celzero.bravedns.databinding.MicCamAccessIndicatorBinding import com.celzero.bravedns.service.FirewallManager import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.VpnController +import com.celzero.bravedns.ui.HomeScreenActivity +import com.celzero.bravedns.util.Utilities.isAtleastN +import com.celzero.bravedns.util.Utilities.isAtleastP import com.celzero.bravedns.util.Utilities.isAtleastT import org.koin.android.ext.android.inject import org.koin.core.component.KoinComponent +// cam and mic access is still not working as expected, need to test it +// commented out the ui code for now, will enable it once the feature is working +// for cam and mic access ref: github.com/NitishGadangi/Privacy-Indicator-App/blob/master/app/src/main/java/com/nitish/privacyindicator +// see: developer.android.com/guide/topics/media/camera#kotlin +// see: developer.android.com/guide/topics/media/audio-capture class BackgroundAccessibilityService : AccessibilityService(), KoinComponent { private val persistentState by inject() + private lateinit var windowManager: WindowManager + private lateinit var b: MicCamAccessIndicatorBinding + private lateinit var lp: WindowManager.LayoutParams + + private var cameraManager: CameraManager? = null + private var audioManager: AudioManager? = null + private var micCallback: AudioManager.AudioRecordingCallback? = null + private var cameraCallback: CameraManager.AvailabilityCallback? = null + + private var cameraOn = false + private var micOn = false + private var notifManager: NotificationManagerCompat? = null + private var notifBuilder: NotificationCompat.Builder? = null + private var possibleUid: Int? = null + private var possibleAppName: String? = null + private val notificationID = 7897 + + companion object { + private const val NOTIF_CHANNEL_ID = "MIC_CAM_ACCESS" + } + + override fun onServiceConnected() { + if (isAtleastN() && persistentState.micCamAccess) { + overlay() + callBacks() + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun callBacks() { + if (!persistentState.micCamAccess) return + + try { + if (cameraManager == null) cameraManager = + getSystemService(CAMERA_SERVICE) as CameraManager + cameraManager!!.registerAvailabilityCallback(getCameraCallback(), null) + + if (audioManager == null) audioManager = getSystemService(AUDIO_SERVICE) as AudioManager + audioManager!!.registerAudioRecordingCallback(getMicCallback(), null) + } catch (e: Exception) { + Logger.e(LOG_TAG_FIREWALL, "Error in registering callbacks: ${e.message}") + } + } + + private fun getCameraCallback(): CameraManager.AvailabilityCallback { + cameraCallback = + object : CameraManager.AvailabilityCallback() { + override fun onCameraAvailable(cameraId: String) { + super.onCameraAvailable(cameraId) + cameraOn = false + hideCam() + dismissNotification() + } + + override fun onCameraUnavailable(cameraId: String) { + super.onCameraUnavailable(cameraId) + cameraOn = true + showCam() + showNotification() + } + } + return cameraCallback as CameraManager.AvailabilityCallback + } + + private fun getMicCallback(): AudioManager.AudioRecordingCallback { + micCallback = + @RequiresApi(Build.VERSION_CODES.N) + object : AudioManager.AudioRecordingCallback() { + override fun onRecordingConfigChanged(configs: List) { + if (configs.isNotEmpty()) { + micOn = true + showMic() + showNotification() + } else { + micOn = false + hideMic() + dismissNotification() + } + } + } + return micCallback as AudioManager.AudioRecordingCallback + } + + private fun overlay() { + windowManager = getSystemService(WINDOW_SERVICE) as WindowManager + lp = WindowManager.LayoutParams() + lp.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY + lp.format = PixelFormat.TRANSLUCENT + lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + lp.width = WindowManager.LayoutParams.WRAP_CONTENT + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + lp.gravity = layoutGravity + b = MicCamAccessIndicatorBinding.inflate(LayoutInflater.from(this)) + windowManager.addView(b.root, lp) + } + + private fun showMic() { + Logger.e(LOG_TAG_APP_OPS, "Mic is being used: ${persistentState.micCamAccess}") + if (persistentState.micCamAccess) { + updateIndicatorProperties() + b.ivMic.visibility = View.VISIBLE + } + } + + private fun hideMic() { + b.ivMic.visibility = View.GONE + } + + private fun showCam() { + Logger.e(LOG_TAG_APP_OPS, "Camera is being used: ${persistentState.micCamAccess}") + if (persistentState.micCamAccess) { + updateIndicatorProperties() + b.ivCam.visibility = View.VISIBLE + } + } + + private fun hideCam() { + b.ivCam.visibility = View.GONE + } + + + private val layoutGravity: Int + get() = Gravity.TOP or Gravity.END + + private fun updateIndicatorProperties() { + updateLayoutGravity() + } + + private fun updateLayoutGravity() { + lp.gravity = layoutGravity + windowManager.updateViewLayout(b.root, lp) + } + + private fun setupNotification() { + createNotificationChannel() + notifBuilder = + NotificationCompat.Builder(applicationContext, NOTIF_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_icon) + .setContentTitle(notificationTitle) + .setContentText(notificationDescription) + .setContentIntent(getPendingIntent()) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + notifManager = NotificationManagerCompat.from(applicationContext) + } + + private val notificationTitle: String + get() { + if (cameraOn && micOn) return "Your Camera and Mic is ON" + if (cameraOn) return "Your Camera is ON" + return if (micOn) "Your MIC is ON" else "Your Camera or Mic is ON" + } + + private val notificationDescription: String + get() { + if (cameraOn && micOn) + return "A third-party app($possibleAppName) is using your Camera and Microphone" + if (cameraOn) return "A third-party app($possibleAppName) is using your Camera" + return if (micOn) "A third-party app($possibleAppName) is using your Microphone" + else "A third-party app($possibleAppName) is using your Camera or Microphone" + } + + private fun showNotification() { + setupNotification() + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED) { + // notification permission request and handling are done in the HomeScreenFragment + // so no need to handle it here + return + } + if (notifManager != null) + notifManager!!.notify(notificationID, notifBuilder!!.build()) + } + + private fun dismissNotification() { + if (cameraOn || micOn) { + showNotification() + } else { + if (notifManager != null) notifManager!!.cancel(notificationID) + } + } + + private fun getPendingIntent(): PendingIntent { + val intent = Intent(applicationContext, HomeScreenActivity::class.java) + return PendingIntent.getActivity( + applicationContext, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + private fun createNotificationChannel() { + val notificationChannel = "Notifications for Camera and Mic access" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val importance = NotificationManager.IMPORTANCE_LOW + val channel = + NotificationChannel(NOTIF_CHANNEL_ID, notificationChannel, importance) + val description = "Notification for Camera and Mic access" + channel.description = description + channel.lightColor = Color.RED + val notificationManager = + applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } override fun onInterrupt() { Logger.w(LOG_TAG_FIREWALL, "BackgroundAccessibilityService Interrupted") } - override fun onRebind(intent: Intent?) { - super.onRebind(intent) + private fun unRegisterCameraCallBack() { + try { + if (cameraManager != null && cameraCallback != null) { + cameraManager!!.unregisterAvailabilityCallback(cameraCallback!!) + } + } catch (e: Exception) { + Logger.e(LOG_TAG_FIREWALL, "Error in unregistering camera callback: ${e.message}") + } + } + + private fun unRegisterMicCallback() { + try { + if (isAtleastN()) { + if (audioManager != null && micCallback != null) { + audioManager!!.unregisterAudioRecordingCallback(micCallback!!) + } + } + } catch (e: Exception) { + Logger.e(LOG_TAG_FIREWALL, "Error in unregistering mic callback: ${e.message}") + } + } + + override fun onDestroy() { + unRegisterCameraCallBack() + unRegisterMicCallback() + super.onDestroy() } override fun onAccessibilityEvent(event: AccessibilityEvent) { @@ -112,14 +367,14 @@ class BackgroundAccessibilityService : AccessibilityService(), KoinComponent { // no need ot handle the events when the vpn is not running if (!VpnController.isOn()) return - if (!persistentState.getBlockAppWhenBackground()) return + if (!persistentState.getBlockAppWhenBackground() && !persistentState.micCamAccess) return val latestTrackedPackage = getEventPackageName(event) if (latestTrackedPackage.isNullOrEmpty()) return val hasContentChanged = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + if (isAtleastP()) { event.eventType == AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED || event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED } else { @@ -127,14 +382,17 @@ class BackgroundAccessibilityService : AccessibilityService(), KoinComponent { } Logger.d( LOG_TAG_FIREWALL, - "onAccessibilityEvent: ${event.packageName}, ${event.eventType}, $hasContentChanged" - ) + "onAccessibilityEvent: ${event.packageName}, ${event.eventType}, $hasContentChanged") if (!hasContentChanged) return // If the package received is Rethink, do nothing. if (event.packageName == this.packageName) return + possibleUid = getEventUid(latestTrackedPackage) ?: return + + possibleAppName = Utilities.getPackageInfoForUid(this, possibleUid!!)?.firstOrNull() + // https://stackoverflow.com/a/27642535 // top window is launcher? try revoke queued up permissions // FIXME: Figure out a fool-proof way to determine is launcher visible @@ -142,7 +400,7 @@ class BackgroundAccessibilityService : AccessibilityService(), KoinComponent { if (isPackageLauncher(latestTrackedPackage)) { FirewallManager.untrackForegroundApps() } else { - val uid = getEventUid(latestTrackedPackage) ?: return + val uid = possibleUid ?: return FirewallManager.trackForegroundApp(uid) } } @@ -183,9 +441,7 @@ class BackgroundAccessibilityService : AccessibilityService(), KoinComponent { .resolveActivity( intent, PackageManager.ResolveInfoFlags.of( - PackageManager.MATCH_DEFAULT_ONLY.toLong() - ) - ) + PackageManager.MATCH_DEFAULT_ONLY.toLong())) ?.activityInfo ?.packageName } else { diff --git a/app/src/full/java/com/celzero/bravedns/util/UIUtils.kt b/app/src/full/java/com/celzero/bravedns/util/UIUtils.kt index 956a9169d..bec0c6da2 100644 --- a/app/src/full/java/com/celzero/bravedns/util/UIUtils.kt +++ b/app/src/full/java/com/celzero/bravedns/util/UIUtils.kt @@ -107,6 +107,15 @@ object UIUtils { } } + enum class ProxyStatus(val id: Long) { + TOK(Backend.TOK), + TUP(Backend.TUP), + TZZ(Backend.TZZ), + TNT(Backend.TNT), + TKO(Backend.TKO), + END(Backend.END) + } + fun formatToRelativeTime(context: Context, timestamp: Long): String { val now = System.currentTimeMillis() return if (DateUtils.isToday(timestamp)) { @@ -254,44 +263,64 @@ object UIUtils { fun fetchToggleBtnColors(context: Context, attr: Int): Int { val attributeFetch = - if (attr == R.color.firewallNoRuleToggleBtnTxt) { - R.attr.firewallNoRuleToggleBtnTxt - } else if (attr == R.color.firewallNoRuleToggleBtnBg) { - R.attr.firewallNoRuleToggleBtnBg - } else if (attr == R.color.firewallBlockToggleBtnTxt) { - R.attr.firewallBlockToggleBtnTxt - } else if (attr == R.color.firewallBlockToggleBtnBg) { - R.attr.firewallBlockToggleBtnBg - } else if (attr == R.color.firewallWhiteListToggleBtnTxt) { - R.attr.firewallWhiteListToggleBtnTxt - } else if (attr == R.color.firewallWhiteListToggleBtnBg) { - R.attr.firewallWhiteListToggleBtnBg - } else if (attr == R.color.firewallExcludeToggleBtnBg) { - R.attr.firewallExcludeToggleBtnBg - } else if (attr == R.color.firewallExcludeToggleBtnTxt) { - R.attr.firewallExcludeToggleBtnTxt - } else if (attr == R.color.defaultToggleBtnBg) { - R.attr.defaultToggleBtnBg - } else if (attr == R.color.defaultToggleBtnTxt) { - R.attr.defaultToggleBtnTxt - } else if (attr == R.color.accentGood) { - R.attr.accentGood - } else if (attr == R.color.accentBad) { - R.attr.accentBad - } else if (attr == R.color.chipBgNeutral) { - R.attr.chipBgColorNeutral - } else if (attr == R.color.chipBgNegative) { - R.attr.chipBgColorNegative - } else if (attr == R.color.chipBgPositive) { - R.attr.chipBgColorPositive - } else if (attr == R.color.chipTextNeutral) { - R.attr.chipTextNeutral - } else if (attr == R.color.chipTextNegative) { - R.attr.chipTextNegative - } else if (attr == R.color.chipTextPositive) { - R.attr.chipTextPositive - } else { - R.attr.chipBgColorPositive + when (attr) { + R.color.firewallNoRuleToggleBtnTxt -> { + R.attr.firewallNoRuleToggleBtnTxt + } + R.color.firewallNoRuleToggleBtnBg -> { + R.attr.firewallNoRuleToggleBtnBg + } + R.color.firewallBlockToggleBtnTxt -> { + R.attr.firewallBlockToggleBtnTxt + } + R.color.firewallBlockToggleBtnBg -> { + R.attr.firewallBlockToggleBtnBg + } + R.color.firewallWhiteListToggleBtnTxt -> { + R.attr.firewallWhiteListToggleBtnTxt + } + R.color.firewallWhiteListToggleBtnBg -> { + R.attr.firewallWhiteListToggleBtnBg + } + R.color.firewallExcludeToggleBtnBg -> { + R.attr.firewallExcludeToggleBtnBg + } + R.color.firewallExcludeToggleBtnTxt -> { + R.attr.firewallExcludeToggleBtnTxt + } + R.color.defaultToggleBtnBg -> { + R.attr.defaultToggleBtnBg + } + R.color.defaultToggleBtnTxt -> { + R.attr.defaultToggleBtnTxt + } + R.color.accentGood -> { + R.attr.accentGood + } + R.color.accentBad -> { + R.attr.accentBad + } + R.color.chipBgNeutral -> { + R.attr.chipBgColorNeutral + } + R.color.chipBgNegative -> { + R.attr.chipBgColorNegative + } + R.color.chipBgPositive -> { + R.attr.chipBgColorPositive + } + R.color.chipTextNeutral -> { + R.attr.chipTextNeutral + } + R.color.chipTextNegative -> { + R.attr.chipTextNegative + } + R.color.chipTextPositive -> { + R.attr.chipTextPositive + } + else -> { + R.attr.chipBgColorPositive + } } return fetchColor(context, attributeFetch) } @@ -301,7 +330,7 @@ object UIUtils { if (isDgaDomain(dnsLog.queryStr)) return - Logger.d(Logger.LOG_TAG_UI, "Glide - fetchFavIcon():${dnsLog.queryStr}") + Logger.d(LOG_TAG_UI, "Glide - fetchFavIcon():${dnsLog.queryStr}") // fetch fav icon in background using glide FavIconDownloader(context, dnsLog.queryStr).run() @@ -632,4 +661,23 @@ object UIUtils { return result.toString().trim() } + + fun formatNetStat(stat: backend.NetStat?): String { + val ip = stat?.ip()?.toString() + val udp = stat?.udp()?.toString() + val tcp = stat?.tcp()?.toString() + val fwd = stat?.fwd()?.toString() + val icmp = stat?.icmp()?.toString() + val nic = stat?.nic()?.toString() + val rdnsInfo = stat?.rdnsinfo()?.toString() + val nicInfo = stat?.nicinfo()?.toString() + val go = stat?.go()?.toString() + + var stats = nic + nicInfo + fwd + ip + icmp + tcp + udp + rdnsInfo + go + stats = stats.replace("{", "\n") + stats = stats.replace("}", "\n\n") + stats = stats.replace(",", "\n") + + return stats + } } diff --git a/app/src/full/java/com/celzero/bravedns/viewmodel/AppConnectionsViewModel.kt b/app/src/full/java/com/celzero/bravedns/viewmodel/AppConnectionsViewModel.kt index 72deb940f..109544378 100644 --- a/app/src/full/java/com/celzero/bravedns/viewmodel/AppConnectionsViewModel.kt +++ b/app/src/full/java/com/celzero/bravedns/viewmodel/AppConnectionsViewModel.kt @@ -27,9 +27,10 @@ import androidx.paging.cachedIn import androidx.paging.liveData import com.celzero.bravedns.data.AppConnection import com.celzero.bravedns.database.ConnectionTrackerDAO +import com.celzero.bravedns.database.RethinkLogDao import com.celzero.bravedns.util.Constants -class AppConnectionsViewModel(private val nwlogDao: ConnectionTrackerDAO) : ViewModel() { +class AppConnectionsViewModel(private val nwlogDao: ConnectionTrackerDAO, private val rinrDao: RethinkLogDao) : ViewModel() { private var ipFilter: MutableLiveData = MutableLiveData() private var domainFilter: MutableLiveData = MutableLiveData() private var uid: Int = Constants.INVALID_UID @@ -99,7 +100,34 @@ class AppConnectionsViewModel(private val nwlogDao: ConnectionTrackerDAO) : View } val appIpLogs = ipFilter.switchMap { input -> fetchIpLogs(uid, input) } - val appDomainLogs = domainFilter.switchMap { input -> fetchAppDomainLogs(uid, input) } + val appDomainLogs = domainFilter.switchMap { + input -> fetchAppDomainLogs(uid, input) + } + + val rinrIpLogs = ipFilter.switchMap { input -> fetchRinrIpLogs(input) } + val rinrDomainLogs = domainFilter.switchMap { input -> fetchRinrDomainLogs(input) } + + private fun fetchRinrIpLogs(input: String): LiveData> { + val to = getStartTime() + return if (input.isEmpty()) { + Pager(pagingConfig) { rinrDao.getIpLogs(to) } + } else { + Pager(pagingConfig) { rinrDao.getIpLogsFiltered(to, "%$input%") } + } + .liveData + .cachedIn(viewModelScope) + } + + private fun fetchRinrDomainLogs(input: String): LiveData> { + val to = getStartTime() + return if (input.isEmpty()) { + Pager(pagingConfig) { rinrDao.getDomainLogs(to) } + } else { + Pager(pagingConfig) { rinrDao.getDomainLogsFiltered(to, "%$input%") } + } + .liveData + .cachedIn(viewModelScope) + } private fun fetchIpLogs(uid: Int, input: String): LiveData> { val to = getStartTime() @@ -123,25 +151,49 @@ class AppConnectionsViewModel(private val nwlogDao: ConnectionTrackerDAO) : View .cachedIn(viewModelScope) } + fun deleteLogs(uid: Int) { + // delete based on the time category + when (timeCategory) { + TimeCategory.ONE_HOUR -> { + nwlogDao.clearLogsByTime(uid, System.currentTimeMillis() - ONE_HOUR_MILLIS) + } + + TimeCategory.TWENTY_FOUR_HOUR -> { + nwlogDao.clearLogsByTime(uid, System.currentTimeMillis() - ONE_DAY_MILLIS) + } + + TimeCategory.SEVEN_DAYS -> { + nwlogDao.clearLogsByUid(uid) // similar to clearing logs for uid + } + } + } + private fun getStartTime(): Long { return startTime.value ?: (System.currentTimeMillis() - ONE_HOUR_MILLIS) } - fun getConnectionsCount(uid: Int): LiveData { - return nwlogDao.getAppConnectionsCount(uid) + fun getDomainLogsLimited(uid: Int): LiveData> { + val to = System.currentTimeMillis() - ONE_WEEK_MILLIS + return Pager(pagingConfig) { nwlogDao.getAppDomainLogsLimited(uid, to) } + .liveData + .cachedIn(viewModelScope) } - fun getAppDomainConnectionsCount(uid: Int): LiveData { - return nwlogDao.getAppDomainConnectionsCount(uid) + fun getRethinkDomainLogsLimited(): LiveData> { + val to = System.currentTimeMillis() - ONE_WEEK_MILLIS + return Pager(pagingConfig) { rinrDao.getDomainLogsLimited(to) } + .liveData + .cachedIn(viewModelScope) } - fun getDomainLogsLimited(uid: Int): LiveData> { + fun getRethinkIpLogsLimited(): LiveData> { val to = System.currentTimeMillis() - ONE_WEEK_MILLIS - return Pager(pagingConfig) { nwlogDao.getAppDomainLogsLimited(uid, to) } + return Pager(pagingConfig) { rinrDao.getIpLogsLimited(to) } .liveData .cachedIn(viewModelScope) } + fun getIpLogsLimited(uid: Int): LiveData> { val to = System.currentTimeMillis() - ONE_WEEK_MILLIS return Pager(pagingConfig) { nwlogDao.getAppIpLogsLimited(uid, to) } diff --git a/app/src/full/java/com/celzero/bravedns/viewmodel/AppInfoViewModel.kt b/app/src/full/java/com/celzero/bravedns/viewmodel/AppInfoViewModel.kt index c3edbb05d..b85c95530 100644 --- a/app/src/full/java/com/celzero/bravedns/viewmodel/AppInfoViewModel.kt +++ b/app/src/full/java/com/celzero/bravedns/viewmodel/AppInfoViewModel.kt @@ -56,13 +56,24 @@ class AppInfoViewModel(private val appInfoDAO: AppInfoDAO) : ViewModel() { } } + private fun getBypassProxyFilter(): Set { + val filter = firewallFilter.getFilter() + val bypassFilter = setOf(2, 7) + if (filter == bypassFilter) { + return setOf(1) + } + return setOf() // empty set (as query uses or condition) + } + private fun allApps(searchString: String): LiveData> { + val includeProxyBypass = getBypassProxyFilter() return if (category.isEmpty()) { Pager(PagingConfig(Constants.LIVEDATA_PAGE_SIZE)) { appInfoDAO.getAppInfos( "%$searchString%", firewallFilter.getFilter(), - firewallFilter.getConnectionStatusFilter() + firewallFilter.getConnectionStatusFilter(), + includeProxyBypass ) } .liveData @@ -73,7 +84,8 @@ class AppInfoViewModel(private val appInfoDAO: AppInfoDAO) : ViewModel() { "%$searchString%", category, firewallFilter.getFilter(), - firewallFilter.getConnectionStatusFilter() + firewallFilter.getConnectionStatusFilter(), + includeProxyBypass ) } .liveData @@ -82,12 +94,14 @@ class AppInfoViewModel(private val appInfoDAO: AppInfoDAO) : ViewModel() { } private fun installedApps(search: String): LiveData> { + val includeProxyBypass = getBypassProxyFilter() return if (category.isEmpty()) { Pager(PagingConfig(Constants.LIVEDATA_PAGE_SIZE)) { appInfoDAO.getInstalledApps( "%$search%", firewallFilter.getFilter(), - firewallFilter.getConnectionStatusFilter() + firewallFilter.getConnectionStatusFilter(), + includeProxyBypass ) } .liveData @@ -98,7 +112,8 @@ class AppInfoViewModel(private val appInfoDAO: AppInfoDAO) : ViewModel() { "%$search%", category, firewallFilter.getFilter(), - firewallFilter.getConnectionStatusFilter() + firewallFilter.getConnectionStatusFilter(), + includeProxyBypass ) } .liveData @@ -107,12 +122,14 @@ class AppInfoViewModel(private val appInfoDAO: AppInfoDAO) : ViewModel() { } private fun systemApps(search: String): LiveData> { + val includeProxyBypass = getBypassProxyFilter() return if (category.isEmpty()) { Pager(PagingConfig(Constants.LIVEDATA_PAGE_SIZE)) { appInfoDAO.getSystemApps( "%$search%", firewallFilter.getFilter(), - firewallFilter.getConnectionStatusFilter() + firewallFilter.getConnectionStatusFilter(), + includeProxyBypass ) } .liveData @@ -123,7 +140,8 @@ class AppInfoViewModel(private val appInfoDAO: AppInfoDAO) : ViewModel() { "%$search%", category, firewallFilter.getFilter(), - firewallFilter.getConnectionStatusFilter() + firewallFilter.getConnectionStatusFilter(), + includeProxyBypass ) } .liveData diff --git a/app/src/full/java/com/celzero/bravedns/viewmodel/ConsoleLogViewModel.kt b/app/src/full/java/com/celzero/bravedns/viewmodel/ConsoleLogViewModel.kt new file mode 100644 index 000000000..37ca94ebf --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/viewmodel/ConsoleLogViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.liveData +import com.celzero.bravedns.database.ConsoleLog +import com.celzero.bravedns.database.ConsoleLogDAO +import com.celzero.bravedns.util.Constants + +class ConsoleLogViewModel(private val dao: ConsoleLogDAO) : ViewModel() { + private var filter: MutableLiveData = MutableLiveData() + + init { + filter.postValue("") + } + + val logs = filter.switchMap { input: String -> getLogs(input) } + + private fun getLogs(filter: String): LiveData> { + // filter is unused for now + return Pager(PagingConfig(Constants.LIVEDATA_PAGE_SIZE)) { dao.getLogs() } + .liveData + .cachedIn(viewModelScope) + } + + suspend fun sinceTime(): Long { + return dao.sinceTime() + } +} diff --git a/app/src/full/java/com/celzero/bravedns/viewmodel/DomainConnectionsViewModel.kt b/app/src/full/java/com/celzero/bravedns/viewmodel/DomainConnectionsViewModel.kt new file mode 100644 index 000000000..cae2807ca --- /dev/null +++ b/app/src/full/java/com/celzero/bravedns/viewmodel/DomainConnectionsViewModel.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.liveData +import com.celzero.bravedns.database.ConnectionTrackerDAO +import com.celzero.bravedns.util.Constants + +class DomainConnectionsViewModel(private val connectionTrackerDAO: ConnectionTrackerDAO) : ViewModel() { + private var domains: MutableLiveData = MutableLiveData() + private var flag: MutableLiveData = MutableLiveData() + private var timeCategory: TimeCategory = TimeCategory.ONE_HOUR + private var startTime: MutableLiveData = MutableLiveData() + + companion object { + private const val ONE_HOUR_MILLIS = 1 * 60 * 60 * 1000L + private const val ONE_DAY_MILLIS = 24 * ONE_HOUR_MILLIS + private const val ONE_WEEK_MILLIS = 7 * ONE_DAY_MILLIS + } + + enum class TimeCategory(val value: Int) { + ONE_HOUR(0), + TWENTY_FOUR_HOUR(1), + SEVEN_DAYS(2); + + companion object { + fun fromValue(value: Int) = entries.firstOrNull { it.value == value } + } + } + + init { + // set from and to time to current and 1 hr before + startTime.value = System.currentTimeMillis() - ONE_HOUR_MILLIS + domains.postValue("") + } + + fun setDomain(domain: String) { + domains.postValue(domain) + } + + fun setFlag(flag: String) { + this.flag.postValue(flag) + } + + fun timeCategoryChanged(tc: TimeCategory) { + timeCategory = tc + when (tc) { + TimeCategory.ONE_HOUR -> { + startTime.value = System.currentTimeMillis() - ONE_HOUR_MILLIS + } + TimeCategory.TWENTY_FOUR_HOUR -> { + startTime.value = System.currentTimeMillis() - ONE_DAY_MILLIS + } + TimeCategory.SEVEN_DAYS -> { + startTime.value = System.currentTimeMillis() - ONE_WEEK_MILLIS + } + } + } + + val domainConnectionList = domains.switchMap { input -> + fetchDomainConnections(input) + } + + val flagConnectionList = flag.switchMap { input -> + fetchFlagConnections(input) + } + + private fun fetchDomainConnections(input: String) = + Pager(PagingConfig(pageSize = Constants.LIVEDATA_PAGE_SIZE)) { + connectionTrackerDAO.getDomainConnections(input, startTime.value!!) + }.liveData.cachedIn(viewModelScope) + + private fun fetchFlagConnections(input: String) = + Pager(PagingConfig(pageSize = Constants.LIVEDATA_PAGE_SIZE)) { + connectionTrackerDAO.getFlagConnections(input, startTime.value!!) + }.liveData.cachedIn(viewModelScope) +} diff --git a/app/src/full/java/com/celzero/bravedns/viewmodel/ViewModelModule.kt b/app/src/full/java/com/celzero/bravedns/viewmodel/ViewModelModule.kt index a1caedf18..867acd7ae 100644 --- a/app/src/full/java/com/celzero/bravedns/viewmodel/ViewModelModule.kt +++ b/app/src/full/java/com/celzero/bravedns/viewmodel/ViewModelModule.kt @@ -33,7 +33,7 @@ object ViewModelModule { viewModel { AppCustomIpViewModel(get()) } viewModel { RethinkRemoteFileTagViewModel(get()) } viewModel { RethinkLocalFileTagViewModel(get()) } - viewModel { AppConnectionsViewModel(get()) } + viewModel { AppConnectionsViewModel(get(), get()) } viewModel { SummaryStatisticsViewModel(get(), get(), get()) } viewModel { DetailedStatisticsViewModel(get(), get(), get()) } viewModel { LocalBlocklistPacksMapViewModel(get()) } @@ -44,6 +44,8 @@ object ViewModelModule { viewModel { ODoHEndpointViewModel(get()) } viewModel { RethinkLogViewModel(get()) } viewModel { AlertsViewModel(get(), get()) } + viewModel { ConsoleLogViewModel(get()) } + viewModel { DomainConnectionsViewModel(get()) } } val modules = listOf(viewModelModule) diff --git a/app/src/full/res/layout/activity_advanced_setting.xml b/app/src/full/res/layout/activity_advanced_setting.xml new file mode 100644 index 000000000..f2bf86e1c --- /dev/null +++ b/app/src/full/res/layout/activity_advanced_setting.xml @@ -0,0 +1,586 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/full/res/layout/activity_app_details.xml b/app/src/full/res/layout/activity_app_details.xml index d0ef2fb80..abbbf8948 100644 --- a/app/src/full/res/layout/activity_app_details.xml +++ b/app/src/full/res/layout/activity_app_details.xml @@ -38,7 +38,7 @@ android:layout_toEndOf="@id/aad_app_detail_icon" android:orientation="vertical"> - - + + - @@ -78,7 +92,7 @@ android:layout_marginEnd="5dp" android:minWidth="24dp" android:minHeight="24dp" - android:src="@drawable/brave_mode_info" /> + android:src="@drawable/ic_info_white" /> @@ -101,7 +115,7 @@ android:layout_marginEnd="5dp" android:orientation="vertical" android:paddingTop="5dp" - android:paddingBottom="5dp"> + android:paddingBottom="15dp"> - - - - - - - - - + + + - - - - @@ -214,7 +217,7 @@ - + app:fastScrollVerticalTrackDrawable="@drawable/fast_scroll_line_drawable" + app:fastScrollAutoHide="true" + app:fastScrollAutoHideDelay="1500" + app:fastScrollEnableThumbInactiveColor="true" + app:fastScrollThumbColor="?attr/chipColorBgNormal" + app:fastScrollThumbEnabled="true" + app:fastScrollThumbInactiveColor="?attr/chipColorBgNormal" + app:fastScrollTrackColor="?attr/background" /> diff --git a/app/src/full/res/layout/activity_console_log.xml b/app/src/full/res/layout/activity_console_log.xml new file mode 100644 index 000000000..3edc55e0d --- /dev/null +++ b/app/src/full/res/layout/activity_console_log.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/full/res/layout/activity_misc_settings.xml b/app/src/full/res/layout/activity_misc_settings.xml index ad7650629..51e767911 100644 --- a/app/src/full/res/layout/activity_misc_settings.xml +++ b/app/src/full/res/layout/activity_misc_settings.xml @@ -13,158 +13,13 @@ android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:src="@drawable/ic_backup_restore" /> - + android:layout_marginEnd="10dp" + android:padding="10dp" + android:src="@drawable/ic_arrow_down_small" /> + android:src="@drawable/ic_logs" /> - + android:padding="10dp" /> + @@ -637,7 +493,7 @@ + android:src="@drawable/ic_translate" /> - + android:src="@drawable/ic_biometric" /> + android:paddingBottom="15dp" + android:visibility="gone"> - diff --git a/app/src/full/res/layout/activity_tunnel_settings.xml b/app/src/full/res/layout/activity_tunnel_settings.xml index 1de24eba7..781142424 100644 --- a/app/src/full/res/layout/activity_tunnel_settings.xml +++ b/app/src/full/res/layout/activity_tunnel_settings.xml @@ -137,9 +137,10 @@ android:layout_marginStart="5dp" android:layout_marginTop="5dp" android:layout_marginEnd="5dp" + android:alpha="0.5" android:layout_marginBottom="5dp" android:padding="10dp" - android:src="@drawable/ic_firewall_exclude_off" /> + android:src="@drawable/ic_loop_back_app" /> + android:src="@drawable/ic_loopback" /> + + + diff --git a/app/src/full/res/layout/bottom_sheet_dns_log.xml b/app/src/full/res/layout/bottom_sheet_dns_log.xml index ed1019b86..9b73eb696 100644 --- a/app/src/full/res/layout/bottom_sheet_dns_log.xml +++ b/app/src/full/res/layout/bottom_sheet_dns_log.xml @@ -177,7 +177,7 @@ android:id="@+id/bsdl_domain_rule_ll" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/dns_block_blocked_desc" + android:layout_below="@id/dns_region" android:layout_marginLeft="2dp" android:layout_marginTop="5dp" android:layout_marginRight="2dp" @@ -225,6 +225,18 @@ android:textSize="@dimen/default_font_text_view" android:visibility="visible" /> + + + diff --git a/app/src/full/res/layout/bottom_sheet_orbot.xml b/app/src/full/res/layout/bottom_sheet_orbot.xml index bc3b71eae..cd8c5f22c 100644 --- a/app/src/full/res/layout/bottom_sheet_orbot.xml +++ b/app/src/full/res/layout/bottom_sheet_orbot.xml @@ -57,7 +57,7 @@ android:layout_marginStart="10dp" android:layout_toEndOf="@id/bs_orbot_app" android:contentDescription="@string/orbot_explanation" - android:src="@drawable/brave_mode_info" /> + android:src="@drawable/ic_info_white" /> - - - - - - - - + android:orientation="vertical" + android:nestedScrollingEnabled="true" + app:fastScrollAutoHide="true" + app:fastScrollAutoHideDelay="1500" + app:fastScrollEnableThumbInactiveColor="true" + app:fastScrollThumbColor="?attr/chipColorBgNormal" + app:fastScrollThumbEnabled="true" + app:fastScrollThumbInactiveColor="?attr/chipColorBgNormal" + app:fastScrollTrackColor="?attr/background" /> + + + + + android:maxLines="1" + android:paddingBottom="5dp" + android:singleLine="true" + android:textSize="@dimen/small_font_text_view" /> @@ -249,6 +269,7 @@ android:layout_marginRight="15dp" android:baselineAligned="true" android:orientation="horizontal" + android:layout_below="@id/fhs_card_top_ll" android:weightSum="1"> + + + + - - + + + - + + - - + + + + + - + + + + - - - + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 681df96af..2a6e15781 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="46" + android:versionName="v055o"> @@ -37,6 +37,7 @@ android:label="@string/app_name" android:largeHeap="true" android:allowNativeHeapPointerTagging="false" + android:gwpAsanMode="never" android:memtagMode="off" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" diff --git a/app/src/main/assets/database/rethink_v22.db b/app/src/main/assets/database/rethink_v22.db index 60c1c8a72..40902c28e 100644 Binary files a/app/src/main/assets/database/rethink_v22.db and b/app/src/main/assets/database/rethink_v22.db differ diff --git a/app/src/main/java/com/celzero/bravedns/backup/BackupAgent.kt b/app/src/main/java/com/celzero/bravedns/backup/BackupAgent.kt index 771a57b6a..7eb9f51e6 100644 --- a/app/src/main/java/com/celzero/bravedns/backup/BackupAgent.kt +++ b/app/src/main/java/com/celzero/bravedns/backup/BackupAgent.kt @@ -24,7 +24,6 @@ import android.os.SystemClock import androidx.preference.PreferenceManager import androidx.work.Worker import androidx.work.WorkerParameters -import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.backup.BackupHelper.Companion.CREATED_TIME import com.celzero.bravedns.backup.BackupHelper.Companion.DATA_BUILDER_BACKUP_URI import com.celzero.bravedns.backup.BackupHelper.Companion.METADATA_FILENAME diff --git a/app/src/main/java/com/celzero/bravedns/backup/BackupHelper.kt b/app/src/main/java/com/celzero/bravedns/backup/BackupHelper.kt index ebf38bf11..852bc47f6 100644 --- a/app/src/main/java/com/celzero/bravedns/backup/BackupHelper.kt +++ b/app/src/main/java/com/celzero/bravedns/backup/BackupHelper.kt @@ -98,7 +98,7 @@ class BackupHelper { fun stopVpn(context: Context) { Logger.i(Logger.LOG_TAG_BACKUP_RESTORE, "calling vpn stop from backup helper") - VpnController.stop(context) + VpnController.stop("bkup", context) } fun startVpn(context: Context) { diff --git a/app/src/main/java/com/celzero/bravedns/backup/RestoreAgent.kt b/app/src/main/java/com/celzero/bravedns/backup/RestoreAgent.kt index fdd39497f..d396bb4f3 100644 --- a/app/src/main/java/com/celzero/bravedns/backup/RestoreAgent.kt +++ b/app/src/main/java/com/celzero/bravedns/backup/RestoreAgent.kt @@ -36,7 +36,6 @@ import com.celzero.bravedns.data.AppConfig import com.celzero.bravedns.database.AppDatabase import com.celzero.bravedns.database.LogDatabase import com.celzero.bravedns.service.PersistentState -import com.celzero.bravedns.service.WireguardManager import com.celzero.bravedns.util.Utilities import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -366,11 +365,13 @@ class RestoreAgent(val context: Context, workerParams: WorkerParameters) : val v: Any? = e.value val key: String = e.key - if (v is Boolean) prefsEditor.putBoolean(key, (v as Boolean?)!!) - else if (v is Float) prefsEditor.putFloat(key, (v as Float?)!!) - else if (v is Int) prefsEditor.putInt(key, (v as Int?)!!) - else if (v is Long) prefsEditor.putLong(key, (v as Long?)!!) - else if (v is String) prefsEditor.putString(key, v as String?) + when (v) { + is Boolean -> prefsEditor.putBoolean(key, (v as Boolean?)!!) + is Float -> prefsEditor.putFloat(key, (v as Float?)!!) + is Int -> prefsEditor.putInt(key, (v as Int?)!!) + is Long -> prefsEditor.putLong(key, (v as Long?)!!) + is String -> prefsEditor.putString(key, v as String?) + } } prefsEditor.apply() Logger.i( diff --git a/app/src/main/java/com/celzero/bravedns/data/AppConfig.kt b/app/src/main/java/com/celzero/bravedns/data/AppConfig.kt index 0a761d0cd..101e9c32f 100644 --- a/app/src/main/java/com/celzero/bravedns/data/AppConfig.kt +++ b/app/src/main/java/com/celzero/bravedns/data/AppConfig.kt @@ -135,7 +135,7 @@ internal constructor( } } - enum class TunFirewallMode(val mode: Long) { + enum class TunFirewallMode(val mode: Int) { FILTER_ANDROID9_ABOVE(Settings.BlockModeFilter), SINK(Settings.BlockModeSink), FILTER_ANDROID8_BELOW(Settings.BlockModeFilterProc), @@ -195,9 +195,9 @@ internal constructor( } enum class TunDnsMode(val mode: Long) { - NONE(Settings.DNSModeNone), - DNS_IP(Settings.DNSModeIP), - DNS_PORT(Settings.DNSModePort) + NONE(Settings.DNSModeNone.toLong()), + DNS_IP(Settings.DNSModeIP.toLong()), + DNS_PORT(Settings.DNSModePort.toLong()) } // TODO: untangle the mess of proxy modes and providers @@ -335,10 +335,10 @@ internal constructor( } } - enum class ProtoTranslationMode(val id: Long) { + enum class ProtoTranslationMode(val id: Int) { PTMODEAUTO(Settings.PtModeAuto), PTMODEFORCE64(Settings.PtModeForce64), - PTMODEMAYBE46(Settings.PtModeNo46) + PTMODENO46(Settings.PtModeNo46) } fun getInternetProtocol(): InternetProtocol { diff --git a/app/src/main/java/com/celzero/bravedns/database/AppDatabase.kt b/app/src/main/java/com/celzero/bravedns/database/AppDatabase.kt index 0862db0d4..dbd4cc9f9 100644 --- a/app/src/main/java/com/celzero/bravedns/database/AppDatabase.kt +++ b/app/src/main/java/com/celzero/bravedns/database/AppDatabase.kt @@ -55,7 +55,7 @@ import java.io.File DoTEndpoint::class, ODoHEndpoint::class ], - version = 23, + version = 24, exportSchema = false ) @TypeConverters(Converters::class) @@ -99,6 +99,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(MIGRATION_20_21) .addMigrations(MIGRATION_21_22) .addMigrations(MIGRATION_22_23) + .addMigrations(MIGRATION_23_24) .build() private val roomCallback: Callback = @@ -971,6 +972,15 @@ abstract class AppDatabase : RoomDatabase() { } } + private val MIGRATION_23_24: Migration = + object : Migration(23, 24) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "UPDATE DoTEndpoint set desc = 'Adguard DNS over TLS. Blocks ads, tracking, and phishing.' where name = 'Adguard' and id = 2" + ) + } + } + // ref: stackoverflow.com/a/57204285 private fun doesColumnExistInTable( db: SupportSQLiteDatabase, diff --git a/app/src/main/java/com/celzero/bravedns/database/AppInfo.kt b/app/src/main/java/com/celzero/bravedns/database/AppInfo.kt index b0e26f800..acc937797 100644 --- a/app/src/main/java/com/celzero/bravedns/database/AppInfo.kt +++ b/app/src/main/java/com/celzero/bravedns/database/AppInfo.kt @@ -41,11 +41,16 @@ class AppInfo { override fun equals(other: Any?): Boolean { if (other !is AppInfo) return false if (packageName != other.packageName) return false + if (firewallStatus != other.firewallStatus) return false + if (connectionStatus != other.connectionStatus) return false return true } override fun hashCode(): Int { - return this.packageName.hashCode() + var result = this.packageName.hashCode() + result += result * 31 + this.firewallStatus + result += result * 31 + this.connectionStatus + return result } constructor(values: ContentValues?) { @@ -99,6 +104,8 @@ class AppInfo { } fun hasInternetPermission(packageManager: PackageManager): Boolean { + if (packageName.startsWith("no_package_")) return true + // INTERNET permission if defined, can not be denied so this is safe to use return packageManager.checkPermission(Manifest.permission.INTERNET, packageName) == PackageManager.PERMISSION_GRANTED } diff --git a/app/src/main/java/com/celzero/bravedns/database/AppInfoDAO.kt b/app/src/main/java/com/celzero/bravedns/database/AppInfoDAO.kt index 4edab12e3..013063e5f 100644 --- a/app/src/main/java/com/celzero/bravedns/database/AppInfoDAO.kt +++ b/app/src/main/java/com/celzero/bravedns/database/AppInfoDAO.kt @@ -54,60 +54,66 @@ interface AppInfoDAO { @Query("select * from AppInfo order by appCategory, uid") fun getAllAppDetails(): List @Query( - "select * from AppInfo where isSystemApp = 1 and (appName like :search or uid like :search or packageName like :search) and firewallStatus in (:firewall) and connectionStatus in (:connectionStatus) order by lower(appName)" + "select * from AppInfo where isSystemApp = 1 and (appName like :search or uid like :search or packageName like :search) and (firewallStatus in (:firewall) or isProxyExcluded in (:isProxyExcluded)) and connectionStatus in (:connectionStatus) order by lower(appName)" ) fun getSystemApps( search: String, firewall: Set, - connectionStatus: Set + connectionStatus: Set, + isProxyExcluded: Set ): PagingSource @Query( - "select * from AppInfo where isSystemApp = 1 and (appName like :search or uid like :search or packageName like :search) and appCategory in (:filter) and firewallStatus in (:firewall) and connectionStatus in (:connectionStatus) order by lower(appName)" + "select * from AppInfo where isSystemApp = 1 and (appName like :search or uid like :search or packageName like :search) and appCategory in (:filter) and (firewallStatus in (:firewall) or isProxyExcluded in (:isProxyExcluded)) and connectionStatus in (:connectionStatus) order by lower(appName)" ) fun getSystemApps( search: String, filter: Set, firewall: Set, - connectionStatus: Set + connectionStatus: Set, + isProxyExcluded: Set ): PagingSource @Query( - "select * from AppInfo where isSystemApp = 0 and (appName like :search or uid like :search or packageName like :search) and firewallStatus in (:firewall) and connectionStatus in (:connectionStatus) order by lower(appName)" + "select * from AppInfo where isSystemApp = 0 and (appName like :search or uid like :search or packageName like :search) and (firewallStatus in (:firewall) or isProxyExcluded in (:isProxyExcluded)) and connectionStatus in (:connectionStatus) order by lower(appName)" ) fun getInstalledApps( search: String, firewall: Set, - connectionStatus: Set + connectionStatus: Set, + isProxyExcluded: Set ): PagingSource @Query( - "select * from AppInfo where isSystemApp = 0 and (appName like :search or uid like :search or packageName like :search) and appCategory in (:filter) and firewallStatus in (:firewall) and connectionStatus in (:connectionStatus) order by lower(appName)" + "select * from AppInfo where isSystemApp = 0 and (appName like :search or uid like :search or packageName like :search) and appCategory in (:filter) and (firewallStatus in (:firewall) or isProxyExcluded in (:isProxyExcluded)) and connectionStatus in (:connectionStatus) order by lower(appName)" ) fun getInstalledApps( search: String, filter: Set, firewall: Set, - connectionStatus: Set + connectionStatus: Set, + isProxyExcluded: Set ): PagingSource @Query( - "select * from AppInfo where (appName like :search or uid like :search or packageName like :search) and firewallStatus in (:firewall) and connectionStatus in (:connectionStatus) order by lower(appName)" + "select * from AppInfo where (appName like :search or uid like :search or packageName like :search) and (firewallStatus in (:firewall) or isProxyExcluded in (:isProxyExcluded)) and connectionStatus in (:connectionStatus) order by lower(appName)" ) fun getAppInfos( search: String, firewall: Set, - connectionStatus: Set + connectionStatus: Set, + isProxyExcluded: Set ): PagingSource @Query( - "select * from AppInfo where (appName like :search or uid like :search or packageName like :search) and appCategory in (:filter) and firewallStatus in (:firewall) and connectionStatus in (:connectionStatus) order by lower(appName)" + "select * from AppInfo where (appName like :search or uid like :search or packageName like :search) and appCategory in (:filter) and (firewallStatus in (:firewall) or isProxyExcluded in (:isProxyExcluded)) and connectionStatus in (:connectionStatus) order by lower(appName)" ) fun getAppInfos( search: String, filter: Set, firewall: Set, - connectionStatus: Set + connectionStatus: Set, + isProxyExcluded: Set ): PagingSource @Query( @@ -152,4 +158,10 @@ interface AppInfoDAO { @Query("update AppInfo set isProxyExcluded = :isProxyExcluded where uid = :uid") fun updateProxyExcluded(uid: Int, isProxyExcluded: Boolean) + + @Query("update AppInfo set firewallStatus = 5, connectionStatus = 3 where packageName = 'com.celzero.bravedns'") + fun resetRethinkAppFirewallMode() + + @Query("select uid from AppInfo where packageName = :packageName") + fun getAppInfoUidForPackageName(packageName: String): Int } diff --git a/app/src/main/java/com/celzero/bravedns/database/AppInfoRepository.kt b/app/src/main/java/com/celzero/bravedns/database/AppInfoRepository.kt index 6f4e88662..7daff09ba 100644 --- a/app/src/main/java/com/celzero/bravedns/database/AppInfoRepository.kt +++ b/app/src/main/java/com/celzero/bravedns/database/AppInfoRepository.kt @@ -86,15 +86,23 @@ class AppInfoRepository(private val appInfoDAO: AppInfoDAO) { return appInfoDAO.deleteByUid(uid) } - fun getDataUsageByUid(uid: Int): DataUsage { + suspend fun getDataUsageByUid(uid: Int): DataUsage { return appInfoDAO.getDataUsageByUid(uid) } - fun updateDataUsageByUid(uid: Int, uploadBytes: Long, downloadBytes: Long) { + suspend fun updateDataUsageByUid(uid: Int, uploadBytes: Long, downloadBytes: Long) { appInfoDAO.updateDataUsageByUid(uid, uploadBytes, downloadBytes) } - fun updateProxyExcluded(uid: Int, isProxyExcluded: Boolean) { + suspend fun updateProxyExcluded(uid: Int, isProxyExcluded: Boolean) { appInfoDAO.updateProxyExcluded(uid, isProxyExcluded) } + + suspend fun resetRethinkAppFirewallMode() { + appInfoDAO.resetRethinkAppFirewallMode() + } + + suspend fun getAppInfoUidForPackageName(packageName: String): Int { + return appInfoDAO.getAppInfoUidForPackageName(packageName) + } } diff --git a/app/src/main/java/com/celzero/bravedns/database/ConnectionTracker.kt b/app/src/main/java/com/celzero/bravedns/database/ConnectionTracker.kt index b4b9be525..8aaa374bc 100644 --- a/app/src/main/java/com/celzero/bravedns/database/ConnectionTracker.kt +++ b/app/src/main/java/com/celzero/bravedns/database/ConnectionTracker.kt @@ -29,6 +29,8 @@ import com.celzero.bravedns.util.Constants.Companion.INIT_TIME_MS Index(value = arrayOf("dnsQuery"), unique = false), Index(value = arrayOf("blockedByRule"), unique = false), Index(value = arrayOf("isBlocked", "timeStamp"), unique = false), + Index(value = arrayOf("connId"), unique = false), + Index(value = arrayOf("proxyDetails"), unique = false) ] ) class ConnectionTracker { diff --git a/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerDAO.kt b/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerDAO.kt index 01ce244e8..8782ed3cc 100644 --- a/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerDAO.kt +++ b/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerDAO.kt @@ -76,7 +76,7 @@ interface ConnectionTrackerDAO { fun getConnectionTrackerByName(): PagingSource @Query( - "select * from ConnectionTracker where (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query) order by id desc LIMIT $MAX_LOGS" + "select * from ConnectionTracker where (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query or proxyDetails like :query) order by id desc LIMIT $MAX_LOGS" ) fun getConnectionTrackerByName(query: String): PagingSource @@ -84,22 +84,22 @@ interface ConnectionTrackerDAO { fun getBlockedConnections(): PagingSource @Query( - "select * from ConnectionTracker where (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query) and isBlocked = 1 order by id desc LIMIT $MAX_LOGS" + "select * from ConnectionTracker where (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query or proxyDetails like :query) and isBlocked = 1 order by id desc LIMIT $MAX_LOGS" ) fun getBlockedConnections(query: String): PagingSource @Query( - "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, GROUP_CONCAT(DISTINCT dnsQuery) as appOrDnsName FROM ConnectionTracker WHERE uid = :uid and timeStamp > :to GROUP BY ipAddress, uid, port ORDER BY count DESC" + "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, GROUP_CONCAT(DISTINCT dnsQuery) as appOrDnsName FROM ConnectionTracker WHERE uid = :uid and timeStamp > :to GROUP BY uid, ipAddress, port ORDER BY count DESC" ) fun getAppIpLogs(uid: Int, to: Long): PagingSource @Query( - "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, '' as appOrDnsName FROM ConnectionTracker WHERE uid = :uid and timeStamp > :to GROUP BY ipAddress, uid, port ORDER BY count DESC LIMIT 3" + "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, '' as appOrDnsName FROM ConnectionTracker WHERE uid = :uid and timeStamp > :to GROUP BY uid, ipAddress, port ORDER BY count DESC LIMIT 3" ) fun getAppIpLogsLimited(uid: Int, to: Long): PagingSource @Query( - "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, GROUP_CONCAT(DISTINCT dnsQuery) as appOrDnsName FROM ConnectionTracker WHERE uid = :uid and timeStamp > :to and ipAddress like :query GROUP BY ipAddress, uid, port ORDER BY count DESC" + "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, GROUP_CONCAT(DISTINCT dnsQuery) as appOrDnsName FROM ConnectionTracker WHERE uid = :uid and timeStamp > :to and ipAddress like :query GROUP BY uid, ipAddress, port ORDER BY count DESC" ) fun getAppIpLogsFiltered(uid: Int, to: Long, query: String): PagingSource @@ -122,14 +122,6 @@ interface ConnectionTrackerDAO { query: String ): PagingSource - @Query("select count(DISTINCT(ipAddress)) from ConnectionTracker where uid = :uid") - fun getAppConnectionsCount(uid: Int): LiveData - - @Query( - "select count(DISTINCT(dnsQuery)) from ConnectionTracker where uid = :uid and dnsQuery != ''" - ) - fun getAppDomainConnectionsCount(uid: Int): LiveData - @Query( "select * from ConnectionTracker where blockedByRule in (:filter) and isBlocked = 1 order by id desc LIMIT $MAX_LOGS" ) @@ -149,7 +141,7 @@ interface ConnectionTrackerDAO { ): PagingSource @Query( - "select * from ConnectionTracker where blockedByRule in (:filter) and isBlocked = 1 and (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query) order by id desc LIMIT $MAX_LOGS" + "select * from ConnectionTracker where blockedByRule in (:filter) and isBlocked = 1 and (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query or proxyDetails like :query) order by id desc LIMIT $MAX_LOGS" ) fun getBlockedConnectionsFiltered( query: String, @@ -160,10 +152,13 @@ interface ConnectionTrackerDAO { @Query("delete from ConnectionTracker where uid = :uid") fun clearLogsByUid(uid: Int) + @Query("delete from ConnectionTracker where uid = :uid and timeStamp > :time") + fun clearLogsByTime(uid: Int, time: Long) + @Query("DELETE FROM ConnectionTracker WHERE timeStamp < :date") fun purgeLogsByDate(date: Long) @Query( - "select * from ConnectionTracker where isBlocked = 0 and (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query) order by id desc LIMIT $MAX_LOGS" + "select * from ConnectionTracker where isBlocked = 0 and (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query or proxyDetails like :query) order by id desc LIMIT $MAX_LOGS" ) fun getAllowedConnections(query: String): PagingSource @@ -176,7 +171,7 @@ interface ConnectionTrackerDAO { fun getAllowedConnectionsFiltered(filter: Set): PagingSource @Query( - "select * from ConnectionTracker where isBlocked = 0 and (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query) and blockedByRule in (:filter) order by id desc LIMIT $MAX_LOGS" + "select * from ConnectionTracker where isBlocked = 0 and (appName like :query or ipAddress like :query or dnsQuery like :query or flag like :query or proxyDetails like :query) and blockedByRule in (:filter) order by id desc LIMIT $MAX_LOGS" ) fun getAllowedConnectionsFiltered( query: String, @@ -298,4 +293,17 @@ interface ConnectionTrackerDAO { "select sum(downloadBytes) as totalDownload, sum(uploadBytes) as totalUpload, count(id) as connectionsCount, ict.meteredDataUsage as meteredDataUsage from ConnectionTracker as ct join (select sum(downloadBytes + uploadBytes) as meteredDataUsage from ConnectionTracker where connType like :meteredTxt and timeStamp > :to) as ict where timeStamp > :to" ) fun getTotalUsages(to: Long, meteredTxt: String): DataUsageSummary + + @Query("select * from ConnectionTracker where blockedByRule in ('Rule #1B', 'Rule #1F', 'Rule #3', 'Rule #4', 'Rule #5', 'Rule #6', 'Http block', 'Universal Lockdown')") + fun getBlockedUniversalRulesCount(): List + + @Query( + "select uid as uid, '' as ipAddress, 0 as port, COUNT(connId) count, '' as flag, 0 as blocked, appName as appOrDnsName, SUM(uploadBytes) AS uploadBytes, SUM(downloadBytes) AS downloadBytes, 0 as totalBytes from ConnectionTracker where timeStamp > :to and dnsQuery like :query and isBlocked = 0 GROUP BY appName, uid ORDER BY count DESC" + ) + fun getDomainConnections(query: String, to: Long): PagingSource + + @Query( + "select uid as uid, '' as ipAddress, 0 as port, COUNT(connId) count, flag as flag, 0 as blocked, appName as appOrDnsName, SUM(uploadBytes) AS uploadBytes, SUM(downloadBytes) AS downloadBytes, 0 as totalBytes from ConnectionTracker where timeStamp > :to and flag like :query and isBlocked = 0 GROUP BY appName, uid ORDER BY count DESC" + ) + fun getFlagConnections(query: String, to: Long): PagingSource } diff --git a/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerRepository.kt b/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerRepository.kt index 7e79e3b0b..9b8369628 100644 --- a/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerRepository.kt +++ b/app/src/main/java/com/celzero/bravedns/database/ConnectionTrackerRepository.kt @@ -16,12 +16,15 @@ package com.celzero.bravedns.database import androidx.lifecycle.LiveData -import com.celzero.bravedns.RethinkDnsApplication -import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.data.ConnectionSummary import com.celzero.bravedns.data.DataUsage +import com.celzero.bravedns.service.PersistentState +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject -class ConnectionTrackerRepository(private val connectionTrackerDAO: ConnectionTrackerDAO) { +class ConnectionTrackerRepository(private val connectionTrackerDAO: ConnectionTrackerDAO): KoinComponent { + + private val persistentState by inject() suspend fun insert(connectionTracker: ConnectionTracker) { connectionTrackerDAO.insert(connectionTracker) @@ -34,7 +37,7 @@ class ConnectionTrackerRepository(private val connectionTrackerDAO: ConnectionTr suspend fun updateBatch(summary: List) { summary.forEach { // update the flag and target ip if in debug mode - if (DEBUG && !it.targetIp.isNullOrEmpty()) { + if (!it.targetIp.isNullOrEmpty()) { val flag = it.flag ?: "" connectionTrackerDAO.updateSummary( it.connId, @@ -82,4 +85,8 @@ class ConnectionTrackerRepository(private val connectionTrackerDAO: ConnectionTr suspend fun getDataUsage(before: Long, current: Long): List { return connectionTrackerDAO.getDataUsage(before, current) } + + suspend fun getBlockedUniversalRulesCount(): List { + return connectionTrackerDAO.getBlockedUniversalRulesCount() + } } diff --git a/app/src/main/java/com/celzero/bravedns/database/ConsoleLog.kt b/app/src/main/java/com/celzero/bravedns/database/ConsoleLog.kt new file mode 100644 index 000000000..e3a02c1e3 --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/database/ConsoleLog.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.database + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "ConsoleLog") +data class ConsoleLog( + @PrimaryKey(autoGenerate = true) var id: Int = 0, + val message: String, + val timestamp: Long +) diff --git a/app/src/main/java/com/celzero/bravedns/database/ConsoleLogDAO.kt b/app/src/main/java/com/celzero/bravedns/database/ConsoleLogDAO.kt new file mode 100644 index 000000000..c0085e3c8 --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/database/ConsoleLogDAO.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.database + +import android.database.Cursor +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery +import com.celzero.bravedns.util.Constants + +@Dao +interface ConsoleLogDAO { + @Insert + suspend fun insert(log: ConsoleLog) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertBatch(log: List) + + @RawQuery + fun getLogsCursor(query: SimpleSQLiteQuery): Cursor + + @Query("select * from ConsoleLog order by id desc LIMIT ${Constants.MAX_LOGS}") + fun getLogs(): PagingSource + + @Query("DELETE FROM ConsoleLog WHERE timestamp < :to") + suspend fun deleteOldLogs(to: Long) + + @Query("select timestamp from ConsoleLog order by id limit 1") + suspend fun sinceTime(): Long + + @Query("select count(*) from ConsoleLog") + suspend fun getLogCount(): Int + + @Query("SELECT * FROM ConsoleLog ORDER BY timestamp DESC LIMIT :limit OFFSET :offset") + suspend fun getLogs(offset: Int, limit: Int): List + + @Query("DELETE FROM ConsoleLog") + suspend fun deleteAllLogs() +} diff --git a/app/src/main/java/com/celzero/bravedns/database/ConsoleLogDatabase.kt b/app/src/main/java/com/celzero/bravedns/database/ConsoleLogDatabase.kt new file mode 100644 index 000000000..394418540 --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/database/ConsoleLogDatabase.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [ConsoleLog::class], version = 1, exportSchema = false) +abstract class ConsoleLogDatabase : RoomDatabase() { + companion object { + fun buildDatabase(context: Context): ConsoleLogDatabase { + return Room.inMemoryDatabaseBuilder( + context.applicationContext, + ConsoleLogDatabase::class.java + ).build() + } + } + + abstract fun consoleLogDAO(): ConsoleLogDAO + + fun consoleLogRepository() = ConsoleLogRepository(consoleLogDAO()) +} diff --git a/app/src/main/java/com/celzero/bravedns/database/ConsoleLogRepository.kt b/app/src/main/java/com/celzero/bravedns/database/ConsoleLogRepository.kt new file mode 100644 index 000000000..8f106dcd0 --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/database/ConsoleLogRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.database + +class ConsoleLogRepository(private val consoleLogDAO: ConsoleLogDAO) { + + var consoleLogStartTimestamp: Long = 0 + + suspend fun insert(log: ConsoleLog) { + consoleLogDAO.insert(log) + } + + suspend fun deleteOldLogs(to: Long) { + consoleLogDAO.deleteOldLogs(to) + } + + suspend fun insertBatch(logs: List) { + consoleLogDAO.insertBatch(logs) + } + + suspend fun getLogCount(): Int { + return consoleLogDAO.getLogCount() + } + + suspend fun deleteAllLogs() { + consoleLogDAO.deleteAllLogs() + } +} diff --git a/app/src/main/java/com/celzero/bravedns/database/CustomDomain.kt b/app/src/main/java/com/celzero/bravedns/database/CustomDomain.kt index af1bcfe4b..c2c380b3e 100644 --- a/app/src/main/java/com/celzero/bravedns/database/CustomDomain.kt +++ b/app/src/main/java/com/celzero/bravedns/database/CustomDomain.kt @@ -35,11 +35,14 @@ class CustomDomain { override fun equals(other: Any?): Boolean { if (other !is CustomDomain) return false if (domain != other.domain) return false + if (status != other.status) return false return true } override fun hashCode(): Int { - return this.domain.hashCode() + var result = this.domain.hashCode() + result += result * 31 + this.status + return result } constructor(values: ContentValues?) { @@ -81,10 +84,10 @@ class CustomDomain { } companion object { - private const val currentVersion: Long = 1L + private const val CURRENT_VERSION: Long = 1L fun getCurrentVersion(): Long { - return currentVersion + return CURRENT_VERSION } } diff --git a/app/src/main/java/com/celzero/bravedns/database/CustomDomainDAO.kt b/app/src/main/java/com/celzero/bravedns/database/CustomDomainDAO.kt index b1df9174e..7dd78a0d9 100644 --- a/app/src/main/java/com/celzero/bravedns/database/CustomDomainDAO.kt +++ b/app/src/main/java/com/celzero/bravedns/database/CustomDomainDAO.kt @@ -36,6 +36,8 @@ interface CustomDomainDAO { @Delete fun delete(customDomain: CustomDomain) + @Delete fun deleteAll(customDomains: List) + @Transaction @Query("select * from CustomDomain order by modifiedTs desc") fun getAllDomains(): List @@ -75,7 +77,7 @@ interface CustomDomainDAO { fun cpUpdate(status: Int, clause: String): Int @Query( - "select * from CustomDomain where uid != ${Constants.UID_EVERYBODY} and domain like :query order by uid" + "SELECT * FROM (SELECT *, (SELECT COUNT(*) FROM CustomDomain cd2 WHERE cd2.uid = cd1.uid AND cd2.rowid <= cd1.rowid) row_num FROM CustomDomain cd1 WHERE uid != ${Constants.UID_EVERYBODY} AND domain LIKE :query) WHERE row_num <= 5 ORDER BY uid, row_num" ) fun getAllDomainRules(query: String): PagingSource } diff --git a/app/src/main/java/com/celzero/bravedns/database/CustomDomainRepository.kt b/app/src/main/java/com/celzero/bravedns/database/CustomDomainRepository.kt index 5e2b3b1c4..b04bbaa08 100644 --- a/app/src/main/java/com/celzero/bravedns/database/CustomDomainRepository.kt +++ b/app/src/main/java/com/celzero/bravedns/database/CustomDomainRepository.kt @@ -84,4 +84,8 @@ class CustomDomainRepository(private val customDomainDAO: CustomDomainDAO) { fun getRulesCursor(): Cursor { return customDomainDAO.getRulesCursor() } + + suspend fun deleteRules(list: List) { + return customDomainDAO.deleteAll(list) + } } diff --git a/app/src/main/java/com/celzero/bravedns/database/CustomIp.kt b/app/src/main/java/com/celzero/bravedns/database/CustomIp.kt index 54ddf48ac..58bdbca93 100644 --- a/app/src/main/java/com/celzero/bravedns/database/CustomIp.kt +++ b/app/src/main/java/com/celzero/bravedns/database/CustomIp.kt @@ -48,12 +48,16 @@ class CustomIp { override fun equals(other: Any?): Boolean { if (other !is CustomIp) return false - return !(ipAddress != other.ipAddress && uid != other.uid) + if (ipAddress != other.ipAddress) return false + if (uid != other.uid) return false + if (status != other.status) return false + return true } override fun hashCode(): Int { var result = this.uid.hashCode() result += result * 31 + this.ipAddress.hashCode() + result += result * 31 + this.status return result } diff --git a/app/src/main/java/com/celzero/bravedns/database/CustomIpDao.kt b/app/src/main/java/com/celzero/bravedns/database/CustomIpDao.kt index 31cdbc0ad..051cb04cc 100644 --- a/app/src/main/java/com/celzero/bravedns/database/CustomIpDao.kt +++ b/app/src/main/java/com/celzero/bravedns/database/CustomIpDao.kt @@ -36,6 +36,8 @@ interface CustomIpDao { @Delete fun delete(customIp: CustomIp) + @Delete fun deleteAll(customIp: List) + @Transaction @Query("select * from CustomIp order by uid") fun getCustomIpRules(): List @@ -97,7 +99,7 @@ interface CustomIpDao { fun getAppWiseCustomIp(query: String, uid: Int): PagingSource @Query( - "select * from CustomIp where ipAddress like :query and isActive = 1 and uid != $UID_EVERYBODY order by uid" + "SELECT * FROM (SELECT *, (SELECT COUNT(*) FROM CustomIp ci2 WHERE ci2.uid = ci1.uid AND ci2.rowid <= ci1.rowid) row_num FROM CustomIp ci1 WHERE ipAddress LIKE :query AND isActive = 1 AND uid != $UID_EVERYBODY) WHERE row_num <= 5 ORDER BY uid, row_num" ) fun getAllCustomIpRules(query: String): PagingSource diff --git a/app/src/main/java/com/celzero/bravedns/database/CustomIpRepository.kt b/app/src/main/java/com/celzero/bravedns/database/CustomIpRepository.kt index dcba9cbaa..9b6130768 100644 --- a/app/src/main/java/com/celzero/bravedns/database/CustomIpRepository.kt +++ b/app/src/main/java/com/celzero/bravedns/database/CustomIpRepository.kt @@ -58,4 +58,8 @@ class CustomIpRepository(private val customIpDao: CustomIpDao) { suspend fun deleteAllAppsRules() { customIpDao.deleteAllAppsRules() } + + suspend fun deleteRules(list: List) { + customIpDao.deleteAll(list) + } } diff --git a/app/src/main/java/com/celzero/bravedns/database/DatabaseModule.kt b/app/src/main/java/com/celzero/bravedns/database/DatabaseModule.kt index e014272f7..9c2a8b6ea 100644 --- a/app/src/main/java/com/celzero/bravedns/database/DatabaseModule.kt +++ b/app/src/main/java/com/celzero/bravedns/database/DatabaseModule.kt @@ -22,6 +22,7 @@ object DatabaseModule { private val databaseModule = module { single { AppDatabase.buildDatabase(androidContext()) } single { LogDatabase.buildDatabase(androidContext()) } + single { ConsoleLogDatabase.buildDatabase(androidContext()) } } private val daoModule = module { single { get().appInfoDAO() } @@ -45,6 +46,7 @@ object DatabaseModule { single { get().dotEndpointDao() } single { get().odohEndpointDao() } single { get().rethinkConnectionLogDAO() } + single { get().consoleLogDAO() } } private val repositoryModule = module { single { get().appInfoRepository() } @@ -68,6 +70,7 @@ object DatabaseModule { single { get().dotEndpointRepository() } single { get().odohEndpointRepository() } single { get().rethinkConnectionLogRepository() } + single { get().consoleLogRepository() } } val modules = listOf(databaseModule, daoModule, repositoryModule) diff --git a/app/src/main/java/com/celzero/bravedns/database/DnsLog.kt b/app/src/main/java/com/celzero/bravedns/database/DnsLog.kt index 40dd7e41e..3c061a7a8 100644 --- a/app/src/main/java/com/celzero/bravedns/database/DnsLog.kt +++ b/app/src/main/java/com/celzero/bravedns/database/DnsLog.kt @@ -57,6 +57,7 @@ class DnsLog { var resolverId: String = "" var msg: String = "" var upstreamBlock: Boolean = false + var region: String = "" override fun equals(other: Any?): Boolean { if (other !is DnsLog) return false diff --git a/app/src/main/java/com/celzero/bravedns/database/LogDatabase.kt b/app/src/main/java/com/celzero/bravedns/database/LogDatabase.kt index 5b49ed8ff..129395484 100644 --- a/app/src/main/java/com/celzero/bravedns/database/LogDatabase.kt +++ b/app/src/main/java/com/celzero/bravedns/database/LogDatabase.kt @@ -17,7 +17,6 @@ package com.celzero.bravedns.database import Logger import android.content.Context -import android.content.pm.PackageInfo import android.database.Cursor import android.database.sqlite.SQLiteException import androidx.room.Database @@ -31,7 +30,7 @@ import com.celzero.bravedns.util.Utilities @Database( entities = [ConnectionTracker::class, DnsLog::class, RethinkLog::class], - version = 7, + version = 10, exportSchema = false ) @TypeConverters(Converters::class) @@ -68,6 +67,9 @@ abstract class LogDatabase : RoomDatabase() { .addMigrations(MIGRATION_4_5) .addMigrations(MIGRATION_5_6) .addMigrations(MIGRATION_6_7) + .addMigrations(MIGRATION_7_8) + .addMigrations(Migration_8_9) + .addMigrations(Migration_9_10) .fallbackToDestructiveMigration() // recreate the database if no migration is found .build() } @@ -262,6 +264,29 @@ abstract class LogDatabase : RoomDatabase() { ) } } + + private val MIGRATION_7_8: Migration = + object : Migration(7, 8) { + override fun migrate(db: SupportSQLiteDatabase) { + // add a new column region to DNS log table with default as empty string + db.execSQL( + "ALTER TABLE DnsLogs ADD COLUMN region TEXT DEFAULT '' NOT NULL" + ) + } + } + + private val Migration_8_9: Migration = object : Migration(8, 9) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE INDEX IF NOT EXISTS index_ConnectionTracker_connId ON ConnectionTracker(connId)") + db.execSQL("CREATE INDEX IF NOT EXISTS index_RethinkLog_connId ON RethinkLog(connId)") + } + } + + private val Migration_9_10: Migration = object : Migration(9, 10) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE INDEX IF NOT EXISTS index_ConnectionTracker_proxyDetails ON ConnectionTracker(proxyDetails)") + } + } } fun checkPoint() { diff --git a/app/src/main/java/com/celzero/bravedns/database/ProxyApplicationMapping.kt b/app/src/main/java/com/celzero/bravedns/database/ProxyApplicationMapping.kt index fbd34dda8..e237c35e3 100644 --- a/app/src/main/java/com/celzero/bravedns/database/ProxyApplicationMapping.kt +++ b/app/src/main/java/com/celzero/bravedns/database/ProxyApplicationMapping.kt @@ -15,6 +15,8 @@ */ package com.celzero.bravedns.database +import android.Manifest +import android.content.pm.PackageManager import androidx.room.Entity @Entity(tableName = "ProxyApplicationMapping", primaryKeys = ["uid", "packageName", "proxyId"]) @@ -29,11 +31,15 @@ class ProxyApplicationMapping { override fun equals(other: Any?): Boolean { if (other !is ProxyApplicationMapping) return false - return packageName == other.packageName + if (packageName != other.packageName) return false + if (uid != other.uid) return false + return true } override fun hashCode(): Int { - return this.packageName.hashCode() + var result = this.uid.hashCode() + result += result * 31 + this.packageName.hashCode() + return result } constructor( @@ -51,4 +57,14 @@ class ProxyApplicationMapping { this.isActive = isActive this.proxyId = proxyId } + + fun hasInternetPermission(packageManager: PackageManager): Boolean { + if (packageName.startsWith("no_package_")) return true + + // INTERNET permission if defined, can not be denied so this is safe to use + return packageManager.checkPermission( + Manifest.permission.INTERNET, + packageName + ) == PackageManager.PERMISSION_GRANTED + } } diff --git a/app/src/main/java/com/celzero/bravedns/database/ProxyEndpointDAO.kt b/app/src/main/java/com/celzero/bravedns/database/ProxyEndpointDAO.kt index e0330ce58..2c5292485 100644 --- a/app/src/main/java/com/celzero/bravedns/database/ProxyEndpointDAO.kt +++ b/app/src/main/java/com/celzero/bravedns/database/ProxyEndpointDAO.kt @@ -49,7 +49,7 @@ interface ProxyEndpointDAO { fun getConnectedProxyLiveData(): LiveData @Query("select * from ProxyEndpoint where proxyMode = 0") // 0 for Custom SOCKS5 - fun getCustomSocks5Endpoint(): ProxyEndpoint + fun getCustomSocks5Endpoint(): ProxyEndpoint? @Query("select * from ProxyEndpoint where isSelected = 1 and proxyMode = 0") fun getConnectedSocks5Proxy(): ProxyEndpoint? diff --git a/app/src/main/java/com/celzero/bravedns/database/RefreshDatabase.kt b/app/src/main/java/com/celzero/bravedns/database/RefreshDatabase.kt index 985143a97..44f47e5ed 100644 --- a/app/src/main/java/com/celzero/bravedns/database/RefreshDatabase.kt +++ b/app/src/main/java/com/celzero/bravedns/database/RefreshDatabase.kt @@ -147,7 +147,7 @@ internal constructor( Logger.i( LOG_TAG_APP_DB, - "reload: fm: ${fm}; ip: ${ipm}; dom: ${dm}; px: ${pxm}; wg: ${wgm}; t: ${tcpm}" + "reload: fm: ${fm}; ip: ${ipm}; dom: ${dm}; px: ${pxm}; wg: ${wgm}; t: $tcpm" ) val trackedApps = FirewallManager.getAllApps() @@ -343,7 +343,7 @@ internal constructor( } val ai = maybeFetchAppInfo(uid) val pkg = ai?.packageName ?: "" - Logger.i(LOG_TAG_APP_DB, "insert app; uid: $uid, pkg: ${pkg}") + Logger.i(LOG_TAG_APP_DB, "insert app; uid: $uid, pkg: $pkg") if (ai != null) { // uid may be different from the one in ai, if the app is installed in a different user insertApp(ai) @@ -583,6 +583,7 @@ internal constructor( Constants.NOTIF_INTENT_EXTRA_NEW_APP_NAME, Constants.NOTIF_INTENT_EXTRA_NEW_APP_VALUE ) + intent.putExtra(Constants.NOTIF_INTENT_EXTRA_APP_UID, app.uid) val pendingIntent = getActivityPendingIntent( @@ -664,7 +665,7 @@ internal constructor( val intent = Intent(ctx, HomeScreenActivity::class.java) val nm = ctx.getSystemService(VpnService.NOTIFICATION_SERVICE) as NotificationManager val pendingIntent = - Utilities.getActivityPendingIntent( + getActivityPendingIntent( ctx, intent, PendingIntent.FLAG_UPDATE_CURRENT, diff --git a/app/src/main/java/com/celzero/bravedns/database/RethinkLocalFileTag.kt b/app/src/main/java/com/celzero/bravedns/database/RethinkLocalFileTag.kt index a03df18fa..d77e1f0a6 100644 --- a/app/src/main/java/com/celzero/bravedns/database/RethinkLocalFileTag.kt +++ b/app/src/main/java/com/celzero/bravedns/database/RethinkLocalFileTag.kt @@ -41,6 +41,7 @@ class RethinkLocalFileTag { override fun equals(other: Any?): Boolean { if (other !is RethinkLocalFileTag) return false if (value != other.value) return false + if (isSelected != other.isSelected) return false return true } diff --git a/app/src/main/java/com/celzero/bravedns/database/RethinkLog.kt b/app/src/main/java/com/celzero/bravedns/database/RethinkLog.kt index 2b42ca626..961a70d6e 100644 --- a/app/src/main/java/com/celzero/bravedns/database/RethinkLog.kt +++ b/app/src/main/java/com/celzero/bravedns/database/RethinkLog.kt @@ -27,6 +27,7 @@ import com.celzero.bravedns.util.Constants.Companion.INIT_TIME_MS Index(value = arrayOf("ipAddress"), unique = false), Index(value = arrayOf("appName"), unique = false), Index(value = arrayOf("dnsQuery"), unique = false), + Index(value = arrayOf("connId"), unique = false) ] ) class RethinkLog { diff --git a/app/src/main/java/com/celzero/bravedns/database/RethinkLogDao.kt b/app/src/main/java/com/celzero/bravedns/database/RethinkLogDao.kt index b4fdce286..37e3d09eb 100644 --- a/app/src/main/java/com/celzero/bravedns/database/RethinkLogDao.kt +++ b/app/src/main/java/com/celzero/bravedns/database/RethinkLogDao.kt @@ -35,7 +35,7 @@ interface RethinkLogDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(log: RethinkLog) @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertBatch(connTrackerList: List) + fun insertBatch(logs: List) @Query( "update RethinkLog set downloadBytes = :downloadBytes, uploadBytes = :uploadBytes, duration = :duration, synack = :synack, message = :message where connId = :connId" @@ -82,9 +82,6 @@ interface RethinkLogDao { ) fun getLogsForAppFiltered(uid: Int, ipAddress: String): PagingSource - @Query("select count(DISTINCT(ipAddress)) from RethinkLog where uid = :uid") - fun getAppConnectionsCount(uid: Int): LiveData - @Query("select * from RethinkLog where isBlocked = 1 order by id desc LIMIT $MAX_LOGS") fun getBlockedConnectionsFiltered(): PagingSource @@ -113,7 +110,39 @@ interface RethinkLogDao { fun getLeastLoggedTime(): Long @Query( - "SELECT uid, SUM(uploadBytes) AS uploadBytes, SUM(downloadBytes) AS downloadBytes FROM RethinkLog where timeStamp >= :fromTime and timeStamp <= :toTime GROUP BY uid" + "SELECT uid, SUM(uploadBytes) AS uploadBytes, SUM(downloadBytes) AS downloadBytes FROM RethinkLog where timeStamp >= :fromTime and timeStamp <= :toTime" + ) + fun getDataUsage(fromTime: Long, toTime: Long): DataUsage + + @Query( + "SELECT uid, '' as ipAddress, port, COUNT(dnsQuery) as count, flag as flag, 0 as blocked, dnsQuery as appOrDnsName FROM RethinkLog WHERE timeStamp > :to and dnsQuery != '' GROUP BY dnsQuery ORDER BY count DESC LIMIT 3" ) - fun getDataUsage(fromTime: Long, toTime: Long): List + fun getDomainLogsLimited(to: Long): PagingSource + + @Query( + "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, '' as appOrDnsName FROM RethinkLog WHERE timeStamp > :to GROUP BY uid, ipAddress, port ORDER BY count DESC LIMIT 3" + ) + fun getIpLogsLimited(to: Long): PagingSource + + @Query( + "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, GROUP_CONCAT(DISTINCT dnsQuery) as appOrDnsName FROM RethinkLog WHERE timeStamp > :to GROUP BY uid, ipAddress, port ORDER BY count DESC" + ) + fun getIpLogs(to: Long): PagingSource + + @Query( + "SELECT uid, ipAddress, port, COUNT(ipAddress) as count, flag as flag, 0 as blocked, GROUP_CONCAT(DISTINCT dnsQuery) as appOrDnsName FROM RethinkLog WHERE timeStamp > :to and ipAddress like :query GROUP BY uid, ipAddress, port ORDER BY count DESC" + ) + fun getIpLogsFiltered(to: Long, query: String): PagingSource + + @Query( + "SELECT uid, GROUP_CONCAT(DISTINCT ipAddress) as ipAddress, port, COUNT(dnsQuery) as count, flag as flag, 0 as blocked, dnsQuery as appOrDnsName FROM RethinkLog WHERE timeStamp > :to and dnsQuery != '' GROUP BY dnsQuery ORDER BY count DESC" + ) + fun getDomainLogs(to: Long): PagingSource + + + @Query( + "SELECT uid, GROUP_CONCAT(DISTINCT ipAddress) as ipAddress, port, COUNT(dnsQuery) as count, flag as flag, 0 as blocked, dnsQuery as appOrDnsName FROM RethinkLog WHERE timeStamp > :to and dnsQuery != '' and dnsQuery like :query GROUP BY dnsQuery ORDER BY count DESC" + ) + fun getDomainLogsFiltered(to: Long, query: String): PagingSource + } diff --git a/app/src/main/java/com/celzero/bravedns/database/RethinkLogRepository.kt b/app/src/main/java/com/celzero/bravedns/database/RethinkLogRepository.kt index 27cb38f02..9b1fba09f 100644 --- a/app/src/main/java/com/celzero/bravedns/database/RethinkLogRepository.kt +++ b/app/src/main/java/com/celzero/bravedns/database/RethinkLogRepository.kt @@ -17,6 +17,7 @@ package com.celzero.bravedns.database import androidx.lifecycle.LiveData import com.celzero.bravedns.data.ConnectionSummary +import com.celzero.bravedns.data.DataUsage class RethinkLogRepository(private val logDao: RethinkLogDao) { @@ -52,4 +53,8 @@ class RethinkLogRepository(private val logDao: RethinkLogDao) { fun logsCount(): LiveData { return logDao.logsCount() } + + fun getDataUsage(from: Long, to: Long): DataUsage { + return logDao.getDataUsage(from, to) + } } diff --git a/app/src/main/java/com/celzero/bravedns/database/RethinkRemoteFileTag.kt b/app/src/main/java/com/celzero/bravedns/database/RethinkRemoteFileTag.kt index 64a6b8974..0c9367a78 100644 --- a/app/src/main/java/com/celzero/bravedns/database/RethinkRemoteFileTag.kt +++ b/app/src/main/java/com/celzero/bravedns/database/RethinkRemoteFileTag.kt @@ -40,6 +40,7 @@ class RethinkRemoteFileTag { override fun equals(other: Any?): Boolean { if (other !is RethinkRemoteFileTag) return false if (value != other.value) return false + if (isSelected != other.isSelected) return false return true } diff --git a/app/src/main/java/com/celzero/bravedns/database/WgConfigFiles.kt b/app/src/main/java/com/celzero/bravedns/database/WgConfigFiles.kt index 507de02fa..698dc72b4 100644 --- a/app/src/main/java/com/celzero/bravedns/database/WgConfigFiles.kt +++ b/app/src/main/java/com/celzero/bravedns/database/WgConfigFiles.kt @@ -46,11 +46,18 @@ class WgConfigFiles { override fun equals(other: Any?): Boolean { if (other !is WgConfigFiles) return false if (id != other.id) return false + if (name != other.name) return false + if (isActive != other.isActive) return false + if (isCatchAll != other.isCatchAll) return false + if (oneWireGuard != other.oneWireGuard) return false + if (isLockdown != other.isLockdown) return false return true } override fun hashCode(): Int { - return this.id.hashCode() + var result = this.id.hashCode() + result += result * 31 + this.name.hashCode() + return result } @Ignore diff --git a/app/src/main/java/com/celzero/bravedns/net/doh/Race.java b/app/src/main/java/com/celzero/bravedns/net/doh/Race.java index bf25fbb51..82632f87b 100644 --- a/app/src/main/java/com/celzero/bravedns/net/doh/Race.java +++ b/app/src/main/java/com/celzero/bravedns/net/doh/Race.java @@ -84,19 +84,11 @@ synchronized void onCompleted(int index, boolean succeeded) { } - private static class Callback implements Prober.Callback { - private final int index; - private final Collector collector; - - - private Callback(int index, Collector collector) { - this.index = index; - this.collector = collector; - } + private record Callback(int index, Collector collector) implements Prober.Callback { @Override - public void onCompleted(boolean succeeded) { - collector.onCompleted(index, succeeded); + public void onCompleted(boolean succeeded) { + collector.onCompleted(index, succeeded); + } } - } } diff --git a/app/src/main/java/com/celzero/bravedns/net/doh/Transaction.kt b/app/src/main/java/com/celzero/bravedns/net/doh/Transaction.kt index b030ee7ec..69f3ee666 100644 --- a/app/src/main/java/com/celzero/bravedns/net/doh/Transaction.kt +++ b/app/src/main/java/com/celzero/bravedns/net/doh/Transaction.kt @@ -36,6 +36,7 @@ class Transaction { var transportType: TransportType = TransportType.DOH var msg: String = "" var upstreamBlock: Boolean = false + var region: String = "" enum class Status(val id: Long) { START(Backend.Start), diff --git a/app/src/main/java/com/celzero/bravedns/net/go/GoProber.java b/app/src/main/java/com/celzero/bravedns/net/go/GoProber.java index 9000788dc..e6adcb744 100644 --- a/app/src/main/java/com/celzero/bravedns/net/go/GoProber.java +++ b/app/src/main/java/com/celzero/bravedns/net/go/GoProber.java @@ -27,10 +27,8 @@ public class GoProber extends Prober { private static final String PROBER_TAG = "Prober"; - private final Context context; public GoProber(Context context) { - this.context = context; } @Override diff --git a/app/src/main/java/com/celzero/bravedns/net/go/GoVpnAdapter.kt b/app/src/main/java/com/celzero/bravedns/net/go/GoVpnAdapter.kt index f24c2a220..053d6da7c 100644 --- a/app/src/main/java/com/celzero/bravedns/net/go/GoVpnAdapter.kt +++ b/app/src/main/java/com/celzero/bravedns/net/go/GoVpnAdapter.kt @@ -21,21 +21,23 @@ import Logger.LOG_TAG_VPN import android.content.Context import android.content.res.Resources import android.os.ParcelFileDescriptor -import android.os.SystemClock import android.widget.Toast import backend.Backend import com.celzero.bravedns.R +import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.data.AppConfig import com.celzero.bravedns.data.AppConfig.Companion.FALLBACK_DNS import com.celzero.bravedns.data.AppConfig.TunnelOptions import com.celzero.bravedns.database.DnsCryptRelayEndpoint import com.celzero.bravedns.database.ProxyEndpoint +import com.celzero.bravedns.scheduler.EnhancedBugReport import com.celzero.bravedns.service.BraveVPNService import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.service.ProxyManager import com.celzero.bravedns.service.ProxyManager.ID_WG_BASE import com.celzero.bravedns.service.RethinkBlocklistManager import com.celzero.bravedns.service.TcpProxyHelper +import com.celzero.bravedns.service.VpnController import com.celzero.bravedns.service.WireguardManager import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Constants.Companion.MAX_ENDPOINT @@ -50,6 +52,7 @@ import com.celzero.bravedns.util.InternetProtocol import com.celzero.bravedns.util.Utilities import com.celzero.bravedns.util.Utilities.blocklistDir import com.celzero.bravedns.util.Utilities.blocklistFile +import com.celzero.bravedns.util.Utilities.isAtleastS import com.celzero.bravedns.util.Utilities.isPlayStoreFlavour import com.celzero.bravedns.util.Utilities.showToastUiCentered import com.celzero.bravedns.wireguard.Config @@ -57,12 +60,11 @@ import intra.Intra import intra.Tunnel import java.net.URI import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import settings.Settings /** * This is a VpnAdapter that captures all traffic and routes it through a go-tun2socks instance with @@ -120,6 +122,11 @@ class GoVpnAdapter : KoinComponent { setRDNS() addTransport() setDnsAlg() + notifyLoopback() + setDialStrategy() + setTransparency() + setCrashFd() + undelegatedDomains() Logger.v(LOG_TAG_VPN, "GoVpnAdapter initResolverProxiesPcap done") } @@ -134,6 +141,17 @@ class GoVpnAdapter : KoinComponent { Logger.v(LOG_TAG_VPN, "GoVpnAdapter setPcapMode done") } + private suspend fun setCrashFd() { + Logger.v(LOG_TAG_VPN, "GoVpnAdapter setCrashFd") + val crashFd = EnhancedBugReport.getFileToWrite(context) + if (crashFd != null) { + val set = Intra.setCrashFd(crashFd.absolutePath) + Logger.i(LOG_TAG_VPN, "set crash fd: $crashFd, set? $set") + } else { + Logger.w(LOG_TAG_VPN, "crash fd is null, cannot set") + } + } + private suspend fun setRoute(tunnelOptions: TunnelOptions) { Logger.v(LOG_TAG_VPN, "GoVpnAdapter setRoute") try { @@ -145,7 +163,7 @@ class GoVpnAdapter : KoinComponent { // on route change, set the tun mode again for ptMode tunnel.setTunMode( - tunnelOptions.tunDnsMode.mode, + tunnelOptions.tunDnsMode.mode.toInt(), tunnelOptions.tunFirewallMode.mode, tunnelOptions.ptMode.id ) @@ -374,6 +392,19 @@ class GoVpnAdapter : KoinComponent { } } + suspend fun unlink() { + if (!tunnel.isConnected) { + Logger.i(LOG_TAG_VPN, "Tunnel NOT connected, skip unlink") + return + } + try { + tunnel.unlink() + Logger.i(LOG_TAG_VPN, "tunnel unlinked") + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err dispose: ${e.message}", e) + } + } + private suspend fun getRdnsStamp(url: String): String { val s = url.split(RETHINKDNS_DOMAIN)[1] val stamp = @@ -406,7 +437,7 @@ class GoVpnAdapter : KoinComponent { private fun setTunnelMode(tunnelOptions: TunnelOptions) { tunnel.setTunMode( - tunnelOptions.tunDnsMode.mode, + tunnelOptions.tunDnsMode.mode.toInt(), tunnelOptions.tunFirewallMode.mode, tunnelOptions.ptMode.id ) @@ -419,7 +450,7 @@ class GoVpnAdapter : KoinComponent { } try { tunnel.setTunMode( - tunnelOptions.tunDnsMode.mode, + tunnelOptions.tunDnsMode.mode.toInt(), tunnelOptions.tunFirewallMode.mode, tunnelOptions.ptMode.id ) @@ -433,7 +464,7 @@ class GoVpnAdapter : KoinComponent { Logger.d(LOG_TAG_VPN, "set brave dns to tunnel (local/remote)") // enable local blocklist if enabled - if (persistentState.blocklistEnabled && !Utilities.isPlayStoreFlavour()) { + if (persistentState.blocklistEnabled && !isPlayStoreFlavour()) { setRDNSLocal() } else { // remove local blocklist, if any @@ -737,7 +768,7 @@ class GoVpnAdapter : KoinComponent { } Logger.i(LOG_TAG_VPN, "remove wireguard proxy with id: $id") } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "error removing wireguard proxy: ${e.message}", e) + Logger.e(LOG_TAG_VPN, "err removing wireguard proxy: ${e.message}", e) } } @@ -755,12 +786,17 @@ class GoVpnAdapter : KoinComponent { val wgConfig = WireguardManager.getConfigById(proxyId) val isOneWg = WireguardManager.getOneWireGuardProxyId() == proxyId - val wgUserSpaceString = wgConfig?.toWgUserspaceString(isOneWg) + val skipListenPort = !isOneWg && persistentState.randomizeListenPort + val wgUserSpaceString = wgConfig?.toWgUserspaceString(skipListenPort) getProxies()?.addProxy(id, wgUserSpaceString) - if (isOneWg) setWireGuardDns(id) + //if (isOneWg) setWireGuardDns(id) + setWireGuardDns(id) + // initiate a ping request to the wg proxy + initiateWgPing(id) Logger.i(LOG_TAG_VPN, "add wireguard proxy with $id; dns? $isOneWg") + Logger.vv(LOG_TAG_VPN, "wg config: $wgUserSpaceString") } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "error adding wireguard proxy: ${e.message}", e) + Logger.e(LOG_TAG_VPN, "err adding wireguard proxy: ${e.message}", e) // do not auto remove failed wg proxy, let the user decide via UI // WireguardManager.disableConfig(id) showWireguardFailureToast( @@ -789,7 +825,7 @@ class GoVpnAdapter : KoinComponent { val res = getProxies()?.refreshProxies() Logger.i(LOG_TAG_VPN, "refresh proxies: $res") } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "error refreshing proxies: ${e.message}", e) + Logger.e(LOG_TAG_VPN, "err refreshing proxies: ${e.message}", e) } } @@ -802,16 +838,31 @@ class GoVpnAdapter : KoinComponent { val res = getProxies()?.getProxy(id)?.refresh() Logger.i(LOG_TAG_VPN, "refresh proxy($id): $res") } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "error refreshing proxy($id): ${e.message}", e) + Logger.e(LOG_TAG_VPN, "err refreshing proxy($id): ${e.message}", e) } } - suspend fun getProxyStats(id: String): backend.Stats? { + suspend fun getProxyStats(id: String): backend.RouterStats? { return try { val stats = getProxies()?.getProxy(id)?.router()?.stat() stats } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "error getting proxy stats($id): ${e.message}") + Logger.e(LOG_TAG_VPN, "err getting proxy stats($id): ${e.message}") + null + } + } + + fun getNetStat(): backend.NetStat? { + return try { + if (DEBUG) { + // print the stack trace for debugging purposes + Intra.printStack() + } + val stat = tunnel.stat() + Logger.i(LOG_TAG_VPN, "net stat: $stat") + stat + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err getting net stat: ${e.message}") null } } @@ -891,7 +942,8 @@ class GoVpnAdapter : KoinComponent { suspend fun closeTun() { try { if (tunnel.isConnected) { - Logger.i(LOG_TAG_VPN, "Tunnel disconnect") + // this is not the only place where tunnel is disconnected + // netstack can also close the tunnel on errors tunnel.disconnect() } else { Logger.i(LOG_TAG_VPN, "Tunnel already disconnected") @@ -965,6 +1017,27 @@ class GoVpnAdapter : KoinComponent { } } + suspend fun getSystemDns(): String { + return try { + val sysDns = getResolver()?.get(Backend.System)?.addr ?: "" + Logger.i(LOG_TAG_VPN, "get system dns: $sysDns") + sysDns + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err get system dns: ${e.message}", e) + "" + } + } + + suspend fun notifyLoopback() { + val t = persistentState.routeRethinkInRethink || VpnController.isVpnLockdown() + try { + Intra.loopback(t) + Logger.i(LOG_TAG_VPN, "notify loopback? $t") + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err notify loopback? $t, ${e.message}", e) + } + } + suspend fun setSystemDns(systemDns: List) { if (!tunnel.isConnected) { Logger.i(LOG_TAG_VPN, "Tunnel NOT connected, skip setting system-dns") @@ -994,7 +1067,7 @@ class GoVpnAdapter : KoinComponent { proto: Long ): Boolean { if (!tunnel.isConnected) { - Logger.e(LOG_TAG_VPN, "updateLink: tunFd is null, returning") + Logger.e(LOG_TAG_VPN, "updateLink: tunnel disconnected, returning") return false } Logger.i(LOG_TAG_VPN, "updateLink with fd(${tunFd.fd}) mtu: ${opts.mtu}") @@ -1056,7 +1129,7 @@ class GoVpnAdapter : KoinComponent { } private suspend fun resetLocalBlocklistStampFromTunnel() { - if (Utilities.isPlayStoreFlavour()) return + if (isPlayStoreFlavour()) return try { val rl = getRDNS(RethinkBlocklistManager.RethinkBlocklistType.LOCAL) @@ -1122,10 +1195,11 @@ class GoVpnAdapter : KoinComponent { } fun setLogLevel(level: Int) { - // 0 - very verbose, 1 - verbose, 2 - debug, 3 - info, 4 - warn, 5 - error, 6 - stacktrace, 7 - none - // TODO: setting for console log level?, set as STACKTRACE for now - Intra.logLevel(level, Logger.LoggerType.STACKTRACE.id) - Logger.i(LOG_TAG_VPN, "set log level: $level, stacktrace") + // 0 - very verbose, 1 - verbose, 2 - debug, 3 - info, 4 - warn, 5 - error, 6 - stacktrace, 7 - user, 8 - none + // from UI, if none is selected, set the log level to 7 (user), usr will send only + // user notifications + Intra.logLevel(level, level) + Logger.i(LOG_TAG_VPN, "set go-log level: ${Logger.LoggerType.fromId(level)}") } } @@ -1177,7 +1251,6 @@ class GoVpnAdapter : KoinComponent { Logger.w(LOG_TAG_VPN, "Tunnel NOT connected, skip get proxies") return null } - val t = System.currentTimeMillis() val px = tunnel.proxies return px } catch (e: Exception) { @@ -1186,14 +1259,14 @@ class GoVpnAdapter : KoinComponent { return null } - suspend fun canRouteIp(wgId: String, ip: String, default: Boolean): Boolean { + suspend fun canRouteIp(proxyId: String, ip: String, default: Boolean): Boolean { return try { - val router = getProxies()?.getProxy(wgId)?.router() ?: return default + val router = getProxies()?.getProxy(proxyId)?.router() ?: return default val res = router.contains(ip) - Logger.i(LOG_TAG_VPN, "canRouteIp($wgId, $ip), res? $res") + Logger.d(LOG_TAG_VPN, "canRouteIp($proxyId, $ip), res? $res") res } catch (e: Exception) { - Logger.w(LOG_TAG_VPN, "err canRouteIp($wgId, $ip): ${e.message}") + Logger.w(LOG_TAG_VPN, "err canRouteIp($proxyId, $ip): ${e.message}") default } } @@ -1203,7 +1276,7 @@ class GoVpnAdapter : KoinComponent { val router = getProxies()?.getProxy(proxyId)?.router() ?: return Pair(false, false) val has4 = router.iP4() val has6 = router.iP6() - Logger.i(LOG_TAG_VPN, "supported ip version($proxyId): has4? $has4, has6? $has6") + Logger.d(LOG_TAG_VPN, "supported ip version($proxyId): has4? $has4, has6? $has6") return Pair(has4, has6) } catch (e: Exception) { Logger.w(LOG_TAG_VPN, "err supported ip version($proxyId): ${e.message}") @@ -1228,7 +1301,7 @@ class GoVpnAdapter : KoinComponent { false } - Logger.i( + Logger.d( LOG_TAG_VPN, "split tunnel proxy($proxyId): ipv4? ${pair.first}, ipv6? ${pair.second}, res? $res" ) @@ -1239,12 +1312,12 @@ class GoVpnAdapter : KoinComponent { } } - suspend fun initiateWgPing(wgId: String) { + suspend fun initiateWgPing(proxyId: String) { try { - val res = getProxies()?.getProxy(wgId)?.ping() - Logger.i(LOG_TAG_VPN, "initiateWgPing($wgId): $res") + val res = getProxies()?.getProxy(proxyId)?.ping() + Logger.i(LOG_TAG_VPN, "initiateWgPing($proxyId): $res") } catch (e: Exception) { - Logger.w(LOG_TAG_VPN, "err initiateWgPing($wgId): ${e.message}") + Logger.w(LOG_TAG_VPN, "err initiateWgPing($proxyId): ${e.message}") } } @@ -1268,9 +1341,9 @@ class GoVpnAdapter : KoinComponent { Logger.i(LOG_TAG_VPN, "low memory, called Intra.lowMem()") } - suspend fun goBuildVersion(): String { + fun goBuildVersion(full: Boolean = false): String { return try { - val version = Intra.build() + val version = Intra.build(full) Logger.i(LOG_TAG_VPN, "go build version: $version") version } catch (e: Exception) { @@ -1279,6 +1352,61 @@ class GoVpnAdapter : KoinComponent { } } + suspend fun setDialStrategy(mode: Int = persistentState.dialStrategy, retry: Int = persistentState.retryStrategy, tcpKeepAlive: Boolean = persistentState.tcpKeepAlive) { + if (!tunnel.isConnected) { + Logger.i(LOG_TAG_VPN, "Tunnel NOT connected, skip set dial strategy") + return + } + try { + Settings.setDialerOpts(mode, retry, tcpKeepAlive) + Logger.i(LOG_TAG_VPN, "set dial strategy: $mode, retry: $retry, tcpKeepAlive: $tcpKeepAlive") + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err set dial strategy: ${e.message}", e) + } + } + + suspend fun setTransparency(eim: Boolean = persistentState.endpointIndependence) { + if (!tunnel.isConnected) { + Logger.i(LOG_TAG_VPN, "Tunnel NOT connected, skip transparency") + return + } + if (!isAtleastS()) { + Intra.transparency(false, false) + Logger.i(LOG_TAG_VPN, "Android version is less than S, set transparency: false") + return + } + // set both endpoint independent mapping and endpoint independent filtering + // as a single value, as for the user is concerned, it is a single setting + Intra.transparency(eim, eim) + Logger.i(LOG_TAG_VPN, "set transparency: $eim") + } + + suspend fun undelegatedDomains(useSystemDns: Boolean = persistentState.useSystemDnsForUndelegatedDomains) { + if (!tunnel.isConnected) { + Logger.i(LOG_TAG_VPN, "Tunnel NOT connected, skip undelegated domains") + return + } + try { + Intra.undelegatedDomains(useSystemDns) + Logger.i(LOG_TAG_VPN, "undelegated domains: $useSystemDns") + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err undelegated domains: ${e.message}", e) + } + } + + suspend fun setSlowdownMode(slowdown: Boolean = persistentState.slowdownMode) { + if (!tunnel.isConnected) { + Logger.i(LOG_TAG_VPN, "Tunnel NOT connected, skip slowdown mode") + return + } + try { + Intra.slowdown(slowdown) + Logger.i(LOG_TAG_VPN, "set slowdown mode: $slowdown") + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err slowdown mode: ${e.message}", e) + } + } + private fun ui(f: suspend () -> Unit) { externalScope.launch(Dispatchers.Main) { f() } } diff --git a/app/src/main/java/com/celzero/bravedns/net/manager/ConnectionTracer.kt b/app/src/main/java/com/celzero/bravedns/net/manager/ConnectionTracer.kt index 8e6b8ff71..6f456fa27 100644 --- a/app/src/main/java/com/celzero/bravedns/net/manager/ConnectionTracer.kt +++ b/app/src/main/java/com/celzero/bravedns/net/manager/ConnectionTracer.kt @@ -26,34 +26,46 @@ class ConnectionTracer(ctx: Context) { private const val SEPARATOR = "|" } - private val cm: ConnectivityManager - private val uidCache: Cache - - init { - cm = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - // Cache the UID for the next 60 seconds. - // the UID will expire after 60 seconds of the write. - // Key for the cache is protocol, local, remote - uidCache = - CacheBuilder.newBuilder() - .maximumSize(CACHE_BUILDER_MAX_SIZE) - .expireAfterWrite(CACHE_BUILDER_WRITE_EXPIRE_SEC, TimeUnit.SECONDS) - .build() + enum class CallerSrc { + PREFLOW, + INFLOW, + FLOW } + private val cm: ConnectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + // Cache the UID for the next 60 seconds. + // the UID will expire after 60 seconds of the write. + // Key for the cache is protocol, local, remote + private val uidCache: Cache = CacheBuilder.newBuilder() + .maximumSize(CACHE_BUILDER_MAX_SIZE) + .expireAfterWrite(CACHE_BUILDER_WRITE_EXPIRE_SEC, TimeUnit.SECONDS) + .build() + @TargetApi(Build.VERSION_CODES.Q) suspend fun getUidQ( - protocol: Int, + proto: Int, sourceIp: String, - sourcePort: Int, + sport: Int, destIp: String, - destPort: Int + dport: Int, + caller: CallerSrc ): Int { var uid = Constants.INVALID_UID - // android.googlesource.com/platform/development/+/da84168fb/ndk/platforms/android-21/include/linux/in.h - if (protocol != Protocol.TCP.protocolType && protocol != Protocol.UDP.protocolType) { + var sourcePort = sport + var destPort = dport + var protocol = proto + + // in-case of ICMP, change the protocol to UDP and source/dest port to 0 + // ref: github.com/Gedsh/InviZible/blob/82a0618662ed2fec0fcb6ec55d030d1b76155924/tordnscrypt/src/main/java/pan/alexander/tordnscrypt/vpn/service/ServiceVPN.java#L540C26-L540C30 + if (protocol == Protocol.ICMP.protocolType || protocol == Protocol.ICMPV6.protocolType) { + sourcePort = 0 + destPort = 0 + protocol = Protocol.UDP.protocolType + } else if (protocol != Protocol.TCP.protocolType && protocol != Protocol.UDP.protocolType) { + // android.googlesource.com/platform/development/+/da84168fb/ndk/platforms/android-21/include/linux/in.h return uid } + val local: InetSocketAddress val remote: InetSocketAddress try { @@ -98,7 +110,7 @@ class ConnectionTracer(ctx: Context) { Logger.e(LOG_TAG_VPN, "err getUidQ: " + ex.message, ex) } - if (retryRequired(uid, protocol, destIp, key)){ + if (retryRequired(uid, protocol, destIp, key, caller)){ // change the destination IP to unspecified IP and try again for unconnected UDP val dip = if (IPAddressString(destIp).isIPv6) { @@ -107,7 +119,7 @@ class ConnectionTracer(ctx: Context) { Constants.UNSPECIFIED_IP_IPV4 } val dport = 0 - val res = getUidQ(protocol, sourceIp, sourcePort, dip, dport) + val res = getUidQ(protocol, sourceIp, sourcePort, dip, dport, caller) Logger.d( LOG_TAG_VPN, "retrying with: $protocol, $sourceIp, $sourcePort, $dip, $dport old($destIp, $destPort), res: $res" @@ -121,7 +133,7 @@ class ConnectionTracer(ctx: Context) { } // handle unconnected UDP requests - private fun retryRequired(uid: Int, protocol: Int, destIp: String, key: String): Boolean { + private fun retryRequired(uid: Int, protocol: Int, destIp: String, key: String, caller: CallerSrc): Boolean { if (uid != Constants.INVALID_UID) { // already got the uid, no need to retry return false } @@ -130,7 +142,7 @@ class ConnectionTracer(ctx: Context) { return false } // no need to retry for protocols other than UDP - if (protocol != Protocol.UDP.protocolType) { + if (protocol != Protocol.UDP.protocolType && caller != CallerSrc.INFLOW) { return false } diff --git a/app/src/main/java/com/celzero/bravedns/scheduler/DataUsageUpdater.kt b/app/src/main/java/com/celzero/bravedns/scheduler/DataUsageUpdater.kt index be566a5c0..1f023847c 100644 --- a/app/src/main/java/com/celzero/bravedns/scheduler/DataUsageUpdater.kt +++ b/app/src/main/java/com/celzero/bravedns/scheduler/DataUsageUpdater.kt @@ -22,6 +22,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.celzero.bravedns.database.AppInfoRepository import com.celzero.bravedns.database.ConnectionTrackerRepository +import com.celzero.bravedns.database.RethinkLogRepository import com.celzero.bravedns.service.PersistentState import com.celzero.bravedns.util.Constants import org.koin.core.component.KoinComponent @@ -30,6 +31,7 @@ import org.koin.core.component.inject class DataUsageUpdater(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams), KoinComponent { private val connTrackRepository by inject() + private val rethinkDb by inject() private val appInfoRepository by inject() private val persistentState by inject() @@ -54,10 +56,38 @@ class DataUsageUpdater(context: Context, workerParams: WorkerParameters) : Logger.d(LOG_TAG_SCHEDULER, "Data usage for ${it.uid}, $upload, $download") appInfoRepository.updateDataUsageByUid(it.uid, upload, download) } catch (e: Exception) { - Logger.e(LOG_TAG_SCHEDULER, "Exception in data usage updater: ${e.message}", e) + Logger.e(LOG_TAG_SCHEDULER, "err in data usage updater: ${e.message}", e) } } + + updateRethinkDataUsage(previousTimestamp, currentTimestamp) + persistentState.prevDataUsageCheck = currentTimestamp Logger.i(LOG_TAG_SCHEDULER, "Data usage updated for all apps at $currentTimestamp") } + + private suspend fun updateRethinkDataUsage(prev: Long, curr: Long) { + try { + // get rethink's uid from the database + val uid = + appInfoRepository.getAppInfoUidForPackageName(Constants.RETHINK_PACKAGE) + + val prevDataUsage = rethinkDb.getDataUsage(prev, curr) + val currDataUsage = appInfoRepository.getDataUsageByUid(uid) + + if (currDataUsage.uploadBytes == 0L && currDataUsage.downloadBytes == 0L) { + // if the data usage is 0, then no need to update the database + Logger.d(LOG_TAG_SCHEDULER, "rinr, data usage is 0 for $uid") + return + } + + val upload = currDataUsage.uploadBytes + prevDataUsage.uploadBytes + val download = currDataUsage.downloadBytes + prevDataUsage.downloadBytes + + Logger.d(LOG_TAG_SCHEDULER, "rinr, data usage:($uid), $upload, $download") + appInfoRepository.updateDataUsageByUid(uid, upload, download) + } catch (e: Exception) { + Logger.e(LOG_TAG_SCHEDULER, "err in rinr data usage updater: ${e.message}", e) + } + } } diff --git a/app/src/main/java/com/celzero/bravedns/service/BraveTileService.kt b/app/src/main/java/com/celzero/bravedns/service/BraveTileService.kt index 1fec5169d..3d3128fdc 100644 --- a/app/src/main/java/com/celzero/bravedns/service/BraveTileService.kt +++ b/app/src/main/java/com/celzero/bravedns/service/BraveTileService.kt @@ -78,7 +78,7 @@ class BraveTileService : TileService(), KoinComponent { val isAppLockEnabled = persistentState.biometricAuth // do not start or stop VPN if app lock is enabled if (VpnController.state().activationRequested && !isAppLockEnabled) { - VpnController.stop(this) + VpnController.stop("tile",this) } else if (VpnService.prepare(this) == null && !isAppLockEnabled) { // Start VPN service when VPN permission has been granted VpnController.start(this) diff --git a/app/src/main/java/com/celzero/bravedns/service/BraveVPNService.kt b/app/src/main/java/com/celzero/bravedns/service/BraveVPNService.kt index ee930c27d..18bc4d5e7 100644 --- a/app/src/main/java/com/celzero/bravedns/service/BraveVPNService.kt +++ b/app/src/main/java/com/celzero/bravedns/service/BraveVPNService.kt @@ -17,6 +17,7 @@ package com.celzero.bravedns.service import Logger +import Logger.LOG_BATCH_LOGGER import Logger.LOG_GO_LOGGER import Logger.LOG_TAG_VPN import android.app.ActivityManager @@ -57,11 +58,13 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import backend.Backend +import backend.DNSOpts import backend.RDNS -import backend.Stats +import backend.RouterStats import com.celzero.bravedns.R import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.data.AppConfig @@ -69,15 +72,18 @@ import com.celzero.bravedns.data.ConnTrackerMetaData import com.celzero.bravedns.data.ConnectionSummary import com.celzero.bravedns.database.AppInfo import com.celzero.bravedns.database.ConnectionTracker +import com.celzero.bravedns.database.ConsoleLog import com.celzero.bravedns.database.RefreshDatabase import com.celzero.bravedns.net.go.GoVpnAdapter import com.celzero.bravedns.net.manager.ConnectionTracer import com.celzero.bravedns.receiver.NotificationActionReceiver import com.celzero.bravedns.scheduler.EnhancedBugReport import com.celzero.bravedns.service.FirewallManager.NOTIF_CHANNEL_ID_FIREWALL_ALERTS +import com.celzero.bravedns.service.ProxyManager.ID_WG_BASE import com.celzero.bravedns.ui.HomeScreenActivity import com.celzero.bravedns.ui.NotificationHandlerDialog import com.celzero.bravedns.util.BackgroundAccessibilityService +import com.celzero.bravedns.util.CoFactory import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Constants.Companion.INIT_TIME_MS import com.celzero.bravedns.util.Constants.Companion.INVALID_UID @@ -85,6 +91,7 @@ import com.celzero.bravedns.util.Constants.Companion.NOTIF_INTENT_EXTRA_ACCESSIB import com.celzero.bravedns.util.Constants.Companion.NOTIF_INTENT_EXTRA_ACCESSIBILITY_VALUE import com.celzero.bravedns.util.Constants.Companion.PRIMARY_USER import com.celzero.bravedns.util.Constants.Companion.UID_EVERYBODY +import com.celzero.bravedns.util.Daemons import com.celzero.bravedns.util.IPUtil import com.celzero.bravedns.util.InternetProtocol import com.celzero.bravedns.util.KnownPorts @@ -106,9 +113,12 @@ import com.google.common.collect.Sets import inet.ipaddr.HostName import inet.ipaddr.IPAddressString import intra.Bridge +import intra.Mark +import intra.PreMark import intra.SocketSummary import java.io.IOException import java.net.InetAddress +import java.net.Socket import java.net.SocketException import java.net.UnknownHostException import java.util.Collections @@ -116,15 +126,19 @@ import java.util.Locale import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.cancellation.CancellationException import kotlin.math.abs import kotlin.math.min import kotlin.random.Random +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.async import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -135,9 +149,25 @@ import rnet.Tab class BraveVPNService : VpnService(), ConnectionMonitor.NetworkListener, Bridge, OnSharedPreferenceChangeListener { + private val vpnScope = MainScope() + private var connectionMonitor: ConnectionMonitor = ConnectionMonitor(this) + + // multiple coroutines call both signalStopService and makeOrUpdateVpnAdapter and so + // set and unset this variable on the serializer thread + @Volatile private var vpnAdapter: GoVpnAdapter? = null - private var isVpnStarted = false // always accessed on the main thread + + // used mostly for service to adapter creation and updates + private var serializer: CoroutineDispatcher = Daemons.make("vpnser") + + private var flowDispatcher = Daemons.ioDispatcher("flow", Mark(), vpnScope) + private var inflowDispatcher = Daemons.ioDispatcher("inflow", Mark(), vpnScope) + private var preflowDispatcher = Daemons.ioDispatcher("preflow", PreMark(), vpnScope) + private var dnsQueryDispatcher = Daemons.ioDispatcher("onQuery", DNSOpts(), vpnScope) + + // channel to perform wg handshakes + private val wgHandshakeChannel = Channel(Channel.CONFLATED) companion object { const val SERVICE_ID = 1 // Only has to be unique within this app. @@ -171,6 +201,9 @@ class BraveVPNService : // TODO: add routes as normal but do not send fd to netstack // repopulateTrackedNetworks also fails open see isAnyNwValidated const val FAIL_OPEN_ON_NO_NETWORK = true + + // route v4 in v6 only networks? + const val ROUTE4IN6 = true } // handshake expiry time for proxy connections @@ -178,8 +211,7 @@ class BraveVPNService : private val wgHandshakeTimeout = TimeUnit.SECONDS.toMillis(160L) private val checkpointInterval = TimeUnit.MINUTES.toMillis(1L) - private var isLockDownPrevious: Boolean = false - private val vpnScope = MainScope() + private var isLockDownPrevious = AtomicBoolean(false) private lateinit var connTracer: ConnectionTracer @@ -215,7 +247,7 @@ class BraveVPNService : private var trackedCids = Collections.newSetFromMap(ConcurrentHashMap()) // store proxyids and their handshake times, refresh proxy when handshake time is expired - private val wgHandShakeCheckpoints: ConcurrentHashMap = ConcurrentHashMap() + private val wgHandShakeCheckpoints: HashMap = HashMap() // data class to store the connection summary data class CidKey(val cid: String, val uid: Int) @@ -231,6 +263,9 @@ class BraveVPNService : private var accessibilityListener: AccessibilityManager.AccessibilityStateChangeListener? = null + // live-data to store the region received from onResponse + val regionLiveData: MutableLiveData = MutableLiveData() + data class OverlayNetworks( val has4: Boolean = false, val has6: Boolean = false, @@ -254,12 +289,27 @@ class BraveVPNService : APP_ERROR } + data class ProxyOperationData(val proxyId: String, val action: ProxyOperation) + + enum class ProxyOperation { + ADD, + REMOVE, + CLEAR, + REFRESH + } + private fun logd(msg: String) { Logger.d(LOG_TAG_VPN, msg) } override fun bind4(who: String, addrPort: String, fid: Long) { - return bindAny(who, addrPort, fid, underlyingNetworks?.ipv4Net ?: emptyList()) + var v4Net = underlyingNetworks?.ipv4Net + val isAuto = InternetProtocol.isAuto(persistentState.internetProtocolType) + if (ROUTE4IN6 && isAuto && v4Net.isNullOrEmpty()) { + v4Net = underlyingNetworks?.ipv6Net + } + + return bindAny(who, addrPort, fid, v4Net ?: emptyList()) } override fun bind6(who: String, addrPort: String, fid: Long) { @@ -299,8 +349,9 @@ class BraveVPNService : // in case of zero, bind only for wg connections, wireguard tries to bind to // network with zero addresses if ( - (destIp.isZero && who.startsWith(ProxyManager.ID_WG_BASE)) || - destIp.isZero || destIp.isLoopback + (destIp.isZero && who.startsWith(ID_WG_BASE)) || + destIp.isZero || + destIp.isLoopback ) { logd("bind: invalid destIp: $destIp, who: $who, $addrPort") return @@ -347,6 +398,15 @@ class BraveVPNService : } } + suspend fun probeIp(ip: String): ConnectionMonitor.ProbeResult? { + return connectionMonitor.probeIp(ip) + } + + fun protectSocket(socket: Socket) { + this.protect(socket) + Logger.v(LOG_TAG_VPN, "socket protected") + } + override fun protect(who: String?, fd: Long) { val rinr = persistentState.routeRethinkInRethink logd("protect: $who, $fd, rinr? $rinr") @@ -358,18 +418,20 @@ class BraveVPNService : } private suspend fun getUid( - _uid: Long, + recdUid: Int, protocol: Int, srcIp: String, srcPort: Int, dstIp: String, - dstPort: Int + dstPort: Int, + caller: ConnectionTracer.CallerSrc ): Int { + // caller: true - called from flow, false - called from inflow return if (VERSION.SDK_INT >= VERSION_CODES.Q) { - ioAsync("getUidQ") { connTracer.getUidQ(protocol, srcIp, srcPort, dstIp, dstPort) } + ioAsync("getUidQ") { connTracer.getUidQ(protocol, srcIp, srcPort, dstIp, dstPort, caller) } .await() } else { - _uid.toInt() // uid must have been retrieved from procfs by the caller + recdUid // uid must have been retrieved from procfs by the caller } } @@ -1009,6 +1071,7 @@ class BraveVPNService : // underlying networks is set to null, which prompts Android to set it to whatever is // the current active network. Later, ConnectionMonitor#onVpnStarted, depending on user // chosen preferences, sets appropriate underlying network/s. + Logger.d(LOG_TAG_VPN, "builder: useActive, set underlying networks to null") builder.setUnderlyingNetworks(null) } else { // add ipv4 and ipv6 networks to the tunnel @@ -1016,11 +1079,14 @@ class BraveVPNService : val ipv6 = curnet.ipv6Net.map { it.network } val allNetworks = ipv4.plus(ipv6) if (allNetworks.isNotEmpty()) { + Logger.d(LOG_TAG_VPN, "builder: underlying networks: ${allNetworks.size}") builder.setUnderlyingNetworks(allNetworks.toTypedArray()) } else { if (FAIL_OPEN_ON_NO_NETWORK) { + Logger.d(LOG_TAG_VPN, "builder: no networks, set underlying networks to null") builder.setUnderlyingNetworks(null) } else { + Logger.d(LOG_TAG_VPN, "builder: no networks, set underlying networks to empty") builder.setUnderlyingNetworks(emptyArray()) } } @@ -1158,7 +1224,11 @@ class BraveVPNService : connTracer = ConnectionTracer(this) VpnController.onVpnCreated(this) - io("loggers") { netLogTracker.restart(vpnScope) } + io("nlt") { + Log.d(LOG_BATCH_LOGGER, "vpn: restart $vpnScope") + netLogTracker.restart(vpnScope) + } + createWgHandshakeChannel() notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager activityManager = this.getSystemService(ACTIVITY_SERVICE) as ActivityManager @@ -1184,7 +1254,7 @@ class BraveVPNService : } private fun makeDnscryptRelayObserver(): Observer { - return Observer { t -> + return Observer { t -> io("dnscryptRelay") { if (t.added) { vpnAdapter?.addDnscryptRelay(t.relay) @@ -1196,7 +1266,7 @@ class BraveVPNService : } private fun makeAppInfoObserver(): Observer> { - return Observer> { t -> + return Observer { t -> try { var latestExcludedApps: Set // adding synchronized block, found a case of concurrent modification @@ -1230,7 +1300,7 @@ class BraveVPNService : } private fun makeOrbotStartStatusObserver(): Observer { - return Observer { settingUpOrbot.set(it) } + return Observer { settingUpOrbot.set(it) } } private fun updateNotificationBuilder(): Notification { @@ -1282,14 +1352,17 @@ class BraveVPNService : // 3. No action button. val isAppLockEnabled = persistentState.biometricAuth && !isAppRunningOnTv() // do not show notification action when app lock is enabled - val notifActionType = if (isAppLockEnabled) { - NotificationActionType.NONE - } else { - NotificationActionType.getNotificationActionType( - persistentState.notificationActionType - ) - } - logd("notification action type: ${persistentState.notificationActionType}, $notifActionType") + val notifActionType = + if (isAppLockEnabled) { + NotificationActionType.NONE + } else { + NotificationActionType.getNotificationActionType( + persistentState.notificationActionType + ) + } + logd( + "notification action type: ${persistentState.notificationActionType}, $notifActionType" + ) when (notifActionType) { NotificationActionType.PAUSE_STOP -> { @@ -1437,19 +1510,19 @@ class BraveVPNService : if (isAtleastU()) { var ok = startForegroundService(FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED) if (!ok) { - Logger.i(LOG_TAG_VPN, "start service failed, retrying with connected device") + Logger.w(LOG_TAG_VPN, "start service failed, retrying with connected device") ok = startForegroundService(FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE) } if (!ok) { - Logger.i(LOG_TAG_VPN, "start service failed, stopping service") - signalStopService(userInitiated = false) // notify and stop + Logger.w(LOG_TAG_VPN, "start service failed, stopping service") + signalStopService("startFg1", userInitiated = false) // notify and stop return@ui } } else { val ok = startForegroundService() if (!ok) { - Logger.i(LOG_TAG_VPN, "start service failed ( > U ), stopping service") - signalStopService(userInitiated = false) // notify and stop + Logger.w(LOG_TAG_VPN, "start service failed ( > U ), stopping service") + signalStopService("startFg2", userInitiated = false) // notify and stop return@ui } } @@ -1460,17 +1533,13 @@ class BraveVPNService : // see restartVpn and updateTun which expect this to be the case persistentState.setVpnEnabled(true) - var isNewVpn = false + val isNewVpn = connectionMonitor.onVpnStart(this) - if (!isVpnStarted) { - Logger.i(LOG_TAG_VPN, "new vpn") - isVpnStarted = true - isNewVpn = true - connectionMonitor.onVpnStart(this) - } - - if (isVpnStarted) { + if (isNewVpn) { + // clear the underlying networks, so that the new vpn can be created with the + // current active network. underlyingNetworks = null + Logger.i(LOG_TAG_VPN, "new vpn") } val mtu = mtu() @@ -1482,7 +1551,7 @@ class BraveVPNService : mtu ) - Logger.i(LOG_TAG_VPN, "start-foreground with opts $opts (for new-vpn? $isNewVpn)") + Logger.i(LOG_TAG_VPN, "start-fg with opts $opts (for new-vpn? $isNewVpn)") if (!isNewVpn) { io("tunUpdate") { // may call signalStopService(userInitiated=false) if go-vpn-adapter is missing @@ -1496,7 +1565,7 @@ class BraveVPNService : // refresh should happen before restartVpn, otherwise the new vpn will not // have app, ip, domain rules. See RefreshDatabase#refresh rdb.refresh(RefreshDatabase.ACTION_REFRESH_AUTO) { - restartVpn(opts, Networks(null, overlayNetworks), why = "startVpn") + restartVpn(this, opts, Networks(null, overlayNetworks), why = "startVpn") // call this *after* a new vpn is created #512 uiCtx("observers") { observeChanges() } } @@ -1623,7 +1692,7 @@ class BraveVPNService : // conn-monitor and go-vpn-adapter exist, but persistent-state tracking vpn goes out // of sync Logger.e(LOG_TAG_VPN, "stop-vpn(updateTun), tracking vpn is out of sync") - io("outOfSync") { signalStopService(userInitiated = false) } + io("outOfSync") { signalStopService("outOfSync", userInitiated = false) } return } @@ -1633,7 +1702,7 @@ class BraveVPNService : // VpnController#onStartComplete if (ok == false) { Logger.w(LOG_TAG_VPN, "Cannot handle vpn adapter changes, no tunnel") - io("noTunnel") { signalStopService(userInitiated = false) } + io("noTunnel") { signalStopService("noTunnel", userInitiated = false) } return } notifyConnectionStateChangeIfNeeded() @@ -1790,6 +1859,7 @@ class BraveVPNService : // restart vpn to allow/disallow rethink traffic in rethink io("routeRethinkInRethink") { restartVpnWithNewAppConfig(reason = "routeRethinkInRethink") + vpnAdapter?.notifyLoopback() } } @@ -1820,9 +1890,64 @@ class BraveVPNService : // no-op, no need to restart vpn as no proxy/dns proxy is enabled } } + + PersistentState.ANTI_CENSORSHIP_TYPE -> { + io("antiCensorship") { + setDialStrategy() + } + } + + PersistentState.RETRY_STRATEGY -> { + io("retryStrategy") { + setDialStrategy() + } + } + PersistentState.ENDPOINT_INDEPENDENCE -> { + io("endpointIndependence") { + setTransparency() + } + } + PersistentState.TCP_KEEP_ALIVE -> { + io("tcpKeepAlive") { + setDialStrategy() + } + } + PersistentState.USE_SYSTEM_DNS_FOR_UNDELEGATED_DOMAINS -> { + io("useSystemDnsForUndelegatedDomains") { + undelegatedDomains() + } + } + PersistentState.SLOWDOWN_MODE -> { + io("slowdownMode") { + setSlowdownMode() + } + } } } + private suspend fun undelegatedDomains() { + Logger.i(LOG_TAG_VPN, "use system dns for undelegated domains: ${persistentState.useSystemDnsForUndelegatedDomains}") + vpnAdapter?.undelegatedDomains(persistentState.useSystemDnsForUndelegatedDomains) + } + + private suspend fun setDialStrategy() { + Logger.d( + LOG_TAG_VPN, + "set dial strategy: ${persistentState.dialStrategy}, retry: ${persistentState.retryStrategy}, tcpKeepAlive: ${persistentState.tcpKeepAlive}" + ) + vpnAdapter?.setDialStrategy() + } + + private suspend fun setSlowdownMode() { + Logger.d(LOG_TAG_VPN, "set slowdown mode: ${persistentState.slowdownMode}") + vpnAdapter?.setSlowdownMode() + } + + private suspend fun setTransparency() { + Logger.d(LOG_TAG_VPN, "set endpoint independence: ${persistentState.endpointIndependence}") + vpnAdapter?.setTransparency(persistentState.endpointIndependence) + } + private suspend fun disableAllowBypassIfNeeded() { if (appConfig.isProxyEnabled() && persistentState.allowBypass) { Logger.i(LOG_TAG_VPN, "disabling allowBypass, as proxy is set.") @@ -1915,7 +2040,9 @@ class BraveVPNService : io("dnsStampUpdate") { vpnAdapter?.setRDNSStamp() } } - private suspend fun notifyConnectionMonitor() { + // invoked on pref / probe-ip changes, so that the connection monitor can + // re-initiate the connectivity checks + suspend fun notifyConnectionMonitor() { connectionMonitor.onUserPreferenceChanged() } @@ -1923,24 +2050,24 @@ class BraveVPNService : vpnAdapter?.setDnsAlg() } - fun signalStopService(userInitiated: Boolean = true) { + fun signalStopService(reason: String, userInitiated: Boolean = true) { if (!userInitiated) notifyUserOnVpnFailure() - stopVpnAdapter() + io(reason) { stopVpnAdapter() } stopSelf() - Logger.i(LOG_TAG_VPN, "stopped vpn adapter and vpn service") + Logger.i(LOG_TAG_VPN, "stopped vpn adapter & service: $reason, $userInitiated") } - private fun stopVpnAdapter() { - io("stopVpn") { + private suspend fun stopVpnAdapter() = + withContext(CoroutineName("stopVpn") + serializer) { if (vpnAdapter == null) { Logger.i(LOG_TAG_VPN, "vpn adapter already stopped") - return@io + return@withContext } + vpnAdapter?.closeTun() vpnAdapter = null Logger.i(LOG_TAG_VPN, "stop vpn adapter") } - } private suspend fun restartVpnWithNewAppConfig( underlyingNws: ConnectionMonitor.UnderlyingNetworks? = underlyingNetworks, @@ -1950,6 +2077,7 @@ class BraveVPNService : logd("restart vpn with new app config") val nws = Networks(underlyingNws, overlayNws) restartVpn( + this, appConfig.newTunnelOptions( this, getFakeDns(), @@ -1982,40 +2110,67 @@ class BraveVPNService : vpnAdapter?.setTunMode(tunnelOptions) } - private suspend fun restartVpn(opts: AppConfig.TunnelOptions, networks: Networks, why: String) { - if (!persistentState.getVpnEnabled()) { - // when persistent-state "thinks" vpn is disabled, stop the service, especially when - // we could be here via onStartCommand -> isNewVpn -> restartVpn while both, - // vpn-service & conn-monitor exists & vpn-enabled state goes out of sync - logAndToastIfNeeded( - "$why, stop-vpn(restartVpn), tracking vpn is out of sync", - Log.ERROR + private suspend fun restartVpn( + ctx: Context, + opts: AppConfig.TunnelOptions, + networks: Networks, + why: String + ) = + withContext(CoroutineName(why) + serializer) { + if (!persistentState.getVpnEnabled()) { + // when persistent-state "thinks" vpn is disabled, stop the service, especially when + // we could be here via onStartCommand -> isNewVpn -> restartVpn while both, + // vpn-service & conn-monitor exists & vpn-enabled state goes out of sync + logAndToastIfNeeded( + "$why, stop-vpn(restartVpn), tracking vpn is out of sync", + Log.ERROR + ) + io("outOfSyncRestart") { + signalStopService("outOfSyncRestart", userInitiated = false) + } + return@withContext + } + Logger.i( + LOG_TAG_VPN, + "---------------------------RESTART-INIT----------------------------" ) - io("outOfSyncRestart") { signalStopService(userInitiated = false) } - return - } + // In vpn lockdown mode, unlink the adapter to close the previous file descriptor (fd) + // and use a new fd after creation. This should only be done in lockdown mode, + // as leaks are not possible. + if (VpnController.isVpnLockdown()) { + vpnAdapter?.unlink() + } + // attempt seamless hand-off as described in VpnService.Builder.establish() docs + val tunFd = establishVpn(networks) + if (tunFd == null) { + logAndToastIfNeeded("$why, cannot restart-vpn, no tun-fd", Log.ERROR) + io("noTunRestart1") { signalStopService("noTunRestart1", userInitiated = false) } + return@withContext + } - // attempt seamless hand-off as described in VpnService.Builder.establish() docs - val tunFd = establishVpn(networks) - if (tunFd == null) { - logAndToastIfNeeded("$why, cannot restart-vpn, no tun-fd", Log.ERROR) - io("noTunRestart") { signalStopService(userInitiated = false) } - return - } + val ok = + makeOrUpdateVpnAdapter( + ctx, + tunFd, + opts, + vpnProtos + ) // vpnProtos set in establishVpn() + if (!ok) { + logAndToastIfNeeded("$why, cannot restart-vpn, no vpn-adapter", Log.ERROR) + io("noTunnelRestart2") { signalStopService("noTunRestart2", userInitiated = false) } + return@withContext + } else { + logAndToastIfNeeded("$why, vpn restarted", Log.INFO) + } + Logger.i( + LOG_TAG_VPN, + "---------------------------RESTART-OK----------------------------" + ) - val ok = makeOrUpdateVpnAdapter(tunFd, opts, vpnProtos) // vpnProtos set in establishVpn() - if (!ok) { - logAndToastIfNeeded("$why, cannot restart-vpn, no vpn-adapter", Log.ERROR) - io("noTunnelRestart") { signalStopService(userInitiated = false) } - return - } else { - logAndToastIfNeeded("$why, vpn restarted", Log.INFO) + notifyConnectionStateChangeIfNeeded() + informVpnControllerForProtoChange(vpnProtos) } - notifyConnectionStateChangeIfNeeded() - informVpnControllerForProtoChange(vpnProtos) - } - private suspend fun logAndToastIfNeeded(msg: String, logLevel: Int = Log.WARN) { when (logLevel) { Log.WARN -> Logger.w(LOG_TAG_VPN, msg) @@ -2048,40 +2203,42 @@ class BraveVPNService : } private suspend fun makeOrUpdateVpnAdapter( + ctx: Context, tunFd: ParcelFileDescriptor, opts: AppConfig.TunnelOptions, p: Pair - ): Boolean { - val ok = true - val noTun = false // should eventually call signalStopService(userInitiated=false) - val protos = InternetProtocol.byProtos(p.first, p.second).value() - try { - if (vpnAdapter != null) { - Logger.i(LOG_TAG_VPN, "vpn-adapter exists, use it") - // in case, if vpn-adapter exists, update the existing vpn-adapter - if (vpnAdapter?.updateLinkAndRoutes(tunFd, opts, protos) == false) { - Logger.e(LOG_TAG_VPN, "err update vpn-adapter") - return noTun - } - return ok - } else { - // create a new vpn adapter - vpnAdapter = GoVpnAdapter(this, vpnScope, tunFd, opts) // may throw - GoVpnAdapter.setLogLevel(persistentState.goLoggerLevel.toInt()) - vpnAdapter!!.initResolverProxiesPcap(opts) - return ok - } - } catch (e: Exception) { - Logger.e(LOG_TAG_VPN, "err new vpn-adapter: ${e.message}", e) - return noTun - } finally { - try { // close the tunFd as GoVpnAdapter has its own copy - tunFd.close() - } catch (ignored: IOException) { - Logger.e(LOG_TAG_VPN, "err closing tunFd: ${ignored.message}", ignored) + ): Boolean = + withContext(CoroutineName("makeVpn") + serializer) { + val ok = true + val noTun = false // should eventually call signalStopService(userInitiated=false) + val protos = InternetProtocol.byProtos(p.first, p.second).value() + try { + if (vpnAdapter == null) { + // create a new vpn adapter + vpnAdapter = GoVpnAdapter(ctx, vpnScope, tunFd, opts) // may throw + GoVpnAdapter.setLogLevel(persistentState.goLoggerLevel.toInt()) + vpnAdapter!!.initResolverProxiesPcap(opts) + return@withContext ok + } else { + Logger.i(LOG_TAG_VPN, "vpn-adapter exists, use it") + // in case, if vpn-adapter exists, update the existing vpn-adapter + if (vpnAdapter?.updateLinkAndRoutes(tunFd, opts, protos) == false) { + Logger.e(LOG_TAG_VPN, "err update vpn-adapter") + return@withContext noTun + } + return@withContext ok + } + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "err new vpn-adapter: ${e.message}", e) + return@withContext noTun + } finally { + try { // close the tunFd as GoVpnAdapter has its own copy + tunFd.close() + } catch (ignored: IOException) { + Logger.e(LOG_TAG_VPN, "err closing tunFd: ${ignored.message}", ignored) + } } } - } // TODO: #294 - Figure out a way to show users that the device is offline instead of status as // failing. @@ -2101,7 +2258,7 @@ class BraveVPNService : override fun onNetworkRegistrationFailed() { Logger.i(LOG_TAG_VPN, "recd nw registration failed, stop vpn service with notification") - signalStopService(userInitiated = false) + signalStopService("nwRegFail", userInitiated = false) } override fun onNetworkConnected(networks: ConnectionMonitor.UnderlyingNetworks) { @@ -2110,11 +2267,12 @@ class BraveVPNService : val isRoutesChanged = hasRouteChangedInAutoMode(out) val isBoundNetworksChanged = out.netChanged val isMtuChanged = out.mtuChanged + val prevDns = networks.dnsServers.keys underlyingNetworks = networks Logger.i(LOG_TAG_VPN, "onNetworkConnected: changes: $out for new: $networks") // always reset the system dns server ip of the active network with the tunnel - setNetworkAndDefaultDnsIfNeeded() + setNetworkAndDefaultDnsIfNeeded(prevDns) if (networks.useActive) { setUnderlyingNetworks(null) @@ -2149,14 +2307,16 @@ class BraveVPNService : } } } - // check if already refresh is triggered, if not, trigger refresh + if (!isRoutesChanged && isBoundNetworksChanged) { // Workaround for WireGuard connection issues after network change // WireGuard may fail to connect to the server when the network changes. - // refresh will do a configuration refresh in tunnel to ensure a successful - // reconnection after detecting a network change event Logger.v(LOG_TAG_VPN, "refresh wg after network change") refreshProxies() + // invalidate the catch-all cache when the network changes, ref: #1706 + io("WgCache") { + WireguardManager.invalidateCatchAllCache() + } } // no need to close the existing connections if the bound networks are changed @@ -2201,7 +2361,7 @@ class BraveVPNService : var new = _new // when old and new are null, no changes if (old == null && new == null) { - return NetworkChanges(false, false, false) + return NetworkChanges(routesChanged = false, netChanged = false, mtuChanged = false) } // no old routes to compare with, return true if (old == null) return NetworkChanges() @@ -2266,9 +2426,9 @@ class BraveVPNService : return NetworkChanges(routesChanged, netChanged, mtuChanged) } - private fun setNetworkAndDefaultDnsIfNeeded() { + private fun setNetworkAndDefaultDnsIfNeeded(prevDns: Set? = null) { val currNet = underlyingNetworks - val useActive = currNet == null || currNet.useActive + /*val useActive = currNet == null || currNet.useActive val dnsServers = if ( persistentState.routeRethinkInRethink || // rinr case is same as multiple networks @@ -2283,6 +2443,9 @@ class BraveVPNService : // get dns servers from the first network or active network val active = connectivityManager.activeNetwork val lp = connectivityManager.getLinkProperties(active) + // here dnsServers are validated with underlyingNetworks, so there may be a case + // where v6 address is added when v6 network is not available + // so, dnsServers will have both v4 and v6 addresses val dnsServers = lp?.dnsServers if (dnsServers.isNullOrEmpty()) { @@ -2304,9 +2467,43 @@ class BraveVPNService : Logger.i(LOG_TAG_VPN, "dns servers for network: $dnsServers") dnsServers } - } + }*/ + + // get dns servers from the first network or active network + val active = connectivityManager.activeNetwork + val dnsServers = if (connectivityManager + .getNetworkCapabilities(active) + ?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true) { + Logger.i(LOG_TAG_VPN, "active network is vpn, so no need get dns servers") + mutableListOf() + } else { + val lp = connectivityManager.getLinkProperties(active) + // here dnsServers are validated with underlyingNetworks, so there may be a case + // where v6 address is added when v6 network is not available + // so, dnsServers will have both v4 and v6 addresses + lp?.dnsServers?.toMutableList() ?: mutableListOf() + } + + if (dnsServers.isEmpty()) { + // first network is considered to be active network + val ipv4 = currNet?.ipv4Net?.firstOrNull() + val ipv6 = currNet?.ipv6Net?.firstOrNull() + val dns4 = ipv4?.linkProperties?.dnsServers + val dns6 = ipv6?.linkProperties?.dnsServers + // if active network is not found in the list of networks, then use dns from + // first network + val dl = mutableListOf() + // add all the dns servers from the first network, depending on the current + // route, netstack will make use of the dns servers + dns4?.let { dl.addAll(it) } + dns6?.let { dl.addAll(it) } + Logger.i(LOG_TAG_VPN, "dns servers for network: $dl") + dnsServers.addAll(dl) + } else { + Logger.i(LOG_TAG_VPN, "dns servers for network: $dnsServers") + } - if (dnsServers.isNullOrEmpty()) { + if (dnsServers.isEmpty()) { // TODO: send an alert/notification instead? Logger.w(LOG_TAG_VPN, "No system dns servers found") if (appConfig.isSystemDns()) { @@ -2323,8 +2520,14 @@ class BraveVPNService : } } io("setSystemAndDefaultDns") { - Logger.i(LOG_TAG_VPN, "Setting dns servers: $dnsServers") + val existingDns = determineSystemDns(prevDns?.toList()) val dns = determineSystemDns(dnsServers) + Logger.d(LOG_TAG_VPN, "dns servers: $dns, existing: $existingDns") + if (dns == existingDns) { + Logger.i(LOG_TAG_VPN, "System dns servers are same, no need to set") + return@io + } + Logger.i(LOG_TAG_VPN, "System dns servers changed, set: $dns") // set system dns whenever there is a change in network vpnAdapter?.setSystemDns(dns) // set default dns server for the tunnel if none is set @@ -2353,16 +2556,24 @@ class BraveVPNService : private fun handleVpnLockdownStateAsync() { if (!syncLockdownState()) return + Logger.i(LOG_TAG_VPN, "vpn lockdown mode change, restarting") - io("lockdownSync") { restartVpnWithNewAppConfig(reason = "lockdownSync") } + io("lockdownSync") { + restartVpnWithNewAppConfig(reason = "lockdownSync") + vpnAdapter?.notifyLoopback() + } } private fun syncLockdownState(): Boolean { if (!isAtleastQ()) return false - val ret = isLockdownEnabled != isLockDownPrevious - isLockDownPrevious = this.isLockdownEnabled - return ret + // cannot set the lockdown status while the vpn is being created, it will return false + // until the vpn is created. so the sync will be done after the vpn is created + // when the first flow call is made. + val prev = isLockDownPrevious.get() + if (isLockdownEnabled == prev) return false + + return isLockDownPrevious.compareAndSet(prev, isLockdownEnabled) } private fun notifyUserOnVpnFailure() { @@ -2416,7 +2627,6 @@ class BraveVPNService : stopPauseTimer() // reset the underlying networks underlyingNetworks = null - isVpnStarted = false // reset the vpn state unobserveOrbotStartStatus() unobserveAppInfos() @@ -2429,6 +2639,10 @@ class BraveVPNService : try { vpnScope.cancel("vpnDestroy") } catch (ignored: IllegalStateException) { + } catch ( + ignored: CancellationException + ) { + } catch (ignored: Exception) { } Logger.w(LOG_TAG_VPN, "Destroying VPN service") @@ -2465,11 +2679,13 @@ class BraveVPNService : fun pauseApp() { startPauseTimer() handleVpnServiceOnAppStateChange() + Logger.i(LOG_TAG_VPN, "App paused") } fun resumeApp() { stopPauseTimer() handleVpnServiceOnAppStateChange() + Logger.i(LOG_TAG_VPN, "App resumed") } private fun handleVpnServiceOnAppStateChange() { // paused or resumed @@ -2507,6 +2723,14 @@ class BraveVPNService : var has6 = route6(n) var has4 = route4(n) + val isAuto = InternetProtocol.isAuto(persistentState.internetProtocolType) + // in auto mode, assume v4 route is available if only v6 route is available, which is true + // for scenarios like 464Xlat and other 4to6 translation mechanisms + if (ROUTE4IN6 && isAuto && (has6 && !has4)) { + Logger.w(LOG_TAG_VPN, "Adding v4 route in v6-only network") + has4 = true + } + if (!has4 && !has6 && !n.overlayNws.failOpen) { // When overlay networks has v6 routes but active network has v4 routes // both has4 and has6 will be false and fail-open may open up BOTH routes @@ -2725,12 +2949,14 @@ class BraveVPNService : } private fun addRoute6(b: Builder): Builder { + // TODO: as of now, vpn lockdown mode is not handled, check if this is required if (persistentState.privateIps) { Logger.i(LOG_TAG_VPN, "addRoute6: privateIps is true, adding routes") // exclude LAN traffic, add only unicast routes // add only unicast routes // range 0000:0000:0000:0000:0000:0000:0000:0000- // 0000:0000:0000:0000:ffff:ffff:ffff:ffff + // fixme: see if the ranges overlap with the default route b.addRoute("0000::", 64) b.addRoute("2000::", 3) // 2000:: - 3fff:: b.addRoute("4000::", 3) // 4000:: - 5fff:: @@ -2757,6 +2983,7 @@ class BraveVPNService : } private fun addRoute4(b: Builder): Builder { + // TODO: as of now, vpn lockdown mode is not handled, check if this is required if (persistentState.privateIps) { Logger.i(LOG_TAG_VPN, "addRoute4: privateIps is true, adding routes") // https://developer.android.com/reference/android/net/VpnService.Builder.html#addRoute(java.lang.String,%20int) @@ -2856,6 +3083,12 @@ class BraveVPNService : }*/ } + private fun go2kt(co: CoFactory, f: suspend() -> T): T = runBlocking { + // runBlocking blocks the current thread until all coroutines within it are complete + // an call a suspending function from a non-suspending context and obtain the result. + return@runBlocking co.tryDispatch(f) + } + private fun io(s: String, f: suspend () -> Unit) = vpnScope.launch(CoroutineName(s) + Dispatchers.IO) { f() } @@ -2869,38 +3102,40 @@ class BraveVPNService : return vpnScope.async(CoroutineName(s) + Dispatchers.IO) { f() } } - override fun onQuery(fqdn: String?, qtype: Long): backend.DNSOpts = runBlocking { + override fun onQuery(fqdn: String?, qtype: Long): DNSOpts = go2kt(dnsQueryDispatcher) { // queryType: see ResourceRecordTypes.kt logd("onQuery: rcvd query: $fqdn, qtype: $qtype") if (fqdn == null) { Logger.e(LOG_TAG_VPN, "onQuery: fqdn is null") // return block all, as it is not expected to reach here - return@runBlocking makeNsOpts(Backend.BlockAll) + return@go2kt makeNsOpts(Backend.BlockAll) } - if (appConfig.getBraveMode().isDnsMode()) { + val appMode = appConfig.getBraveMode() + if (appMode.isDnsMode()) { val res = getTransportIdForDnsMode(fqdn) logd("onQuery (Dns):$fqdn, dnsx: $res") - return@runBlocking res + return@go2kt res } - if (appConfig.getBraveMode().isDnsFirewallMode()) { - val res = getTransportIdForDnsFirewallMode(fqdn) + if (appMode.isDnsFirewallMode()) { + val res = getTransportIdForDnsFirewallMode(fqdn, true) logd("onQuery (Dns+Firewall):$fqdn, dnsx: $res") - return@runBlocking res + return@go2kt res } val res = makeNsOpts(Backend.Preferred) // should not reach here Logger.e( LOG_TAG_VPN, - "onQuery: unknown mode ${appConfig.getBraveMode()}, $fqdn, returning $res" + "onQuery: unknown mode ${appMode}, $fqdn, returning $res" ) - return@runBlocking res + return@go2kt res } // function to decide which transport id to return on Dns only mode - private suspend fun getTransportIdForDnsMode(fqdn: String): backend.DNSOpts { - val tid = determineDnsTransportId() + private suspend fun getTransportIdForDnsMode(fqdn: String): DNSOpts { + // useFixedTransport is false in Dns only mode + val tid = determineDnsTransportId(false) // check for global domain rules when (DomainRulesManager.getDomainRule(fqdn, UID_EVERYBODY)) { @@ -2913,20 +3148,24 @@ class BraveVPNService : } // function to decide which transport id to return on DnsFirewall mode - private suspend fun getTransportIdForDnsFirewallMode(fqdn: String): backend.DNSOpts { - val tid = determineDnsTransportId() + // two types of src, from onQuery and from preFlow (onQuery: true, preFlow: false) + private suspend fun getTransportIdForDnsFirewallMode(fqdn: String, src: Boolean = false): DNSOpts { + val useFixedTransport = shouldUseFixedTransport(src) + + val tid = determineDnsTransportId(useFixedTransport) val forceBypassLocalBlocklists = isAppPaused() && VpnController.isVpnLockdown() return if (forceBypassLocalBlocklists) { // if the app is paused and vpn is in lockdown mode, then bypass the local blocklists - makeNsOpts(tid, true) - } else if (FirewallManager.isAnyAppBypassesDns()) { - // if any app is bypassed (dns + firewall) set isBlockFree as true, so that the - // domain is resolved amd the decision is made by in flow() - makeNsOpts(transportIdsAlg(tid), true) + makeNsOpts(appendDnsCacheIfNeeded(tid), true) + } else if (FirewallManager.isAnyAppBypassesDns() || (persistentState.bypassBlockInDns && !persistentState.splitDns)) { + // if any app is bypassed (dns + firewall) or bypassBlockInDns is enabled, then + // set bypass local blocklist as true, so that the domain is resolved and the decision + // is made by in flow() + makeNsOpts(transportIdsAlg(tid, useFixedTransport), true) } else if (DomainRulesManager.isDomainTrusted(fqdn)) { // set isBlockFree as true so that the decision is made by in flow() function - makeNsOpts(transportIdsAlg(tid), true) + makeNsOpts(transportIdsAlg(tid, useFixedTransport), true) } else if ( DomainRulesManager.status(fqdn, UID_EVERYBODY) == DomainRulesManager.Status.BLOCK ) { @@ -2935,14 +3174,28 @@ class BraveVPNService : makeNsOpts(Backend.BlockAll) } else { // no global rule, no app-wise trust, return the tid as it is - makeNsOpts(tid) + makeNsOpts(appendDnsCacheIfNeeded(tid)) } } - private fun determineDnsTransportId(): String { + private fun shouldUseFixedTransport(src: Boolean): Boolean { + return if (src) { + // if the source is onQuery, then use the fixed transport id only if the alg + // and advanced WireGuard is enabled + // persistentState.enableDnsAlg && WireguardManager.isAdvancedWgActive() + + // instead of above condition, introduce new splitDns setting + persistentState.splitDns && WireguardManager.isAdvancedWgActive() + } else { + // if the source is preFlow, no need to check for advanced WireGuard + false + } + } + + private fun determineDnsTransportId(useFixedTransport: Boolean): String { val oneWgId = WireguardManager.getOneWireGuardProxyId() - return if (oneWgId != null) { - ProxyManager.ID_WG_BASE + oneWgId + val tid = if (oneWgId != null) { + ID_WG_BASE + oneWgId } else if (appConfig.isSystemDns() || (isAppPaused() && VpnController.isVpnLockdown())) { // in vpn-lockdown mode+appPause , use system dns if the app is paused to mimic // as if the apps are excluded from vpn @@ -2950,27 +3203,41 @@ class BraveVPNService : } else { Backend.Preferred } + return if (useFixedTransport) { + val sb = StringBuilder() + val tr1 = appendDnsCacheIfNeeded(tid) + val tr2 = Backend.Fixed // fixed transport id + sb.append(tr1).append(",").append(tr2) + sb.toString() + } else { + tid + } } private suspend fun makeNsOpts( tid: String, bypassLocalBlocklists: Boolean = false - ): backend.DNSOpts { - val opts = backend.DNSOpts() + ): DNSOpts { + val opts = DNSOpts() opts.ipcsv = "" // as of now, no suggested ips - opts.tidcsv = appendDnsCacheIfNeeded(tid) + opts.tidcsv = tid opts.pid = proxyIdForOnQuery() opts.noblock = bypassLocalBlocklists return opts } - private fun transportIdsAlg(preferredId: String): String { + private fun transportIdsAlg(preferredId: String, useFixedTransport: Boolean): String { + if (useFixedTransport) { + // case when useFixedTransport is true, then tid will already be appended with Fixed + // so no need to append BlockFree again + return preferredId // ex: CT+Preferred,Fixed + } // case when userPreferredId is Alg, then return BlockFree + tid // tid can be System / ProxyId / Preferred return if (isRethinkDnsEnabled()) { val sb = StringBuilder() val tr1 = appendDnsCacheIfNeeded(Backend.BlockFree) - val tr2 = appendDnsCacheIfNeeded(preferredId) // ideally, it should be Preferred + val tr2 = preferredId // ideally, it should be Preferred sb.append(tr1).append(",").append(tr2) sb.toString() } else { @@ -3022,22 +3289,20 @@ class BraveVPNService : // only for one-wireguard, the dns queries are proxied if (WireguardManager.oneWireGuardEnabled()) { val id = WireguardManager.getOneWireGuardProxyId() ?: return Backend.Base - ProxyManager.ID_WG_BASE + id + ID_WG_BASE + id } else if (WireguardManager.catchAllEnabled()) { // if the enabled wireguard is catchall-wireguard, then return wireguard id - val endpoint = appConfig.getSelectedDnsProxyDetails() - val id = WireguardManager.getOptimalCatchAllConfigId(endpoint?.proxyIP) ?: return Backend.Base - ProxyManager.ID_WG_BASE + id + val id = WireguardManager.getOptimalCatchAllConfigId() ?: return Backend.Base + ID_WG_BASE + id } else { // if the enabled wireguard is not one-wireguard, then return base Backend.Base } } else if (WireguardManager.catchAllEnabled()) { // check even if wireguard is not enabled // if the enabled wireguard is catchall-wireguard, then return wireguard id - val endpoint = appConfig.getSelectedDnsProxyDetails() - val id = WireguardManager.getOptimalCatchAllConfigId(endpoint?.proxyIP) ?: return Backend.Base + val id = WireguardManager.getOptimalCatchAllConfigId() ?: return Backend.Base // in this case, no need to check if the proxy is available - ProxyManager.ID_WG_BASE + id + ID_WG_BASE + id } else { Backend.Base } @@ -3048,24 +3313,47 @@ class BraveVPNService : Logger.i(LOG_TAG_VPN, "received null summary for dns") return } - logd("onResponse: $summary") + if (!DEBUG) { + if (summary.id.contains(Backend.Fixed)) { + return + } + } netLogTracker.processDnsLog(summary) + setRegionLiveDataIfRequired(summary) + } + + private fun setRegionLiveDataIfRequired(summary: backend.DNSSummary) { + if (summary.region == null) { + return + } + + val region = summary.region + val regionLiveData = regionLiveData + if (regionLiveData.value != region) { + regionLiveData.postValue(region) + } + } + + fun getRegionLiveData(): LiveData { + return regionLiveData } override fun onProxiesStopped() { // clear the proxy handshake times logd("onProxiesStopped; clear the handshake times") - wgHandShakeCheckpoints.clear() + val action = ProxyOperationData("-1", ProxyOperation.CLEAR) + handleProxyActions(action) WireguardManager.clearCatchAllCache() } override fun onProxyAdded(id: String) { - if (!id.contains(ProxyManager.ID_WG_BASE)) { + if (!id.contains(ID_WG_BASE)) { // only wireguard proxies are considered for overlay network return } - wgHandShakeCheckpoints[id] = elapsedRealtime() + val action = ProxyOperationData(id, ProxyOperation.ADD) + handleProxyActions(action) // new proxy added, refresh overlay network pair io("onProxyAdded") { val nw: OverlayNetworks? = vpnAdapter?.getActiveProxiesIpAndMtu() @@ -3075,11 +3363,12 @@ class BraveVPNService : } override fun onProxyRemoved(id: String) { - if (!id.contains(ProxyManager.ID_WG_BASE)) { + if (!id.contains(ID_WG_BASE)) { // only wireguard proxies are considered for overlay network return } - wgHandShakeCheckpoints.remove(id) + val action = ProxyOperationData(id, ProxyOperation.REMOVE) + handleProxyActions(action) WireguardManager.clearCatchAllCacheForApp(id) // proxy removed, refresh overlay network pair io("onProxyRemoved") { @@ -3119,14 +3408,16 @@ class BraveVPNService : } override fun log(level: Int, msg: String) { + if (msg.isEmpty()) return + val l = Logger.LoggerType.fromId(level) if (l.stacktrace()) { - Logger.crash(LOG_GO_LOGGER, msg) // log the stack trace + Logger.crash(LOG_GO_LOGGER, msg) // log the stack trace and write to in-mem db EnhancedBugReport.writeLogsToFile(this, msg) } else if (l.user()) { showNwEngineNotification(msg) } else { - Logger.i(LOG_GO_LOGGER, msg) + Logger.goLog(msg, l) } } @@ -3142,14 +3433,14 @@ class BraveVPNService : PendingIntent.FLAG_UPDATE_CURRENT, mutable = false ) - val builder = NotificationCompat.Builder(this, WARNING_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notification_icon) - .setContentTitle(msg) - .setContentIntent(pendingIntent) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - builder.color = - ContextCompat.getColor(this, getAccentColor(persistentState.theme)) + val builder = + NotificationCompat.Builder(this, WARNING_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_icon) + .setContentTitle(msg) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + builder.color = ContextCompat.getColor(this, getAccentColor(persistentState.theme)) notificationManager.notify(NW_ENGINE_NOTIFICATION_ID, builder.build()) } @@ -3199,22 +3490,86 @@ class BraveVPNService : } } + override fun preflow( + protocol: Int, + uid: Int, + src: String, + dst: String, + domains: String + ): PreMark = go2kt(preflowDispatcher) { + val first = HostName(src) + val second = HostName(dst) + + val srcIp = if (first.asAddress() == null) "" else first.asAddress().toString() + val srcPort = first.port ?: 0 + val dstIp = if (second.asAddress() == null) "" else second.asAddress().toString() + val dstPort = second.port ?: 0 + + val newUid = if (uid == INVALID_UID) { // fetch uid only if it is invalid + getUid( + uid, + protocol, + srcIp, + srcPort, + dstIp, + dstPort, + ConnectionTracer.CallerSrc.PREFLOW + ) + } else { + uid + } + Logger.d( + LOG_TAG_VPN, + "preflow: $newUid, $srcIp, $srcPort, $dstIp, $dstPort, $domains" + ) + + // check if newUid is part of wireguard proxy, if so, return tid as wireguard proxy id + //val pid = ProxyManager.getProxyIdForApp(newUid) + // if catch-all needs to be considered, then use below code + val config = WireguardManager.getConfigIdForApp(newUid) + val pid = if (config != null) { + ID_WG_BASE + config.id + } else { + "" + } + + if (pid.contains(ID_WG_BASE)) { + val p = PreMark() + p.uid = newUid.toString() + p.tidcsv = pid + Logger.i( + LOG_TAG_VPN, + "preflow: wireguard proxy, returning ${p.tidcsv}, ${p.uid}" + ) + return@go2kt p + } else { + val d = domains.split(",").first() + val opts = getTransportIdForDnsFirewallMode(d) + val p = PreMark() + p.uid = newUid.toString() + p.tidcsv = opts.tidcsv + Logger.i(LOG_TAG_VPN, "preflow: returning ${p.tidcsv}, ${p.uid}") + return@go2kt p + } + } + override fun flow( protocol: Int, - _uid: Long, - dup: Boolean, + _uid: Int, src: String, dest: String, realIps: String, d: String, possibleDomains: String, blocklists: String - ): intra.Mark = runBlocking { - // runBlocking blocks the current thread until all coroutines within it are complete - // an call a suspending function from a non-suspending context and obtain the result. - logd("flow: $_uid, $dup, $src, $dest, $realIps, $d, $blocklists") + ): Mark = go2kt(flowDispatcher) { + logd("flow: $_uid, $src, $dest, $realIps, $d, $blocklists") handleVpnLockdownStateAsync() + // in case of double loopback, all traffic will be part of rinr instead of just rethink's + // own traffic. flip the doubleLoopback flag to true if we need that behavior + val doubleLoopback = false + val first = HostName(src) val second = HostName(dest) @@ -3224,7 +3579,8 @@ class BraveVPNService : val dstPort = second.port ?: 0 val ips = realIps.split(",") - val fip = ips.firstOrNull()?.trim() + // take the first non-unspecified ip as the real destination ip + val fip = ips.firstOrNull { !isUnspecifiedIp(it.trim()) }?.trim() // use realIps; as of now, netstack uses the first ip // TODO: apply firewall rules on all real ips val realDestIp = @@ -3233,7 +3589,15 @@ class BraveVPNService : } else { fip } - var uid = getUid(_uid, protocol, srcIp, srcPort, dstIp, dstPort) + var uid = getUid( + _uid, + protocol, + srcIp, + srcPort, + dstIp, + dstPort, + ConnectionTracer.CallerSrc.FLOW + ) uid = FirewallManager.appId(uid, isPrimaryUser()) val userId = FirewallManager.userId(uid) @@ -3271,22 +3635,23 @@ class BraveVPNService : val trapVpnDns = isDns(dstPort) && isVpnDns(dstIp) val trapVpnPrivateDns = isVpnDns(dstIp) && isPrivateDns(dstPort) - // app is considered as spl when it is selected to forward dns proxy, socks5 or http proxy - val isSplApp = isSpecialApp(uid) // always block, since the vpn tunnel doesn't serve dns-over-tls if (trapVpnPrivateDns) { logd("flow: dns-over-tls, returning Ipn.Block, $uid") cm.isBlocked = true - cm.blockedByRule = FirewallRuleset.RULE1C.id - return@runBlocking persistAndConstructFlowResponse(cm, Backend.Block, connId, uid) + cm.blockedByRule = FirewallRuleset.RULE14.id + return@go2kt persistAndConstructFlowResponse(cm, Backend.Block, connId, uid) } + // app is considered as spl when it is selected to forward dns proxy, socks5 or http proxy + val isSplApp = isSpecialApp(uid) + val isRethink = uid == rethinkUid if (isRethink) { // case when uid is rethink, return Ipn.Base logd( - "flow: Ipn.Exit for rethink, $uid, $dup, $packageName, $srcIp, $srcPort, $realDestIp, $dstPort, $possibleDomains" + "flow: Ipn.Exit for rethink, $uid, $packageName, $srcIp, $srcPort, $realDestIp, $dstPort, $possibleDomains" ) if (cm.query.isNullOrEmpty()) { // possible domains only used for logging purposes, it may be available if @@ -3304,30 +3669,22 @@ class BraveVPNService : val proxy = if (trapVpnDns) { Backend.Base + // do not add the trackedCids for dns entries as there will not be any + // onSocketClosed event for dns entries } else { + // add to trackedCids, so that the connection can be removed from the list when the + // connection is closed (onSocketClosed), use: ui to show the active connections + val key = CidKey(cm.connId, uid) + trackedCids.add(key) Backend.Exit } - // add to trackedCids, so that the connection can be removed from the list when the - // connection is closed (onSocketClosed), use: ui to show the active connections - val key = CidKey(cm.connId, uid) - trackedCids.add(key) - // TODO: set dup as true for now (v055f), need to handle dup properly in future - val duplicate = dup || true - // if the connection is Rethink's uid and if the dup is false, then the connections - // are rethink's own connections, so add it in network log as well - if (!duplicate) { - // no need to consider return value as the function is called only for logging - persistAndConstructFlowResponse(cm, proxy, connId, uid) - } - // make the cm obj to null so that the db write will not happen - val c = if (duplicate) cm else null - return@runBlocking persistAndConstructFlowResponse(c, proxy, connId, uid, isRethink) + return@go2kt persistAndConstructFlowResponse(cm, proxy, connId, uid, isRethink) } if (trapVpnDns) { logd("flow: dns-request, returning ${Backend.Base}, $uid, $connId") - return@runBlocking persistAndConstructFlowResponse(null, Backend.Base, connId, uid) + return@go2kt persistAndConstructFlowResponse(null, Backend.Base, connId, uid) } processFirewallRequest(cm, anyRealIpBlocked, blocklists, isSplApp) @@ -3335,7 +3692,7 @@ class BraveVPNService : if (cm.isBlocked) { // return Ipn.Block, no need to check for other rules logd("flow: received rule: block, returning Ipn.Block, $connId, $uid") - return@runBlocking persistAndConstructFlowResponse(cm, Backend.Block, connId, uid) + return@go2kt persistAndConstructFlowResponse(cm, Backend.Block, connId, uid) } // add to trackedCids, so that the connection can be removed from the list when the @@ -3343,7 +3700,73 @@ class BraveVPNService : val key = CidKey(cm.connId, uid) trackedCids.add(key) - return@runBlocking determineProxyDetails(cm, isSplApp) + return@go2kt determineProxyDetails(cm, doubleLoopback) + } + + override fun inflow(protocol: Int, uid: Int, src: String?, dst: String?): Mark = go2kt(inflowDispatcher) { + val doubleLoopback = false + val first = HostName(src) + val second = HostName(dst) + + val srcIp = if (first.asAddress() == null) "" else first.asAddress().toString() + val srcPort = first.port ?: 0 + val dstIp = if (second.asAddress() == null) "" else second.asAddress().toString() + val dstPort = second.port ?: 0 + + var recvdUid = getUid( + uid, + protocol, + srcIp, + srcPort, + dstIp, + dstPort, + ConnectionTracer.CallerSrc.INFLOW + ) + recvdUid = FirewallManager.appId(recvdUid, isPrimaryUser()) + val userId = FirewallManager.userId(recvdUid) + + logd("inflow: $uid($recvdUid), $srcIp, $srcPort, $dstIp, $dstPort") + + val connId = Utilities.getRandomString(8) + + val connType = + if (isConnectionMetered(dstIp)) { + ConnectionTracker.ConnType.METERED + } else { + ConnectionTracker.ConnType.UNMETERED + } + + val cm = + createConnTrackerMetaData( + uid, + userId, + srcIp, + srcPort, + dstIp, + dstPort, + protocol, + proxyDetails = "", + "", + "", + connId, + connType + ) + + processFirewallRequest(cm, false, "") + + if (cm.isBlocked) { + // return Ipn.Block, no need to check for other rules + logd("inflow: received rule: block, returning Ipn.Block, $connId, $uid") + return@go2kt persistAndConstructFlowResponse(cm, Backend.Block, connId, uid) + } + + // add to trackedCids, so that the connection can be removed from the list when the + // connection is closed (onSocketClosed), use: ui to show the active connections + val key = CidKey(cm.connId, uid) + trackedCids.add(key) + + logd("inflow: determine proxy and other dtls for $connId, $uid") + return@go2kt determineProxyDetails(cm, doubleLoopback) } private suspend fun isSpecialApp(uid: Int): Boolean { @@ -3404,70 +3827,74 @@ class BraveVPNService : private suspend fun determineProxyDetails( connTracker: ConnTrackerMetaData, - isSplApp: Boolean - ): intra.Mark { + doubleLoopback: Boolean + ): Mark { val baseOrExit = - if (isSplApp) { - Backend.Exit - } else { + if (doubleLoopback) { + Backend.Base + } else if (connTracker.blockedByRule == FirewallRuleset.RULE9.id) { + // special case: proxied dns traffic should not Backed.Exit as is. Only traffic + // marked with Backend.Base will be handled (proxied) by vpnAdapter's dns-transport Backend.Base + } else { + Backend.Exit } val connId = connTracker.connId val uid = connTracker.uid if (FirewallManager.isAppExcludedFromProxy(uid)) { - logd("flow: app is excluded from proxy, returning Ipn.Base, $connId, $uid") + logd("flow/inflow: app is excluded from proxy, returning Ipn.Base, $connId, $uid") + connTracker.blockedByRule = FirewallRuleset.RULE15.id return persistAndConstructFlowResponse(connTracker, baseOrExit, connId, uid) } // check for one-wireguard, if enabled, return wireguard proxy for all connections val oneWgId = WireguardManager.getOneWireGuardProxyId() if (oneWgId != null && oneWgId != WireguardManager.INVALID_CONF_ID) { - val proxyId = "${ProxyManager.ID_WG_BASE}${oneWgId}" + val proxyId = "${ID_WG_BASE}${oneWgId}" // regardless of whether this proxyId exists in go, use it to avoid leaks + val ipSupported = isIpSupportedByWireGuardId(proxyId, connTracker.destIP) val canRoute = canRouteIp(proxyId, connTracker.destIP, true) - return if (canRoute) { - handleProxyHandshake(proxyId) - logd("flow: one-wg is enabled, returning $proxyId, $connId, $uid") + return if (ipSupported && canRoute) { + val actions = ProxyOperationData(proxyId, ProxyOperation.REFRESH) + handleProxyActions(actions) + logd("flow/inflow: one-wg is enabled, returning $proxyId, $connId, $uid") persistAndConstructFlowResponse(connTracker, proxyId, connId, uid) } else { // in some configurations the allowed ips will not be 0.0.0.0/0, so the connection // will be dropped, in those cases, return base (connection will be forwarded to // base proxy) - logd("flow: one-wg is enabled, but no route; ret:Ipn.Base, $connId, $uid") + logd("flow/inflow: one-wg is enabled, but no route/ip; ret:Ipn.Base, $connId, $uid") persistAndConstructFlowResponse(connTracker, baseOrExit, connId, uid) } } - val wgConfig = WireguardManager.getConfigIdForApp(uid, connTracker.destIP) // also accounts for catch-all + val wgConfig = + WireguardManager.getConfigIdForApp( + uid, + connTracker.destIP + ) // also accounts for catch-all if (wgConfig != null && wgConfig.id != WireguardManager.INVALID_CONF_ID) { - val proxyId = "${ProxyManager.ID_WG_BASE}${wgConfig.id}" - // even if inactive, route connections to wg if lockdown/catch-all is enabled to - // avoid leaks - if (wgConfig.isActive || wgConfig.isLockdown || wgConfig.isCatchAll) { - // if lockdown is enabled, canRoute checks peer configuration and if it returns - // "false", then the connection will be sent to base and not dropped - // if lockdown is disabled, then canRoute returns default (true) which - // will have the effect of blocking all connections - // ie, if lockdown is enabled, split-tunneling happens as expected but if - // lockdown is disabled, it has the effect of blocking all connections - val canRoute = canRouteIp(proxyId, connTracker.destIP, true) - logd("flow: wg is active/lockdown/catch-all; $proxyId, $connId, $uid; canRoute? $canRoute") - return if (canRoute) { - handleProxyHandshake(proxyId) - persistAndConstructFlowResponse(connTracker, proxyId, connId, uid) - } else { - persistAndConstructFlowResponse(connTracker, baseOrExit, connId, uid) - } + val proxyId = "${ID_WG_BASE}${wgConfig.id}" + // TODO: WgMgr takes care of giving the correct proxyId, check for canRouteIp to + // trigger the handshake if needed + val canRoute = canRouteIp(proxyId, connTracker.destIP, true) + logd( + "flow/inflow: wg is active/lockdown/catch-all; $proxyId, $connId, $uid; canRoute? $canRoute" + ) + return if (canRoute) { + val actions = ProxyOperationData(proxyId, ProxyOperation.REFRESH) + handleProxyActions(actions) + persistAndConstructFlowResponse(connTracker, proxyId, connId, uid) } else { - // fall-through, no lockdown/catch-all/active wg found, so proceed with other checks + persistAndConstructFlowResponse(connTracker, baseOrExit, connId, uid) } } // carry out this check after wireguard, because wireguard has catchAll and lockdown // if no proxy or dns proxy is enabled, return baseOrExit if (!appConfig.isProxyEnabled() && !appConfig.isDnsProxyActive()) { - logd("flow: no proxy/dnsproxy enabled, returning Ipn.Base, $connId, $uid") + logd("flow/inflow: no proxy/dnsproxy enabled, returning Ipn.Base, $connId, $uid") return persistAndConstructFlowResponse(connTracker, baseOrExit, connId, uid) } @@ -3506,16 +3933,16 @@ class BraveVPNService : val endpoint = appConfig.getConnectedOrbotProxy() val packageName = FirewallManager.getPackageNameByUid(uid) if (endpoint?.proxyAppName == packageName) { - logd("flow: orbot exit for $packageName, $connId, $uid") + logd("flow/inflow: orbot exit for $packageName, $connId, $uid") return persistAndConstructFlowResponse(connTracker, Backend.Exit, connId, uid) } val activeId = ProxyManager.getProxyIdForApp(uid) if (!activeId.contains(ProxyManager.ID_ORBOT_BASE)) { - Logger.e(LOG_TAG_VPN, "flow: orbot proxy is enabled but app is not included") + Logger.e(LOG_TAG_VPN, "flow/inflow: orbot proxy is enabled but app is not included") // pass-through } else { - logd("flow: orbot proxy for $uid, $connId") + logd("flow/inflow: orbot proxy for $uid, $connId") return persistAndConstructFlowResponse( connTracker, ProxyManager.ID_ORBOT_BASE, @@ -3529,14 +3956,14 @@ class BraveVPNService : if (appConfig.isCustomSocks5Enabled()) { val endpoint = appConfig.getSocks5ProxyDetails() val packageName = FirewallManager.getPackageNameByUid(uid) - logd("flow: socks5 proxy is enabled, $packageName, ${endpoint.proxyAppName}") + logd("flow/inflow: socks5 proxy is enabled, $packageName, ${endpoint.proxyAppName}") // do not block the app if the app is set to forward the traffic via socks5 proxy if (endpoint.proxyAppName == packageName) { - logd("flow: socks5 exit for $packageName, $connId, $uid") + logd("flow/inflow: socks5 exit for $packageName, $connId, $uid") return persistAndConstructFlowResponse(connTracker, Backend.Exit, connId, uid) } - logd("flow: socks5 proxy for $connId, $uid") + logd("flow/inflow: socks5 proxy for $connId, $uid") return persistAndConstructFlowResponse( connTracker, ProxyManager.ID_S5_BASE, @@ -3550,11 +3977,11 @@ class BraveVPNService : val packageName = FirewallManager.getPackageNameByUid(uid) // do not block the app if the app is set to forward the traffic via http proxy if (endpoint.proxyAppName == packageName) { - logd("flow: http exit for $packageName, $connId, $uid") + logd("flow/inflow: http exit for $packageName, $connId, $uid") return persistAndConstructFlowResponse(connTracker, Backend.Exit, connId, uid) } - logd("flow: http proxy for $connId, $uid") + logd("flow/inflow: http proxy for $connId, $uid") return persistAndConstructFlowResponse( connTracker, ProxyManager.ID_HTTP_BASE, @@ -3568,17 +3995,62 @@ class BraveVPNService : val packageName = FirewallManager.getPackageNameByUid(uid) // do not block the app if the app is set to forward the traffic via dns proxy if (endpoint?.proxyAppName == packageName) { - logd("flow: dns proxy enabled for $packageName, return exit, $connId, $uid") + logd("flow/inflow: dns proxy enabled for $packageName, return exit, $connId, $uid") return persistAndConstructFlowResponse(connTracker, Backend.Exit, connId, uid) } } - logd("flow: no proxies, $baseOrExit, $connId, $uid") + logd("flow/inflow: no proxies, $baseOrExit, $connId, $uid") return persistAndConstructFlowResponse(connTracker, baseOrExit, connId, uid) } + private suspend fun isIpSupportedByWireGuardId(id: String, ip: String): Boolean { + val ipVersion = IPAddressString(ip).toAddress().ipVersion + val supportedVersion = VpnController.getSupportedIpVersion(id) + logd( + "isSupportedIpVersion: $id? ipVersion: $ipVersion, supportedVersion: $supportedVersion" + ) + if (ipVersion.isIPv4 && supportedVersion.first) { + return true + } + if (ipVersion.isIPv6 && supportedVersion.second) { + return true + } + return false + } + + private fun createWgHandshakeChannel() { + // no need to cancel the channel as vpnScope is cancelled when vpn is stopped + vpnScope.launch { + wgHandshakeChannel.consumeEach { action -> + when (action.action) { + ProxyOperation.ADD -> { + wgHandShakeCheckpoints[action.proxyId] = elapsedRealtime() + } + + ProxyOperation.REMOVE -> { + wgHandShakeCheckpoints.remove(action.proxyId) + } + + ProxyOperation.REFRESH -> handleProxyHandshake(action.proxyId) + ProxyOperation.CLEAR -> { + wgHandShakeCheckpoints.clear() + } + } + } + } + } + + private fun handleProxyActions(action: ProxyOperationData) { + try { + vpnScope.launch { wgHandshakeChannel.send(action) } + } catch (e: Exception) { + Logger.e(LOG_TAG_VPN, "performProxyHandshake: ${e.message}") + } + } + private suspend fun handleProxyHandshake(id: String) { - if (!id.startsWith(ProxyManager.ID_WG_BASE)) { + if (!id.startsWith(ID_WG_BASE)) { // only wireguard proxies are considered for handshakes return } @@ -3598,25 +4070,26 @@ class BraveVPNService : val realtime = elapsedRealtime() val cpInterval = realtime - latestCheckpoint val cpIntervalSecs = TimeUnit.MILLISECONDS.toSeconds(cpInterval) - if (cpInterval < this.checkpointInterval) { + if (cpInterval < checkpointInterval) { logd("flow: skip refresh for $id, within interval: $cpIntervalSecs") return } wgHandShakeCheckpoints[id] = realtime val lastHandShake = stats.lastOK - val mustRefresh = if (lastHandShake <= 0) { - Logger.w(LOG_TAG_VPN, "flow: force refresh, handshake never done for $id") - true // always refresh if handshake never done - } else { - logd("flow: handshake check for $id, $lastHandShake, interval: $cpIntervalSecs") - val currTimeMs = System.currentTimeMillis() - val durationMs = currTimeMs - lastHandShake - val durationSecs = TimeUnit.MILLISECONDS.toSeconds(durationMs) - val ref = durationMs > wgHandshakeTimeout - Logger.i(LOG_TAG_VPN, "flow: refresh $id after $durationSecs? $ref") - // if the last handshake is older than the timeout, refresh the proxy - ref - } + val mustRefresh = + if (lastHandShake <= 0) { + Logger.w(LOG_TAG_VPN, "flow: force refresh, handshake never done for $id") + true // always refresh if handshake never done + } else { + logd("flow: handshake check for $id, $lastHandShake, interval: $cpIntervalSecs") + val currTimeMs = System.currentTimeMillis() + val durationMs = currTimeMs - lastHandShake + val durationSecs = TimeUnit.MILLISECONDS.toSeconds(durationMs) + val ref = durationMs > wgHandshakeTimeout + Logger.i(LOG_TAG_VPN, "flow: refresh $id after $durationSecs? $ref") + // if the last handshake is older than the timeout, refresh the proxy + ref + } if (mustRefresh) { io("proxyHandshake") { vpnAdapter?.refreshProxy(id) } } @@ -3652,8 +4125,8 @@ class BraveVPNService : return vpnAdapter?.getRDNS(type) } - suspend fun goBuildVersion(): String { - return vpnAdapter?.goBuildVersion() ?: "" + fun goBuildVersion(full: Boolean): String { + return vpnAdapter?.goBuildVersion(full) ?: "" } private fun persistAndConstructFlowResponse( @@ -3662,7 +4135,7 @@ class BraveVPNService : connId: String, uid: Int, isRethink: Boolean = false - ): intra.Mark { + ): Mark { // persist ConnTrackerMetaData if (cm != null) { cm.proxyDetails = proxyId @@ -3675,10 +4148,10 @@ class BraveVPNService : } else { netLogTracker.writeIpLog(cm) } - logd("flow: connTracker: $cm") + logd("flow/inflow: connTracker: $cm") } - val mark = intra.Mark() + val mark = Mark() mark.pid = proxyId mark.cid = connId // if rethink, then set uid as rethink, so that go process can handle it accordingly @@ -3690,12 +4163,12 @@ class BraveVPNService : if (cm == null) { Logger.i( LOG_TAG_VPN, - "flow: returning mark: $mark for connId: $connId, uid: $uid, cm: null" + "flow/inflow: returning mark: $mark for connId: $connId, uid: $uid, cm: null" ) } else { Logger.i( LOG_TAG_VPN, - "flow: returning mark: $mark for src(${cm.sourceIP}: ${cm.sourcePort}), dest(${cm.destIP}:${cm.destPort})" + "flow/inflow: returning mark: $mark for src(${cm.sourceIP}: ${cm.sourcePort}), dest(${cm.destIP}:${cm.destPort})" ) } return mark @@ -3705,7 +4178,7 @@ class BraveVPNService : metadata: ConnTrackerMetaData, anyRealIpBlocked: Boolean = false, blocklists: String = "", - isSplApp: Boolean + isSplApp: Boolean = false ) { val rule = firewall(metadata, anyRealIpBlocked, isSplApp) @@ -3770,7 +4243,7 @@ class BraveVPNService : } } - suspend fun getProxyStats(id: String): Stats? { + suspend fun getProxyStats(id: String): RouterStats? { return if (vpnAdapter != null) { vpnAdapter?.getProxyStats(id) } else { @@ -3805,6 +4278,8 @@ class BraveVPNService : } override fun onTrimMemory(level: Int) { + // override onLowMemory is deprecated, so use onTrimMemory + // ref: developer.android.com/reference/android/net/VpnService super.onTrimMemory(level) Logger.i(LOG_TAG_VPN, "onTrimMemory: $level") if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { @@ -3812,6 +4287,7 @@ class BraveVPNService : // show notification to user, that the app is consuming more memory showMemoryNotification() } + io("onLowMem") { vpnAdapter?.onLowMemory() } } private fun showMemoryNotification() { @@ -3823,15 +4299,42 @@ class BraveVPNService : mutable = false ) - val builder = NotificationCompat.Builder(this, WARNING_CHANNEL_ID) - .setContentTitle(getString(R.string.memory_notification_text)) - .setSmallIcon(R.drawable.ic_notification_icon) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setContentIntent(pendingIntent) - .setAutoCancel(true) + val builder = + NotificationCompat.Builder(this, WARNING_CHANNEL_ID) + .setContentTitle(getString(R.string.memory_notification_text)) + .setSmallIcon(R.drawable.ic_notification_icon) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true) builder.color = ContextCompat.getColor(this, getAccentColor(persistentState.theme)) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(MEMORY_NOTIFICATION_ID, builder.build()) } + + override fun onRevoke() { + Logger.i(LOG_TAG_VPN, "onRevoke, stop vpn adapter") + signalStopService("revoked", false) + } + + suspend fun getSystemDns(): String { + return vpnAdapter?.getSystemDns() ?: "" + } + + fun getNetStat(): backend.NetStat? { + return vpnAdapter?.getNetStat() + } + + fun writeConsoleLog(log: ConsoleLog) { + netLogTracker.writeConsoleLog(log) + } + + /*override fun onUnbind(intent: Intent?): Boolean { + Logger.w(LOG_TAG_VPN, "onUnbind, stop vpn adapter") + // onUnbind is called when the vpn is disconnected by signalStopService or if + // some other vpn service is started by the user, so stop the vpn adapter in onUnbind which + // will close tunFd which is a prerequisite for onDestroy() + stopVpnAdapter() + return super.onUnbind(intent) + }*/ } diff --git a/app/src/main/java/com/celzero/bravedns/service/ConnectionMonitor.kt b/app/src/main/java/com/celzero/bravedns/service/ConnectionMonitor.kt index 4b6490224..4622a9034 100644 --- a/app/src/main/java/com/celzero/bravedns/service/ConnectionMonitor.kt +++ b/app/src/main/java/com/celzero/bravedns/service/ConnectionMonitor.kt @@ -41,7 +41,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.Closeable import java.io.IOException -import java.net.DatagramSocket import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress @@ -111,6 +110,8 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : val testReachability: Boolean ) + data class ProbeResult(val ip: String, val ok: Boolean, val capabilities: NetworkCapabilities?) + interface NetworkListener { fun onNetworkDisconnected(networks: UnderlyingNetworks) @@ -158,14 +159,25 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : handleNetworkChange(isForceUpdate = true) } + suspend fun probeIp(ip: String): ProbeResult? { + Logger.d(LOG_TAG_CONNECTION, "pingIp: $ip") + return serviceHandler?.let { + val res = it.probeIp(ip, networkSet) + Logger.d(LOG_TAG_CONNECTION, "pingIp: $ip, $res") + res + } + } + /** * Force updates the VPN's underlying network based on the preference. Will be initiated when - * the VPN start is completed. + * the VPN start is completed. Always called from the main thread */ - fun onVpnStart(context: Context) { + fun onVpnStart(context: Context): Boolean { + val isNewVpn = serviceHandler == null + if (this.serviceHandler != null) { Logger.w(LOG_TAG_CONNECTION, "connection monitor is already running") - return + return isNewVpn } Logger.i(LOG_TAG_CONNECTION, "new vpn is created force update the network") @@ -177,16 +189,24 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : } catch (e: Exception) { Logger.w(LOG_TAG_CONNECTION, "Exception while registering network callback", e) networkListener.onNetworkRegistrationFailed() - return + return isNewVpn } val handlerThread = HandlerThread(NetworkRequestHandler::class.simpleName) handlerThread.start() + val ips = IpsToProbe( + persistentState.pingv4Ips.split(",").map { it.trim() }, + persistentState.pingv6Ips.split(",").map { it.trim() } + ) this.serviceHandler = - NetworkRequestHandler(connectivityManager, handlerThread.looper, networkListener) + NetworkRequestHandler(connectivityManager, handlerThread.looper, networkListener, ips) handleNetworkChange(isForceUpdate = true) + + return isNewVpn } + + // Always called from the main thread fun onVpnStop() { try { this.serviceHandler?.removeCallbacksAndMessages(null) @@ -251,11 +271,17 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : val dnsServers: Map ) + data class IpsToProbe( + val ip4probes: Collection, + val ip6probes: Collection + ) + // Handles the network messages from the callback from the connectivity manager private class NetworkRequestHandler( val connectivityManager: ConnectivityManager, looper: Looper, - val listener: NetworkListener + val listener: NetworkListener, + ips: IpsToProbe ) : Handler(looper) { // number of times the reachability check is performed due to failures @@ -267,21 +293,10 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : private const val MIN_MTU = 1280 } - private val ip4probes = - listOf( - "216.239.32.27", // google org - "104.16.132.229", // cloudflare - "31.13.79.53" // whatsapp.net - ) - + val ip4probes = ips.ip4probes // probing with domain names is not viable because some domains will resolve to both // ipv4 and ipv6 addresses. So, we use ipv6 addresses for probing ipv6 connectivity. - private val ip6probes = - listOf( - "2001:4860:4802:32::1b", // google org - "2606:4700::6810:84e5", // cloudflare - "2606:4700:3033::ac43:a21b" // rethinkdns - ) + val ip6probes = ips.ip6probes // ref - https://developer.android.com/reference/kotlin/java/util/LinkedHashSet // The network list is maintained in a linked-hash-set to preserve insertion and iteration @@ -503,7 +518,10 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : // for active network, ICMP echo is additionally used with TCP and UDP checks // but ICMP echo will always return reachable when app is in rinr mode // so till we have checks for rinr mode, we should not use ICMP reachability - val canUseIcmp = false // for now, need to check for rinr mode + + // icmp checks won't work on rinr mode, because + // the socket is not accessible to protect, so always return false + val canUseIcmp = false val useIcmp = isActive && canUseIcmp val has4 = probeConnectivity(ip4probes, network, useIcmp) val has6 = probeConnectivity(ip6probes, network, useIcmp) @@ -613,6 +631,22 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : return newNetworks } + private fun rearrangeNetworks(nws: Set): Set { + val newNetworks: LinkedHashSet = linkedSetOf() + val activeNetwork = connectivityManager.activeNetwork + val n = nws.firstOrNull { isNetworkSame(it, activeNetwork) } + if (n != null) { + newNetworks.add(n) + } + nws + .filter { isConnectionNotMetered(connectivityManager.getNetworkCapabilities(it)) } + .forEach { newNetworks.add(it) } + nws + .filter { !isConnectionNotMetered(connectivityManager.getNetworkCapabilities(it)) } + .forEach { newNetworks.add(it) } + return newNetworks + } + private fun isConnectionNotMetered(capabilities: NetworkCapabilities?): Boolean { return capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: false @@ -697,12 +731,23 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : return@runBlocking ok } + suspend fun probeIp(ip: String, nws: Set): ProbeResult { + val newNws = rearrangeNetworks(nws) + newNws.forEach { nw -> + if (isReachableTcpUdp(nw, ip)) { + val cap = connectivityManager.getNetworkCapabilities(nw) + return ProbeResult(ip, true, cap) + } + } + return ProbeResult(ip, false, null) + } + private suspend fun isReachableTcpUdp(nw: Network?, host: String): Boolean { try { // https://developer.android.com/reference/android/net/Network#bindSocket(java.net.Socket) TrafficStats.setThreadStatsTag(Thread.currentThread().id.toIntOrDefault()) - val yes = tcp80(nw, host) || udp53(nw, host) || tcp53(nw, host) + val yes = tcp80(nw, host) || tcp53(nw, host) Logger.d(LOG_TAG_CONNECTION, "$host isReachable on network($nw): $yes") return yes @@ -747,6 +792,7 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : val s = InetSocketAddress(host, port80) socket = Socket() nw?.bindSocket(socket) + VpnController.protectSocket(socket) socket.connect(s, timeout) val c = socket.isConnected val b = socket.isBound @@ -778,6 +824,7 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : socket = Socket() val s = InetSocketAddress(host, port53) nw?.bindSocket(socket) + VpnController.protectSocket(socket) socket.connect(s, timeout) val c = socket.isConnected val b = socket.isBound @@ -798,36 +845,6 @@ class ConnectionMonitor(private val networkListener: NetworkListener) : return false } - private suspend fun udp53(nw: Network?, host: String): Boolean { - val timeout = 500 // ms - val port53 = 53 // port - var socket: DatagramSocket? = null - - try { - socket = DatagramSocket() - val s = InetSocketAddress(host, port53) - nw?.bindSocket(socket) - socket.soTimeout = timeout - socket.connect(s) - val c = socket.isConnected - val b = socket.isBound - Logger.d(LOG_TAG_CONNECTION, "udpEcho: $host, ${nw?.networkHandle}: $c, $b") - return true - } catch (e: IOException) { - Logger.w(LOG_TAG_CONNECTION, "err udpEcho: ${e.message}, ${e.cause}") - val cause: Throwable = e.cause ?: return false - - return (cause is ErrnoException && cause.errno == ECONNREFUSED) - } catch (e: IllegalArgumentException) { - Logger.w(LOG_TAG_CONNECTION, "err udpEcho: ${e.message}, ${e.cause}") - } catch (e: SecurityException) { - Logger.w(LOG_TAG_CONNECTION, "err udpEcho: ${e.message}, ${e.cause}") - } finally { - clos(socket) - } - return false - } - private fun clos(socket: Closeable?) { try { socket?.close() diff --git a/app/src/main/java/com/celzero/bravedns/service/ConsoleLogManager.kt b/app/src/main/java/com/celzero/bravedns/service/ConsoleLogManager.kt new file mode 100644 index 000000000..ad6aebc98 --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/service/ConsoleLogManager.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.service + +import com.celzero.bravedns.database.ConsoleLog +import com.celzero.bravedns.database.ConsoleLogRepository + +class ConsoleLogManager(private val repository: ConsoleLogRepository) { + + suspend fun insert(log: ConsoleLog) { + repository.insert(log) + } + + suspend fun insertBatch(logs: List<*>) { + val l = logs as? List ?: return + + l.sortedBy { it.id } + repository.insertBatch(l) + } +} diff --git a/app/src/main/java/com/celzero/bravedns/service/DnsLogTracker.kt b/app/src/main/java/com/celzero/bravedns/service/DnsLogTracker.kt index d31a58406..622cffffe 100644 --- a/app/src/main/java/com/celzero/bravedns/service/DnsLogTracker.kt +++ b/app/src/main/java/com/celzero/bravedns/service/DnsLogTracker.kt @@ -85,6 +85,7 @@ internal constructor( transaction.relayName = summary.relayServer ?: "" transaction.msg = summary.msg ?: "" transaction.upstreamBlock = summary.upstreamBlocks + transaction.region = summary.region return transaction } @@ -103,6 +104,7 @@ internal constructor( dnsLog.time = transaction.responseCalendar.timeInMillis dnsLog.msg = transaction.msg dnsLog.upstreamBlock = transaction.upstreamBlock + dnsLog.region = transaction.region val typeName = ResourceRecordTypes.getTypeName(transaction.type.toInt()) if (typeName == ResourceRecordTypes.UNKNOWN) { dnsLog.typeName = transaction.type.toString() diff --git a/app/src/main/java/com/celzero/bravedns/service/DomainRulesManager.kt b/app/src/main/java/com/celzero/bravedns/service/DomainRulesManager.kt index 0f785b286..ef66c5382 100644 --- a/app/src/main/java/com/celzero/bravedns/service/DomainRulesManager.kt +++ b/app/src/main/java/com/celzero/bravedns/service/DomainRulesManager.kt @@ -312,6 +312,14 @@ object DomainRulesManager : KoinComponent { clearTrustedMap(uid) } + suspend fun deleteRules(list: List) { + list.forEach { cd -> + removeFromTrie(cd) + removeIfInTrustedMap(cd.uid, cd.domain) + } + db.deleteRules(list) + } + suspend fun deleteAllRules() { db.deleteAllRules() trie.clear() diff --git a/app/src/main/java/com/celzero/bravedns/service/EncryptedFileManager.kt b/app/src/main/java/com/celzero/bravedns/service/EncryptedFileManager.kt index 1a7bc184d..08235816c 100644 --- a/app/src/main/java/com/celzero/bravedns/service/EncryptedFileManager.kt +++ b/app/src/main/java/com/celzero/bravedns/service/EncryptedFileManager.kt @@ -134,7 +134,7 @@ object EncryptedFileManager { dir.mkdirs() } val fileToWrite = File(dir, fileName) - write(ctx, cfg, fileToWrite) + return write(ctx, cfg, fileToWrite) } catch (e: Exception) { Logger.e(Logger.LOG_TAG_PROXY, "Encrypted File Write: ${e.message}") } diff --git a/app/src/main/java/com/celzero/bravedns/service/FirewallRuleset.kt b/app/src/main/java/com/celzero/bravedns/service/FirewallRuleset.kt index 368d8c699..808e854f5 100644 --- a/app/src/main/java/com/celzero/bravedns/service/FirewallRuleset.kt +++ b/app/src/main/java/com/celzero/bravedns/service/FirewallRuleset.kt @@ -200,6 +200,18 @@ enum class FirewallRuleset(val id: String, val title: Int, val desc: Int, val ac R.string.firewall_rule_block_app_desc, R.integer.stall ), + RULE14( + "Private DNS", + R.string.firewall_rule_private_dns, + R.string.firewall_rule_private_dns_desc, + R.integer.stall + ), + RULE15( + "Bypass Proxy", + R.string.firewall_rule_bypass_proxy, + R.string.firewall_rule_bypass_proxy_desc, + R.integer.allow + ), ; companion object { @@ -234,6 +246,9 @@ enum class FirewallRuleset(val id: String, val title: Int, val desc: Int, val ac RULE10.id -> RULE10 RULE11.id -> RULE11 RULE12.id -> RULE12 + RULE13.id -> RULE13 + RULE14.id -> RULE14 + RULE15.id -> RULE15 else -> null } } @@ -270,16 +285,19 @@ enum class FirewallRuleset(val id: String, val title: Int, val desc: Int, val ac RULE10.id -> R.drawable.ic_http RULE11.id -> R.drawable.ic_global_lockdown RULE12.id -> R.drawable.ic_proxy_white + RULE13.id -> R.drawable.ic_proxy_white + RULE14.id -> R.drawable.bs_dns_home_screen + RULE15.id -> R.drawable.ic_bypass else -> R.drawable.bs_dns_home_screen } } fun getAllowedRules(): List { - return values().toList().filter { it.act == R.integer.allow } + return entries.filter { it.act == R.integer.allow } } fun getBlockedRules(): List { - return values().toList().filter { it.act != R.integer.allow } + return entries.filter { it.act != R.integer.allow } } fun ground(rule: FirewallRuleset): Boolean { @@ -304,6 +322,7 @@ enum class FirewallRuleset(val id: String, val title: Int, val desc: Int, val ac RULE2F.id -> true RULE2I.id -> true RULE12.id -> true + RULE15.id -> true else -> false } } diff --git a/app/src/main/java/com/celzero/bravedns/service/IPTracker.kt b/app/src/main/java/com/celzero/bravedns/service/IPTracker.kt index 5f25072a3..5372d460e 100644 --- a/app/src/main/java/com/celzero/bravedns/service/IPTracker.kt +++ b/app/src/main/java/com/celzero/bravedns/service/IPTracker.kt @@ -161,7 +161,7 @@ internal constructor( val androidUidConfig = AndroidUidConfig.fromFileSystemUid(uid) Logger.i( LOG_TAG_FIREWALL, - "android-uid for ${uid} is uid: ${androidUidConfig.uid}, n: ${androidUidConfig.name}" + "android-uid for $uid is uid: ${androidUidConfig.uid}, n: ${androidUidConfig.name}" ) if (androidUidConfig.uid == INVALID_UID) { diff --git a/app/src/main/java/com/celzero/bravedns/service/IpRulesManager.kt b/app/src/main/java/com/celzero/bravedns/service/IpRulesManager.kt index 3866ea0c5..15da140a9 100644 --- a/app/src/main/java/com/celzero/bravedns/service/IpRulesManager.kt +++ b/app/src/main/java/com/celzero/bravedns/service/IpRulesManager.kt @@ -320,6 +320,20 @@ object IpRulesManager : KoinComponent { resultsCache.invalidateAll() } + suspend fun deleteRules(list: List) { + list.forEach { + val pair = it.getCustomIpAddress() + val ipaddr = pair.first + val port = pair.second + val k = normalize(ipaddr) + if (!k.isNullOrEmpty()) { + iptree.esc(k, treeVal(it.uid, port, it.status)) + } + } + db.deleteRules(list) + resultsCache.invalidateAll() + } + suspend fun deleteAllAppsRules() { db.deleteAllAppsRules() iptree.clear() diff --git a/app/src/main/java/com/celzero/bravedns/service/NetLogTracker.kt b/app/src/main/java/com/celzero/bravedns/service/NetLogTracker.kt index 534881657..6cbd55b5b 100644 --- a/app/src/main/java/com/celzero/bravedns/service/NetLogTracker.kt +++ b/app/src/main/java/com/celzero/bravedns/service/NetLogTracker.kt @@ -16,32 +16,34 @@ package com.celzero.bravedns.service +import Logger.LOG_BATCH_LOGGER import android.content.Context +import android.util.Log import backend.DNSSummary -import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.data.ConnTrackerMetaData import com.celzero.bravedns.data.ConnectionSummary import com.celzero.bravedns.database.ConnectionTracker import com.celzero.bravedns.database.ConnectionTrackerRepository +import com.celzero.bravedns.database.ConsoleLog +import com.celzero.bravedns.database.ConsoleLogRepository import com.celzero.bravedns.database.DnsLog import com.celzero.bravedns.database.DnsLogRepository import com.celzero.bravedns.database.RethinkLog import com.celzero.bravedns.database.RethinkLogRepository +import com.celzero.bravedns.util.Daemons import com.celzero.bravedns.util.NetLogBatcher +import java.util.Calendar import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job +import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.util.Calendar class NetLogTracker internal constructor( @@ -49,48 +51,106 @@ internal constructor( connectionTrackerRepository: ConnectionTrackerRepository, rethinkLogRepository: RethinkLogRepository, dnsLogRepository: DnsLogRepository, + consoleLogRepository: ConsoleLogRepository, private val persistentState: PersistentState ) : KoinComponent { private val dnsLatencyTracker by inject() - private var scope: CoroutineScope? = null + @Volatile private var scope: CoroutineScope? = null private var dnsdb: DnsLogTracker = DnsLogTracker(dnsLogRepository, persistentState, context) private var ipdb: IPTracker = IPTracker(connectionTrackerRepository, rethinkLogRepository, context) + private var consoleLogDb: ConsoleLogManager = ConsoleLogManager(consoleLogRepository) private var dnsBatcher: NetLogBatcher? = null private var ipBatcher: NetLogBatcher? = null - private var rrBatcher :NetLogBatcher? = null + private var rrBatcher: NetLogBatcher? = null + private var consoleLogBatcher: NetLogBatcher? = null + + // dispatch buffer to consumer if greater than batch size for dns, ip and rr logs + private val logBatchSize = 20 + // dispatch buffer to consumer if greater than batch size, for console logs + private val consoleLogBatchSize = 100 // a single thread to run sig and batch co-routines in; // to avoid use of mutex/semaphores over shared-state // looper is never closed / cancelled and is always active - @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) - private val looper = newSingleThreadContext("nlbLooper") + private val looper = Daemons.make("netlog") + + private val consoleLogLooper = Daemons.make("consoleLog") + + companion object { + private const val UPDATE_DELAY = 2500L + } - @OptIn(ExperimentalCoroutinesApi::class) suspend fun restart(s: CoroutineScope) { this.scope = s + serializer("restart", looper) { + + // create new batchers on every new scope as their lifecycle is tied to the scope + val b1 = NetLogBatcher("dns", looper, logBatchSize, dnsdb::insertBatch) + val b2 = + NetLogBatcher( + "ip", + looper, + logBatchSize, + ipdb::insertBatch, + ipdb::updateBatch + ) + val b3 = + NetLogBatcher( + "rr", + looper, + logBatchSize, + ipdb::insertRethinkBatch, + ipdb::updateRethinkBatch + ) + val b4 = + NetLogBatcher("console", consoleLogLooper, consoleLogBatchSize, consoleLogDb::insertBatch) + + b1.begin(s) + b2.begin(s) + b3.begin(s) + b4.begin(s) + + this.dnsBatcher = b1 + this.ipBatcher = b2 + this.rrBatcher = b3 + this.consoleLogBatcher = b4 + + s.launch(Dispatchers.IO) { monitorCancellation() } + Log.d(LOG_BATCH_LOGGER, "tracker: restart, $scope") + } + } - // create new batchers on every new scope as their lifecycle is tied to the scope - val b1 = NetLogBatcher("dns", looper, dnsdb::insertBatch) - val b2 = NetLogBatcher("ip", looper, ipdb::insertBatch, ipdb::updateBatch) - val b3 = NetLogBatcher("rr", looper, ipdb::insertRethinkBatch, ipdb::updateRethinkBatch) - - b1.begin(s) - b2.begin(s) - b3.begin(s) - this.dnsBatcher = b1 - this.ipBatcher = b2 - this.rrBatcher = b3 + // stackoverflow.com/a/68905423 + private suspend fun monitorCancellation() { + try { + awaitCancellation() + } finally { + withContext(looper + NonCancellable) { + dnsBatcher?.close() + ipBatcher?.close() + rrBatcher?.close() + dnsBatcher = null + ipBatcher = null + rrBatcher = null + Logger.d(LOG_BATCH_LOGGER, "tracker: close scope") + } + withContext(consoleLogLooper + NonCancellable) { + consoleLogBatcher?.close() + consoleLogBatcher = null + Logger.d(LOG_BATCH_LOGGER, "tracker: close consoleLogLooper") + } + } } fun writeIpLog(info: ConnTrackerMetaData) { if (!persistentState.logsEnabled) return - io("writeIpLog") { + serializer("writeIpLog", looper) { val connTracker = ipdb.makeConnectionTracker(info) ipBatcher?.add(connTracker) } @@ -99,8 +159,8 @@ internal constructor( fun writeRethinkLog(info: ConnTrackerMetaData) { if (!persistentState.logsEnabled) return - io("writeRethinkLog") { - val rlog = ipdb.makeRethinkLogs(info) ?: return@io + serializer("writeRethinkLog", looper) { + val rlog = ipdb.makeRethinkLogs(info) rrBatcher?.add(rlog) } } @@ -108,14 +168,17 @@ internal constructor( fun updateIpSummary(summary: ConnectionSummary) { if (!persistentState.logsEnabled) return - io("updateIpSmm") { + serializer("updateIpSmm", looper) { val s = - if (DEBUG && summary.targetIp?.isNotEmpty() == true) { + if (summary.targetIp?.isNotEmpty() == true) { ipdb.makeSummaryWithTarget(summary) } else { summary } + // add a delay to ensure the insert is complete before updating + delay(UPDATE_DELAY) + ipBatcher?.update(s) } } @@ -123,14 +186,17 @@ internal constructor( fun updateRethinkSummary(summary: ConnectionSummary) { if (!persistentState.logsEnabled) return - io("updateRethinkSmm") { + serializer("updateRethinkSmm", looper) { val s = - if (DEBUG && summary.targetIp?.isNotEmpty() == true) { + if (summary.targetIp?.isNotEmpty() == true) { ipdb.makeSummaryWithTarget(summary) } else { summary } + // add a delay to ensure the insert is complete before updating + delay(UPDATE_DELAY) + rrBatcher?.update(s) } } @@ -141,8 +207,8 @@ internal constructor( val transaction = dnsdb.processOnResponse(summary) transaction.responseCalendar = Calendar.getInstance() - // refresh latency from GoVpnAdapter - io("refreshDnsLatency") { dnsLatencyTracker.refreshLatencyIfNeeded(transaction) } + // TODO: move this to generic Dispatcher.IO; serializer is not required + serializer("refreshDnsLatency", looper) { dnsLatencyTracker.refreshLatencyIfNeeded(transaction) } // TODO: This method should be part of BraveVPNService dnsdb.updateVpnConnectionState(transaction) @@ -150,9 +216,16 @@ internal constructor( if (!persistentState.logsEnabled) return val dnsLog = dnsdb.makeDnsLogObj(transaction) - io("writeDnsLog") { dnsBatcher?.add(dnsLog) } + serializer("writeDnsLog", looper) { dnsBatcher?.add(dnsLog) } + } + + fun writeConsoleLog(log: ConsoleLog) { + serializer("writeConsoleLog", consoleLogLooper) { + consoleLogBatcher?.add(log) + } } - private fun io(s: String, f: suspend () -> Unit) = - scope?.launch(CoroutineName(s) + Dispatchers.IO) { f() } + private fun serializer(s: String, e: ExecutorCoroutineDispatcher, f: suspend () -> Unit) = + scope?.launch(CoroutineName(s) + e) { f() } + ?: Log.e(LOG_BATCH_LOGGER, "scope is null", Exception()) } diff --git a/app/src/main/java/com/celzero/bravedns/service/PauseTimer.kt b/app/src/main/java/com/celzero/bravedns/service/PauseTimer.kt index 42a3e7e26..65314747b 100644 --- a/app/src/main/java/com/celzero/bravedns/service/PauseTimer.kt +++ b/app/src/main/java/com/celzero/bravedns/service/PauseTimer.kt @@ -51,7 +51,9 @@ object PauseTimer { } } finally { Logger.d(LOG_TAG_VPN, "pause timer complete") - VpnController.resumeApp() + if (VpnController.isAppPaused()) { + VpnController.resumeApp() + } setCountdown(INIT_TIME_MS) } } diff --git a/app/src/main/java/com/celzero/bravedns/service/PersistentState.kt b/app/src/main/java/com/celzero/bravedns/service/PersistentState.kt index 32ea508c2..ec5527f13 100644 --- a/app/src/main/java/com/celzero/bravedns/service/PersistentState.kt +++ b/app/src/main/java/com/celzero/bravedns/service/PersistentState.kt @@ -20,6 +20,8 @@ import androidx.lifecycle.MutableLiveData import com.celzero.bravedns.R import com.celzero.bravedns.data.AppConfig import com.celzero.bravedns.database.DnsCryptRelayEndpoint +import com.celzero.bravedns.ui.activity.AntiCensorshipActivity +import com.celzero.bravedns.util.Constants import com.celzero.bravedns.util.Constants.Companion.INIT_TIME_MS import com.celzero.bravedns.util.Constants.Companion.INVALID_PORT import com.celzero.bravedns.util.InternetProtocol @@ -59,6 +61,12 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { const val NOTIFICATION_PERMISSION = "notification_permission_request" const val EXCLUDE_APPS_IN_PROXY = "exclude_apps_in_proxy" const val BIOMETRIC_AUTH = "biometric_authentication" + const val ANTI_CENSORSHIP_TYPE = "dial_strategy" + const val RETRY_STRATEGY = "retry_strategy" + const val ENDPOINT_INDEPENDENCE = "endpoint_independence" + const val TCP_KEEP_ALIVE = "tcp_keep_alive" + const val USE_SYSTEM_DNS_FOR_UNDELEGATED_DOMAINS = "use_system_dns_for_undelegated_domains" + const val SLOWDOWN_MODE = "slowdown_mode" } // when vpn is started by the user, this is set to true; set to false when user stops @@ -121,9 +129,8 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { stringPref("http_proxy_ipaddress").withDefault("http://127.0.0.1:8118") // whether apps subject to the RethinkDNS VPN tunnel can bypass the tunnel on-demand - // default: false for fdroid flavour - var allowBypass by - booleanPref("allow_bypass").withDefault(!Utilities.isFdroidFlavour()) + // default: false + var allowBypass by booleanPref("allow_bypass").withDefault(false) // user set among AppConfig.DnsType enum; RETHINK_REMOTE is default which is Rethink-DoH var dnsType by @@ -190,9 +197,10 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { private var _blockNewlyInstalledApp by booleanPref("block_new_app").withDefault(false) // user setting to use custom download manager or android's default download manager - // default: false, i.e., use android's default download manager + // default: true, i.e., use in-build download manager, as we see lot of failures with + // android's download manager because of the blocking nature of the app var useCustomDownloadManager by - booleanPref("use_custom_download_managet").withDefault(false) + booleanPref("use_custom_download_managet").withDefault(true) // custom download manager's last generated id var customDownloaderLastGeneratedId by @@ -242,14 +250,17 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { // make notification persistent (Android 13 and above), default false var persistentNotification by booleanPref("persistent_notification").withDefault(false) - // biometric authentication + // biometric authentication TODO: remove this var biometricAuth by booleanPref("biometric_authentication").withDefault(false) + // bio-metric authentication type + var biometricAuthType by intPref("biometric_authentication_type").withDefault(0) + // enable dns alg var enableDnsAlg by booleanPref("dns_alg").withDefault(false) // default dns url - var defaultDnsUrl by stringPref("default_dns_query").withDefault("") + var defaultDnsUrl by stringPref("default_dns_query").withDefault(Constants.DEFAULT_DNS_LIST[1].url) // packet capture type var pcapMode by intPref("pcap_mode").withDefault(PcapMode.NONE.id) @@ -282,6 +293,43 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { // exclude apps which are configured in proxy (socks5, http, dns proxy) var excludeAppsInProxy by booleanPref("exclude_apps_in_proxy").withDefault(true) + var pingv4Ips by stringPref("ping_ipv4_ips").withDefault(Constants.ip4probes.joinToString(",")) + + var pingv6Ips by stringPref("ping_ipv6_ips").withDefault(Constants.ip6probes.joinToString(",")) + + // TODO: do we need this? instead use in-memory variable + var consoleLogEnabled by booleanPref("console_log_enabled").withDefault(false) + + // camera and mic access + var micCamAccess by booleanPref("mic_camera_access").withDefault(false) + + // anti-censorship type (auto, split_tls, split_tcp, desync) + var dialStrategy by intPref("dial_strategy").withDefault(AntiCensorshipActivity.DialStrategies.SPLIT_AUTO.mode) + + // retry strategy type (before split, after split, never) + var retryStrategy by intPref("retry_strategy").withDefault(AntiCensorshipActivity.RetryStrategies.RETRY_AFTER_SPLIT.mode) + + // bypass blocking in dns level, decision is made in flow() (see BraveVPNService#flow) + var bypassBlockInDns by booleanPref("bypass_block_in_dns").withDefault(false) + + // randomize listen port for advanced wireguard configuration, default false + // restart of tunnel when wireguard is enabled is required to randomize the port to work properly + // this is not a user facing option, but a developer option + var randomizeListenPort by booleanPref("randomize_listen_port").withDefault(true) + + // endpoint independent mapping/filtering + var endpointIndependence by booleanPref("endpoint_independence").withDefault(false) + + var tcpKeepAlive by booleanPref("tcp_keep_alive").withDefault(false) + + // enable split dns + var splitDns by booleanPref("split_dns").withDefault(false) + + // use system dns for undelegatedDomains + var useSystemDnsForUndelegatedDomains by booleanPref("use_system_dns_for_undelegated_domains").withDefault(false) + + var slowdownMode by booleanPref("slowdown_mode").withDefault(false) + var orbotConnectionStatus: MutableLiveData = MutableLiveData() var median: MutableLiveData = MutableLiveData() var vpnEnabledLiveData: MutableLiveData = MutableLiveData() @@ -420,7 +468,7 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { } fun getProxyStatus(): MutableLiveData { - if (proxyStatus.value == null) updateProxyStatus() + if (proxyStatus.value == null || proxyStatus.value == -1) updateProxyStatus() return proxyStatus } @@ -438,12 +486,16 @@ class PersistentState(context: Context) : SimpleKrate(context), KoinComponent { } AppConfig.ProxyProvider.CUSTOM -> { val type = AppConfig.ProxyType.of(proxyType) - if (type == AppConfig.ProxyType.SOCKS5) { - R.string.lbl_socks5 - } else if (type == AppConfig.ProxyType.HTTP) { - R.string.lbl_http - } else { - R.string.lbl_http_socks5 + when (type) { + AppConfig.ProxyType.SOCKS5 -> { + R.string.lbl_socks5 + } + AppConfig.ProxyType.HTTP -> { + R.string.lbl_http + } + else -> { + R.string.lbl_http_socks5 + } } } else -> { diff --git a/app/src/main/java/com/celzero/bravedns/service/RethinkBlocklistManager.kt b/app/src/main/java/com/celzero/bravedns/service/RethinkBlocklistManager.kt index 348c718d4..cc851229c 100644 --- a/app/src/main/java/com/celzero/bravedns/service/RethinkBlocklistManager.kt +++ b/app/src/main/java/com/celzero/bravedns/service/RethinkBlocklistManager.kt @@ -159,7 +159,7 @@ object RethinkBlocklistManager : KoinComponent { packsBlocklistMapping.put(PacksMappingKey(s, 0), l.value) return@forEachIndexed } - val level = l.level?.elementAt(index) ?: 2 + val level = l.level?.getOrNull(index) ?: 2 packsBlocklistMapping.put(PacksMappingKey(s, level), l.value) } } @@ -182,7 +182,7 @@ object RethinkBlocklistManager : KoinComponent { key.pack, key.level, packsBlocklistMapping.get(key).toList(), - dbFileTagLocal.first { it.pack?.contains(key.pack) == true }.group + dbFileTagLocal.firstOrNull { it.pack?.contains(key.pack) == true }?.group ?: "" ) } ) @@ -239,7 +239,7 @@ object RethinkBlocklistManager : KoinComponent { return@forEachIndexed } // if the level is empty, then set the level to 2 (assume highest) #756 - val level = r.level?.elementAt(index) ?: 2 + val level = r.level?.getOrNull(index) ?: 2 packsBlocklistMapping.put(PacksMappingKey(s, level), r.value) } } @@ -266,7 +266,7 @@ object RethinkBlocklistManager : KoinComponent { key.pack, key.level, packsBlocklistMapping.get(key).toList(), - dbFileTagRemote.first { it.pack?.contains(key.pack) == true }.group + dbFileTagRemote.firstOrNull { it.pack?.contains(key.pack) == true }?.group ?: "" ) } ) diff --git a/app/src/main/java/com/celzero/bravedns/service/ServiceModule.kt b/app/src/main/java/com/celzero/bravedns/service/ServiceModule.kt index 3018011b8..7c4df140a 100644 --- a/app/src/main/java/com/celzero/bravedns/service/ServiceModule.kt +++ b/app/src/main/java/com/celzero/bravedns/service/ServiceModule.kt @@ -23,7 +23,7 @@ object ServiceModule { private val serviceModules = module { single { PersistentState(androidContext()) } single { QueryTracker(get()) } - single { NetLogTracker(androidContext(), get(), get(), get(), get()) } + single { NetLogTracker(androidContext(), get(), get(), get(), get(), get()) } single { RefreshDatabase(androidContext(), get(), get(), get()) } } diff --git a/app/src/main/java/com/celzero/bravedns/service/VpnController.kt b/app/src/main/java/com/celzero/bravedns/service/VpnController.kt index c83cec5c6..39f2a613d 100644 --- a/app/src/main/java/com/celzero/bravedns/service/VpnController.kt +++ b/app/src/main/java/com/celzero/bravedns/service/VpnController.kt @@ -22,13 +22,17 @@ import android.content.Context import android.content.Intent import android.os.SystemClock import androidx.core.content.ContextCompat +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import backend.RDNS -import backend.Stats +import backend.RouterStats import com.celzero.bravedns.R +import com.celzero.bravedns.database.ConsoleLog import com.celzero.bravedns.service.BraveVPNService.Companion.FAIL_OPEN_ON_NO_NETWORK import com.celzero.bravedns.util.Constants.Companion.INVALID_UID import com.celzero.bravedns.util.Utilities +import java.net.Socket +import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -96,8 +100,10 @@ object VpnController : KoinComponent { states?.cancel() vpnStartElapsedTime = SystemClock.elapsedRealtime() try { + // externalScope?.coroutineContext?.get(Job)?.cancel("VPNController - onVpnDestroyed") externalScope?.cancel("VPNController - onVpnDestroyed") - } catch (ignored: IllegalStateException) {} + } catch (ignored: IllegalStateException) {} catch ( + ignored: CancellationException) {} catch (ignored: Exception) {} } fun uptimeMs(): Long { @@ -139,11 +145,11 @@ object VpnController : KoinComponent { Logger.i(LOG_TAG_VPN, "VPNController - Start(Synchronized) executed - $context") } - fun stop(context: Context) { + fun stop(reason: String, context: Context) { Logger.i(LOG_TAG_VPN, "VPN Controller stop with context: $context") connectionState = null - onConnectionStateChanged(connectionState) - braveVpnService?.signalStopService(userInitiated = true) + onConnectionStateChanged(null) + braveVpnService?.signalStopService(reason, userInitiated = true) } fun state(): VpnState { @@ -212,7 +218,7 @@ object VpnController : KoinComponent { return braveVpnService?.getProxyStatusById(id) } - suspend fun getProxyStats(id: String): Stats? { + suspend fun getProxyStats(id: String): RouterStats? { return braveVpnService?.getProxyStats(id) } @@ -232,6 +238,10 @@ object VpnController : KoinComponent { braveVpnService?.syncP50Latency(id) } + fun getRegionLiveData(): LiveData { + return braveVpnService?.getRegionLiveData() ?: MutableLiveData() + } + fun protocols(): String { val ipv4 = protocol.first val ipv6 = protocol.second @@ -316,7 +326,31 @@ object VpnController : KoinComponent { return braveVpnService?.getRDNS(type) } - suspend fun goBuildVersion(): String { - return braveVpnService?.goBuildVersion() ?: "" + fun goBuildVersion(full: Boolean): String { + return braveVpnService?.goBuildVersion(full) ?: "" + } + + fun protectSocket(socket: Socket) { + braveVpnService?.protectSocket(socket) + } + + suspend fun probeIp(ip: String): ConnectionMonitor.ProbeResult? { + return braveVpnService?.probeIp(ip) + } + + suspend fun notifyConnectionMonitor() { + braveVpnService?.notifyConnectionMonitor() + } + + suspend fun getSystemDns(): String { + return braveVpnService?.getSystemDns() ?: "" + } + + fun getNetStat(): backend.NetStat? { + return braveVpnService?.getNetStat() + } + + fun writeConsoleLog(log: ConsoleLog) { + braveVpnService?.writeConsoleLog(log) } } diff --git a/app/src/main/java/com/celzero/bravedns/util/AndroidUidConfig.kt b/app/src/main/java/com/celzero/bravedns/util/AndroidUidConfig.kt index cec494977..5e3695a2c 100644 --- a/app/src/main/java/com/celzero/bravedns/util/AndroidUidConfig.kt +++ b/app/src/main/java/com/celzero/bravedns/util/AndroidUidConfig.kt @@ -148,7 +148,7 @@ enum class AndroidUidConfig(val uid: Int) { OTHER(Constants.INVALID_UID); companion object { - private val map = values().associateBy(AndroidUidConfig::uid) + private val map = entries.associateBy(AndroidUidConfig::uid) fun fromFileSystemUid(uid: Int): AndroidUidConfig { return map[uid.hashCode()] ?: OTHER diff --git a/app/src/main/java/com/celzero/bravedns/util/Constants.kt b/app/src/main/java/com/celzero/bravedns/util/Constants.kt index ccbb61cec..0b6e47b84 100644 --- a/app/src/main/java/com/celzero/bravedns/util/Constants.kt +++ b/app/src/main/java/com/celzero/bravedns/util/Constants.kt @@ -319,5 +319,20 @@ class Constants { "RDNS Security", "RDNS Privacy" ) + + val ip4probes = + listOf( + "216.239.32.27", // google org + "104.16.132.229", // cloudflare + "31.13.79.53" // whatsapp.net + ) + + + val ip6probes = + listOf( + "2001:4860:4802:32::1b", // google org + "2606:4700::6810:84e5", // cloudflare + "2606:4700:3033::ac43:a21b" // rethinkdns + ) } } diff --git a/app/src/main/java/com/celzero/bravedns/util/Daemons.kt b/app/src/main/java/com/celzero/bravedns/util/Daemons.kt new file mode 100644 index 000000000..92642acee --- /dev/null +++ b/app/src/main/java/com/celzero/bravedns/util/Daemons.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2024 RethinkDNS and its authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.celzero.bravedns.util + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + +object Daemons { + + fun make(tag: String) = Executors.newSingleThreadExecutor(Factory(tag)).asCoroutineDispatcher() + fun ioDispatcher(tag: String, default: T, s: CoroutineScope) = CoFactory(tag, default, s, make(tag)) +} + +class CoFactory( + private val tag: String, + private val default: T, + private val scope: CoroutineScope, + private val d: CoroutineDispatcher = Dispatchers.IO +) { + data class Msg(val m: suspend () -> T, val reply: Channel>) + + private val taskChannel = Channel>(Channel.UNLIMITED) + + init { + tasks() + } + + private fun ioAsync(f: suspend () -> T): Deferred { + return scope.async(CoroutineName(tag) + d) { f() } + } + + private fun io(f: suspend () -> Unit) = + scope.launch(CoroutineName(tag) + d) { f() } + + private suspend fun dispatch(f: suspend () -> T): Deferred { + val reply: Channel> = takeChannel() + taskChannel.send(Msg(f, reply)) + val d = reply.receive() + recycleChannel(reply) + return d + } + + suspend fun tryDispatch(f: suspend() -> T): T { + return try { + dispatch(f).await() + } catch (e: Exception) { + Logger.v(Logger.LOG_TAG_VPN, "err in tryDispatch: ${e.message}") + default + } + } + + private fun tasks() = io { + for (task in taskChannel) { + val d = ioAsync(task.m) + task.reply.send(d) + } + } + + // create a stack pooling for reply channels, pool for 20 channels with get and recycle + private val channels = ArrayDeque>>(20) + private val channelsMutex: Mutex = Mutex() + + private suspend fun takeChannel(): Channel> { + channelsMutex.lock() + if (channels.isEmpty()) { + channelsMutex.unlock() + return Channel(Channel.RENDEZVOUS) + } + val x = channels.removeLast() + channelsMutex.unlock() + return x + } + + // always recycle the exhausted channel, ie., the channel that is completed all the receives + private suspend fun recycleChannel(c: Channel>) { + channelsMutex.lock() + if (channels.size < 20) { + channels.add(c) + } + channelsMutex.unlock() + } + +} + +// adopted from: java.util.concurrent.Executors.DefaultThreadFactory +class Factory(tag: String = "d") : ThreadFactory { + private val group: ThreadGroup? + private val threadNumber = AtomicInteger(1) + private val namePrefix: String + + init { + val s = System.getSecurityManager() + group = if ((s != null)) s.threadGroup else Thread.currentThread().threadGroup + namePrefix = tag + poolNumber.getAndIncrement() + "t" + } + + override fun newThread(r: Runnable): Thread { + val t = Thread( + group, r, + namePrefix + threadNumber.getAndIncrement(), + 0 + ) + if (t.isDaemon) t.isDaemon = false + if (t.priority != Thread.NORM_PRIORITY) t.priority = Thread.NORM_PRIORITY + return t + } + + companion object { + private val poolNumber = AtomicInteger(1) + } +} diff --git a/app/src/main/java/com/celzero/bravedns/util/IPUtil.kt b/app/src/main/java/com/celzero/bravedns/util/IPUtil.kt index 0e25f415e..69c5e5e5f 100644 --- a/app/src/main/java/com/celzero/bravedns/util/IPUtil.kt +++ b/app/src/main/java/com/celzero/bravedns/util/IPUtil.kt @@ -81,11 +81,7 @@ class IPUtil { // get the first segment from ipv6 address val segment = ipv6.getSegment(0) // Decimal value for the 64(hexa) is 100 - if (segment.segmentValue == 100) { - return true - } - - return false + return segment.segmentValue == 100 } fun getIpAddress(ip: String): IPAddress? { diff --git a/app/src/play/java/com/celzero/bravedns/util/Logger.kt b/app/src/main/java/com/celzero/bravedns/util/Logger.kt similarity index 64% rename from app/src/play/java/com/celzero/bravedns/util/Logger.kt rename to app/src/main/java/com/celzero/bravedns/util/Logger.kt index 88cd79abc..e79f9d6db 100644 --- a/app/src/play/java/com/celzero/bravedns/util/Logger.kt +++ b/app/src/main/java/com/celzero/bravedns/util/Logger.kt @@ -14,14 +14,16 @@ * limitations under the License. */ import android.util.Log +import com.celzero.bravedns.database.ConsoleLog +import com.celzero.bravedns.database.ConsoleLogRepository import com.celzero.bravedns.service.PersistentState -import com.celzero.bravedns.util.Utilities -import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.celzero.bravedns.service.VpnController import org.koin.core.component.KoinComponent import org.koin.core.component.inject object Logger : KoinComponent { private val persistentState by inject() + private val inMemDb by inject() private var logLevel = persistentState.goLoggerLevel const val LOG_TAG_APP_UPDATE = "NonStoreAppUpdater" @@ -40,9 +42,10 @@ object Logger : KoinComponent { const val LOG_TAG_PROXY = "ProxyLogs" const val LOG_QR_CODE = "QrCodeFromFileScanner" const val LOG_GO_LOGGER = "LibLogger" + const val LOG_TAG_APP_OPS = "AppOpsService" - // github.com/celzero/firestack/blob/bce8de917fec5e48a41ed1e96c9d942ee0f7996b/intra/log/logger.go#L76 - enum class LoggerType(val id: Int) { + // github.com/celzero/firestack/blob/bce8de917f/intra/log/logger.go#L76 + enum class LoggerType(val id: Long) { VERY_VERBOSE(0), VERBOSE(1), DEBUG(2), @@ -74,6 +77,10 @@ object Logger : KoinComponent { return this == STACKTRACE } + fun isLessThan(level: LoggerType): Boolean { + return this.id < level.id + } + fun user(): Boolean { return this == USR } @@ -105,18 +112,6 @@ object Logger : KoinComponent { fun crash(tag: String, message: String, e: Exception? = null) { log(tag, message, LoggerType.ERROR, e) - if (Utilities.isPlayStoreFlavour()) { - try { - val crashlytics = FirebaseCrashlytics.getInstance() - crashlytics.log("$tag: $message") - if (e != null) crashlytics.recordException(e) - else crashlytics.recordException(Exception(message)) - // send the unsent reports, if any as the crash is important to be reported. - crashlytics.sendUnsentReports() - } catch (ex: Exception) { - Log.e(LOG_TAG_APP_UPDATE, "Error in logging to crashlytics: ${ex.message}") - } - } } fun updateConfigLevel(level: Long) { @@ -131,7 +126,12 @@ object Logger : KoinComponent { } } - private fun log(tag: String, msg: String, type: LoggerType, e: Exception? = null) { + fun goLog(message: String, type: LoggerType) { + // no need to log the go logs, add it to the database + dbWrite("", message, type) + } + + fun log(tag: String, msg: String, type: LoggerType, e: Exception? = null) { when (type) { LoggerType.VERY_VERBOSE -> if (logLevel <= LoggerType.VERY_VERBOSE.id) Log.v(tag, msg) LoggerType.VERBOSE -> if (logLevel <= LoggerType.VERBOSE.id) Log.v(tag, msg) @@ -143,5 +143,46 @@ object Logger : KoinComponent { LoggerType.USR -> {} // Do nothing LoggerType.NONE -> {} // Do nothing } + dbWrite(tag, msg, type, e) + } + + private fun dbWrite(tag: String, msg: String, type: LoggerType, e: Exception? = null) { + // write to the database only if console log is set to true + if (!persistentState.consoleLogEnabled) return + + try { + // cannot check for log levels when tag is empty; tag is empty for logs coming from go + if (tag.isEmpty()) { + val log = ConsoleLog(0, msg, System.currentTimeMillis()) + VpnController.writeConsoleLog(log) + // TODO: use send instead of trySend + //inMemDb.logChannel.trySend(log) + } else if (type.id >= logLevel) { + val l = when (type) { + LoggerType.VERBOSE -> "V" + LoggerType.DEBUG -> "D" + LoggerType.INFO -> "I" + LoggerType.WARN -> "W" + LoggerType.ERROR -> "E" + LoggerType.STACKTRACE -> "E" + else -> "V" + } + val log = if (e != null) { + ConsoleLog( + 0, + "$l $tag: $msg\n${Log.getStackTraceString(e)}", + System.currentTimeMillis() + ) + } else { + ConsoleLog(0, "$l $tag: $msg", System.currentTimeMillis()) + } + //inMemDb.logChannel.trySend(log) + VpnController.writeConsoleLog(log) + } else { + // Do nothing + } + } catch (ex: Exception) { + Log.e(LOG_GO_LOGGER, "err while writing log to the database: ${ex.message}", ex) + } } } diff --git a/app/src/main/java/com/celzero/bravedns/util/NetLogBatcher.kt b/app/src/main/java/com/celzero/bravedns/util/NetLogBatcher.kt index ccd50883b..9f31f42c8 100644 --- a/app/src/main/java/com/celzero/bravedns/util/NetLogBatcher.kt +++ b/app/src/main/java/com/celzero/bravedns/util/NetLogBatcher.kt @@ -16,23 +16,18 @@ package com.celzero.bravedns.util -import Logger import Logger.LOG_BATCH_LOGGER -import kotlinx.coroutines.CloseableCoroutineDispatcher +import android.util.Log +import co.touchlab.stately.concurrency.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext // channel buffer receives batched entries of batchsize or once every waitms from a batching @@ -40,18 +35,21 @@ import kotlinx.coroutines.withContext class NetLogBatcher( private val tag: String, private val looper: CoroutineDispatcher, + private val batchSize: Int, private val processor: suspend (List) -> Unit, - private val updator: suspend (List) -> Unit = { _ -> } + private val updator: suspend (List) -> Unit = { _ -> }, ) { + companion object { + private const val DEBUG = true + } + // i keeps track of currently in-use buffer var lsn = 0 private val nprod = CoroutineName(tag + "Producer") // batches writes private val nsig = CoroutineName(tag + "Signal") private val ncons = CoroutineName(tag + "Consumer") // writes batches to db - - // dispatch buffer to consumer if greater than batch size - private val batchSize = 20 + private val closed = AtomicBoolean(false) // no of batch-sized buffers to hold in a channel private val qsize = 2 @@ -77,23 +75,16 @@ class NetLogBatcher( scope.async { sig() } scope.async { consumeAdd() } scope.async { consumeUpdate() } - - // monitor for cancellation on the default dispatcher - scope.launch { monitorCancellation() } } // stackoverflow.com/a/68905423 - private suspend fun monitorCancellation() { - try { - awaitCancellation() - } finally { - withContext(NonCancellable) { - signal.close() - buffersCh.close() - updatesCh.close() - Logger.i(LOG_BATCH_LOGGER, "end") - } - } + suspend fun close() = withContext(NonCancellable) { + if (closed.compareAndSet(expected = false, new = true)) { + signal.close() + buffersCh.close() + updatesCh.close() + logd("$tag end") + } } private suspend fun consumeAdd() = @@ -110,16 +101,21 @@ class NetLogBatcher( } } + private fun logd(msg: String) { + // write batcher logs only in DEBUG mode to avoid log spam + if (DEBUG) Log.d(LOG_BATCH_LOGGER, msg) + } + private suspend fun txswap() { val b = batches - batches = mutableListOf() // swap buffers + batches = mutableListOf() // swap buffers buffersCh.send(b) val u = updates - updates = mutableListOf() // swap buffers + updates = mutableListOf() // swap buffers updatesCh.send(u) - Logger.d(LOG_BATCH_LOGGER, "txswap (${lsn}) b: ${b.size}, u: ${u.size}") + logd( "txswap (${lsn}) b: ${b.size}, u: ${u.size}") lsn = (lsn + 1) } @@ -150,25 +146,22 @@ class NetLogBatcher( // consume all signals for (tracklsn in signal) { if (tracklsn < lsn) { - Logger.d(LOG_BATCH_LOGGER, "dup signal skip $tracklsn") + logd("dup signal skip $tracklsn") continue } // do not honor the signal for 'l' if a[l] is empty // this can happen if the signal for 'l' is processed // after the fact that 'l' has been swapped out by 'batch' if (batches.size <= 0 && updates.size <= 0) { - Logger.d(LOG_BATCH_LOGGER, "signal continue") + logd("signal continue") continue } else { - Logger.d(LOG_BATCH_LOGGER, "signal sleep $waitms ms") + logd("signal sleep $waitms ms") } // wait for 'batch' to dispatch delay(waitms) - Logger.d( - LOG_BATCH_LOGGER, - "signal wait over, sz(b: ${batches.size}, u: ${updates.size}) / cur-buf(${lsn})" - ) + logd("signal wait over, sz(b: ${batches.size}, u: ${updates.size}) / cur-buf(${lsn})") // 'l' is the current buffer, that is, 'l == i', // and 'batch' hasn't dispatched it, diff --git a/app/src/main/java/com/celzero/bravedns/util/OrbotHelper.kt b/app/src/main/java/com/celzero/bravedns/util/OrbotHelper.kt index 71ca5b7c0..716acb41c 100644 --- a/app/src/main/java/com/celzero/bravedns/util/OrbotHelper.kt +++ b/app/src/main/java/com/celzero/bravedns/util/OrbotHelper.kt @@ -87,7 +87,7 @@ class OrbotHelper( private const val STATUS_STOPPING = "STOPPING" private const val STATUS_OFF = "OFF" - private const val orbot = "ORBOT" + private const val ORBOT = "ORBOT" const val ORBOT_NOTIFICATION_ACTION_TEXT = "OPEN_ORBOT_INTENT" @@ -432,7 +432,7 @@ class OrbotHelper( return ProxyEndpoint( id, - orbot, + ORBOT, proxyMode.value, proxyType = "NONE", ORBOT_PACKAGE_NAME, diff --git a/app/src/main/java/com/celzero/bravedns/util/PlayStoreCategory.kt b/app/src/main/java/com/celzero/bravedns/util/PlayStoreCategory.kt index 0250770d1..f1683f58d 100644 --- a/app/src/main/java/com/celzero/bravedns/util/PlayStoreCategory.kt +++ b/app/src/main/java/com/celzero/bravedns/util/PlayStoreCategory.kt @@ -52,7 +52,7 @@ enum class PlayStoreCategory(val rawValue: Int) { GAMES("GAMES".hashCode()); companion object { - private val map = values().associateBy(PlayStoreCategory::rawValue) + private val map = entries.associateBy(PlayStoreCategory::rawValue) const val GENERAL_GAMES_CATEGORY_NAME = "GAMES" fun fromCategoryName(name: String): PlayStoreCategory { diff --git a/app/src/main/java/com/celzero/bravedns/util/Protocol.kt b/app/src/main/java/com/celzero/bravedns/util/Protocol.kt index 54492ab90..6dfcf4473 100644 --- a/app/src/main/java/com/celzero/bravedns/util/Protocol.kt +++ b/app/src/main/java/com/celzero/bravedns/util/Protocol.kt @@ -33,6 +33,7 @@ enum class Protocol(val protocolType: Int) { GRE(47), ESP(50), AH(51), + ICMPV6(58), MTP(92), BEETPH(94), ENCAP(98), @@ -44,7 +45,7 @@ enum class Protocol(val protocolType: Int) { OTHER(-1); companion object { - private val map = values().associateBy(Protocol::protocolType) + private val map = entries.associateBy(Protocol::protocolType) fun getProtocolName(protocolType: Int): Protocol { return map[protocolType.hashCode()] ?: OTHER diff --git a/app/src/main/java/com/celzero/bravedns/util/ResourceRecordTypes.kt b/app/src/main/java/com/celzero/bravedns/util/ResourceRecordTypes.kt index aba66b241..a2b2737ba 100644 --- a/app/src/main/java/com/celzero/bravedns/util/ResourceRecordTypes.kt +++ b/app/src/main/java/com/celzero/bravedns/util/ResourceRecordTypes.kt @@ -94,7 +94,7 @@ enum class ResourceRecordTypes(val value: Int, val desc: String) { UNKNOWN(-1, "Unknown"); companion object { - private val map = values().associateBy(ResourceRecordTypes::value) + private val map = entries.associateBy(ResourceRecordTypes::value) fun getTypeName(value: Int): ResourceRecordTypes { return map[value.hashCode()] ?: UNKNOWN diff --git a/app/src/main/java/com/celzero/bravedns/util/Themes.kt b/app/src/main/java/com/celzero/bravedns/util/Themes.kt index 69ac2e772..6fdf09b32 100644 --- a/app/src/main/java/com/celzero/bravedns/util/Themes.kt +++ b/app/src/main/java/com/celzero/bravedns/util/Themes.kt @@ -26,7 +26,7 @@ enum class Themes(val id: Int) { companion object { fun getThemeCount(): Int { - return values().count() + return entries.count() } fun getTheme(id: Int): Int { diff --git a/app/src/main/java/com/celzero/bravedns/util/Utilities.kt b/app/src/main/java/com/celzero/bravedns/util/Utilities.kt index c3ac8226a..765979cf3 100644 --- a/app/src/main/java/com/celzero/bravedns/util/Utilities.kt +++ b/app/src/main/java/com/celzero/bravedns/util/Utilities.kt @@ -45,6 +45,7 @@ import androidx.core.content.getSystemService import androidx.lifecycle.LifecycleCoroutineScope import com.celzero.bravedns.BuildConfig import com.celzero.bravedns.R +import com.celzero.bravedns.RethinkDnsApplication.Companion.DEBUG import com.celzero.bravedns.database.AppInfoRepository.Companion.NO_PACKAGE import com.celzero.bravedns.net.doh.CountryMap import com.celzero.bravedns.service.BraveVPNService @@ -288,7 +289,34 @@ object Utilities { fun showToastUiCentered(context: Context, message: String, toastLength: Int) { try { - Toast.makeText(context, message, toastLength).show() + // check if the context is ui context or not + if (context is androidx.appcompat.app.AppCompatActivity) { + context.runOnUiThread { + Toast.makeText(context, message, toastLength).show() + } + return + } else if (context is androidx.fragment.app.FragmentActivity) { + context.runOnUiThread { + Toast.makeText(context, message, toastLength).show() + } + return + } else if (context is android.app.Activity) { + context.runOnUiThread { + Toast.makeText(context, message, toastLength).show() + } + return + } else if (context is android.app.Application) { + Logger.w(LOG_TAG_VPN, "toast err: context not found") + if (DEBUG) { // for testing purpose + Toast.makeText(context, message, toastLength).show() + } + } else { + Logger.w(LOG_TAG_VPN, "toast err: context not found") + if (DEBUG) { // for testing purpose + Toast.makeText(context, message, toastLength).show() + } + } + } catch (e: IllegalStateException) { Logger.w(LOG_TAG_VPN, "toast err: ${e.message}") } catch (e: IllegalAccessException) { @@ -600,11 +628,7 @@ object Utilities { val remoteFile = blocklistFile(remoteDir.absolutePath, Constants.ONDEVICE_BLOCKLIST_FILE_TAG) ?: return false - if (remoteFile.exists()) { - return true - } - - return false + return remoteFile.exists() } fun blocklistDir(ctx: Context?, which: String, timestamp: Long): File? { diff --git a/app/src/main/java/com/celzero/bravedns/wireguard/Config.kt b/app/src/main/java/com/celzero/bravedns/wireguard/Config.kt index ebcda39cf..1c9fe9cb1 100644 --- a/app/src/main/java/com/celzero/bravedns/wireguard/Config.kt +++ b/app/src/main/java/com/celzero/bravedns/wireguard/Config.kt @@ -117,10 +117,8 @@ class Config private constructor(builder: Builder) { * * @return the `Config` represented as a series of "key=value" lines */ - fun toWgUserspaceString(isOneWg: Boolean = false): String { - // Skip the listen port if we're in advanced mode. - // In advanced mode, the port may already be in use by another interface. - val skipListenPort = !isOneWg + fun toWgUserspaceString(skipListenPort: Boolean = false): String { + // Skip the listen port if we're in advanced mode or randomize (adv) setting is enabled. val sb = StringBuilder() sb.append(wgInterface?.toWgUserspaceString(skipListenPort) ?: "") sb.append("replace_peers=true\n") diff --git a/app/src/main/java/com/celzero/bravedns/wireguard/InetAddresses.kt b/app/src/main/java/com/celzero/bravedns/wireguard/InetAddresses.kt index ebb1327db..6815765db 100644 --- a/app/src/main/java/com/celzero/bravedns/wireguard/InetAddresses.kt +++ b/app/src/main/java/com/celzero/bravedns/wireguard/InetAddresses.kt @@ -63,7 +63,7 @@ object InetAddresses { * Parses a numeric IPv4 or IPv6 address without performing any DNS lookups. * * @param address a string representing the IP address - * @return an instance of [Inet4Address] or [Inet6Address], as appropriate + * @return an instance of Inet4Address or Inet6Address, as appropriate */ @Throws(ParseException::class) fun parse(address: String): InetAddress { diff --git a/app/src/main/java/com/celzero/bravedns/wireguard/InetEndpoint.kt b/app/src/main/java/com/celzero/bravedns/wireguard/InetEndpoint.kt index 3a1d20fd3..cb6d69101 100644 --- a/app/src/main/java/com/celzero/bravedns/wireguard/InetEndpoint.kt +++ b/app/src/main/java/com/celzero/bravedns/wireguard/InetEndpoint.kt @@ -38,8 +38,7 @@ private constructor(val host: String, private val isResolved: Boolean, val port: override fun equals(obj: Any?): Boolean { if (obj !is InetEndpoint) return false - val other = obj - return host == other.host && port == other.port + return host == obj.host && port == obj.port } /** diff --git a/app/src/main/java/com/celzero/bravedns/wireguard/InetNetwork.kt b/app/src/main/java/com/celzero/bravedns/wireguard/InetNetwork.kt index f668d7ea3..d90a5c76a 100644 --- a/app/src/main/java/com/celzero/bravedns/wireguard/InetNetwork.kt +++ b/app/src/main/java/com/celzero/bravedns/wireguard/InetNetwork.kt @@ -31,8 +31,7 @@ class InetNetwork private constructor(val address: InetAddress, val mask: Int) { override fun equals(obj: Any?): Boolean { if (obj !is InetNetwork) return false - val other = obj - return address == other.address && mask == other.mask + return address == obj.address && mask == obj.mask } override fun hashCode(): Int { diff --git a/app/src/main/java/com/celzero/bravedns/wireguard/Peer.kt b/app/src/main/java/com/celzero/bravedns/wireguard/Peer.kt index 28b039c61..fd4d88bb7 100644 --- a/app/src/main/java/com/celzero/bravedns/wireguard/Peer.kt +++ b/app/src/main/java/com/celzero/bravedns/wireguard/Peer.kt @@ -60,13 +60,12 @@ class Peer private constructor(builder: Builder) { override fun equals(obj: Any?): Boolean { if (obj !is Peer) return false - val other = obj - return allowedIps == other.allowedIps && - endpoint == other.endpoint && - unresolvedEndpoint == other.unresolvedEndpoint && - persistentKeepalive == other.persistentKeepalive && - preSharedKey == other.preSharedKey && - publicKey == other.publicKey + return allowedIps == obj.allowedIps && + endpoint == obj.endpoint && + unresolvedEndpoint == obj.unresolvedEndpoint && + persistentKeepalive == obj.persistentKeepalive && + preSharedKey == obj.preSharedKey && + publicKey == obj.publicKey } /** diff --git a/app/src/main/java/com/celzero/bravedns/wireguard/WgInterface.kt b/app/src/main/java/com/celzero/bravedns/wireguard/WgInterface.kt index d836250bc..5f7e41c32 100644 --- a/app/src/main/java/com/celzero/bravedns/wireguard/WgInterface.kt +++ b/app/src/main/java/com/celzero/bravedns/wireguard/WgInterface.kt @@ -154,6 +154,34 @@ class WgInterface private constructor(builder: Builder) { * file. * * @return The `Interface` represented as a series of "Key = Value" lines + * + * below is the sample file content for the Interface section, similar will be stored in the + * wg[0-9].conf file + * [Interface] + * Address = 100.80.213.126/32 + * DNS = 10.255.255.3 + * PrivateKey = KBdV2Iv+poXZkCOZukSo30modVHqyO5+GCdfhP4n40I= + * + * [Peer] + * AllowedIPs = 0.0.0.0/0, ::/0 + * Endpoint = yul-359-wg.whiskergalaxy.com:443 + * Endpoint = yul-359-wg.whiskergalaxy.com:443 + * PreSharedKey = VQoBVQxMmgttgGh9JCBwOPiwnFYV6/Py1tRi/f38DEI= + * PublicKey = nfFRpFZ0ZXWVoz8C4gP5ti7V1snFT1gV8EcIxTWJtB4= + * + * below is converted to wg-quick format + * [Interface] + * Address = 100.80.213.126/32 + * DNS = 10.255.255.3 + * PrivateKey = KBdV2Iv+poXZkCOZukSo30modVHqyO5+GCdfhP4n40I= + * + * [Peer] + * AllowedIPs = 0.0.0.0/0, ::/0 + * Endpoint = yul-359-wg.whiskergalaxy.com:443 + * Endpoint = yul-359-wg.whiskergalaxy.com:443 + * PreSharedKey = VQoBVQxMmgttgGh9JCBwOPiwnFYV6/Py1tRi/f38DEI= + * PublicKey = nfFRpFZ0ZXWVoz8C4gP5ti7V1snFT1gV8EcIxTWJtB4= + * */ fun toWgQuickString(): String { val sb = StringBuilder() @@ -187,6 +215,23 @@ class WgInterface private constructor(builder: Builder) { * not all attributes are included in this representation. * * @return the `Interface` represented as a series of "KEY=VALUE" lines + * + * see #toWgQuickString() for more details on the format + * + * below is the converted format for the userspace string + * + * private_key=281755d88bfea685d9902399ba44a8df49a87551eac8ee7e18275f84fe27e342 + * address=100.80.213.126/32 + * dns=10.255.255.3 + * mtu=1280 + * replace_peers=true + * public_key=9df151a45674657595a33f02e203f9b62ed5d6c9c54f5815f04708c53589b41e + * allowed_ip=0.0.0.0/0 + * allowed_ip=::/0 + * endpoint=172.98.68.207:443 + * endpoint=yul-359-wg.whiskergalaxy.com:443 + * preshared_key=550a01550c4c9a0b6d80687d24207038f8b09c5615ebf3f2d6d462fdfdfc0c42 + * */ fun toWgUserspaceString(skipListenPort: Boolean): String { val dnsServerStrings = diff --git a/app/src/main/res/drawable/edittext_default.xml b/app/src/main/res/drawable/edittext_default.xml new file mode 100644 index 000000000..3453d789c --- /dev/null +++ b/app/src/main/res/drawable/edittext_default.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/drawable/edittext_error.xml b/app/src/main/res/drawable/edittext_error.xml new file mode 100644 index 000000000..d3b858b65 --- /dev/null +++ b/app/src/main/res/drawable/edittext_error.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/drawable/fast_scoller_bubble.xml b/app/src/main/res/drawable/fast_scoller_bubble.xml new file mode 100644 index 000000000..d5b1a6ae6 --- /dev/null +++ b/app/src/main/res/drawable/fast_scoller_bubble.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/fast_scroller_handle.xml b/app/src/main/res/drawable/fast_scroller_handle.xml new file mode 100644 index 000000000..4fc05fe12 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroller_handle.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_advanced_settings.xml b/app/src/main/res/drawable/ic_advanced_settings.xml new file mode 100644 index 000000000..42fe30f79 --- /dev/null +++ b/app/src/main/res/drawable/ic_advanced_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_android_icon.xml b/app/src/main/res/drawable/ic_android_icon.xml new file mode 100644 index 000000000..e5de7a52b --- /dev/null +++ b/app/src/main/res/drawable/ic_android_icon.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_anti_dpi.xml b/app/src/main/res/drawable/ic_anti_dpi.xml new file mode 100644 index 000000000..7d7c19955 --- /dev/null +++ b/app/src/main/res/drawable/ic_anti_dpi.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_app_log.xml b/app/src/main/res/drawable/ic_app_log.xml new file mode 100644 index 000000000..a757f49ce --- /dev/null +++ b/app/src/main/res/drawable/ic_app_log.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_camera.png b/app/src/main/res/drawable/ic_camera.png new file mode 100644 index 000000000..1250ecda6 Binary files /dev/null and b/app/src/main/res/drawable/ic_camera.png differ diff --git a/app/src/main/res/drawable/ic_delete_accent.xml b/app/src/main/res/drawable/ic_delete_accent.xml new file mode 100644 index 000000000..11ff4675a --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_accent.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_dns_rules_as_firewall.xml b/app/src/main/res/drawable/ic_dns_rules_as_firewall.xml new file mode 100644 index 000000000..272626764 --- /dev/null +++ b/app/src/main/res/drawable/ic_dns_rules_as_firewall.xml @@ -0,0 +1,39 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_element.xml b/app/src/main/res/drawable/ic_element.xml new file mode 100644 index 000000000..bd8ff98de --- /dev/null +++ b/app/src/main/res/drawable/ic_element.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/drawable/ic_endpoint_independent.xml b/app/src/main/res/drawable/ic_endpoint_independent.xml new file mode 100644 index 000000000..a83cedec4 --- /dev/null +++ b/app/src/main/res/drawable/ic_endpoint_independent.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_experimental.xml b/app/src/main/res/drawable/ic_experimental.xml new file mode 100644 index 000000000..f0136ce20 --- /dev/null +++ b/app/src/main/res/drawable/ic_experimental.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml index 045e8306e..0699ad2d3 100644 --- a/app/src/main/res/drawable/ic_info.xml +++ b/app/src/main/res/drawable/ic_info.xml @@ -1,27 +1,27 @@ - - - + android:viewportWidth="512" + android:viewportHeight="512"> + + + + diff --git a/app/src/main/res/drawable/ic_info_white.xml b/app/src/main/res/drawable/ic_info_white.xml index 83726c60e..5c06702b8 100644 --- a/app/src/main/res/drawable/ic_info_white.xml +++ b/app/src/main/res/drawable/ic_info_white.xml @@ -1,27 +1,27 @@ + android:viewportWidth="512" + android:viewportHeight="512"> + android:fillColor="?attr/svgFillColor" + android:pathData="m248,64c-101.61,0 -184,82.39 -184,184s82.39,184 184,184 184,-82.39 184,-184 -82.39,-184 -184,-184z" + android:strokeWidth="32" + android:strokeColor="?attr/svgStrokeColor" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" /> + diff --git a/app/src/main/res/drawable/ic_info_white_16.xml b/app/src/main/res/drawable/ic_info_white_16.xml new file mode 100644 index 000000000..6808fd15d --- /dev/null +++ b/app/src/main/res/drawable/ic_info_white_16.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_log_level.xml b/app/src/main/res/drawable/ic_log_level.xml new file mode 100644 index 000000000..23eeeaeab --- /dev/null +++ b/app/src/main/res/drawable/ic_log_level.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_logs_accent.xml b/app/src/main/res/drawable/ic_logs_accent.xml index 48339fa5f..aa6db256f 100644 --- a/app/src/main/res/drawable/ic_logs_accent.xml +++ b/app/src/main/res/drawable/ic_logs_accent.xml @@ -1,7 +1,6 @@ + + diff --git a/app/src/main/res/drawable/ic_mastodon.xml b/app/src/main/res/drawable/ic_mastodon.xml new file mode 100644 index 000000000..b911e16fb --- /dev/null +++ b/app/src/main/res/drawable/ic_mastodon.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_microphone.png b/app/src/main/res/drawable/ic_microphone.png new file mode 100644 index 000000000..460d502ef Binary files /dev/null and b/app/src/main/res/drawable/ic_microphone.png differ diff --git a/app/src/main/res/drawable/ic_plus_accent.xml b/app/src/main/res/drawable/ic_plus_accent.xml new file mode 100644 index 000000000..4ce96a127 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_accent.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_randomize_wg_port.xml b/app/src/main/res/drawable/ic_randomize_wg_port.xml new file mode 100644 index 000000000..2af24bd91 --- /dev/null +++ b/app/src/main/res/drawable/ic_randomize_wg_port.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_reddit.xml b/app/src/main/res/drawable/ic_reddit.xml new file mode 100644 index 000000000..e43812e12 --- /dev/null +++ b/app/src/main/res/drawable/ic_reddit.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_rethink_plus.xml b/app/src/main/res/drawable/ic_rethink_plus.xml new file mode 100644 index 000000000..e3c3049d8 --- /dev/null +++ b/app/src/main/res/drawable/ic_rethink_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..3b9a1ac48 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_single_threaded.xml b/app/src/main/res/drawable/ic_single_threaded.xml new file mode 100644 index 000000000..d1eb3ce78 --- /dev/null +++ b/app/src/main/res/drawable/ic_single_threaded.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_undelegated_domain.xml b/app/src/main/res/drawable/ic_undelegated_domain.xml new file mode 100644 index 000000000..a43e82ba0 --- /dev/null +++ b/app/src/main/res/drawable/ic_undelegated_domain.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/osom_logo_stacked.xml b/app/src/main/res/drawable/osom_logo_stacked.xml deleted file mode 100644 index f4a8346fa..000000000 --- a/app/src/main/res/drawable/osom_logo_stacked.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/osom_logos_horizontal.xml b/app/src/main/res/drawable/osom_logos_horizontal.xml deleted file mode 100644 index 14cb0f14f..000000000 --- a/app/src/main/res/drawable/osom_logos_horizontal.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/report_bug_icon.xml b/app/src/main/res/drawable/report_bug_icon.xml deleted file mode 100644 index 8232f9242..000000000 --- a/app/src/main/res/drawable/report_bug_icon.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/spinner_outline.xml b/app/src/main/res/drawable/spinner_outline.xml index 61fd88b56..3ffbc59f6 100644 --- a/app/src/main/res/drawable/spinner_outline.xml +++ b/app/src/main/res/drawable/spinner_outline.xml @@ -2,6 +2,6 @@ + android:color="?attr/primaryTextColor" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/spinner_outline_with_bg.xml b/app/src/main/res/drawable/spinner_outline_with_bg.xml index 2910898c3..0bf584ecb 100644 --- a/app/src/main/res/drawable/spinner_outline_with_bg.xml +++ b/app/src/main/res/drawable/spinner_outline_with_bg.xml @@ -2,7 +2,7 @@ + android:color="?attr/primaryTextColor" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_anti_censorship.xml b/app/src/main/res/layout/activity_anti_censorship.xml new file mode 100644 index 000000000..f79c844c1 --- /dev/null +++ b/app/src/main/res/layout/activity_anti_censorship.xml @@ -0,0 +1,629 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_app_wise_ip_logs.xml b/app/src/main/res/layout/activity_app_wise_ip_logs.xml index 596b0e186..06e71625a 100644 --- a/app/src/main/res/layout/activity_app_wise_ip_logs.xml +++ b/app/src/main/res/layout/activity_app_wise_ip_logs.xml @@ -6,37 +6,6 @@ android:background="?attr/background" android:orientation="vertical"> - - - - - - - - + + + app:queryHint="@string/search_universal_ips" + app:searchHintIcon="@null" + app:searchIcon="@null" /> + diff --git a/app/src/main/res/layout/activity_detailed_statistics.xml b/app/src/main/res/layout/activity_detailed_statistics.xml index 74c0d133d..d22aa5052 100644 --- a/app/src/main/res/layout/activity_detailed_statistics.xml +++ b/app/src/main/res/layout/activity_detailed_statistics.xml @@ -1,6 +1,4 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_network_logs.xml b/app/src/main/res/layout/activity_network_logs.xml index 5780d8e7b..b4dc01b36 100644 --- a/app/src/main/res/layout/activity_network_logs.xml +++ b/app/src/main/res/layout/activity_network_logs.xml @@ -8,11 +8,30 @@ android:orientation="vertical" tools:context=".ui.activity.FirewallActivity"> - + android:layout_height="wrap_content"> + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_marginBottom="2dp"> + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_wg_detail.xml b/app/src/main/res/layout/activity_wg_detail.xml index 0333dd425..31953c44f 100644 --- a/app/src/main/res/layout/activity_wg_detail.xml +++ b/app/src/main/res/layout/activity_wg_detail.xml @@ -56,178 +56,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - @@ -257,31 +86,32 @@ android:id="@+id/config_name_text" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentStart="true" - android:layout_centerVertical="true" - android:layout_toStartOf="@id/interface_edit" + android:paddingTop="5dp" android:text="" android:textAppearance="?attr/textAppearanceHeadline6" - android:textColor="?attr/secondaryTextColor" /> + android:textColor="?attr/primaryTextColor" /> - + android:layout_gravity="center" + android:layout_marginStart="2dp" + android:layout_marginEnd="3dp" + android:alpha="0.7" + android:maxLength="5" /> - + - + + app:layout_constraintTop_toBottomOf="@id/status_text" /> @@ -310,6 +142,7 @@ android:layout_marginTop="8dp" android:labelFor="@+id/config_addresses_text" android:text="@string/lbl_addresses" + android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/public_key_text" /> @@ -317,6 +150,7 @@ android:id="@+id/addresses_text" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/addresses_label" /> @@ -410,10 +244,225 @@ app:layout_constraintTop_toBottomOf="@+id/mtu_label" tools:text="1500" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/other_settings_card" /> @@ -433,7 +482,9 @@ android:layout_height="wrap_content" android:layout_marginEnd="25dp" android:layout_marginBottom="40dp" + android:clickable="true" android:contentDescription="@string/add_peer" + android:focusable="true" android:text="@string/add_peer" android:textColor="?attr/primaryTextColor" app:backgroundTint="?attr/chipColorBgNormal" @@ -441,8 +492,6 @@ app:icon="@drawable/ic_add" app:iconTint="@android:color/transparent" app:iconTintMode="add" - android:clickable="true" - android:focusable="true" app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/dialog_input_ips.xml b/app/src/main/res/layout/dialog_input_ips.xml new file mode 100644 index 000000000..4cc518898 --- /dev/null +++ b/app/src/main/res/layout/dialog_input_ips.xml @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_sponsor_info.xml b/app/src/main/res/layout/dialog_sponsor_info.xml new file mode 100644 index 000000000..85d9e239d --- /dev/null +++ b/app/src/main/res/layout/dialog_sponsor_info.xml @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index aa2db1f97..275fbc5a6 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -86,6 +86,19 @@ android:text="@string/about_bravedns_whoarewe" android:textSize="@dimen/large_font_text_view" /> + + @@ -212,7 +225,7 @@ android:layout_height="wrap_content" android:background="@drawable/rectangle_border_background" android:clickable="true" - android:drawableStart="@drawable/report_bug_icon" + android:drawableStart="@drawable/ic_android_icon" android:drawablePadding="-15dp" android:focusable="true" android:foreground="?attr/selectableItemBackground" @@ -450,6 +463,23 @@ android:textSize="@dimen/extra_large_font_text_view" android:textStyle="bold" /> + + + + + + + + @@ -539,6 +605,22 @@ android:text="@string/about_settings_vpn_profile" android:textSize="@dimen/large_font_text_view" /> + + - - + android:orientation="vertical" + android:paddingBottom="60dp"> - - - + + + + + + + + + + + + - + - - - + android:layout_gravity="center" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" + android:layout_marginBottom="10dp"> - - - - - + android:src="@drawable/ic_info_white_16" /> - + - - - + android:drawableEnd="@drawable/ic_right_arrow_white" + android:drawablePadding="10dp" + android:enabled="true" + android:minHeight="48dp" + android:padding="10dp" + android:text="@string/dc_custom_dns_radio" /> - - + + + + + + @@ -271,6 +270,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:src="@drawable/bs_dns_home_screen" /> + + android:src="@drawable/ic_dns_rules_as_firewall" /> + - @@ -698,6 +836,63 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_dns_logs.xml b/app/src/main/res/layout/fragment_dns_logs.xml index 10f845c3f..0c139a4e4 100644 --- a/app/src/main/res/layout/fragment_dns_logs.xml +++ b/app/src/main/res/layout/fragment_dns_logs.xml @@ -93,13 +93,20 @@ android:textSize="@dimen/large_font_text_view" android:visibility="gone" /> - + app:fastScrollAutoHide="true" + app:fastScrollAutoHideDelay="1500" + android:nestedScrollingEnabled="true" + app:fastScrollThumbInactiveColor="?attr/chipColorBgNormal" + app:fastScrollThumbEnabled="true" + app:fastScrollThumbColor="?attr/chipColorBgNormal" + app:fastScrollEnableThumbInactiveColor="true" + app:fastScrollTrackColor="?attr/background" /> - diff --git a/app/src/main/res/layout/list_item_alert_registry.xml b/app/src/main/res/layout/list_item_alert_registry.xml index ff2fb05ff..b5b4d903e 100644 --- a/app/src/main/res/layout/list_item_alert_registry.xml +++ b/app/src/main/res/layout/list_item_alert_registry.xml @@ -19,14 +19,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_wg_general_interface.xml b/app/src/main/res/layout/list_item_wg_general_interface.xml index 98a00c574..d683e8f83 100644 --- a/app/src/main/res/layout/list_item_wg_general_interface.xml +++ b/app/src/main/res/layout/list_item_wg_general_interface.xml @@ -41,18 +41,25 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - + android:textAppearance="?attr/textAppearanceHeadline6" /> + + diff --git a/app/src/main/res/layout/list_item_wg_one_interface.xml b/app/src/main/res/layout/list_item_wg_one_interface.xml index 3f03ec9a8..85f92f5ce 100644 --- a/app/src/main/res/layout/list_item_wg_one_interface.xml +++ b/app/src/main/res/layout/list_item_wg_one_interface.xml @@ -47,12 +47,19 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginStart="3dp" - android:layout_marginEnd="3dp" android:ellipsize="end" - android:maxLength="15" + android:maxLength="16" android:padding="5dp" - android:textAppearance="?attr/textAppearanceHeadline6" - android:textColor="?attr/secondaryTextColor" /> + android:textAppearance="?attr/textAppearanceHeadline6" /> + + diff --git a/app/src/main/res/layout/list_item_wg_peers.xml b/app/src/main/res/layout/list_item_wg_peers.xml index 3d372ec23..194d97e8b 100644 --- a/app/src/main/res/layout/list_item_wg_peers.xml +++ b/app/src/main/res/layout/list_item_wg_peers.xml @@ -26,7 +26,7 @@ android:layout_height="wrap_content" android:text="@string/lbl_peer" android:textAppearance="?attr/textAppearanceHeadline6" - android:textColor="?attr/secondaryTextColor" + android:textColor="?attr/primaryTextColor" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/mic_cam_access_indicator.xml b/app/src/main/res/layout/mic_cam_access_indicator.xml new file mode 100644 index 000000000..0a60ef239 --- /dev/null +++ b/app/src/main/res/layout/mic_cam_access_indicator.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 75065b941..8bf60c53a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -371,8 +371,8 @@ ऑन-डिवाइस ब्लॉकलिस्ट गोपनीयता नीति पढ़े ऐप जानकारी - ट्वीटर पर हमें फोलो करे अनुवाद करे + ट्वीटर पर हमें फोलो करे इस ईमेल को यहां भेजें: hello@celzero.com %1$s लॉन्च नहीं हुए कोई जवाब नहीं diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 48adc1035..edde76df5 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -199,8 +199,8 @@ התחל מה חדש פרוקסי פעיל - עקבו אחרינו בטוויטר למחוק הכל + עקבו אחרינו בטוויטר RethinkDNS היא חלק מתוכנית ה-MVP של Mozilla Builders. לא ניתן היה להפעיל את פרופיל VPN. חיצוני diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 9119f0b4a..4e999caa9 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -23,6 +23,7 @@ 18sp 22sp 24sp + 72sp 0dp 70dp diff --git a/app/src/main/res/values/servers.xml b/app/src/main/res/values/servers.xml index d690c3527..08f6226e8 100644 --- a/app/src/main/res/values/servers.xml +++ b/app/src/main/res/values/servers.xml @@ -5,11 +5,11 @@ https://basic.rethinkdns.com/ 172.67.162.27,104.21.10.13 https://sky.rethinkdns.com/ - 104.21.10.13,172.67.162.27,2606:4700:3033::ac43:a21b + 172.67.214.246,104.21.83.62,2606:4700:3030::ac43:d6f6,2606:4700:3030::6815:533e https://max.rethinkdns.com/ 137.66.7.89,2a09:8280:1::1:7432 https://zero.rethinkdns.com/ - 104.21.10.13,172.67.162.27 + 104.21.83.62,172.67.214.246,2606:4700:3030::6815:533e,2606:4700:3030::ac43:d6f6 https://dns.google/dns-query 8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844 https://cloudflare-dns.com/dns-query diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf1e177dc..345a90489 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,12 +100,19 @@ IP Unselected notification + Mastodon + Matrix + Reddit + test + protos Rethink is experiencing low memory. System actions may be limited. %1$s 🔺 %1$s 🔻 %1$s + (%1$s) + %1$s%2$s %1$s %2$s %1$s: %2$s %1$s / %2$s @@ -125,6 +132,9 @@ \u25b4 🔒 + %1$s/S + MB + $ 1 24 @@ -134,7 +144,7 @@ Rethink is the easiest way to monitor network activity, bypass Internet censorship, and firewall apps on your Android device. Free and Open Source - Rethink is an open source software backed by Mozilla Builders MVP program. + Rethink is an open source software backed by Mozilla Builders MVP program, FOSS United Foundation. RDNS Plus @@ -628,6 +638,12 @@ Error creating pcap file, Try again! + Biometric Authentication + None + Immediately + After 5 minute + After 15 minutes + Application Icon @@ -710,6 +726,8 @@ Disable %1$s %1$s may result in connectivity loss. Change setting to let Rethink use data and other resources when in background. + You’ve been using Rethink for %1$s days, which translates to a usage cost of $%2$s. Would you consider sponsoring to sustain its development? + rules @@ -737,6 +755,11 @@ Never proxy DNS Do not proxy DNS over Always-on WireGuard, SOCKS5, HTTP proxies. + Use System DNS for undelegated domains + Use System DNS for undelgated domains like .lan, .internal, etc. + + filtering + Rethink is the easiest way to monitor app activity, circumvent Internet censorship, and firewall apps on your Android device. Rethink is a free and open-source project, led by ex-engineers from Amazon, IBM, and Scientific Games. @@ -764,7 +787,7 @@ Notification Settings What\'s new in %1$s Follow us on Twitter - Rethink is supported by Mozilla Builders, FOSS United Foundation, OSOM Products Inc. + Rethink is supported by Mozilla Builders, FOSS United Foundation. Check for app updates Authors Translate @@ -779,13 +802,15 @@ https://docs.rethinkdns.com/ https://www.rethinkdns.com/faq https://github.com/celzero/rethink-app - https://twitter.com/rethinkdns + https://x.com/rethinkdns https://www.rethinkdns.com/ https://builders.mozilla.community/old/alumni.html https://fossunited.org/grants - https://www.osomprivacy.com https://svc.rethinkdns.com/r/translate https://www.rethinkdns.com/privacy + https://mastodon.social/@rdns + https://www.reddit.com/r/rethinkdns + https://matrix.to/#/!jrTSpJiEkFNNBMhSaE:matrix.org %1$s %1$s (%2$s) Suggest features @@ -815,13 +840,13 @@ Uldiniad
Poussinou
pjosingh
+ RohitSurwase

🚂 Network engine
alalamav
bemasc
dmcardle
fortuna
ignoramous
- Lanius-collaris
PeterDaveHello
santhosh-ponnusamy
SeanBurford
@@ -902,6 +927,12 @@ Advanced DNS filtering (experimental) Assign unique IP per DNS request + Split DNS (experimental) + Use fixed DNS servers for specific apps + + Treat DNS rules as firewall rules (experimental) + DNS blocking will be bypassed during resolution; the decision will be made at connection time. + %1$s (%2$s) @@ -1132,6 +1163,8 @@ Domain (Univ) Trusted Domain (Univ) Proxied + Private DNS + Bypass Proxy No firewall rules matched this connection. app.

To change go to All Apps tab.]]>
@@ -1161,6 +1194,8 @@ domain is set as blocked in Universal domain rules.

To change this behaviour go to Configure DNS tab.]]>
domain is set as trusted in Universal domain rules.

To change this behaviour go to Configure DNS tab.]]>

To change go to Proxy from Configure screen.]]>
+ Private DNS server that doesn\'t exist.]]> + source app is set to bypass proxy rules.

To change go to App-specific Firewall screen.]]>
Attention @@ -1264,7 +1299,7 @@ Connected to %1$s IP address(es) 0 IP address - No network logs for this app. + No network logs for this category. is allowed is blocked @@ -1518,6 +1553,8 @@ Include remaining apps Include all remaining apps not routed by any other Proxy + Invalid WireGuard configuration + Does not block any DNS requests. Uses Cloudflare\'s 1.1.1.1 DNS endpoint. Blocks malware and adult content. Uses Cloudflare\'s 1.1.1.3 DNS endpoint. @@ -1541,6 +1578,60 @@ Anonymized DNS relay hosted in the US - Los Angeles, CA provided by https://cryptostrom.is/. Anonymized DNS relay hosted in Singapore. + + App Log + Logs may contain sensitive information. + Delete logs? + Delete all logs from this app? + Logs deleted + + + + Do not randomize listen port + Do not randomize the WireGuard listen port on Advanced mode. + + Endpoint independent mapping + Allow incoming packets to be forwarded to the same endpoint, regardless of the source IP address. + + Anti-censorship + Use anti-censorship techniques to bypass network restrictions. + + TCP keep alive + Send keep alive packets to prevent the connection from being closed. + + + anti-censorship + Anti-Censorship mode is a feature that helps you bypass censorship and access the internet freely. It is recommended to use this feature only if you are in a country where the internet is censored. + + Never split + Do not split packets when sending them to the server. + + Split auto + Automatically split packets when sending them to the server. + + Split TCP + Split TCP packets when sending them to the server. + + Split TLS + Split TLS packets when sending them to the server. + + Desync (experimental) + Desynchronize packets when sending them to the server. + + retry options + Retry options will help you bypass censorship and access the internet freely. It is recommended to use this feature only if you are in a country where the internet is censored. + + Never + Never retry sending packets. + + Retry with Split + Retry sending packets with split. + + Retry after Split + Retry sending packets after split. + + Retry strategy is disabled for never split mode +