Commit 2180a51f authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Merge branch 'master' of...

Merge branch 'master' of https://git.frostnerd.com/PublicAndroidApps/smokescreen into 185-request-advanced-backup-and-restore
parents 9f2c6766 22924ff9
......@@ -79,6 +79,13 @@ fun Context.startForegroundServiceCompat(intent: Intent) {
} else startService(intent)
}
fun Context.tryUnregisterReceiver(receiver: BroadcastReceiver) {
try {
unregisterReceiver(receiver)
} catch (e: Exception) {
}
}
fun Context.registerReceiver(filteredActions: List<String>, receiver: (intent: Intent?) -> Unit): BroadcastReceiver {
val filter = IntentFilter()
for (filteredAction in filteredActions) {
......@@ -170,22 +177,6 @@ fun Context.isAppBatteryOptimized(): Boolean {
return !pwrm.isIgnoringBatteryOptimizations(packageName)
}
fun Array<*>.toStringArray(): Array<String> {
val stringArray = arrayOfNulls<String>(size)
for ((index, value) in withIndex()) {
stringArray[index] = value.toString()
}
return stringArray as Array<String>
}
fun IntArray.toStringArray(): Array<String> {
val stringArray = arrayOfNulls<String>(size)
for ((index, value) in withIndex()) {
stringArray[index] = value.toString()
}
return stringArray as Array<String>
}
fun <T:Activity>Activity.restart(activityClass:Class<T>? = null) {
val intent = (if(activityClass != null) Intent(this, activityClass) else intent)
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION)
......@@ -201,16 +192,6 @@ fun Context.showEmailChooser(chooserTitle: String, subject: String, recipent: St
startActivity(Intent.createChooser(intent, chooserTitle))
}
fun ConnectivityManager.isMobileNetwork(network: Network): Boolean {
val capabilities = getNetworkCapabilities(network)
return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
}
fun ConnectivityManager.isWifiNetwork(network: Network): Boolean {
val capabilities = getNetworkCapabilities(network)
return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
fun ConnectivityManager.isVpnNetwork(network: Network): Boolean {
val capabilities = getNetworkCapabilities(network)
return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
......
......@@ -15,6 +15,7 @@ import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_privacypolicy.view.*
import okhttp3.internal.toHexString
import java.util.*
/*
......@@ -84,7 +85,7 @@ fun isPackageInstalled(context: Context, packageName: String): Boolean {
}
fun colorToHexString(@ColorInt color:Int):String {
return String.format("#%06X", 0xFFFFFF and color)
return String.format(Locale.ROOT, "#%06X", 0xFFFFFF and color)
}
@ColorInt
......
......@@ -7,10 +7,10 @@ import androidx.fragment.app.Fragment
import com.frostnerd.smokescreen.database.AppDatabase
import com.frostnerd.smokescreen.database.EXECUTED_MIGRATIONS
import com.frostnerd.smokescreen.util.preferences.Crashreporting
import io.sentry.Sentry
import io.sentry.event.Event
import io.sentry.event.EventBuilder
import io.sentry.event.interfaces.ExceptionInterface
import io.sentry.core.Sentry
import io.sentry.core.SentryEvent
import io.sentry.core.SentryLevel
import io.sentry.core.protocol.Message
import leakcanary.LeakSentry
import java.io.*
import java.text.SimpleDateFormat
......@@ -48,30 +48,33 @@ private fun Context.logErrorSentry(e: Throwable, extras: Map<String, String>? =
}
} || publishedExceptions.put(e, e.stackTrace.toHashSet()) != null) return
else {
Sentry.getContext().addExtra("database_migrations", EXECUTED_MIGRATIONS.sortedBy { it.first }.joinToString {
EXECUTED_MIGRATIONS.sortedBy { it.first }.joinToString {
"${it.first} -> ${it.second}"
})
}.takeIf { it.isNotBlank() }?.apply {
Sentry.setExtra("database_migrations", this)
}
if (e is OutOfMemoryError) {
EventBuilder().withMessage(e.message)
.withLevel(Event.Level.ERROR)
.withExtra("retainedInstanceCount", LeakSentry.refWatcher.retainedInstanceCount)
.withSentryInterface(ExceptionInterface(e)).build().apply {
Sentry.capture(this)
Sentry.captureEvent(SentryEvent(e).apply {
message = Message().apply {
this.message = e.message
}
level = SentryLevel.ERROR
setExtra("retainedInstanceCount", LeakSentry.refWatcher.retainedInstanceCount)
})
} else if (getPreferences().crashreportingType == Crashreporting.FULL && extras != null && extras.isNotEmpty()) {
// Extra data is only passed when not in data-saving mode.
EventBuilder().withMessage(e.message)
.withLevel(Event.Level.ERROR)
.apply {
extras.forEach { (key, value) ->
withExtra(key, value)
}
Sentry.captureEvent(SentryEvent(e).apply {
message = Message().apply {
this.message = e.message
}
.withSentryInterface(ExceptionInterface(e)).build().apply {
Sentry.capture(this)
level = SentryLevel.ERROR
extras.forEach { (key, value) ->
setTag(key, value)
}
setExtra("retainedInstanceCount", LeakSentry.refWatcher.retainedInstanceCount)
})
} else {
Sentry.capture(e)
Sentry.captureException(e)
}
}
}
......@@ -148,11 +151,6 @@ fun Fragment.log(e: Throwable) {
if (context != null) requireContext().log(e)
}
fun Fragment.closeLogger() {
if (Logger.isOpen())
Logger.getInstance(requireContext()).destroy()
}
class Logger private constructor(context: Context) {
private val logFile: File
private val fileWriter: BufferedWriter
......
......@@ -12,12 +12,15 @@ import com.frostnerd.smokescreen.activity.PinActivity
import com.frostnerd.smokescreen.database.AppDatabase
import com.frostnerd.smokescreen.util.Notifications
import com.frostnerd.smokescreen.util.RequestCodes
import com.frostnerd.smokescreen.util.crashhelpers.DatasavingSentryEventHelper
import com.frostnerd.smokescreen.util.crashhelpers.DataSavingSentryEventProcessor
import com.frostnerd.smokescreen.util.preferences.AppSettings
import com.frostnerd.smokescreen.util.preferences.Crashreporting
import io.sentry.Sentry
import io.sentry.android.AndroidSentryClientFactory
import io.sentry.event.User
import io.sentry.android.core.*
import io.sentry.core.Integration
import io.sentry.core.Sentry
import io.sentry.core.SentryOptions
import io.sentry.core.UncaughtExceptionHandlerIntegration
import io.sentry.core.protocol.User
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
......@@ -53,10 +56,11 @@ class SmokeScreen : Application() {
val customUncaughtExceptionHandler = EnrichableUncaughtExceptionHandler()
private fun showCrashNotification() {
val notification =
NotificationCompat.Builder(this, Notifications.noConnectionNotificationChannelId(this))
NotificationCompat.Builder(this, Notifications.getHighPriorityChannelId(this))
.setSmallIcon(R.drawable.ic_cloud_warn)
.setOngoing(false)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(
PendingIntent.getActivity(
this, RequestCodes.CRASH_NOTIFICATION,
......@@ -116,54 +120,53 @@ class SmokeScreen : Application() {
// This passes some device-related data, but nothing which allows user actions to be tracked across the app
// Info: Some data is attached by the AndroidEventBuilderHelper class, which is present by default
Sentry.init(
BuildConfig.SENTRY_DSN,
AndroidSentryClientFactory(this@SmokeScreen)
)
Sentry.getContext().user =
User(getPreferences().crashReportingUUID, null, null, null)
Sentry.getStoredClient().apply {
addTag("user.language", Locale.getDefault().displayLanguage)
addTag("app.database_version", AppDatabase.currentVersion.toString())
addTag("app.dns_server_name", getPreferences().dnsServerConfig.name)
addTag(
"app.dns_server_primary",
getPreferences().dnsServerConfig.servers[0].address.formatToString()
)
addTag(
"app.dns_server_secondary",
getPreferences().dnsServerConfig.servers.getOrNull(1)?.address?.formatToString()
)
addTag(
"app.installer_package",
packageManager.getInstallerPackageName(packageName)
)
addTag("richdata", "true")
addTag("app.fromCi", BuildConfig.FROM_CI.toString())
addTag("app.commit", BuildConfig.COMMIT_HASH)
SentryAndroid.init(this@SmokeScreen) {
it.dsn = BuildConfig.SENTRY_DSN
}
Sentry.setUser(User().apply {
this.username = getPreferences().crashReportingUUID
})
Sentry.setTag("user.language", Locale.getDefault().displayLanguage)
Sentry.setTag(
"app.database_version",
AppDatabase.currentVersion.toString()
)
Sentry.setTag(
"app.dns_server_name",
getPreferences().dnsServerConfig.name
)
Sentry.setTag(
"app.dns_server_primary",
getPreferences().dnsServerConfig.servers[0].address.formatToString()
)
Sentry.setTag(
"app.dns_server_secondary",
getPreferences().dnsServerConfig.servers.getOrNull(1)?.address?.formatToString()
?: ""
)
Sentry.setTag(
"app.installer_package",
packageManager.getInstallerPackageName(packageName) ?: ""
)
Sentry.setTag("richdata", "true")
Sentry.setTag("app.fromCi", BuildConfig.FROM_CI.toString())
Sentry.setTag("app.commit", BuildConfig.COMMIT_HASH)
sentryReady = true
} else if (enabledType == Crashreporting.MINIMAL || forceStatus == Status.DATASAVING) {
// Inits Sentry in datasaving mode
// Only data absolutely necessary is transmitted (Android version, app version).
// Only crashes will be reported, no regular events.
Sentry.init(
BuildConfig.SENTRY_DSN,
AndroidSentryClientFactory(this@SmokeScreen)
)
Sentry.getContext().user =
User("anon-" + BuildConfig.VERSION_CODE, null, null, null)
Sentry.getStoredClient().apply {
addTag("richdata", "false")
addTag("dist", BuildConfig.VERSION_CODE.toString())
addTag("app.commit", BuildConfig.COMMIT_HASH)
addTag("app.fromCi", BuildConfig.FROM_CI.toString())
addExtra("dist", BuildConfig.VERSION_CODE)
this.builderHelpers.forEach {
this.removeBuilderHelper(it)
}
this.addBuilderHelper(DatasavingSentryEventHelper())
SentryAndroid.init(this@SmokeScreen) {
it.dsn = BuildConfig.SENTRY_DSN
setupSentryForDatasaving(it)
}
Sentry.setUser(User().apply {
this.username = "anon-" + BuildConfig.VERSION_CODE
})
Sentry.setTag("richdata", "false")
Sentry.setTag("dist", BuildConfig.VERSION_CODE.toString())
Sentry.setTag("app.commit", BuildConfig.COMMIT_HASH)
Sentry.setTag("app.fromCi", BuildConfig.FROM_CI.toString())
sentryReady = true
}
}
......@@ -174,6 +177,21 @@ class SmokeScreen : Application() {
}
}
private fun setupSentryForDatasaving(sentryOptions: SentryOptions) {
val remove = mutableListOf<Integration>()
sentryOptions.integrations.forEach {
if (it is PhoneStateBreadcrumbsIntegration ||
it is SystemEventsBreadcrumbsIntegration ||
it is TempSensorBreadcrumbsIntegration ||
it is AppComponentsBreadcrumbsIntegration ||
it is SystemEventsBreadcrumbsIntegration ||
it is AppLifecycleIntegration
) remove.add(it)
}
remove.forEach { sentryOptions.integrations.remove(it) }
sentryOptions.eventProcessors.add(DataSavingSentryEventProcessor())
}
override fun onLowMemory() {
super.onLowMemory()
log("The system seems to have low memory")
......
......@@ -49,23 +49,26 @@ class BackgroundVpnConfigureActivity : BaseActivity() {
private const val VPN_REQUEST_CODE = 1
fun prepareVpn(context: Context, serverInfo:DnsServerInformation<*>? = null) {
val vpnIntent = try {
VpnService.prepare(context).apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
if(context.getPreferences().runWithoutVpn) DnsVpnService.startVpn(context, serverInfo)
else {
val vpnIntent = try {
VpnService.prepare(context).apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
} catch (ex:NullPointerException) { // Caused by VpnService.prepare(), maybe Android Bug?
Intent()
}
} catch (ex:NullPointerException) { // Caused by VpnService.prepare(), maybe Android Bug?
Intent()
}
if (vpnIntent == null) {
DnsVpnService.startVpn(context, serverInfo)
} else {
val intent = Intent(context, BackgroundVpnConfigureActivity::class.java)
if(serverInfo != null) {
writeServerInfoToIntent(serverInfo, intent)
if (vpnIntent == null) {
DnsVpnService.startVpn(context, serverInfo)
} else {
val intent = Intent(context, BackgroundVpnConfigureActivity::class.java)
if(serverInfo != null) {
writeServerInfoToIntent(serverInfo, intent)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
......@@ -133,8 +136,12 @@ class BackgroundVpnConfigureActivity : BaseActivity() {
supportActionBar?.hide()
actionBar?.hide()
val vpnIntent = VpnService.prepare(this).apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
val vpnIntent = if(getPreferences().runWithoutVpn) {
null
} else {
VpnService.prepare(this).apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
}
}
if (vpnIntent == null) {
startService()
......
......@@ -8,6 +8,7 @@ import android.net.*
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.frostnerd.dnstunnelproxy.KnownDnsServers
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
import com.frostnerd.general.service.isServiceRunning
......@@ -19,7 +20,7 @@ import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.dialog.BatteryOptimizationInfoDialog
import com.frostnerd.smokescreen.dialog.ChangelogDialog
import com.frostnerd.smokescreen.dialog.NewServerDialog
import com.frostnerd.smokescreen.dialog.ServerChoosalDialog
import com.frostnerd.smokescreen.fragment.*
import com.frostnerd.smokescreen.service.DnsVpnService
import com.frostnerd.smokescreen.util.DeepActionState
......@@ -50,6 +51,7 @@ import kotlin.random.Random
class MainActivity : NavigationDrawerActivity() {
companion object {
const val BROADCAST_RELOAD_MENU = "main.reloadMenu"
private const val PIN_TIMEOUT = 2*60*1000
}
override val drawerOverActionBar: Boolean = true
private var textColor: Int = 0
......@@ -57,37 +59,55 @@ class MainActivity : NavigationDrawerActivity() {
private var inputElementColor: Int = 0
private var cardNetworkCallback:ConnectivityManager.NetworkCallback? = null
private val networkManager by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
private var pinLastPassed:Long? = null
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(LanguageContextWrapper.attachFromSettings(this, newBase))
}
override fun onResume() {
super.onResume()
if(getPreferences().enablePin) {
pinLastPassed = intent?.getLongExtra("pin_validated_at", 0)
if(pinLastPassed == null || System.currentTimeMillis() >= pinLastPassed!! + PIN_TIMEOUT) {
startActivity(PinActivity.openAppIntent(this, intent?.extras))
finish()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getPreferences().theme.layoutStyle)
super.onCreate(savedInstanceState)
AbstractHttpsDNSHandle // Loads the known servers.
AbstractTLSDnsHandle
KnownDnsServers
setCardView { viewParent, suggestedHeight ->
val view = layoutInflater.inflate(R.layout.menu_cardview, viewParent, false)
val update = {
val server = getPreferences().dnsServerConfig
view.serverName.text = server.name
view.dns1.text = server.servers.first().address.addressCreator.resolveOrGetResultOrNull(
retryIfError = true,
runResolveNow = true
)?.firstOrNull()?.hostAddress ?: "-"
view.dns2.text = (server.servers.lastOrNull()?.address?.addressCreator?.resolveOrGetResultOrNull(
retryIfError = true,
runResolveNow = true
)?.lastOrNull()?.hostAddress ?: "-").let {
if(it == view.dns1.text.toString()) "-" else it
}
launchWithLifecylce(false) {
val latency = DnsSpeedTest(server, log= {}).runTest(1)
val server = getPreferences().dnsServerConfig
val primaryAddress = server.servers.first().address.addressCreator.resolveOrGetResultOrNull(
retryIfError = true,
runResolveNow = true
)?.firstOrNull()?.hostAddress ?: "-"
val secondaryAddress = (server.servers.lastOrNull()?.address?.addressCreator?.resolveOrGetResultOrNull(
retryIfError = true,
runResolveNow = true
)?.lastOrNull()?.hostAddress ?: "-").let {
if(it == primaryAddress) "-" else it
}
runOnUiThread {
view.latency.text = if(latency != null && latency > 0) {
"$latency ms"
view.serverName.text = server.name
view.dns1.text = primaryAddress
view.dns2.text = secondaryAddress
}
val latency = DnsSpeedTest(server, log = {}).runTest(1)
runOnUiThread {
view.latency.text = if (latency != null && latency > 0) {
"$latency ms"
} else "- ms"
}
}
......@@ -96,17 +116,13 @@ class MainActivity : NavigationDrawerActivity() {
getPreferences().listenForChanges(
"dns_server_config",
getPreferences().preferenceChangeListener {
runOnUiThread {
update()
}
update()
}.unregisterOn(lifecycle)
)
cardNetworkCallback = object: ConnectivityManager.NetworkCallback() {
cardNetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
runOnUiThread {
update()
}
update()
}
}
networkManager.registerNetworkCallback(NetworkRequest.Builder().apply {
......@@ -191,7 +207,7 @@ class MainActivity : NavigationDrawerActivity() {
private fun handleDeepAction(intent:Intent? = null) {
if(intent?.hasExtra("deep_action") == true) {
whenDrawerIsReady {
when(intent.getSerializableExtra("deep_action")) {
when(val deepAction = intent.getSerializableExtra("deep_action")) {
DeepActionState.DNS_RULES -> {
clickItem(drawerItems.find {
it is ClickableDrawerItem && it.title == getString(R.string.button_main_dnsrules)
......@@ -200,6 +216,13 @@ class MainActivity : NavigationDrawerActivity() {
DeepActionState.BATTERY_OPTIMIZATION_DIALOG -> {
BatteryOptimizationInfoDialog(this).show()
}
DeepActionState.DNSSERVERMODE_SETTINGS -> {
drawerItems.find {
it is ClickableDrawerItem && it.title == getString(R.string.menu_settings)
}?.apply {
clickItem(this, Bundle().apply { putSerializable("deep_action", deepAction) })
}
}
}
}
}
......@@ -213,7 +236,10 @@ class MainActivity : NavigationDrawerActivity() {
)
fragmentItem(getString(R.string.menu_settings),
iconLeft = getDrawable(R.drawable.ic_menu_settings),
fragmentCreator = singleInstanceFragment { SettingsOverviewFragment() })
fragmentCreator = singleInstanceFragment { args ->
SettingsOverviewFragment().also {
it.arguments = args
} })
if (getPreferences().queryLoggingEnabled) {
divider()
fragmentItem(getString(R.string.menu_querylogging),
......@@ -228,14 +254,9 @@ class MainActivity : NavigationDrawerActivity() {
iconLeft = getDrawable(R.drawable.ic_external_link),
onLongClick = null,
onSimpleClick = { _, _, _ ->
NewServerDialog(
this@MainActivity,
title = getString(R.string.menu_create_shortcut),
onServerAdded = {
ShortcutActivity.createShortcut(this@MainActivity, it)
},
dnsOverHttps = true
).show()
ServerChoosalDialog(this@MainActivity, onEntrySelected = {
ShortcutActivity.createShortcut(this@MainActivity, it)
}).show()
false
})
fragmentItem(getString(R.string.button_main_dnsrules),
......@@ -286,10 +307,6 @@ class MainActivity : NavigationDrawerActivity() {
}
}
private fun askRateApp() {
}
private fun rateApp() {
val appPackageName = this.packageName
try {
......
......@@ -52,9 +52,26 @@ import java.security.NoSuchAlgorithmException
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class PinActivity: BaseActivity() {
companion object {
const val PIN_TIMEOUTMS = 2*60*1000
fun shouldValidatePin(context: Context, intent: Intent?): Boolean {
return context.getPreferences().enablePin && (intent == null || !intent.getBooleanExtra("pin_validated", false))
return context.getPreferences().enablePin
&& (intent == null
|| !intent.getBooleanExtra("pin_validated", false)
|| System.currentTimeMillis() - intent.getLongExtra("pin_validated_at", System.currentTimeMillis()) >= PIN_TIMEOUTMS)
}
fun passPinExtras():Bundle {
return Bundle().apply {
putLong("pin_validated_at", System.currentTimeMillis())
putBoolean("pin_validated", true)
}
}
fun passPin(`for`:Intent):Intent {
return `for`.putExtras(passPinExtras())
}
fun openAppIntent(context: Context, appExtras:Bundle? = null):Intent {
......@@ -76,8 +93,7 @@ class PinActivity: BaseActivity() {
if(intent.extras != null) intent.putExtra("extras", extras)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra("pin_type", pinType)
// TODO Replace with qualifier for Android Q
if(Build.VERSION.SDK_INT >= 29 && context is Service) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && context is Service) {
NotificationCompat.Builder(context, Notifications.getPinNotificationChannelId(context))
.setSmallIcon(R.drawable.ic_launcher_flat)
.setContentTitle(context.getString(R.string.notification_pin_title))
......@@ -154,7 +170,7 @@ class PinActivity: BaseActivity() {
val pinInput = view.findViewById<EditText>(R.id.pinInput)
dialog?.getButton(DialogInterface.BUTTON_POSITIVE)?.setOnClickListener {
if(pinInput.