Repo created
This commit is contained in:
parent
6e9a0d01ce
commit
7ee9806fba
2415 changed files with 312708 additions and 2 deletions
36
app/.gitignore
vendored
Normal file
36
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#Android specific
|
||||
bin
|
||||
gen
|
||||
obj
|
||||
libs/armeabi
|
||||
libs/armeabi-v7a
|
||||
libs/arm64-v8a
|
||||
libs/mips
|
||||
libs/mips64
|
||||
libs/x86
|
||||
libs/x86_64
|
||||
local.properties
|
||||
release.properties
|
||||
ant.properties
|
||||
*.class
|
||||
*.apk
|
||||
|
||||
#Gradle
|
||||
.gradle
|
||||
build
|
||||
gradle.properties
|
||||
|
||||
#Maven
|
||||
target
|
||||
pom.xml.*
|
||||
|
||||
#Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
.metadata
|
||||
|
||||
#IntelliJ IDEA
|
||||
.idea
|
||||
*.iml
|
||||
/.externalNativeBuild/
|
||||
149
app/build.gradle
Normal file
149
app/build.gradle
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
boolean keyStoreDefined = project.hasProperty('signingStoreLocation') &&
|
||||
project.hasProperty('signingStorePassword') &&
|
||||
project.hasProperty('signingKeyAlias') &&
|
||||
project.hasProperty('signingKeyPassword')
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
ndkVersion '25.2.9519653'
|
||||
namespace 'org.adaway'
|
||||
|
||||
defaultConfig {
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode libs.versions.appCode.get() as int
|
||||
versionName libs.versions.appName.get()
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = [
|
||||
"room.schemaLocation": "$projectDir/schemas".toString(),
|
||||
"room.incremental" : "true"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
/*
|
||||
* To sign release build, create file gradle.properties in ~/.gradle/ with this content:
|
||||
*
|
||||
* signingStoreLocation=/home/key.store
|
||||
* signingStorePassword=xxx
|
||||
* signingKeyAlias=alias
|
||||
* signingKeyPassword=xxx
|
||||
*/
|
||||
if (keyStoreDefined) {
|
||||
println "Found signature properties in gradle.properties. Build will be signed."
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(signingStoreLocation)
|
||||
storePassword signingStorePassword
|
||||
keyAlias signingKeyAlias
|
||||
keyPassword signingKeyPassword
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes.debug.signingConfig = signingConfigs.release
|
||||
buildTypes.release.signingConfig = signingConfigs.release
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// debug {
|
||||
// shrinkResources false
|
||||
// minifyEnabled false
|
||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
// }
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
// Do not abort build if lint finds errors
|
||||
lint {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Native modules
|
||||
implementation project(':tcpdump')
|
||||
implementation project(':webserver')
|
||||
|
||||
// AndroidX components
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.fragment:fragment:1.8.1'
|
||||
// "fragment-ktx" is not used but was added to fix the following dependency error:
|
||||
// Duplicate class androidx.lifecycle.ViewModelLazy found in modules lifecycle-viewmodel-2.5.0-runtime (androidx.lifecycle:lifecycle-viewmodel:2.5.0) and lifecycle-viewmodel-ktx-2.3.1-runtime
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.1'
|
||||
implementation 'androidx.paging:paging-runtime:3.3.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.room:room-runtime:2.6.1'
|
||||
implementation 'androidx.room:room-paging:2.6.1'
|
||||
annotationProcessor 'androidx.room:room-compiler:2.6.1'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.work:work-runtime:2.8.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
|
||||
// Collections related
|
||||
implementation libs.guava
|
||||
// Network related
|
||||
implementation libs.okhttp3.okhttp
|
||||
// Logging related
|
||||
implementation libs.timber
|
||||
if (keyStoreDefined) {
|
||||
implementation project(':sentrystub')
|
||||
} else {
|
||||
implementation platform('io.sentry:sentry-bom:7.8.0')
|
||||
implementation('io.sentry:sentry-android')
|
||||
implementation('io.sentry:sentry-android-fragment')
|
||||
implementation('io.sentry:sentry-android-timber')
|
||||
}
|
||||
|
||||
// Root related
|
||||
implementation libs.libsu
|
||||
|
||||
// VPN related
|
||||
implementation libs.bundles.pcap4j
|
||||
implementation libs.dnsjava
|
||||
implementation libs.slf4j.android
|
||||
implementation libs.okhttp.dnsoverhttps
|
||||
|
||||
// Test related
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.json
|
||||
androidTestImplementation libs.bundles.androidx.test
|
||||
androidTestImplementation libs.junit
|
||||
}
|
||||
10
app/lint.xml
Normal file
10
app/lint.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<!-- list of issues to configure -->
|
||||
<issue id="InvalidPackage">
|
||||
<!-- Ignore dnsjava -->
|
||||
<ignore path="**/dnsjava-3.0.2*.jar" />
|
||||
<!-- Ignore pcap4j dependency -->
|
||||
<ignore path="**/jna-5.3.1.jar"/>
|
||||
</issue>
|
||||
</lint>
|
||||
38
app/proguard-rules.pro
vendored
Normal file
38
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
-keep public class * extends android.content.ContentProvider
|
||||
|
||||
# Temporary fix for androidx preference fragement reference
|
||||
# See https://issuetracker.google.com/issues/145316223
|
||||
-keep public class org.adaway.ui.prefs.PrefsBackupRestoreFragment
|
||||
-keep public class org.adaway.ui.prefs.PrefsRootFragment
|
||||
-keep public class org.adaway.ui.prefs.PrefsUpdateFragment
|
||||
-keep public class org.adaway.ui.prefs.PrefsVpnFragment
|
||||
|
||||
-keepclassmembers class io.sentry.Sentry {
|
||||
public static final boolean STUB;
|
||||
}
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
### Android Jetpack ###
|
||||
-dontwarn com.google.**
|
||||
|
||||
### Sentry ###
|
||||
-dontwarn io.sentry.**
|
||||
|
||||
### OkHttp ###
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
# Generated rules from R8
|
||||
-dontwarn org.bouncycastle.jsse.**
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
### dnsjava ###
|
||||
-dontwarn lombok.Generated
|
||||
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor
|
||||
90
app/schemas/org.adaway.db.AppDatabase/1.json
Normal file
90
app/schemas/org.adaway.db.AppDatabase/1.json
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "5175df445bc75bbbb5ea672750d7b425",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER, PRIMARY KEY(`url`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastLocalModification",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastOnlineModification",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"5175df445bc75bbbb5ea672750d7b425\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
146
app/schemas/org.adaway.db.AppDatabase/2.json
Normal file
146
app/schemas/org.adaway.db.AppDatabase/2.json
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "e9b86296a34de1d881f8530fdf2c535d",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastLocalModification",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastOnlineModification",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_sources_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_sources_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "source_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_lists_host",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_lists_host` ON `${TABLE_NAME}` (`host`)"
|
||||
},
|
||||
{
|
||||
"name": "index_hosts_lists_source_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"source_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_source_id` ON `${TABLE_NAME}` (`source_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "hosts_sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e9b86296a34de1d881f8530fdf2c535d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
151
app/schemas/org.adaway.db.AppDatabase/3.json
Normal file
151
app/schemas/org.adaway.db.AppDatabase/3.json
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "ace31b365ff79d4497319c74157538d7",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastLocalModification",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastOnlineModification",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_sources_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_sources_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "source_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_lists_host",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_host` ON `${TABLE_NAME}` (`host`)"
|
||||
},
|
||||
{
|
||||
"name": "index_hosts_lists_source_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"source_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_source_id` ON `${TABLE_NAME}` (`source_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "hosts_sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [
|
||||
{
|
||||
"viewName": "host_entries",
|
||||
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT `host`, `type`, `redirection` FROM `hosts_lists` WHERE `enabled` = 1 AND ((`type` = 0 AND `host` NOT LIKE (SELECT `host` FROM `hosts_lists` WHERE `enabled` = 1 and `type` = 1)) OR `type` = 2) ORDER BY `host` ASC, `type` DESC, `redirection` ASC"
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ace31b365ff79d4497319c74157538d7')"
|
||||
]
|
||||
}
|
||||
}
|
||||
187
app/schemas/org.adaway.db.AppDatabase/4.json
Normal file
187
app/schemas/org.adaway.db.AppDatabase/4.json
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "80b1c1d47fbd109a4f052817c9faf980",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localModificationDate",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "onlineModificationDate",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_sources_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_sources_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "source_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_lists_host",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_host` ON `${TABLE_NAME}` (`host`)"
|
||||
},
|
||||
{
|
||||
"name": "index_hosts_lists_source_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"source_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_source_id` ON `${TABLE_NAME}` (`source_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "hosts_sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "host_entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `type` INTEGER NOT NULL, `redirection` TEXT, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_host_entries_host",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_host_entries_host` ON `${TABLE_NAME}` (`host`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '80b1c1d47fbd109a4f052817c9faf980')"
|
||||
]
|
||||
}
|
||||
}
|
||||
187
app/schemas/org.adaway.db.AppDatabase/5.json
Normal file
187
app/schemas/org.adaway.db.AppDatabase/5.json
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "80b1c1d47fbd109a4f052817c9faf980",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localModificationDate",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "onlineModificationDate",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_sources_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_sources_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "source_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_lists_host",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_host` ON `${TABLE_NAME}` (`host`)"
|
||||
},
|
||||
{
|
||||
"name": "index_hosts_lists_source_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"source_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_source_id` ON `${TABLE_NAME}` (`source_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "hosts_sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "host_entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `type` INTEGER NOT NULL, `redirection` TEXT, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_host_entries_host",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_host_entries_host` ON `${TABLE_NAME}` (`host`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '80b1c1d47fbd109a4f052817c9faf980')"
|
||||
]
|
||||
}
|
||||
}
|
||||
211
app/schemas/org.adaway.db.AppDatabase/6.json
Normal file
211
app/schemas/org.adaway.db.AppDatabase/6.json
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "c53f309b3cbcdeda90c9f22573023ac2",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `label` TEXT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `allowEnabled` INTEGER NOT NULL, `redirectEnabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER, `size` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "allowEnabled",
|
||||
"columnName": "allowEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirectEnabled",
|
||||
"columnName": "redirectEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localModificationDate",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "onlineModificationDate",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "size",
|
||||
"columnName": "size",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_sources_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_sources_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "source_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_lists_host",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_host` ON `${TABLE_NAME}` (`host`)"
|
||||
},
|
||||
{
|
||||
"name": "index_hosts_lists_source_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"source_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_source_id` ON `${TABLE_NAME}` (`source_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "hosts_sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "host_entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `type` INTEGER NOT NULL, `redirection` TEXT, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_host_entries_host",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_host_entries_host` ON `${TABLE_NAME}` (`host`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c53f309b3cbcdeda90c9f22573023ac2')"
|
||||
]
|
||||
}
|
||||
}
|
||||
221
app/schemas/org.adaway.db.AppDatabase/7.json
Normal file
221
app/schemas/org.adaway.db.AppDatabase/7.json
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "dccd6211bcef97caed75ea42d7df1b32",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "hosts_sources",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `label` TEXT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `allowEnabled` INTEGER NOT NULL, `redirectEnabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER, `entityTag` TEXT, `size` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "label",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "allowEnabled",
|
||||
"columnName": "allowEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirectEnabled",
|
||||
"columnName": "redirectEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localModificationDate",
|
||||
"columnName": "last_modified_local",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "onlineModificationDate",
|
||||
"columnName": "last_modified_online",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityTag",
|
||||
"columnName": "entityTag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "size",
|
||||
"columnName": "size",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_sources_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_hosts_sources_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "hosts_lists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "source_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_hosts_lists_host",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_host` ON `${TABLE_NAME}` (`host`)"
|
||||
},
|
||||
{
|
||||
"name": "index_hosts_lists_source_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"source_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_hosts_lists_source_id` ON `${TABLE_NAME}` (`source_id`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "hosts_sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"source_id"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "host_entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `type` INTEGER NOT NULL, `redirection` TEXT, PRIMARY KEY(`host`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "redirection",
|
||||
"columnName": "redirection",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_host_entries_host",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"host"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_host_entries_host` ON `${TABLE_NAME}` (`host`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dccd6211bcef97caed75ea42d7df1b32')"
|
||||
]
|
||||
}
|
||||
}
|
||||
151
app/src/androidTest/java/org/adaway/db/DbTest.java
Normal file
151
app/src/androidTest/java/org/adaway/db/DbTest.java
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package org.adaway.db;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_URL;
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.room.Room;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.adaway.db.dao.HostEntryDao;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* This class is a base class for testing database feature.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public abstract class DbTest {
|
||||
protected static final int EXTERNAL_SOURCE_ID = 2;
|
||||
@Rule
|
||||
public TestRule rule = new InstantTaskExecutorRule();
|
||||
protected AppDatabase db;
|
||||
protected HostsSourceDao hostsSourceDao;
|
||||
protected HostListItemDao hostListItemDao;
|
||||
protected HostEntryDao hostEntryDao;
|
||||
protected LiveData<Integer> blockedHostCount;
|
||||
protected LiveData<Integer> allowedHostCount;
|
||||
protected LiveData<Integer> redirectedHostCount;
|
||||
protected HostsSource externalHostSource;
|
||||
|
||||
protected static <T> T getOrAwaitValue(final LiveData<T> liveData) throws InterruptedException {
|
||||
final Object[] data = new Object[1];
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
Observer<T> observer = new Observer<T>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable T o) {
|
||||
data[0] = o;
|
||||
latch.countDown();
|
||||
liveData.removeObserver(this);
|
||||
}
|
||||
};
|
||||
liveData.observeForever(observer);
|
||||
if (!latch.await(2, TimeUnit.SECONDS)) {
|
||||
fail("Failed to get LiveData value in time");
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (T) data[0];
|
||||
}
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
createDb();
|
||||
loadDao();
|
||||
createSources();
|
||||
}
|
||||
|
||||
protected void createDb() {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
this.db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class)
|
||||
.allowMainThreadQueries()
|
||||
.build();
|
||||
}
|
||||
|
||||
protected void loadDao() {
|
||||
this.hostsSourceDao = this.db.hostsSourceDao();
|
||||
this.hostListItemDao = this.db.hostsListItemDao();
|
||||
this.hostEntryDao = this.db.hostEntryDao();
|
||||
this.blockedHostCount = this.hostListItemDao.getBlockedHostCount();
|
||||
this.allowedHostCount = this.hostListItemDao.getAllowedHostCount();
|
||||
this.redirectedHostCount = this.hostListItemDao.getRedirectHostCount();
|
||||
}
|
||||
|
||||
protected void createSources() {
|
||||
// Insert at least user source and external source to allow duplicate hosts to be inserted
|
||||
insertSource(USER_SOURCE_ID, USER_SOURCE_URL);
|
||||
insertSource(EXTERNAL_SOURCE_ID, "https://adaway.org/hosts.txt");
|
||||
this.externalHostSource = getSourceFromId(EXTERNAL_SOURCE_ID);
|
||||
}
|
||||
|
||||
@After
|
||||
public void closeDb() {
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
protected void insertSource(int id, String url) {
|
||||
HostsSource source = new HostsSource();
|
||||
source.setId(id);
|
||||
source.setLabel(url);
|
||||
source.setUrl(url);
|
||||
source.setEnabled(true);
|
||||
this.hostsSourceDao.insert(source);
|
||||
}
|
||||
|
||||
protected void insertBlockedHost(String host, int sourceId) {
|
||||
HostListItem item = new HostListItem();
|
||||
item.setType(BLOCKED);
|
||||
item.setHost(host);
|
||||
item.setEnabled(true);
|
||||
item.setSourceId(sourceId);
|
||||
this.hostListItemDao.insert(item);
|
||||
}
|
||||
|
||||
protected void insertAllowedHost(String host, int sourceId) {
|
||||
HostListItem item = new HostListItem();
|
||||
item.setType(ALLOWED);
|
||||
item.setHost(host);
|
||||
item.setEnabled(true);
|
||||
item.setSourceId(sourceId);
|
||||
this.hostListItemDao.insert(item);
|
||||
}
|
||||
|
||||
protected void insertRedirectedHost(String host, String redirection, int sourceId) {
|
||||
HostListItem item = new HostListItem();
|
||||
item.setType(REDIRECTED);
|
||||
item.setHost(host);
|
||||
item.setEnabled(true);
|
||||
item.setRedirection(redirection);
|
||||
item.setSourceId(sourceId);
|
||||
this.hostListItemDao.insert(item);
|
||||
}
|
||||
|
||||
protected HostsSource getSourceFromId(int id) {
|
||||
return this.hostsSourceDao.getAll()
|
||||
.stream()
|
||||
.filter(hostsSource -> hostsSource.getId() == id)
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
90
app/src/androidTest/java/org/adaway/db/HostDbTest.java
Normal file
90
app/src/androidTest/java/org/adaway/db/HostDbTest.java
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package org.adaway.db;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class tests {@link org.adaway.db.entity.HostListItem} database manipulations.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HostDbTest extends DbTest {
|
||||
@Test
|
||||
public void testEmptyByDefault() throws InterruptedException {
|
||||
assertEquals(0, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(0, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertThenDeleteHosts() throws InterruptedException {
|
||||
// Insert blocked hosts
|
||||
insertBlockedHost("advertising.apple.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("an.facebook.com", USER_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Check inserting
|
||||
assertEquals(2, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(2, this.hostEntryDao.getAll().size());
|
||||
// Remove block hosts
|
||||
this.hostListItemDao.deleteUserFromHost("advertising.apple.com");
|
||||
this.hostEntryDao.sync();
|
||||
// Check deletion
|
||||
assertEquals(1, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateBlockedHosts() throws InterruptedException {
|
||||
insertBlockedHost("advertising.apple.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("advertising.apple.com", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateAllowedHosts() throws InterruptedException {
|
||||
insertAllowedHost("adaway.org", USER_SOURCE_ID);
|
||||
insertAllowedHost("adaway.org", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(0, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateRedirectedHosts() throws InterruptedException {
|
||||
insertRedirectedHost("github.com", "1.1.1.1", USER_SOURCE_ID);
|
||||
insertRedirectedHost("github.com", "2.2.2.2", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectionPriority() throws InterruptedException {
|
||||
// Insert two redirects for the same host
|
||||
insertRedirectedHost("adaway.org", "1.1.1.1", USER_SOURCE_ID);
|
||||
insertRedirectedHost("adaway.org", "2.2.2.2", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted redirected hosts
|
||||
assertEquals(1, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
// Test inserted redirect
|
||||
List<HostEntry> entries = this.hostEntryDao.getAll();
|
||||
assertEquals(1, entries.size());
|
||||
// Test user redirect is applied in priority
|
||||
HostEntry entry = this.hostEntryDao.getEntry("adaway.org");
|
||||
assertNotNull(entry);
|
||||
assertEquals("1.1.1.1", entry.getRedirection());
|
||||
}
|
||||
}
|
||||
113
app/src/androidTest/java/org/adaway/db/SourceDbTest.java
Normal file
113
app/src/androidTest/java/org/adaway/db/SourceDbTest.java
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package org.adaway.db;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class tests {@link HostsSource} database manipulations.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class SourceDbTest extends DbTest {
|
||||
@Test
|
||||
public void testSourceCount() {
|
||||
// Test only external source is found
|
||||
List<HostsSource> sources = this.hostsSourceDao.getAll();
|
||||
assertEquals(1, sources.size());
|
||||
assertEquals("https://adaway.org/hosts.txt", sources.get(0).getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSourceDeletion() throws InterruptedException {
|
||||
// Insert blocked hosts
|
||||
insertBlockedHost("bingads.microsoft.com", EXTERNAL_SOURCE_ID);
|
||||
insertBlockedHost("ads.yahoo.com", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted blocked hosts
|
||||
assertEquals(2, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(2, this.hostEntryDao.getAll().size());
|
||||
// Delete source
|
||||
this.hostsSourceDao.delete(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
List<HostsSource> sources = this.hostsSourceDao.getAll();
|
||||
assertEquals(0, sources.size());
|
||||
// Check related hosts cleaning
|
||||
assertEquals(0, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(0, this.hostEntryDao.getAll().size());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockedHostsFromDisabledSource() throws InterruptedException {
|
||||
// Insert blocked hosts
|
||||
insertBlockedHost("advertising.apple.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("an.facebook.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("ads.google.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("bingads.microsoft.com", EXTERNAL_SOURCE_ID);
|
||||
insertBlockedHost("ads.yahoo.com", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted blocked hosts
|
||||
assertEquals(5, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(5, this.hostEntryDao.getAll().size());
|
||||
// Disabled external source
|
||||
this.hostsSourceDao.toggleEnabled(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(3, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(3, this.hostEntryDao.getAll().size());
|
||||
// Re-enable external source
|
||||
this.hostsSourceDao.toggleEnabled(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(5, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(5, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowedHostsFromDisabledSource() throws InterruptedException {
|
||||
// Insert blocked and allowed host
|
||||
insertBlockedHost("adaway.org", USER_SOURCE_ID);
|
||||
insertAllowedHost("adaway.org", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted blocked hosts
|
||||
assertEquals(1, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(1, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(0, this.hostEntryDao.getAll().size());
|
||||
// Disabled a source
|
||||
this.hostsSourceDao.toggleEnabled(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(0, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
// Re-enable a source
|
||||
this.hostsSourceDao.toggleEnabled(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(1, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(0, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectedHostsFromDisabledSource() throws InterruptedException {
|
||||
// Insert redirected hosts
|
||||
insertRedirectedHost("github.com", "1.1.1.1", USER_SOURCE_ID);
|
||||
insertRedirectedHost("github.com", "2.2.2.2", EXTERNAL_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted blocked hosts
|
||||
assertEquals(1, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
// Disabled a source
|
||||
this.hostsSourceDao.toggleEnabled(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
// Re-enable a source
|
||||
this.hostsSourceDao.toggleEnabled(this.externalHostSource);
|
||||
this.hostEntryDao.sync();
|
||||
assertEquals(1, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
assertEquals(1, this.hostEntryDao.getAll().size());
|
||||
}
|
||||
}
|
||||
157
app/src/androidTest/java/org/adaway/db/UserListTest.java
Normal file
157
app/src/androidTest/java/org/adaway/db/UserListTest.java
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
package org.adaway.db;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* This class the user lists use case where user can freely add blocked, allowed and redirected hosts.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class UserListTest extends DbTest {
|
||||
@Test
|
||||
public void testUserList() throws InterruptedException {
|
||||
testUserBlockedHosts();
|
||||
testUserAllowedHosts();
|
||||
testUserRedirectedHosts();
|
||||
}
|
||||
|
||||
protected void testUserBlockedHosts() throws InterruptedException {
|
||||
// Insert blocked hosts
|
||||
insertBlockedHost("advertising.apple.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("an.facebook.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("ads.google.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("bingads.microsoft.com", USER_SOURCE_ID);
|
||||
insertBlockedHost("ads.yahoo.com", USER_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted blocked hosts
|
||||
assertEquals(5, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(5, this.hostEntryDao.getAll().size());
|
||||
// Test each host type
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("advertising.apple.com"));
|
||||
HostEntry entry = this.hostEntryDao.getEntry("advertising.apple.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("advertising.apple.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("an.facebook.com"));
|
||||
entry = this.hostEntryDao.getEntry("an.facebook.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("an.facebook.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("ads.google.com"));
|
||||
entry = this.hostEntryDao.getEntry("ads.google.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("ads.google.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("bingads.microsoft.com"));
|
||||
entry = this.hostEntryDao.getEntry("bingads.microsoft.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("bingads.microsoft.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("ads.yahoo.com"));
|
||||
entry = this.hostEntryDao.getEntry("ads.yahoo.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("ads.yahoo.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
}
|
||||
|
||||
protected void testUserAllowedHosts() throws InterruptedException {
|
||||
// Insert allowed hosts
|
||||
insertAllowedHost("*.google.com", USER_SOURCE_ID);
|
||||
insertAllowedHost("ads.yahoo.com", USER_SOURCE_ID);
|
||||
insertAllowedHost("adaway.org", USER_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted allowed hosts
|
||||
assertEquals(3, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
// Test overall list
|
||||
assertEquals(5, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(3, this.hostEntryDao.getAll().size());
|
||||
// Test each host type
|
||||
assertEquals(ALLOWED, this.hostEntryDao.getTypeForHost("adaway.org"));
|
||||
HostEntry entry = this.hostEntryDao.getEntry("adaway.org");
|
||||
assertNull(entry);
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("advertising.apple.com"));
|
||||
entry = this.hostEntryDao.getEntry("advertising.apple.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("advertising.apple.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("an.facebook.com"));
|
||||
entry = this.hostEntryDao.getEntry("an.facebook.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("an.facebook.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(ALLOWED, this.hostEntryDao.getTypeForHost("ads.google.com"));
|
||||
entry = this.hostEntryDao.getEntry("ads.google.com");
|
||||
assertNull(entry);
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("bingads.microsoft.com"));
|
||||
entry = this.hostEntryDao.getEntry("bingads.microsoft.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("bingads.microsoft.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
assertEquals(ALLOWED, this.hostEntryDao.getTypeForHost("ads.yahoo.com"));
|
||||
entry = this.hostEntryDao.getEntry("ads.yahoo.com");
|
||||
assertNull(entry);
|
||||
|
||||
}
|
||||
|
||||
protected void testUserRedirectedHosts() throws InterruptedException {
|
||||
// Insert redirected hosts
|
||||
insertRedirectedHost("ads.yahoo.com", "1.2.3.4", USER_SOURCE_ID);
|
||||
insertRedirectedHost("github.com", "1.2.3.4", USER_SOURCE_ID);
|
||||
this.hostEntryDao.sync();
|
||||
// Test inserted redirected hosts
|
||||
assertEquals(2, getOrAwaitValue(this.redirectedHostCount).intValue());
|
||||
// Test overall list
|
||||
assertEquals(5, getOrAwaitValue(this.blockedHostCount).intValue());
|
||||
assertEquals(3, getOrAwaitValue(this.allowedHostCount).intValue());
|
||||
assertEquals(5, this.hostEntryDao.getAll().size()); // 3 blocked, 2 redirected
|
||||
// Test each host type
|
||||
assertEquals(ALLOWED, this.hostEntryDao.getTypeForHost("adaway.org"));
|
||||
HostEntry entry = this.hostEntryDao.getEntry("adaway.org");
|
||||
assertNull(entry);
|
||||
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("advertising.apple.com"));
|
||||
entry = this.hostEntryDao.getEntry("advertising.apple.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("advertising.apple.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("an.facebook.com"));
|
||||
entry = this.hostEntryDao.getEntry("an.facebook.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("an.facebook.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
|
||||
assertEquals(REDIRECTED, this.hostEntryDao.getTypeForHost("github.com"));
|
||||
entry = this.hostEntryDao.getEntry("github.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("github.com", entry.getHost());
|
||||
assertEquals(REDIRECTED, entry.getType());
|
||||
assertEquals("1.2.3.4", entry.getRedirection());
|
||||
|
||||
assertEquals(ALLOWED, this.hostEntryDao.getTypeForHost("ads.google.com"));
|
||||
entry = this.hostEntryDao.getEntry("ads.google.com");
|
||||
assertNull(entry);
|
||||
|
||||
assertEquals(BLOCKED, this.hostEntryDao.getTypeForHost("bingads.microsoft.com"));
|
||||
entry = this.hostEntryDao.getEntry("bingads.microsoft.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("bingads.microsoft.com", entry.getHost());
|
||||
assertEquals(BLOCKED, entry.getType());
|
||||
|
||||
assertEquals(REDIRECTED, this.hostEntryDao.getTypeForHost("ads.yahoo.com"));
|
||||
entry = this.hostEntryDao.getEntry("ads.yahoo.com");
|
||||
assertNotNull(entry);
|
||||
assertEquals("ads.yahoo.com", entry.getHost());
|
||||
assertEquals(REDIRECTED, entry.getType());
|
||||
assertEquals("1.2.3.4", entry.getRedirection());
|
||||
}
|
||||
}
|
||||
177
app/src/main/AndroidManifest.xml
Normal file
177
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
|
||||
<permission-group
|
||||
android:name="org.adaway.permission-group.API"
|
||||
android:description="@string/permission_group_api_description"
|
||||
android:label="@string/permission_group_api_label" />
|
||||
|
||||
<permission
|
||||
android:name="org.adaway.permission.SEND_COMMAND"
|
||||
android:description="@string/permission_send_command_description"
|
||||
android:label="@string/permission_send_command_label"
|
||||
android:permissionGroup="org.adaway.permission-group.API"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".AdAwayApplication"
|
||||
android:allowBackup="true"
|
||||
android:backupAgent=".model.backup.AppBackupAgent"
|
||||
android:fullBackupOnly="false"
|
||||
android:icon="@mipmap/icon"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.AdAway"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity
|
||||
android:name=".ui.home.HomeActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.AdAway.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.welcome.WelcomeActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/welcome_title"
|
||||
android:parentActivityName=".ui.home.HomeActivity"
|
||||
android:theme="@style/Theme.AdAway.NoActionBar.Red" />
|
||||
<activity
|
||||
android:name=".ui.hosts.HostsSourcesActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/hosts_title"
|
||||
android:parentActivityName=".ui.home.HomeActivity" />
|
||||
<activity
|
||||
android:name=".ui.source.SourceEditActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/source_edit_title"
|
||||
android:parentActivityName=".ui.hosts.HostsSourcesActivity" />
|
||||
<activity
|
||||
android:name=".ui.log.LogActivity"
|
||||
android:label="@string/shortcut_dns_requests"
|
||||
android:parentActivityName=".ui.home.HomeActivity" />
|
||||
<activity
|
||||
android:name=".ui.lists.ListsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/lists_title"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".ui.home.HomeActivity">
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/list_searchable" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.help.HelpActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/menu_help"
|
||||
android:parentActivityName=".ui.home.HomeActivity" />
|
||||
<activity
|
||||
android:name=".ui.support.SupportActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/support_title"
|
||||
android:parentActivityName=".ui.home.HomeActivity"
|
||||
android:theme="@style/Theme.AdAway.NoActionBar.Red" />
|
||||
<activity
|
||||
android:name=".ui.prefs.PrefsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/preferences_drawer_item"
|
||||
android:parentActivityName=".ui.home.HomeActivity" />
|
||||
<activity
|
||||
android:name=".ui.prefs.exclusion.PrefsVpnExcludedAppsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/pref_vpn_exclude_user_apps_activity" />
|
||||
<activity
|
||||
android:name=".ui.update.UpdateActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/update_title"
|
||||
android:parentActivityName=".ui.home.HomeActivity"
|
||||
android:theme="@style/Theme.AdAway.NoActionBar.Red" />
|
||||
|
||||
<service
|
||||
android:name=".vpn.VpnService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".tile.AdBlockingTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/logo"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".broadcast.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".broadcast.CommandReceiver"
|
||||
android:exported="true"
|
||||
android:permission="org.adaway.permission.SEND_COMMAND">
|
||||
<intent-filter>
|
||||
<action android:name="org.adaway.action.SEND_COMMAND" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".broadcast.UpdateReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data
|
||||
android:name="io.sentry.auto-init"
|
||||
android:value="false" />
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
android:value="https://8dac17b798fb45e492278a678c5ab028@o209266.ingest.us.sentry.io/1331667" />
|
||||
<meta-data android:name="io.sentry.traces.user-interaction.enable" android:value="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
113
app/src/main/assets/icon.svg
Normal file
113
app/src/main/assets/icon.svg
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 32 32"
|
||||
id="svg3167"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
sodipodi:docname="icon-lite.svg">
|
||||
<defs
|
||||
id="defs3169">
|
||||
<radialGradient
|
||||
r="17.497915"
|
||||
fy="40.636124"
|
||||
fx="33.772423"
|
||||
cy="40.636124"
|
||||
cx="33.772423"
|
||||
gradientTransform="matrix(1.161721,0,0,1.0498451,-2.6955965,978.41529)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="radialGradient3878"
|
||||
xlink:href="#linearGradient4263"
|
||||
inkscape:collect="always" />
|
||||
<linearGradient
|
||||
id="linearGradient4263">
|
||||
<stop
|
||||
id="stop4265"
|
||||
offset="0"
|
||||
style="stop-color:#bfbfbf;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop4267"
|
||||
offset="1"
|
||||
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4263"
|
||||
id="radialGradient3233"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.161721,0,0,1.0498451,-2.6955965,978.41529)"
|
||||
cx="33.772423"
|
||||
cy="40.636124"
|
||||
fx="33.772423"
|
||||
fy="40.636124"
|
||||
r="17.497915" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4263"
|
||||
id="radialGradient3238"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.61137216,0,0,0.55249588,-4.1660353,-4.330286)"
|
||||
cx="33.772423"
|
||||
cy="40.636124"
|
||||
fx="33.772423"
|
||||
fy="40.636124"
|
||||
r="17.497915" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4263"
|
||||
id="radialGradient3241"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.61137216,0,0,0.55249588,-3.9874288,-4.330286)"
|
||||
cx="33.772423"
|
||||
cy="40.636124"
|
||||
fx="33.772423"
|
||||
fy="40.636124"
|
||||
r="17.497915" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="15.836083"
|
||||
inkscape:cx="5.6448078"
|
||||
inkscape:cy="16.057137"
|
||||
inkscape:current-layer="svg3167"
|
||||
showgrid="true"
|
||||
inkscape:grid-bbox="true"
|
||||
inkscape:document-units="px"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1387"
|
||||
inkscape:window-x="1672"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata3172">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
style="fill:#cc0000;fill-opacity:0.11640212;stroke:#333333;stroke-width:0.46215421000000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:0.23529412000000000;stroke-dasharray:none;stroke-dashoffset:0"
|
||||
d="M 9.90625 1.21875 L 1.25 9.875 L 1.21875 22.09375 L 9.875 30.75 L 22.09375 30.78125 L 30.75 22.125 L 30.78125 9.90625 L 22.125 1.25 L 9.90625 1.21875 z M 6 8.1875 C 6.6763721 8.1197446 10.285042 9.6406437 14.5625 9.65625 C 16.759542 9.791605 17.570434 10.416353 18.1875 11.0625 C 19.571912 12.366793 19.873633 13.03126 21.625 12.1875 C 22.338438 11.843849 23.228376 9.8114647 24.21875 9.59375 C 24.990847 9.481077 26.122457 10.438725 26.40625 10.71875 C 26.716053 11.02451 26.985342 11.333132 27.28125 11.46875 C 26.265273 11.52259 24.879528 13.892184 24.84375 15.90625 C 25.046345 21.533487 22.49174 21.215441 20 23.96875 C 19.205534 24.725992 18.781983 26.303089 18.78125 27.75 L 17.71875 27.71875 C 17.098386 27.73296 16.405511 26.947039 16.0625 26.40625 C 15.407551 25.314883 14.737631 27.401017 13.71875 25.6875 C 13.7659 25.120872 10.972706 25.011002 12.5625 23.6875 C 16.317406 21.029656 18.172296 20.45754 18.09375 19.65625 C 18.00095 18.709606 15.800894 19.560638 13.8125 19.46875 C 10.527778 19.317028 10.884928 17.249787 10.59375 16.28125 C 7.8361706 14.745664 12.704357 14.015508 10.5625 13.5625 C 9.5159322 13.396095 7.6703939 13.778783 7.28125 11.96875 C 7.6312051 12.401076 12.220507 11.538235 10.375 11.25 C 7.814251 10.088009 7.0930254 10.68347 6.03125 8.5 C 5.8473548 8.2768081 5.8439141 8.2031359 6 8.1875 z "
|
||||
id="path3852" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
20
app/src/main/assets/localhost-2410.crt
Normal file
20
app/src/main/assets/localhost-2410.crt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDTTCCAjWgAwIBAgIUR9ZJhU2vy/hB7LChIPgUuBEzqkQwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MTAyNzA2NTEwNVoXDTI3MTEy
|
||||
NzA2NTEwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAoVNvAzvXCjH07lgGTwBRMOFNuvsIBzi5rtJw8EQI1+np
|
||||
h8jZjGmTq759VoqsRSrzPSb8W4XNmGlg8gm0nRN9VJXE9IccWQmpbl61MG6zu9Ae
|
||||
/RZ8iAuydztn16/sOVQMT3Y0dXl3Fz9VtSEAMWZG9iATlVuugShLbod+Smw3mQow
|
||||
2Z6Mfg6u4vpPCLG13Hdilv+UGs19dUKlFJdsz2M/gsk0vIlyhaFc9PcyzKJG4Tk2
|
||||
XZOHEWMj7VjmEMZoUogihi9EWPLpBoi86dIWBBQfhu9HTwzl1BE33TMtYLPdKdxD
|
||||
9I9QPg59wbquiVYZ3sPVISdks4qmg4NgzBJVB/tWhQIDAQABo4GWMIGTMEIGA1Ud
|
||||
EQQ7MDmCCWxvY2FsaG9zdIIRKi5kb3VibGVjbGljay5uZXSCEyouZy5kb3VibGVj
|
||||
bGljay5uZXSHBH8AAAEwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
|
||||
MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFPyh/TOOyMqOumAhxn1Jsyb09CQ3MA0G
|
||||
CSqGSIb3DQEBCwUAA4IBAQAdxRNksyLjIwVaKjmC+Yx87zKw8bSmH2mm85KJ0Y7z
|
||||
uoWLdiw5Lc7/uZoXEl4KGJEnAXb83lo2042WCn49bOhAtYv7tOBTvTGhCHCZ6p7u
|
||||
Lk+dN8eOkfKgIpKv+tyt0y+Dl+K9m0TegBqky03xf+gjfMaNctwkxxl7Ls4CR9Lz
|
||||
Bufvt1HxJkmSILmTy8ByJPG5ePN6By4YiEwg9h2Hdx7zEqAKkgHvr4Yn/c4GzDQ+
|
||||
UZZW9NCIsKkaV32UfpCSodlpc+xeayX8jxZW2GnXs31IjJd4KuNfK5g7ZuOIZN0S
|
||||
iKvTwJkScrfvLlmbCqEJ7Nu4l/XR+5EAy21YNlV+mo7D
|
||||
-----END CERTIFICATE-----
|
||||
28
app/src/main/assets/localhost-2410.key
Normal file
28
app/src/main/assets/localhost-2410.key
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQChU28DO9cKMfTu
|
||||
WAZPAFEw4U26+wgHOLmu0nDwRAjX6emHyNmMaZOrvn1WiqxFKvM9Jvxbhc2YaWDy
|
||||
CbSdE31UlcT0hxxZCaluXrUwbrO70B79FnyIC7J3O2fXr+w5VAxPdjR1eXcXP1W1
|
||||
IQAxZkb2IBOVW66BKEtuh35KbDeZCjDZnox+Dq7i+k8IsbXcd2KW/5QazX11QqUU
|
||||
l2zPYz+CyTS8iXKFoVz09zLMokbhOTZdk4cRYyPtWOYQxmhSiCKGL0RY8ukGiLzp
|
||||
0hYEFB+G70dPDOXUETfdMy1gs90p3EP0j1A+Dn3Buq6JVhnew9UhJ2SziqaDg2DM
|
||||
ElUH+1aFAgMBAAECggEAErRYPjE9dP6mzc2h6Z35S+gLeZ7qZt/yU20t0AWrWtFR
|
||||
lL86Tffdub9ry9FnONvKePAguUHRvRaWuWlbqgyc7uYwgEN8C2y92sCbVGK5bxCp
|
||||
zyFAzgtBJWbbWtwYUOtIRBxJ58buAmGC/+20FoYruxSsAJixKmNwH4ARKfLTHWis
|
||||
bwzULTA4fhmvPgv+DWg7rxOTZP+7Jl5xFQnj4lXQaTt44B9DQF7ArtVjwHJWGbAN
|
||||
muc4Ij/2lzD1EFNv9hXRvPjnhdN8WuDXZ52BTNj3ZXEYYyjw6Mvy7Qtsiz4FovpT
|
||||
UvIsQgUyvkyukKX+8rQb+JAcfZQHnfXFSoZyIJ/KDwKBgQDQAY7L6o/ZwQ2FjyyN
|
||||
K0Zu4oozQh1VM6C8eGtOsa6b+1y1d3RQg0JYb4ay1KwyFAUVnDSx/dNVg8LxVbL+
|
||||
st0EAUYmyNtozAerkAAdbYLr/mDKM4pYp9OzamqJYaUVXDa70zEOe/Q86a4KKa8l
|
||||
mr6jfd2+OHhBjFfc/UWfZKWAiwKBgQDGjJXM8KvFIqkBnNqcy1NhhtUUAwzV9oye
|
||||
HsagZBiTDXeCq2FUOJRh1474az+X12PTCDHNobvhGwz+eYe8yfDmILE9RmjbtCwO
|
||||
YzTFuEyzFxG4aWAVV9/u+qWu8LQFeXRVOyztNFmIXqWaaCg0tahaVKI0dukMk+/1
|
||||
M4b/fGzXLwKBgCBxfcJUjadbMy63zC0gqNW2w/OGxmh5qwJ6jdIyaJevtyAex6ef
|
||||
MYP1sT7HaSxObxSVzqpMeuAFsyxNP6P2Zf6v7C80ePR5jmC2Dy6H3DnO7W3caCG3
|
||||
249Kc9+FuWgBgA//utEViFzP3fN72PO2lTGO+j0nNaqTp0iywF9CJYZNAoGAXo/k
|
||||
ZKgXVxuL3K3E3Lpl6uQZpZ9SRLFZBZHozcj+f0MBsWVIRKFx4iuU9zG1Ju85pu+X
|
||||
MLWf0rVcefKNuFeBeUkGwQVAuarU9MFBCA4f0YfiM69USLYCfEI6GNihFJ5kzpcR
|
||||
baPqJG3Xd3O1+myuUt9OJaiglBH9Tg4NdK7g85cCgYAYL3r9a8LosXGwtqiahX9o
|
||||
Hyu/lRrKDH6yvUN2YeHN10pyY8rkdLAZ6E6imgxHqA8n8AibhkoaGpTrBlKRgP1/
|
||||
MMjeqy3C3hYjBQdfs+Vv6YtsufhegHs84/tgDxyfkWcljeXg2GoZvPQxANNZe8QE
|
||||
JTaivHj2kiF8ZakITrXT+A==
|
||||
-----END PRIVATE KEY-----
|
||||
13
app/src/main/assets/test.html
Normal file
13
app/src/main/assets/test.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Web server test page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Web server works fine</h1>
|
||||
<img width="60%" src="icon.png" alt="AdAway logo when option is enabled" style="display: block; margin: auto;">
|
||||
<p>If you can see this page, that means the web server is working fine and its certificate is installed.</p>
|
||||
</body>
|
||||
</html>
|
||||
BIN
app/src/main/icon-web.png
Normal file
BIN
app/src/main/icon-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
76
app/src/main/java/org/adaway/AdAwayApplication.java
Normal file
76
app/src/main/java/org/adaway/AdAwayApplication.java
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package org.adaway;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.adaway.helper.NotificationHelper;
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
import org.adaway.model.adblocking.AdBlockMethod;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.source.SourceModel;
|
||||
import org.adaway.model.update.UpdateModel;
|
||||
import org.adaway.util.log.ApplicationLog;
|
||||
|
||||
/**
|
||||
* This class is a custom {@link Application} for AdAway app.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AdAwayApplication extends Application {
|
||||
/**
|
||||
* The common source model for the whole application.
|
||||
*/
|
||||
private SourceModel sourceModel;
|
||||
/**
|
||||
* The common ad block model for the whole application.
|
||||
*/
|
||||
private AdBlockModel adBlockModel;
|
||||
/**
|
||||
* The common update model for the whole application.
|
||||
*/
|
||||
private UpdateModel updateModel;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Delegate application creation
|
||||
super.onCreate();
|
||||
// Initialize logging
|
||||
ApplicationLog.init(this);
|
||||
// Create notification channels
|
||||
NotificationHelper.createNotificationChannels(this);
|
||||
// Create models
|
||||
this.sourceModel = new SourceModel(this);
|
||||
this.updateModel = new UpdateModel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source model.
|
||||
*
|
||||
* @return The common source model for the whole application.
|
||||
*/
|
||||
public SourceModel getSourceModel() {
|
||||
return this.sourceModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ad block model.
|
||||
*
|
||||
* @return The common ad block model for the whole application.
|
||||
*/
|
||||
public AdBlockModel getAdBlockModel() {
|
||||
// Check cached model
|
||||
AdBlockMethod method = PreferenceHelper.getAdBlockMethod(this);
|
||||
if (this.adBlockModel == null || this.adBlockModel.getMethod() != method) {
|
||||
this.adBlockModel = AdBlockModel.build(this, method);
|
||||
}
|
||||
return this.adBlockModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the update model.
|
||||
*
|
||||
* @return Teh common update model for the whole application.
|
||||
*/
|
||||
public UpdateModel getUpdateModel() {
|
||||
return this.updateModel;
|
||||
}
|
||||
}
|
||||
64
app/src/main/java/org/adaway/broadcast/BootReceiver.java
Normal file
64
app/src/main/java/org/adaway/broadcast/BootReceiver.java
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.broadcast;
|
||||
|
||||
import static android.content.Intent.ACTION_BOOT_COMPLETED;
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.ROOT;
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.VPN;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
import org.adaway.model.adblocking.AdBlockMethod;
|
||||
import org.adaway.util.WebServerUtils;
|
||||
import org.adaway.vpn.VpnServiceControls;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This broadcast receiver is executed after boot.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
Timber.d("BootReceiver invoked.");
|
||||
AdBlockMethod adBlockMethod = PreferenceHelper.getAdBlockMethod(context);
|
||||
// Start web server on boot if enabled in preferences
|
||||
if (adBlockMethod == ROOT && PreferenceHelper.getWebServerEnabled(context)) {
|
||||
WebServerUtils.startWebServer(context);
|
||||
}
|
||||
if (adBlockMethod == VPN && PreferenceHelper.getVpnServiceOnBoot(context)) {
|
||||
// Ensure VPN is prepared
|
||||
Intent prepareIntent = android.net.VpnService.prepare(context);
|
||||
if (prepareIntent != null) {
|
||||
context.startActivity(prepareIntent);
|
||||
}
|
||||
// Start VPN service if enabled in preferences
|
||||
VpnServiceControls.start(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/src/main/java/org/adaway/broadcast/Command.java
Normal file
57
app/src/main/java/org/adaway/broadcast/Command.java
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package org.adaway.broadcast;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This enumerate lists the commands of {@link CommandReceiver}.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum Command {
|
||||
/**
|
||||
* Start the ad-blocking.
|
||||
*/
|
||||
START,
|
||||
/**
|
||||
* Stop the ad-blocking.
|
||||
*/
|
||||
STOP,
|
||||
/**
|
||||
* Unknown command.
|
||||
*/
|
||||
UNKNOWN;
|
||||
|
||||
private static final String INTENT_EXTRA_COMMAND = "COMMAND";
|
||||
|
||||
/**
|
||||
* Read command from intent.
|
||||
*
|
||||
* @param intent The intent to read command from.
|
||||
* @return The read intent.
|
||||
*/
|
||||
public static Command readFromIntent(Intent intent) {
|
||||
Command command = UNKNOWN;
|
||||
if (intent != null && intent.hasExtra(INTENT_EXTRA_COMMAND)) {
|
||||
String commandName = intent.getStringExtra(INTENT_EXTRA_COMMAND);
|
||||
if (commandName != null) {
|
||||
try {
|
||||
command = Command.valueOf(commandName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Timber.w("Failed to read command named %s.", commandName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append command to intent.
|
||||
*
|
||||
* @param intent The intent to append command to.
|
||||
*/
|
||||
public void appendToIntent(Intent intent) {
|
||||
intent.putExtra(INTENT_EXTRA_COMMAND, name());
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/org/adaway/broadcast/CommandReceiver.java
Normal file
53
app/src/main/java/org/adaway/broadcast/CommandReceiver.java
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package org.adaway.broadcast;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This broadcast receiver listens to commands from broadcast.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class CommandReceiver extends BroadcastReceiver {
|
||||
/**
|
||||
* This action allows to send commands to the application. See {@link Command} for extra values.
|
||||
*/
|
||||
public static final String SEND_COMMAND_ACTION = "org.adaway.action.SEND_COMMAND";
|
||||
private static final AppExecutors EXECUTORS = AppExecutors.getInstance();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (SEND_COMMAND_ACTION.equals(intent.getAction())) {
|
||||
AdBlockModel adBlockModel = ((AdAwayApplication) context.getApplicationContext()).getAdBlockModel();
|
||||
Command command = Command.readFromIntent(intent);
|
||||
Timber.i("CommandReceiver invoked with command %s.", command);
|
||||
EXECUTORS.diskIO().execute(() -> executeCommand(adBlockModel, command));
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCommand(AdBlockModel adBlockModel, Command command) {
|
||||
try {
|
||||
switch (command) {
|
||||
case START:
|
||||
adBlockModel.apply();
|
||||
break;
|
||||
case STOP:
|
||||
adBlockModel.revert();
|
||||
break;
|
||||
case UNKNOWN:
|
||||
Timber.i("Failed to run an unsupported command.");
|
||||
break;
|
||||
}
|
||||
} catch (HostErrorException e) {
|
||||
Timber.w(e, "Failed to apply ad block command " + command + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/src/main/java/org/adaway/broadcast/UpdateReceiver.java
Normal file
28
app/src/main/java/org/adaway/broadcast/UpdateReceiver.java
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package org.adaway.broadcast;
|
||||
|
||||
import static android.content.Intent.ACTION_MY_PACKAGE_REPLACED;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This broadcast receiver is executed at application update.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class UpdateReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
|
||||
AdAwayApplication application = (AdAwayApplication) context.getApplicationContext();
|
||||
String versionName = application.getUpdateModel().getVersionName();
|
||||
Timber.d("UpdateReceiver invoked");
|
||||
Timber.i("Application update to version %s", versionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
134
app/src/main/java/org/adaway/db/AppDatabase.java
Normal file
134
app/src/main/java/org/adaway/db/AppDatabase.java
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package org.adaway.db;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
import androidx.room.TypeConverters;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.converter.ListTypeConverter;
|
||||
import org.adaway.db.converter.ZonedDateTimeConverter;
|
||||
import org.adaway.db.dao.HostEntryDao;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import static org.adaway.db.Migrations.MIGRATION_1_2;
|
||||
import static org.adaway.db.Migrations.MIGRATION_2_3;
|
||||
import static org.adaway.db.Migrations.MIGRATION_3_4;
|
||||
import static org.adaway.db.Migrations.MIGRATION_4_5;
|
||||
import static org.adaway.db.Migrations.MIGRATION_5_6;
|
||||
import static org.adaway.db.Migrations.MIGRATION_6_7;
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_URL;
|
||||
|
||||
/**
|
||||
* This class is the application database based on Room.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Database(entities = {HostsSource.class, HostListItem.class, HostEntry.class}, version = 7)
|
||||
@TypeConverters({ListTypeConverter.class, ZonedDateTimeConverter.class})
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
/**
|
||||
* The database singleton instance.
|
||||
*/
|
||||
private static volatile AppDatabase instance;
|
||||
|
||||
/**
|
||||
* Get the database instance.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @return The database instance.
|
||||
*/
|
||||
public static AppDatabase getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (AppDatabase.class) {
|
||||
if (instance == null) {
|
||||
instance = Room.databaseBuilder(
|
||||
context.getApplicationContext(),
|
||||
AppDatabase.class,
|
||||
"app.db"
|
||||
).addCallback(new Callback() {
|
||||
@Override
|
||||
public void onCreate(@NonNull SupportSQLiteDatabase db) {
|
||||
AppExecutors.getInstance().diskIO().execute(
|
||||
() -> AppDatabase.initialize(context, instance)
|
||||
);
|
||||
}
|
||||
}).addMigrations(
|
||||
MIGRATION_1_2,
|
||||
MIGRATION_2_3,
|
||||
MIGRATION_3_4,
|
||||
MIGRATION_4_5,
|
||||
MIGRATION_5_6,
|
||||
MIGRATION_6_7
|
||||
).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database content.
|
||||
*/
|
||||
private static void initialize(Context context, AppDatabase database) {
|
||||
// Check if there is no hosts source
|
||||
HostsSourceDao hostsSourceDao = database.hostsSourceDao();
|
||||
if (!hostsSourceDao.getAll().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// User list
|
||||
HostsSource userSource = new HostsSource();
|
||||
userSource.setLabel(context.getString(R.string.hosts_user_source));
|
||||
userSource.setId(USER_SOURCE_ID);
|
||||
userSource.setUrl(USER_SOURCE_URL);
|
||||
userSource.setAllowEnabled(true);
|
||||
userSource.setRedirectEnabled(true);
|
||||
hostsSourceDao.insert(userSource);
|
||||
// AdAway official
|
||||
HostsSource source1 = new HostsSource();
|
||||
source1.setLabel(context.getString(R.string.hosts_adaway_source));
|
||||
source1.setUrl("https://adaway.org/hosts.txt");
|
||||
hostsSourceDao.insert(source1);
|
||||
// StevenBlack
|
||||
HostsSource source2 = new HostsSource();
|
||||
source2.setLabel(context.getString(R.string.hosts_stevenblack_source));
|
||||
source2.setUrl("https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts");
|
||||
hostsSourceDao.insert(source2);
|
||||
// Pete Lowe
|
||||
HostsSource source3 = new HostsSource();
|
||||
source3.setLabel(context.getString(R.string.hosts_peterlowe_source));
|
||||
source3.setUrl("https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext");
|
||||
hostsSourceDao.insert(source3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hosts source DAO.
|
||||
*
|
||||
* @return The hosts source DAO.
|
||||
*/
|
||||
public abstract HostsSourceDao hostsSourceDao();
|
||||
|
||||
/**
|
||||
* Get the hosts list item DAO.
|
||||
*
|
||||
* @return The hosts list item DAO.
|
||||
*/
|
||||
public abstract HostListItemDao hostsListItemDao();
|
||||
|
||||
/**
|
||||
* Get the hosts entry DAO.
|
||||
*
|
||||
* @return The hosts entry DAO.
|
||||
*/
|
||||
public abstract HostEntryDao hostEntryDao();
|
||||
}
|
||||
123
app/src/main/java/org/adaway/db/Migrations.java
Normal file
123
app/src/main/java/org/adaway/db/Migrations.java
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package org.adaway.db;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_URL;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
/**
|
||||
* This class declares database schema migrations.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
final class Migrations {
|
||||
/**
|
||||
* Private constructor of utility class.
|
||||
*/
|
||||
private Migrations() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The migration script from v1 to v2.
|
||||
*/
|
||||
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
// Add hosts sources id column and migrate data
|
||||
database.execSQL("CREATE TABLE `hosts_sources_new` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `last_modified_local` INTEGER, `last_modified_online` INTEGER)");
|
||||
database.execSQL("INSERT INTO `hosts_sources_new` (`id`, `url`, `enabled`) VALUES (" + USER_SOURCE_ID + ", '" + USER_SOURCE_URL + "', 1)");
|
||||
database.execSQL("INSERT INTO `hosts_sources_new` (`url`, `enabled`, `last_modified_local`, `last_modified_online`) SELECT `url`, `enabled`, `last_modified_local`, `last_modified_online` FROM `hosts_sources`");
|
||||
database.execSQL("DROP TABLE `hosts_sources`");
|
||||
database.execSQL("ALTER TABLE `hosts_sources_new` RENAME TO `hosts_sources`");
|
||||
// Add hosts list source id and migrate data
|
||||
database.execSQL("CREATE TABLE `hosts_lists_new` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `host` TEXT NOT NULL, `type` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `redirection` TEXT, `source_id` INTEGER NOT NULL, FOREIGN KEY(`source_id`) REFERENCES `hosts_sources`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("INSERT INTO `hosts_lists_new` (`host`, `type`, `enabled`, `redirection`, `source_id`) SELECT `host`, `type`, `enabled`, `redirection`, " + USER_SOURCE_ID + " FROM `hosts_lists`");
|
||||
database.execSQL("DROP TABLE `hosts_lists`");
|
||||
database.execSQL("ALTER TABLE `hosts_lists_new` RENAME TO `hosts_lists`");
|
||||
// Create index
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_hosts_sources_url` ON `hosts_sources` (`url`)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_hosts_lists_host` ON `hosts_lists` (`host`)");
|
||||
database.execSQL("CREATE INDEX `index_hosts_lists_source_id` ON `hosts_lists` (`source_id`)");
|
||||
}
|
||||
};
|
||||
/**
|
||||
* The migration script from v2 to v3.
|
||||
*/
|
||||
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("CREATE VIEW `host_entries` AS SELECT `host`, `type`, `redirection` FROM `hosts_lists` WHERE `enabled` = 1 AND ((`type` = 0 AND `host` NOT LIKE (SELECT `host` FROM `hosts_lists` WHERE `enabled` = 1 and `type` = 1)) OR `type` = 2) ORDER BY `host` ASC, `type` DESC, `redirection` ASC");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration script from v3 to v4.
|
||||
*/
|
||||
static final Migration MIGRATION_3_4 = new Migration(3, 4) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
// Remove unique constraint to hosts_lists.host column
|
||||
database.execSQL("DROP INDEX `index_hosts_lists_host`");
|
||||
database.execSQL("CREATE INDEX `index_hosts_lists_host` ON `hosts_lists` (`host`)");
|
||||
// Update host_entries view
|
||||
database.execSQL("DROP VIEW `host_entries`");
|
||||
database.execSQL("CREATE VIEW `host_entries` AS SELECT `host`, `type`, `redirection` FROM `hosts_lists` WHERE `enabled` = 1 AND ((`type` = 0 AND `host` NOT LIKE (SELECT `host` FROM `hosts_lists` WHERE `enabled` = 1 and `type` = 1)) OR `type` = 2) GROUP BY `host` ORDER BY `host` ASC, `type` DESC, `redirection` ASC");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration script from v4 to v5.
|
||||
*/
|
||||
static final Migration MIGRATION_4_5 = new Migration(4, 5) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
// Remove host_entries view
|
||||
database.execSQL("DROP VIEW `host_entries`");
|
||||
// Create new host_entries table
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `host_entries` (`host` TEXT NOT NULL, `type` INTEGER NOT NULL, `redirection` TEXT, PRIMARY KEY(`host`))");
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_host_entries_host` ON `host_entries` (`host`)");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration script from v5 to v6.
|
||||
*/
|
||||
static final Migration MIGRATION_5_6 = new Migration(5, 6) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
// Update hosts_sources table
|
||||
database.execSQL("ALTER TABLE `hosts_sources` ADD `label` TEXT NOT NULL DEFAULT \"\"");
|
||||
database.execSQL("ALTER TABLE `hosts_sources` ADD `allowEnabled` INTEGER NOT NULL DEFAULT 0");
|
||||
database.execSQL("ALTER TABLE `hosts_sources` ADD `redirectEnabled` INTEGER NOT NULL DEFAULT 0");
|
||||
database.execSQL("ALTER TABLE `hosts_sources` ADD `size` INTEGER NOT NULL DEFAULT 0");
|
||||
// Set default values to new source attributes
|
||||
database.execSQL("UPDATE `hosts_sources` SET `label` = `url`");
|
||||
// Update user hosts list
|
||||
database.execSQL("UPDATE `hosts_sources` SET `url` = \"content://org.adaway/user/hosts\", `allowEnabled` = 1, `redirectEnabled` = 1 WHERE `url` = \"file://app/user/hosts\"");
|
||||
// Update default hosts source label
|
||||
database.execSQL("UPDATE `hosts_sources` SET `label` = \"AdAway official hosts\" WHERE `url` = \"https://adaway.org/hosts.txt\"");
|
||||
database.execSQL("UPDATE `hosts_sources` SET `label` = \"StevenBlack Unified hosts\" WHERE `url` = \"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\"");
|
||||
database.execSQL("UPDATE `hosts_sources` SET `label` = \"Pete Lowe blocklist hosts\" WHERE `url` = \"https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext\"");
|
||||
// Reset local date to rebuild cache
|
||||
database.execSQL("UPDATE `hosts_sources` SET `last_modified_local` = NULL");
|
||||
// Update hosts source date format
|
||||
database.execSQL("UPDATE `hosts_sources` SET `last_modified_online` = `last_modified_online` / 1000");
|
||||
// Clear previous file type hosts sources
|
||||
database.execSQL("DELETE FROM `hosts_sources` WHERE `url` LIKE \"file://%\"");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration script from v6 to v7.
|
||||
*/
|
||||
static final Migration MIGRATION_6_7 = new Migration(6, 7) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
// Update hosts_sources table
|
||||
database.execSQL("ALTER TABLE `hosts_sources` ADD `entityTag` TEXT DEFAULT NULL");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package org.adaway.db.converter;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
import org.adaway.db.entity.ListType;
|
||||
|
||||
/**
|
||||
* This class is a type converter for Room to support {@link ListType} type.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class ListTypeConverter {
|
||||
private ListTypeConverter() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static ListType fromValue(Integer value) {
|
||||
return value == null ? null : ListType.fromValue(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Integer typeToValue(ListType type) {
|
||||
return type == null ? null : type.getValue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.adaway.db.converter;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import static java.time.ZoneOffset.UTC;
|
||||
|
||||
/**
|
||||
* This class is a type converter for Room to support {@link java.time.ZonedDateTime} type.
|
||||
* It is stored as a Unix epoc timestamp.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class ZonedDateTimeConverter {
|
||||
private ZonedDateTimeConverter() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static ZonedDateTime fromTimestamp(Long value) {
|
||||
return value == null ? null : ZonedDateTime.of(LocalDateTime.ofEpochSecond(value, 0, UTC), UTC);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Long toTimestamp(ZonedDateTime zonedDateTime) {
|
||||
return zonedDateTime == null ? null : zonedDateTime.toEpochSecond();
|
||||
}
|
||||
}
|
||||
78
app/src/main/java/org/adaway/db/dao/HostEntryDao.java
Normal file
78
app/src/main/java/org/adaway/db/dao/HostEntryDao.java
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package org.adaway.db.dao;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.ListType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static androidx.room.OnConflictStrategy.REPLACE;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
|
||||
/**
|
||||
* This interface is the DAO for {@link HostEntry} records.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Dao
|
||||
public interface HostEntryDao {
|
||||
Pattern ANY_CHAR_PATTERN = Pattern.compile("\\*");
|
||||
Pattern A_CHAR_PATTERN = Pattern.compile("\\?");
|
||||
|
||||
@Query("DELETE FROM `host_entries`")
|
||||
void clear();
|
||||
|
||||
@Query("INSERT INTO `host_entries` SELECT DISTINCT `host`, `type`, `redirection` FROM `hosts_lists` WHERE `type` = 0 AND `enabled` = 1")
|
||||
void importBlocked();
|
||||
|
||||
@Query("SELECT host FROM hosts_lists WHERE type = 1 AND enabled = 1")
|
||||
List<String> getEnabledAllowedHosts();
|
||||
|
||||
@Query("DELETE FROM `host_entries` WHERE `host` LIKE :hostPattern")
|
||||
void allowHost(String hostPattern);
|
||||
|
||||
@Query("SELECT * FROM hosts_lists WHERE type = 2 AND enabled = 1 ORDER BY host ASC, source_id DESC")
|
||||
List<HostListItem> getEnabledRedirectedHosts();
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
void redirectHost(HostEntry redirection);
|
||||
|
||||
/**
|
||||
* Synchronize the host entries based on the current hosts lists table records.
|
||||
*/
|
||||
default void sync() {
|
||||
clear();
|
||||
importBlocked();
|
||||
for (String allowedHost : getEnabledAllowedHosts()) {
|
||||
allowedHost = ANY_CHAR_PATTERN.matcher(allowedHost).replaceAll("%");
|
||||
allowedHost = A_CHAR_PATTERN.matcher(allowedHost).replaceAll("_");
|
||||
allowHost(allowedHost);
|
||||
}
|
||||
for (HostListItem redirectedHost : getEnabledRedirectedHosts()) {
|
||||
HostEntry entry = new HostEntry();
|
||||
entry.setHost(redirectedHost.getHost());
|
||||
entry.setType(REDIRECTED);
|
||||
entry.setRedirection(redirectedHost.getRedirection());
|
||||
redirectHost(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM `host_entries` ORDER BY `host`")
|
||||
List<HostEntry> getAll();
|
||||
|
||||
@Query("SELECT `type` FROM `host_entries` WHERE `host` == :host LIMIT 1")
|
||||
ListType getTypeOfHost(String host);
|
||||
|
||||
@Query("SELECT IFNULL((SELECT `type` FROM `host_entries` WHERE `host` == :host LIMIT 1), 1)")
|
||||
ListType getTypeForHost(String host);
|
||||
|
||||
@Nullable
|
||||
@Query("SELECT * FROM `host_entries` WHERE `host` == :host LIMIT 1")
|
||||
HostEntry getEntry(String host);
|
||||
}
|
||||
63
app/src/main/java/org/adaway/db/dao/HostListItemDao.java
Normal file
63
app/src/main/java/org/adaway/db/dao/HostListItemDao.java
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package org.adaway.db.dao;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagingSource;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static androidx.room.OnConflictStrategy.REPLACE;
|
||||
|
||||
/**
|
||||
* This interface is the DAO for {@link HostListItem} entities.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Dao
|
||||
public interface HostListItemDao {
|
||||
@Insert(onConflict = REPLACE)
|
||||
void insert(HostListItem... item);
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
void insert(List<HostListItem> items);
|
||||
|
||||
@Update
|
||||
void update(HostListItem item);
|
||||
|
||||
@Delete
|
||||
void delete(HostListItem item);
|
||||
|
||||
@Query("DELETE FROM hosts_lists WHERE source_id = 1 AND host = :host")
|
||||
void deleteUserFromHost(String host);
|
||||
|
||||
@Query("SELECT * FROM hosts_lists WHERE type = :type AND host LIKE :query AND ((:includeSources == 0 AND source_id == 1) || (:includeSources == 1)) GROUP BY host ORDER BY host ASC")
|
||||
PagingSource<Integer, HostListItem> loadList(int type, boolean includeSources, String query);
|
||||
|
||||
@Query("SELECT * FROM hosts_lists ORDER BY host ASC")
|
||||
List<HostListItem> getAll();
|
||||
|
||||
@Query("SELECT * FROM hosts_lists WHERE source_id = 1")
|
||||
List<HostListItem> getUserList();
|
||||
|
||||
@Query("SELECT id FROM hosts_lists WHERE host = :host AND source_id = 1 LIMIT 1")
|
||||
Optional<Integer> getHostId(String host);
|
||||
|
||||
@Query("SELECT COUNT(DISTINCT host) FROM hosts_lists WHERE type = 0 AND enabled = 1")
|
||||
LiveData<Integer> getBlockedHostCount();
|
||||
|
||||
@Query("SELECT COUNT(DISTINCT host) FROM hosts_lists WHERE type = 1 AND enabled = 1")
|
||||
LiveData<Integer> getAllowedHostCount();
|
||||
|
||||
@Query("SELECT COUNT(DISTINCT host) FROM hosts_lists WHERE type = 2 AND enabled = 1")
|
||||
LiveData<Integer> getRedirectHostCount();
|
||||
|
||||
@Query("DELETE FROM hosts_lists WHERE source_id = :sourceId")
|
||||
void clearSourceHosts(int sourceId);
|
||||
}
|
||||
80
app/src/main/java/org/adaway/db/dao/HostsSourceDao.java
Normal file
80
app/src/main/java/org/adaway/db/dao/HostsSourceDao.java
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package org.adaway.db.dao;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static androidx.room.OnConflictStrategy.IGNORE;
|
||||
|
||||
/**
|
||||
* This interface is the DAO for {@link HostsSource} entities.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Dao
|
||||
public interface HostsSourceDao {
|
||||
@Insert(onConflict = IGNORE)
|
||||
void insert(HostsSource source);
|
||||
|
||||
@Update
|
||||
void update(HostsSource source);
|
||||
|
||||
@Delete
|
||||
void delete(HostsSource source);
|
||||
|
||||
@Query("SELECT * FROM hosts_sources WHERE enabled = 1 AND id != 1 ORDER BY url ASC")
|
||||
List<HostsSource> getEnabled();
|
||||
|
||||
default void toggleEnabled(HostsSource source) {
|
||||
int id = source.getId();
|
||||
boolean enabled = !source.isEnabled();
|
||||
source.setEnabled(enabled);
|
||||
setSourceEnabled(id, enabled);
|
||||
setSourceItemsEnabled(id, enabled);
|
||||
}
|
||||
|
||||
@Query("UPDATE hosts_sources SET enabled = :enabled WHERE id =:id")
|
||||
void setSourceEnabled(int id, boolean enabled);
|
||||
|
||||
@Query("UPDATE hosts_lists SET enabled = :enabled WHERE source_id =:id")
|
||||
void setSourceItemsEnabled(int id, boolean enabled);
|
||||
|
||||
@Query("SELECT * FROM hosts_sources WHERE id = :id")
|
||||
Optional<HostsSource> getById(int id);
|
||||
|
||||
@Query("SELECT * FROM hosts_sources WHERE id != 1 ORDER BY label ASC")
|
||||
List<HostsSource> getAll();
|
||||
|
||||
@Query("SELECT * FROM hosts_sources WHERE id != 1 ORDER BY label ASC")
|
||||
LiveData<List<HostsSource>> loadAll();
|
||||
|
||||
@Query("UPDATE hosts_sources SET last_modified_online = :dateTime WHERE id = :id")
|
||||
void updateOnlineModificationDate(int id, ZonedDateTime dateTime);
|
||||
|
||||
@Query("UPDATE hosts_sources SET last_modified_local = :localModificationDate, last_modified_online = :onlineModificationDate WHERE id = :id")
|
||||
void updateModificationDates(int id, ZonedDateTime localModificationDate, ZonedDateTime onlineModificationDate);
|
||||
|
||||
@Query("UPDATE hosts_sources SET entityTag = :entityTag WHERE id = :id")
|
||||
void updateEntityTag(int id, String entityTag);
|
||||
|
||||
@Query("UPDATE hosts_sources SET size = (SELECT count(id) FROM hosts_lists WHERE source_id = :id) WHERE id = :id")
|
||||
void updateSize(int id);
|
||||
|
||||
@Query("SELECT count(id) FROM hosts_sources WHERE enabled = 1 AND last_modified_online > last_modified_local")
|
||||
LiveData<Integer> countOutdated();
|
||||
|
||||
@Query("SELECT count(id) FROM hosts_sources WHERE enabled = 1 AND last_modified_online <= last_modified_local")
|
||||
LiveData<Integer> countUpToDate();
|
||||
|
||||
@Query("UPDATE hosts_sources SET last_modified_local = NULL, last_modified_online = NULL, entityTag = NULL, size = 0 WHERE id = :id")
|
||||
void clearProperties(int id);
|
||||
}
|
||||
50
app/src/main/java/org/adaway/db/entity/HostEntry.java
Normal file
50
app/src/main/java/org/adaway/db/entity/HostEntry.java
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package org.adaway.db.entity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
/**
|
||||
* This entity represents an entry of the built hosts file.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "host_entries",
|
||||
indices = {@Index(value = "host", unique = true)}
|
||||
)
|
||||
public class HostEntry {
|
||||
@PrimaryKey
|
||||
@NonNull
|
||||
private String host;
|
||||
@NonNull
|
||||
private ListType type;
|
||||
private String redirection;
|
||||
|
||||
@NonNull
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public void setHost(@NonNull String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ListType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(@NonNull ListType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getRedirection() {
|
||||
return redirection;
|
||||
}
|
||||
|
||||
public void setRedirection(String redirection) {
|
||||
this.redirection = redirection;
|
||||
}
|
||||
}
|
||||
121
app/src/main/java/org/adaway/db/entity/HostListItem.java
Normal file
121
app/src/main/java/org/adaway/db/entity/HostListItem.java
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package org.adaway.db.entity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
|
||||
/**
|
||||
* This entity represents a black, white or redirect list item.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "hosts_lists",
|
||||
indices = {
|
||||
@Index(value = "host"),
|
||||
@Index(value = "source_id")
|
||||
},
|
||||
foreignKeys = @ForeignKey(
|
||||
entity = HostsSource.class,
|
||||
parentColumns = "id",
|
||||
childColumns = "source_id",
|
||||
onUpdate = CASCADE,
|
||||
onDelete = CASCADE
|
||||
)
|
||||
)
|
||||
public class HostListItem {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private int id;
|
||||
@NonNull
|
||||
private String host;
|
||||
@NonNull
|
||||
private ListType type;
|
||||
private boolean enabled;
|
||||
private String redirection;
|
||||
@ColumnInfo(name = "source_id")
|
||||
private int sourceId;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public void setHost(@NonNull String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ListType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(@NonNull ListType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getRedirection() {
|
||||
return redirection;
|
||||
}
|
||||
|
||||
public void setRedirection(String redirection) {
|
||||
this.redirection = redirection;
|
||||
}
|
||||
|
||||
public int getSourceId() {
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
public void setSourceId(int sourceId) {
|
||||
this.sourceId = sourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
HostListItem item = (HostListItem) o;
|
||||
|
||||
if (id != item.id) return false;
|
||||
if (enabled != item.enabled) return false;
|
||||
if (sourceId != item.sourceId) return false;
|
||||
if (!host.equals(item.host)) return false;
|
||||
if (type != item.type) return false;
|
||||
return Objects.equals(redirection, item.redirection);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = id;
|
||||
result = 31 * result + host.hashCode();
|
||||
result = 31 * result + type.hashCode();
|
||||
result = 31 * result + (enabled ? 1 : 0);
|
||||
result = 31 * result + (redirection != null ? redirection.hashCode() : 0);
|
||||
result = 31 * result + sourceId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
187
app/src/main/java/org/adaway/db/entity/HostsSource.java
Normal file
187
app/src/main/java/org/adaway/db/entity/HostsSource.java
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package org.adaway.db.entity;
|
||||
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.adaway.db.entity.SourceType.FILE;
|
||||
import static org.adaway.db.entity.SourceType.UNSUPPORTED;
|
||||
import static org.adaway.db.entity.SourceType.URL;
|
||||
|
||||
/**
|
||||
* This entity represents a source to get hosts list.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "hosts_sources",
|
||||
indices = {@Index(value = "url", unique = true)}
|
||||
)
|
||||
public class HostsSource {
|
||||
/**
|
||||
* The user source ID.
|
||||
*/
|
||||
public static final int USER_SOURCE_ID = 1;
|
||||
/**
|
||||
* The user source URL.
|
||||
*/
|
||||
public static final String USER_SOURCE_URL = "content://org.adaway/user/hosts";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private int id;
|
||||
@NonNull
|
||||
private String label;
|
||||
@NonNull
|
||||
private String url;
|
||||
private boolean enabled = true;
|
||||
private boolean allowEnabled = false;
|
||||
private boolean redirectEnabled = false;
|
||||
@ColumnInfo(name = "last_modified_local")
|
||||
private ZonedDateTime localModificationDate;
|
||||
@ColumnInfo(name = "last_modified_online")
|
||||
private ZonedDateTime onlineModificationDate;
|
||||
/**
|
||||
* The HTTP ETag (strong from, may be <code>null</code>).
|
||||
*/
|
||||
private String entityTag;
|
||||
/**
|
||||
* The number of hosts list items (<code>0</code> until synced).
|
||||
*/
|
||||
private int size;
|
||||
|
||||
/**
|
||||
* Check whether an URL is valid for as host source.<br>
|
||||
* A valid URL is a HTTPS URL or file URL.
|
||||
*
|
||||
* @param url The URL to check.
|
||||
* @return {@code true} if the URL is valid, {@code false} otherwise.
|
||||
*/
|
||||
public static boolean isValidUrl(String url) {
|
||||
return (!"https://".equals(url) && URLUtil.isHttpsUrl(url)) || URLUtil.isContentUrl(url);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(@NonNull String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(@NonNull String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public SourceType getType() {
|
||||
if (this.url.startsWith("https://")) {
|
||||
return URL;
|
||||
} else if (this.url.startsWith("content://")) {
|
||||
return FILE;
|
||||
} else {
|
||||
return UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isAllowEnabled() {
|
||||
return allowEnabled;
|
||||
}
|
||||
|
||||
public void setAllowEnabled(boolean allowEnabled) {
|
||||
this.allowEnabled = allowEnabled;
|
||||
}
|
||||
|
||||
public boolean isRedirectEnabled() {
|
||||
return redirectEnabled;
|
||||
}
|
||||
|
||||
public void setRedirectEnabled(boolean redirectEnabled) {
|
||||
this.redirectEnabled = redirectEnabled;
|
||||
}
|
||||
|
||||
public ZonedDateTime getLocalModificationDate() {
|
||||
return localModificationDate;
|
||||
}
|
||||
|
||||
public void setLocalModificationDate(ZonedDateTime localModificationDate) {
|
||||
this.localModificationDate = localModificationDate;
|
||||
}
|
||||
|
||||
public ZonedDateTime getOnlineModificationDate() {
|
||||
return onlineModificationDate;
|
||||
}
|
||||
|
||||
public void setOnlineModificationDate(ZonedDateTime lastOnlineModification) {
|
||||
this.onlineModificationDate = lastOnlineModification;
|
||||
}
|
||||
|
||||
public String getEntityTag() {
|
||||
return this.entityTag;
|
||||
}
|
||||
|
||||
public void setEntityTag(String entityTag) {
|
||||
this.entityTag = entityTag;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
HostsSource that = (HostsSource) o;
|
||||
|
||||
if (id != that.id) return false;
|
||||
if (enabled != that.enabled) return false;
|
||||
if (!url.equals(that.url)) return false;
|
||||
if (!Objects.equals(localModificationDate, that.localModificationDate))
|
||||
return false;
|
||||
return Objects.equals(onlineModificationDate, that.onlineModificationDate);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = id;
|
||||
result = 31 * result + url.hashCode();
|
||||
result = 31 * result + (enabled ? 1 : 0);
|
||||
result = 31 * result + (localModificationDate != null ? localModificationDate.hashCode() : 0);
|
||||
result = 31 * result + (onlineModificationDate != null ? onlineModificationDate.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/org/adaway/db/entity/ListType.java
Normal file
31
app/src/main/java/org/adaway/db/entity/ListType.java
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package org.adaway.db.entity;
|
||||
|
||||
/**
|
||||
* This enumerate specifies the type of {@link HostListItem}.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum ListType {
|
||||
BLOCKED(0),
|
||||
ALLOWED(1),
|
||||
REDIRECTED(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
ListType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static ListType fromValue(int value) {
|
||||
for (ListType listType : ListType.values()) {
|
||||
if (listType.value == value) {
|
||||
return listType;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid value for list type: " + value);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/org/adaway/db/entity/SourceType.java
Normal file
21
app/src/main/java/org/adaway/db/entity/SourceType.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package org.adaway.db.entity;
|
||||
|
||||
/**
|
||||
* This enumerate specifies the type of {@link HostsSource}.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum SourceType {
|
||||
/**
|
||||
* The URL type represents online source to download from URL.
|
||||
*/
|
||||
URL,
|
||||
/**
|
||||
* The FILE type represents file stored source to retrieve using SAF API.
|
||||
*/
|
||||
FILE,
|
||||
/**
|
||||
* The UNSUPPORTED type represents unhandled source type.
|
||||
*/
|
||||
UNSUPPORTED
|
||||
}
|
||||
167
app/src/main/java/org/adaway/helper/NotificationHelper.java
Normal file
167
app/src/main/java/org/adaway/helper/NotificationHelper.java
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package org.adaway.helper;
|
||||
|
||||
import static android.app.PendingIntent.FLAG_IMMUTABLE;
|
||||
import static android.app.PendingIntent.getActivity;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static androidx.core.app.NotificationCompat.PRIORITY_LOW;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.ui.home.HomeActivity;
|
||||
import org.adaway.ui.update.UpdateActivity;
|
||||
|
||||
/**
|
||||
* This class is an helper class to deals with notifications.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class NotificationHelper {
|
||||
/**
|
||||
* The notification channel for updates.
|
||||
*/
|
||||
public static final String UPDATE_NOTIFICATION_CHANNEL = "UpdateChannel";
|
||||
/**
|
||||
* The notification channel for VPN service.
|
||||
*/
|
||||
public static final String VPN_SERVICE_NOTIFICATION_CHANNEL = "VpnServiceChannel";
|
||||
/**
|
||||
* The update hosts notification identifier.
|
||||
*/
|
||||
private static final int UPDATE_HOSTS_NOTIFICATION_ID = 10;
|
||||
/**
|
||||
* The update application notification identifier.
|
||||
*/
|
||||
private static final int UPDATE_APP_NOTIFICATION_ID = 11;
|
||||
/**
|
||||
* The VPN running service notification identifier.
|
||||
*/
|
||||
public static final int VPN_RUNNING_SERVICE_NOTIFICATION_ID = 20;
|
||||
/**
|
||||
* The VPN resume service notification identifier.
|
||||
*/
|
||||
public static final int VPN_RESUME_SERVICE_NOTIFICATION_ID = 21;
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private NotificationHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the application notification channel.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void createNotificationChannels(@NonNull Context context) {
|
||||
// Create update notification channel
|
||||
NotificationChannel updateChannel = new NotificationChannel(
|
||||
UPDATE_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.notification_update_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
updateChannel.setDescription(context.getString(R.string.notification_update_channel_description));
|
||||
// Create VPN service notification channel
|
||||
NotificationChannel vpnServiceChannel = new NotificationChannel(
|
||||
VPN_SERVICE_NOTIFICATION_CHANNEL,
|
||||
context.getString(R.string.notification_vpn_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
updateChannel.setDescription(context.getString(R.string.notification_vpn_channel_description));
|
||||
// Register the channels with the system; you can't change the importance or other notification behaviors after this
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.createNotificationChannel(updateChannel);
|
||||
notificationManager.createNotificationChannel(vpnServiceChannel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the notification about new hosts update available.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void showUpdateHostsNotification(@NonNull Context context) {
|
||||
// Get notification manager
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
if (notificationManager == null || !notificationManager.areNotificationsEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Build notification
|
||||
int color = context.getColor(R.color.notification);
|
||||
String title = context.getString(R.string.notification_update_host_available_title);
|
||||
String text = context.getString(R.string.notification_update_host_available_text);
|
||||
Intent intent = new Intent(context, HomeActivity.class);
|
||||
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = getActivity(context, 0, intent, FLAG_IMMUTABLE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, UPDATE_NOTIFICATION_CHANNEL)
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setColorized(true)
|
||||
.setColor(color)
|
||||
.setShowWhen(false)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(PRIORITY_LOW)
|
||||
.setAutoCancel(true);
|
||||
// Notify the built notification
|
||||
notificationManager.notify(UPDATE_HOSTS_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the notification about new application update available.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void showUpdateApplicationNotification(@NonNull Context context) {
|
||||
// Get notification manager
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
if (notificationManager == null || !notificationManager.areNotificationsEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Build notification
|
||||
int color = context.getColor(R.color.notification);
|
||||
String title = context.getString(R.string.notification_update_app_available_title);
|
||||
String text = context.getString(R.string.notification_update_app_available_text);
|
||||
Intent intent = new Intent(context, UpdateActivity.class);
|
||||
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = getActivity(context, 0, intent, FLAG_IMMUTABLE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, UPDATE_NOTIFICATION_CHANNEL)
|
||||
.setSmallIcon(R.drawable.logo)
|
||||
.setColorized(true)
|
||||
.setColor(color)
|
||||
.setShowWhen(false)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(PRIORITY_LOW)
|
||||
.setAutoCancel(true);
|
||||
// Notify the built notification
|
||||
notificationManager.notify(UPDATE_HOSTS_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the notification about new hosts update available.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void clearUpdateNotifications(@NonNull Context context) {
|
||||
// Get notification manager
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
if (notificationManager == null) {
|
||||
return;
|
||||
}
|
||||
// Cancel the notification
|
||||
notificationManager.cancel(UPDATE_HOSTS_NOTIFICATION_ID);
|
||||
notificationManager.cancel(UPDATE_APP_NOTIFICATION_ID);
|
||||
}
|
||||
}
|
||||
361
app/src/main/java/org/adaway/helper/PreferenceHelper.java
Normal file
361
app/src/main/java/org/adaway/helper/PreferenceHelper.java
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.model.adblocking.AdBlockMethod;
|
||||
import org.adaway.util.Constants;
|
||||
import org.adaway.vpn.VpnStatus;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public final class PreferenceHelper {
|
||||
private PreferenceHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static int getDarkThemeMode(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
String pref = prefs.getString(
|
||||
context.getString(R.string.pref_dark_theme_mode_key),
|
||||
context.getResources().getString(R.string.pref_dark_theme_mode_def)
|
||||
);
|
||||
switch (pref) {
|
||||
case "MODE_NIGHT_NO":
|
||||
return AppCompatDelegate.MODE_NIGHT_NO;
|
||||
case "MODE_NIGHT_YES":
|
||||
return AppCompatDelegate.MODE_NIGHT_YES;
|
||||
default:
|
||||
return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean getUpdateCheck(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_update_check_key),
|
||||
context.getResources().getBoolean(R.bool.pref_update_check_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getNeverReboot(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_never_reboot_key),
|
||||
context.getResources().getBoolean(R.bool.pref_never_reboot_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static void setNeverReboot(Context context, boolean value) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(context.getString(R.string.pref_never_reboot_key), value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getEnableIpv6(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_enable_ipv6_key),
|
||||
context.getResources().getBoolean(R.bool.pref_enable_ipv6_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getUpdateCheckAppStartup(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_update_check_app_startup_key),
|
||||
context.getResources().getBoolean(R.bool.pref_update_check_app_startup_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getUpdateCheckAppDaily(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_update_check_app_daily_key),
|
||||
context.getResources().getBoolean(R.bool.pref_update_check_app_daily_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getIncludeBetaReleases(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_update_include_beta_releases_key),
|
||||
context.getResources().getBoolean(R.bool.pref_update_include_beta_releases_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getUpdateCheckHostsDaily(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_update_check_hosts_daily_key),
|
||||
context.getResources().getBoolean(R.bool.pref_update_check_hosts_daily_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getAutomaticUpdateDaily(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_automatic_update_daily_key),
|
||||
context.getResources().getBoolean(R.bool.pref_automatic_update_daily_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getUpdateOnlyOnWifi(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_update_only_on_wifi_key),
|
||||
context.getResources().getBoolean(R.bool.pref_update_only_on_wifi_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static String getRedirectionIpv4(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getString(
|
||||
context.getString(R.string.pref_redirection_ipv4_key),
|
||||
context.getString(R.string.pref_redirection_ipv4_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static String getRedirectionIpv6(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getString(
|
||||
context.getString(R.string.pref_redirection_ipv6_key),
|
||||
context.getString(R.string.pref_redirection_ipv6_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getWebServerEnabled(Context context) {
|
||||
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_webserver_enabled_key),
|
||||
context.getResources().getBoolean(R.bool.pref_webserver_enabled_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getWebServerIcon(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_webserver_icon_key),
|
||||
context.getResources().getBoolean(R.bool.pref_webserver_icon_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static AdBlockMethod getAdBlockMethod(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return AdBlockMethod.fromCode(prefs.getInt(
|
||||
context.getString(R.string.pref_ad_block_method_key),
|
||||
context.getResources().getInteger(R.integer.pref_ad_block_method_key_def)
|
||||
));
|
||||
}
|
||||
|
||||
public static void setAbBlockMethod(Context context, AdBlockMethod method) {
|
||||
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putInt(context.getString(R.string.pref_ad_block_method_key), method.toCode());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static VpnStatus getVpnServiceStatus(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return VpnStatus.fromCode(prefs.getInt(
|
||||
context.getString(R.string.pref_vpn_service_status_key),
|
||||
context.getResources().getInteger(R.integer.pref_vpn_service_status_def)
|
||||
));
|
||||
}
|
||||
|
||||
public static void setVpnServiceStatus(Context context, VpnStatus status) {
|
||||
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putInt(context.getString(R.string.pref_vpn_service_status_key), status.toCode());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getVpnServiceOnBoot(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_vpn_service_on_boot_key),
|
||||
context.getResources().getBoolean(R.bool.pref_vpn_service_on_boot_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getVpnWatchdogEnabled(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_vpn_watchdog_enabled_key),
|
||||
context.getResources().getBoolean(R.bool.pref_vpn_watchdog_enabled_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getDebugEnabled(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_enable_debug_key),
|
||||
context.getResources().getBoolean(R.bool.pref_enable_debug_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean getTelemetryEnabled(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_enable_telemetry_key),
|
||||
context.getResources().getBoolean(R.bool.pref_enable_telemetry_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static void setTelemetryEnabled(Context context, boolean enabled) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(context.getString(R.string.pref_enable_telemetry_key), enabled);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getDisplayTelemetryConsent(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.pref_display_telemetry_consent_key),
|
||||
context.getResources().getBoolean(R.bool.pref_display_telemetry_consent_def)
|
||||
);
|
||||
}
|
||||
|
||||
public static void setDisplayTelemetryConsent(Context context, boolean displayTelemetryConsent) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putBoolean(context.getString(R.string.pref_display_telemetry_consent_key), displayTelemetryConsent);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static String getVpnExcludedSystemApps(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getString(
|
||||
context.getString(R.string.pref_vpn_excluded_system_apps_key),
|
||||
context.getString(R.string.pref_vpn_excluded_system_apps_default)
|
||||
);
|
||||
}
|
||||
|
||||
public static Set<String> getVpnExcludedApps(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
return prefs.getStringSet(
|
||||
context.getString(R.string.pref_vpn_excluded_user_apps_key),
|
||||
Collections.emptySet()
|
||||
);
|
||||
}
|
||||
|
||||
public static void setVpnExcludedApps(Context context, Set<String> excludedApplicationPackageNames) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.PREFS_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putStringSet(context.getString(R.string.pref_vpn_excluded_user_apps_key), excludedApplicationPackageNames);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
29
app/src/main/java/org/adaway/helper/ThemeHelper.java
Normal file
29
app/src/main/java/org/adaway/helper/ThemeHelper.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package org.adaway.helper;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
/**
|
||||
* This class is a helper to apply user selected theme on the application activity.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class ThemeHelper {
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private ThemeHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the user selected theme.
|
||||
*
|
||||
* @param context The context to apply theme.
|
||||
*/
|
||||
public static void applyTheme(Context context) {
|
||||
AppCompatDelegate.setDefaultNightMode(PreferenceHelper.getDarkThemeMode(context));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package org.adaway.model.adblocking;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* This enum represents the ad blocking methods.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum AdBlockMethod {
|
||||
/**
|
||||
* Not defined ad block method.
|
||||
*/
|
||||
UNDEFINED(0),
|
||||
/**
|
||||
* The system hosts file ad block method.
|
||||
*/
|
||||
ROOT(1),
|
||||
/**
|
||||
* The VPN based ad block method.
|
||||
*/
|
||||
VPN(2);
|
||||
|
||||
private int code;
|
||||
|
||||
AdBlockMethod(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static AdBlockMethod fromCode(int code) {
|
||||
return Arrays.stream(AdBlockMethod.values())
|
||||
.filter(method -> method.code == code)
|
||||
.findAny()
|
||||
.orElse(UNDEFINED);
|
||||
}
|
||||
|
||||
public int toCode() {
|
||||
return this.code;
|
||||
}
|
||||
}
|
||||
140
app/src/main/java/org/adaway/model/adblocking/AdBlockModel.java
Normal file
140
app/src/main/java/org/adaway/model/adblocking/AdBlockModel.java
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package org.adaway.model.adblocking;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.model.root.RootModel;
|
||||
import org.adaway.model.vpn.VpnModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is the base model for all ad block model.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public abstract class AdBlockModel {
|
||||
/**
|
||||
* The application context.
|
||||
*/
|
||||
protected final Context context;
|
||||
/**
|
||||
* The hosts installation status:
|
||||
* <ul>
|
||||
* <li>{@code null} if not defined,</li>
|
||||
* <li>{@code true} if hosts list is installed,</li>
|
||||
* <li>{@code false} if default hosts file.</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected final MutableLiveData<Boolean> applied;
|
||||
/**
|
||||
* The model state.
|
||||
*/
|
||||
private final MutableLiveData<String> state;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
protected AdBlockModel(Context context) {
|
||||
this.context = context;
|
||||
this.state = new MutableLiveData<>();
|
||||
this.applied = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate ad block model.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param method The ad block method to get model.
|
||||
* @return The instantiated model.
|
||||
*/
|
||||
public static AdBlockModel build(Context context, AdBlockMethod method) {
|
||||
switch (method) {
|
||||
case ROOT:
|
||||
return new RootModel(context);
|
||||
case VPN:
|
||||
return new VpnModel(context);
|
||||
default:
|
||||
return new UndefinedBlockModel(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ad block method.
|
||||
*
|
||||
* @return The ad block method of this model.
|
||||
*/
|
||||
public abstract AdBlockMethod getMethod();
|
||||
|
||||
/**
|
||||
* Checks if hosts list is applied.
|
||||
*
|
||||
* @return {@code true} if applied, {@code false} if default.
|
||||
*/
|
||||
public LiveData<Boolean> isApplied() {
|
||||
return this.applied;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply hosts list.
|
||||
*
|
||||
* @throws HostErrorException If the model configuration could not be applied.
|
||||
*/
|
||||
public abstract void apply() throws HostErrorException;
|
||||
|
||||
/**
|
||||
* Revert the hosts list to the default one.
|
||||
*
|
||||
* @throws HostErrorException If the model configuration could not be revert.
|
||||
*/
|
||||
public abstract void revert() throws HostErrorException;
|
||||
|
||||
/**
|
||||
* Get the model state.
|
||||
*
|
||||
* @return The model state.
|
||||
*/
|
||||
public LiveData<String> getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
protected void setState(@StringRes int stateResId, Object... details) {
|
||||
String state = this.context.getString(stateResId, details);
|
||||
Timber.d(state);
|
||||
this.state.postValue(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether log are recoding or not.
|
||||
*
|
||||
* @return {@code true} if logs are recoding, {@code false} otherwise.
|
||||
*/
|
||||
public abstract boolean isRecordingLogs();
|
||||
|
||||
/**
|
||||
* Set log recoding.
|
||||
*
|
||||
* @param recording {@code true} to record logs, {@code false} otherwise.
|
||||
*/
|
||||
public abstract void setRecordingLogs(boolean recording);
|
||||
|
||||
/**
|
||||
* Get logs.
|
||||
*
|
||||
* @return The logs unique and sorted by date, older first.
|
||||
*/
|
||||
public abstract List<String> getLogs();
|
||||
|
||||
/**
|
||||
* Clear logs.
|
||||
*/
|
||||
public abstract void clearLogs();
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package org.adaway.model.adblocking;
|
||||
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.UNDEFINED;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class is a stub model when no ad block method is defined.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class UndefinedBlockModel extends AdBlockModel {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public UndefinedBlockModel(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdBlockMethod getMethod() {
|
||||
return UNDEFINED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
// Unsupported operation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revert() {
|
||||
// Unsupported operation
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecordingLogs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordingLogs(boolean recording) {
|
||||
// Unsupported operation
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLogs() {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearLogs() {
|
||||
// Unsupported operation
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package org.adaway.model.backup;
|
||||
|
||||
import static org.adaway.util.Constants.PREFS_NAME;
|
||||
|
||||
import android.app.backup.BackupAgentHelper;
|
||||
import android.app.backup.BackupDataInputStream;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.app.backup.FileBackupHelper;
|
||||
import android.app.backup.SharedPreferencesBackupHelper;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a {@link android.app.backup.BackupAgent} to backup and restore application state
|
||||
* using Android Backup Service. It is based on key-value pairs backup to prevent killing the
|
||||
* application during the backup (leaving the VPN foreground service always running). It backs up
|
||||
* and restores the application preferences and the user rules.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AppBackupAgent extends BackupAgentHelper {
|
||||
private static final String PREFS_BACKUP_KEY = "prefs";
|
||||
private static final String RULES_BACKUP_KEY = "rules";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
addHelper(PREFS_BACKUP_KEY, new SharedPreferencesBackupHelper(this, PREFS_NAME));
|
||||
addHelper(RULES_BACKUP_KEY, new SourceBackupHelper(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a {@link android.app.backup.BackupHelper} to backup and restore user rules.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
private static class SourceBackupHelper extends FileBackupHelper {
|
||||
private static final String RULES_FILE_NAME = "rules-backup.json";
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public SourceBackupHelper(Context context) {
|
||||
super(context, RULES_FILE_NAME);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
|
||||
try {
|
||||
BackupExporter.exportBackup(this.context, getRulesFileUri());
|
||||
super.performBackup(oldState, data, newState);
|
||||
} catch (IOException e) {
|
||||
Timber.w(e, "Failed to export rules to backup.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreEntity(BackupDataInputStream data) {
|
||||
super.restoreEntity(data);
|
||||
try {
|
||||
BackupImporter.importBackup(this.context, getRulesFileUri());
|
||||
} catch (IOException e) {
|
||||
Timber.w(e, "Failed to import rules from backup.");
|
||||
}
|
||||
}
|
||||
|
||||
private Uri getRulesFileUri() {
|
||||
File ruleFile = new File(this.context.getFilesDir(), RULES_FILE_NAME);
|
||||
return Uri.fromFile(ruleFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
app/src/main/java/org/adaway/model/backup/BackupExporter.java
Normal file
141
app/src/main/java/org/adaway/model/backup/BackupExporter.java
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package org.adaway.model.backup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.util.AppExecutors;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.adaway.model.backup.BackupFormat.ALLOWED_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.BLOCKED_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.REDIRECTED_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.SOURCES_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.hostToJson;
|
||||
import static org.adaway.model.backup.BackupFormat.sourceToJson;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a helper class to export user lists and hosts sources to a backup file.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class BackupExporter {
|
||||
private static final Executor DISK_IO_EXECUTOR = AppExecutors.getInstance().diskIO();
|
||||
private static final Executor MAIN_THREAD_EXECUTOR = AppExecutors.getInstance().mainThread();
|
||||
|
||||
private BackupExporter() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all user lists and hosts sources to a backup file on the external storage.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void exportToBackup(Context context, Uri backupUri) {
|
||||
DISK_IO_EXECUTOR.execute(() -> {
|
||||
boolean imported = true;
|
||||
try {
|
||||
exportBackup(context, backupUri);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Failed to import backup.");
|
||||
imported = false;
|
||||
}
|
||||
boolean successful = imported;
|
||||
String fileName = getFileNameFromUri(backupUri);
|
||||
MAIN_THREAD_EXECUTOR.execute(() -> notifyExportEnd(context, successful, fileName));
|
||||
});
|
||||
}
|
||||
|
||||
private static String getFileNameFromUri(Uri backupUri) {
|
||||
String path = backupUri.getPath();
|
||||
return path == null ? "" : new File(path).getName();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private static void notifyExportEnd(Context context, boolean successful, String backupUri) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(successful ? R.string.export_success : R.string.export_failed, backupUri),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
}
|
||||
|
||||
static void exportBackup(Context context, Uri backupUri) throws IOException {
|
||||
// Open writer on the export file
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(backupUri);
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
|
||||
JSONObject backup = makeBackup(context);
|
||||
writer.write(backup.toString(4));
|
||||
} catch (JSONException e) {
|
||||
throw new IOException("Failed to generate backup.", e);
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Could not write file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject makeBackup(Context context) throws JSONException {
|
||||
AppDatabase database = AppDatabase.getInstance(context);
|
||||
HostsSourceDao hostsSourceDao = database.hostsSourceDao();
|
||||
HostListItemDao hostListItemDao = database.hostsListItemDao();
|
||||
|
||||
List<HostListItem> userHosts = hostListItemDao.getUserList();
|
||||
List<HostListItem> blockedHosts = userHosts.stream()
|
||||
.filter(value -> value.getType() == BLOCKED)
|
||||
.collect(toList());
|
||||
List<HostListItem> allowedHosts = userHosts.stream()
|
||||
.filter(value -> value.getType() == ALLOWED)
|
||||
.collect(toList());
|
||||
List<HostListItem> redirectedHosts = userHosts.stream()
|
||||
.filter(value -> value.getType() == REDIRECTED)
|
||||
.collect(toList());
|
||||
|
||||
JSONObject backupObject = new JSONObject();
|
||||
backupObject.put(SOURCES_KEY, buildSourcesBackup(hostsSourceDao.getAll()));
|
||||
backupObject.put(BLOCKED_KEY, buildListBackup(blockedHosts));
|
||||
backupObject.put(ALLOWED_KEY, buildListBackup(allowedHosts));
|
||||
backupObject.put(REDIRECTED_KEY, buildListBackup(redirectedHosts));
|
||||
|
||||
return backupObject;
|
||||
}
|
||||
|
||||
private static JSONArray buildSourcesBackup(List<HostsSource> sources) throws JSONException {
|
||||
JSONArray sourceArray = new JSONArray();
|
||||
for (HostsSource source : sources) {
|
||||
sourceArray.put(sourceToJson(source));
|
||||
}
|
||||
return sourceArray;
|
||||
}
|
||||
|
||||
private static JSONArray buildListBackup(List<HostListItem> hosts) throws JSONException {
|
||||
JSONArray listArray = new JSONArray();
|
||||
for (HostListItem host : hosts) {
|
||||
listArray.put(hostToJson(host));
|
||||
}
|
||||
return listArray;
|
||||
}
|
||||
}
|
||||
84
app/src/main/java/org/adaway/model/backup/BackupFormat.java
Normal file
84
app/src/main/java/org/adaway/model/backup/BackupFormat.java
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package org.adaway.model.backup;
|
||||
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
|
||||
/**
|
||||
* This class defines user lists and hosts sources file format.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
final class BackupFormat {
|
||||
/*
|
||||
* Source backup format.
|
||||
*/
|
||||
static final String SOURCES_KEY = "sources";
|
||||
static final String SOURCE_LABEL_ATTRIBUTE = "label";
|
||||
static final String SOURCE_URL_ATTRIBUTE = "url";
|
||||
static final String SOURCE_ENABLED_ATTRIBUTE = "enabled";
|
||||
static final String SOURCE_ALLOW_ATTRIBUTE = "allow";
|
||||
static final String SOURCE_REDIRECT_ATTRIBUTE = "redirect";
|
||||
/*
|
||||
* User source backup format.
|
||||
*/
|
||||
static final String BLOCKED_KEY = "blocked";
|
||||
static final String ALLOWED_KEY = "allowed";
|
||||
static final String REDIRECTED_KEY = "redirected";
|
||||
static final String ENABLED_ATTRIBUTE = "enabled";
|
||||
static final String HOST_ATTRIBUTE = "host";
|
||||
static final String REDIRECT_ATTRIBUTE = "redirect";
|
||||
|
||||
BackupFormat() {
|
||||
|
||||
}
|
||||
|
||||
static JSONObject sourceToJson(HostsSource source) throws JSONException {
|
||||
JSONObject sourceObject = new JSONObject();
|
||||
sourceObject.put(SOURCE_LABEL_ATTRIBUTE, source.getLabel());
|
||||
sourceObject.put(SOURCE_URL_ATTRIBUTE, source.getUrl());
|
||||
sourceObject.put(SOURCE_ENABLED_ATTRIBUTE, source.isEnabled());
|
||||
sourceObject.put(SOURCE_ALLOW_ATTRIBUTE, source.isAllowEnabled());
|
||||
sourceObject.put(SOURCE_REDIRECT_ATTRIBUTE, source.isRedirectEnabled());
|
||||
return sourceObject;
|
||||
}
|
||||
|
||||
static HostsSource sourceFromJson(JSONObject sourceObject) throws JSONException {
|
||||
HostsSource source = new HostsSource();
|
||||
source.setLabel(sourceObject.getString(SOURCE_LABEL_ATTRIBUTE));
|
||||
String url = sourceObject.getString(SOURCE_URL_ATTRIBUTE);
|
||||
if (!HostsSource.isValidUrl(url)) {
|
||||
throw new JSONException("Invalid source URL: "+url);
|
||||
}
|
||||
source.setUrl(url);
|
||||
source.setEnabled(sourceObject.getBoolean(SOURCE_ENABLED_ATTRIBUTE));
|
||||
source.setAllowEnabled(sourceObject.getBoolean(SOURCE_ALLOW_ATTRIBUTE));
|
||||
source.setRedirectEnabled(sourceObject.getBoolean(SOURCE_REDIRECT_ATTRIBUTE));
|
||||
return source;
|
||||
}
|
||||
|
||||
static JSONObject hostToJson(HostListItem host) throws JSONException {
|
||||
JSONObject hostObject = new JSONObject();
|
||||
hostObject.put(HOST_ATTRIBUTE, host.getHost());
|
||||
String redirection = host.getRedirection();
|
||||
if (redirection != null && !redirection.isEmpty()) {
|
||||
hostObject.put(REDIRECT_ATTRIBUTE, redirection);
|
||||
}
|
||||
hostObject.put(ENABLED_ATTRIBUTE, host.isEnabled());
|
||||
return hostObject;
|
||||
}
|
||||
|
||||
static HostListItem hostFromJson(JSONObject hostObject) throws JSONException {
|
||||
HostListItem host = new HostListItem();
|
||||
host.setHost(hostObject.getString(HOST_ATTRIBUTE));
|
||||
if (hostObject.has(REDIRECT_ATTRIBUTE)) {
|
||||
host.setRedirection(hostObject.getString(REDIRECT_ATTRIBUTE));
|
||||
}
|
||||
host.setEnabled(hostObject.getBoolean(ENABLED_ATTRIBUTE));
|
||||
host.setSourceId(USER_SOURCE_ID);
|
||||
return host;
|
||||
}
|
||||
}
|
||||
136
app/src/main/java/org/adaway/model/backup/BackupImporter.java
Normal file
136
app/src/main/java/org/adaway/model/backup/BackupImporter.java
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package org.adaway.model.backup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.ListType;
|
||||
import org.adaway.util.AppExecutors;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.adaway.model.backup.BackupFormat.ALLOWED_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.BLOCKED_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.REDIRECTED_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.SOURCES_KEY;
|
||||
import static org.adaway.model.backup.BackupFormat.hostFromJson;
|
||||
import static org.adaway.model.backup.BackupFormat.sourceFromJson;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a helper class to import user lists and hosts sources to a backup file.<br>
|
||||
* Importing a file source will no restore read access from Storage Access Framework.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class BackupImporter {
|
||||
|
||||
private BackupImporter() {
|
||||
|
||||
}
|
||||
|
||||
private static final Executor DISK_IO_EXECUTOR = AppExecutors.getInstance().diskIO();
|
||||
private static final Executor MAIN_THREAD_EXECUTOR = AppExecutors.getInstance().mainThread();
|
||||
|
||||
/**
|
||||
* Import a backup file.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param backupUri The URI of a backup file.
|
||||
*/
|
||||
@UiThread
|
||||
public static void importFromBackup(Context context, Uri backupUri) {
|
||||
DISK_IO_EXECUTOR.execute(() -> {
|
||||
boolean imported = true;
|
||||
try {
|
||||
importBackup(context, backupUri);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Failed to import backup.");
|
||||
imported = false;
|
||||
}
|
||||
boolean successful = imported;
|
||||
MAIN_THREAD_EXECUTOR.execute(() -> notifyImportEnd(context, successful));
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private static void notifyImportEnd(Context context, boolean successful) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(successful ? R.string.import_success : R.string.import_failed),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
}
|
||||
|
||||
static void importBackup(Context context, Uri backupUri) throws IOException {
|
||||
try (InputStream inputStream = context.getContentResolver().openInputStream(backupUri);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
contentBuilder.append(line);
|
||||
}
|
||||
JSONObject backupObject = new JSONObject(contentBuilder.toString());
|
||||
importBackup(context, backupObject);
|
||||
} catch (JSONException exception) {
|
||||
throw new IOException("Failed to parse backup file.", exception);
|
||||
} catch (FileNotFoundException exception) {
|
||||
throw new IOException("Failed to find backup file.", exception);
|
||||
} catch (IOException exception) {
|
||||
throw new IOException("Failed to read backup file.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void importBackup(Context context, JSONObject backupObject) throws JSONException {
|
||||
AppDatabase database = AppDatabase.getInstance(context);
|
||||
HostsSourceDao hostsSourceDao = database.hostsSourceDao();
|
||||
HostListItemDao hostListItemDao = database.hostsListItemDao();
|
||||
|
||||
importSourceBackup(hostsSourceDao, backupObject.getJSONArray(SOURCES_KEY));
|
||||
importListBackup(hostListItemDao, BLOCKED, backupObject.getJSONArray(BLOCKED_KEY));
|
||||
importListBackup(hostListItemDao, ALLOWED, backupObject.getJSONArray(ALLOWED_KEY));
|
||||
importListBackup(hostListItemDao, REDIRECTED, backupObject.getJSONArray(REDIRECTED_KEY));
|
||||
}
|
||||
|
||||
private static void importSourceBackup(HostsSourceDao hostsSourceDao, JSONArray sources) throws JSONException {
|
||||
for (int index = 0; index < sources.length(); index++) {
|
||||
JSONObject sourceObject = sources.getJSONObject(index);
|
||||
hostsSourceDao.insert(sourceFromJson(sourceObject));
|
||||
}
|
||||
}
|
||||
|
||||
private static void importListBackup(HostListItemDao hostListItemDao, ListType type, JSONArray hosts) throws JSONException {
|
||||
for (int index = 0; index < hosts.length(); index++) {
|
||||
JSONObject hostObject = hosts.getJSONObject(index);
|
||||
HostListItem host = hostFromJson(hostObject);
|
||||
host.setType(type);
|
||||
Optional<Integer> id = hostListItemDao.getHostId(host.getHost());
|
||||
if (id.isPresent()) {
|
||||
host.setId(id.get());
|
||||
hostListItemDao.update(host);
|
||||
} else {
|
||||
hostListItemDao.insert(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
app/src/main/java/org/adaway/model/error/HostError.java
Normal file
44
app/src/main/java/org/adaway/model/error/HostError.java
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package org.adaway.model.error;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.adaway.R;
|
||||
|
||||
/**
|
||||
* This enumeration represents the hosts error case.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum HostError {
|
||||
// Source model errors
|
||||
NO_CONNECTION(R.string.error_no_connection_message, R.string.error_no_connection_details),
|
||||
DOWNLOAD_FAILED(R.string.error_download_failed_message, R.string.error_no_connection_details),
|
||||
// Host install model errors
|
||||
PRIVATE_FILE_FAILED(R.string.error_private_file_failed_message, R.string.error_private_file_failed_details),
|
||||
NOT_ENOUGH_SPACE(R.string.error_not_enough_space_message, R.string.error_not_enough_space_details),
|
||||
COPY_FAIL(R.string.error_copy_failed_message, R.string.error_copy_failed_details),
|
||||
REVERT_FAIL(R.string.error_revert_failed_message, R.string.error_revert_failed_details),
|
||||
// VPN model error
|
||||
ENABLE_VPN_FAIL(R.string.error_enable_vpn_failed_message, R.string.error_enable_vpn_failed_details),
|
||||
DISABLE_VPN_FAIL(R.string.error_disable_vpn_failed_message, R.string.error_disable_vpn_failed_details);
|
||||
|
||||
@StringRes
|
||||
private final int messageKey;
|
||||
@StringRes
|
||||
private final int detailsKey;
|
||||
|
||||
HostError(int messageKey, int detailsKey) {
|
||||
this.messageKey = messageKey;
|
||||
this.detailsKey = detailsKey;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getMessageKey() {
|
||||
return this.messageKey;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public int getDetailsKey() {
|
||||
return this.detailsKey;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package org.adaway.model.error;
|
||||
|
||||
/**
|
||||
* This class in an {@link Exception} thrown on hosts error.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HostErrorException extends Exception {
|
||||
/**
|
||||
* The exception error type.
|
||||
*/
|
||||
private final HostError error;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param error The exception error type.
|
||||
*/
|
||||
public HostErrorException(HostError error) {
|
||||
super("An host error " + error.name() + " occurred");
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param error The exception error type.
|
||||
* @param cause The cause of this exception.
|
||||
*/
|
||||
public HostErrorException(HostError error, Throwable cause) {
|
||||
super("An host error " + error.name() + " occurred", cause);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error type.
|
||||
*
|
||||
* @return The exception error type
|
||||
*/
|
||||
public HostError getError() {
|
||||
return this.error;
|
||||
}
|
||||
}
|
||||
61
app/src/main/java/org/adaway/model/git/GistHostsSource.java
Normal file
61
app/src/main/java/org/adaway/model/git/GistHostsSource.java
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package org.adaway.model.git;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an utility class to get information from GitHub gist hosting.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class GistHostsSource extends GitHostsJsonApiSource {
|
||||
/**
|
||||
* The gist identifier.
|
||||
*/
|
||||
private final String gistIdentifier;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param url The hosts file URL hosted on GitHub gist.
|
||||
* @throws MalformedURLException If the URl is not a gist URL.
|
||||
*/
|
||||
GistHostsSource(String url) throws MalformedURLException {
|
||||
// Check URL path
|
||||
URL parsedUrl = new URL(url);
|
||||
String path = parsedUrl.getPath();
|
||||
String[] pathParts = path.split("/");
|
||||
if (pathParts.length < 2) {
|
||||
throw new MalformedURLException("The GitHub gist URL " + url + " is not valid.");
|
||||
}
|
||||
// Extract gist identifier from path
|
||||
this.gistIdentifier = pathParts[2];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCommitApiUrl() {
|
||||
return "https://api.github.com/gists/" + this.gistIdentifier;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ZonedDateTime parseJsonBody(String body) throws JSONException {
|
||||
JSONObject gistObject = new JSONObject(body);
|
||||
String dateString = gistObject.getString("updated_at");
|
||||
ZonedDateTime date = null;
|
||||
try {
|
||||
date = ZonedDateTime.parse(dateString);
|
||||
} catch (DateTimeParseException exception) {
|
||||
Timber.w(exception, "Failed to parse commit date: " + dateString + ".");
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.adaway.model.git;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an utility class to get information from Git hosted hosts sources from JSON API.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public abstract class GitHostsJsonApiSource extends GitHostsSource {
|
||||
@Override
|
||||
@Nullable
|
||||
public ZonedDateTime getLastUpdate() {
|
||||
return getLastUpdateFromApi(getCommitApiUrl());
|
||||
}
|
||||
|
||||
protected abstract String getCommitApiUrl();
|
||||
|
||||
@Nullable
|
||||
protected ZonedDateTime getLastUpdateFromApi(String commitApiUrl) {
|
||||
// Create client and request
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
Request request = new Request.Builder().url(commitApiUrl).build();
|
||||
try (Response response = client.newCall(request).execute();
|
||||
ResponseBody body = response.body()) {
|
||||
if (response.isSuccessful()) {
|
||||
return parseJsonBody(body.string());
|
||||
}
|
||||
} catch (UnknownHostException | SocketTimeoutException exception) {
|
||||
Timber.i(exception, "Unable to reach API backend.");
|
||||
} catch (IOException | JSONException exception) {
|
||||
Timber.e(exception, "Unable to get commits from API.");
|
||||
}
|
||||
// Return failed
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract ZonedDateTime parseJsonBody(String body) throws JSONException;
|
||||
}
|
||||
65
app/src/main/java/org/adaway/model/git/GitHostsSource.java
Normal file
65
app/src/main/java/org/adaway/model/git/GitHostsSource.java
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package org.adaway.model.git;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/**
|
||||
* This class is an utility class to get information from Git hosted hosts sources.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public abstract class GitHostsSource {
|
||||
/**
|
||||
* The GitHub repository URL.
|
||||
*/
|
||||
private static final String GITHUB_REPO_URL = "https://raw.githubusercontent.com/";
|
||||
/**
|
||||
* The GitHub gist URL.
|
||||
*/
|
||||
private static final String GITHUB_GIST_URL = "https://gist.githubusercontent.com";
|
||||
/**
|
||||
* The GitLab URL.
|
||||
*/
|
||||
private static final String GITLAB_URL = "https://gitlab.com/";
|
||||
|
||||
/**
|
||||
* Check if a hosts file url is hosted on Git hosting.
|
||||
*
|
||||
* @param url The url to check.
|
||||
* @return {@code true} if the hosts file is hosted on Git hosting, {@code false} otherwise.
|
||||
*/
|
||||
public static boolean isHostedOnGit(String url) {
|
||||
return url.startsWith(GITHUB_REPO_URL) ||
|
||||
url.startsWith(GITHUB_GIST_URL) ||
|
||||
url.startsWith(GITLAB_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GitHub hosts source.
|
||||
*
|
||||
* @param url The URL to get source from.
|
||||
* @return The GitHub hosts source.
|
||||
* @throws MalformedURLException If the URL is not a GitHub URL or not a supported GitHub URL.
|
||||
*/
|
||||
public static GitHostsSource getSource(String url) throws MalformedURLException {
|
||||
if (url.startsWith(GITHUB_REPO_URL)) {
|
||||
return new GitHubHostsSource(url);
|
||||
} else if (url.startsWith(GITHUB_GIST_URL)) {
|
||||
return new GistHostsSource(url);
|
||||
} else if (url.startsWith(GITLAB_URL)) {
|
||||
return new GitLabHostsSource(url);
|
||||
} else {
|
||||
throw new MalformedURLException("URL is not a supported Git hosting URL");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last update of the hosts file.
|
||||
*
|
||||
* @return The last update date, {@code null} if the date could not be retrieved.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract ZonedDateTime getLastUpdate();
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package org.adaway.model.git;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an utility class to get information from GitHub repository hosting.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class GitHubHostsSource extends GitHostsJsonApiSource {
|
||||
/**
|
||||
* The GitHub owner name.
|
||||
*/
|
||||
private final String owner;
|
||||
/**
|
||||
* The GitHub repository name.
|
||||
*/
|
||||
private final String repo;
|
||||
/**
|
||||
* The GitHub blob (hosts file) path.
|
||||
*/
|
||||
private final String blobPath;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param url The hosts file URL hosted on GitHub.
|
||||
* @throws MalformedURLException If the URl is not a GitHub URL.
|
||||
*/
|
||||
GitHubHostsSource(String url) throws MalformedURLException {
|
||||
// Check URL path
|
||||
URL parsedUrl = new URL(url);
|
||||
String path = parsedUrl.getPath();
|
||||
String[] pathParts = path.split("/");
|
||||
if (pathParts.length < 5) {
|
||||
throw new MalformedURLException("The GitHub user content URL " + url + " is not valid.");
|
||||
}
|
||||
// Extract components from path
|
||||
this.owner = pathParts[1];
|
||||
this.repo = pathParts[2];
|
||||
this.blobPath = Arrays.stream(pathParts)
|
||||
.skip(4)
|
||||
.collect(joining("/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCommitApiUrl() {
|
||||
return "https://api.github.com/repos/" + this.owner + "/" + this.repo +
|
||||
"/commits?per_page=1&path=" + this.blobPath;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ZonedDateTime parseJsonBody(String body) throws JSONException {
|
||||
JSONArray commitArray = new JSONArray(body);
|
||||
int nbrOfCommits = commitArray.length();
|
||||
ZonedDateTime date = null;
|
||||
for (int i = 0; i < nbrOfCommits && date == null; i++) {
|
||||
JSONObject commitItemObject = commitArray.getJSONObject(i);
|
||||
JSONObject commitObject = commitItemObject.getJSONObject("commit");
|
||||
JSONObject committerObject = commitObject.getJSONObject("committer");
|
||||
String dateString = committerObject.getString("date");
|
||||
try {
|
||||
date = ZonedDateTime.parse(dateString);
|
||||
} catch (DateTimeParseException exception) {
|
||||
Timber.w(exception, "Failed to parse commit date: " + dateString + ".");
|
||||
}
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package org.adaway.model.git;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an utility class to get information from GitLab hosts source hosting.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class GitLabHostsSource extends GitHostsJsonApiSource {
|
||||
/**
|
||||
* The GitHub owner name.
|
||||
*/
|
||||
private final String owner;
|
||||
/**
|
||||
* The GitHub repository name.
|
||||
*/
|
||||
private final String repo;
|
||||
/**
|
||||
* The GitLab reference name.
|
||||
*/
|
||||
private final String ref;
|
||||
/**
|
||||
* The GitLab (hosts) file path.
|
||||
*/
|
||||
private final String path;
|
||||
|
||||
GitLabHostsSource(String url) throws MalformedURLException {
|
||||
// Check URL path
|
||||
URL parsedUrl = new URL(url);
|
||||
String path = parsedUrl.getPath();
|
||||
String[] pathParts = path.split("/");
|
||||
if (pathParts.length < 5) {
|
||||
throw new MalformedURLException("The GitLab user content URL " + url + " is not valid.");
|
||||
}
|
||||
// Extract components from path
|
||||
this.owner = pathParts[1];
|
||||
this.repo = pathParts[2];
|
||||
this.ref = pathParts[4];
|
||||
this.path = Arrays.stream(pathParts)
|
||||
.skip(5)
|
||||
.collect(joining("/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCommitApiUrl() {
|
||||
return "https://gitlab.com/api/v4/projects/" + this.owner + "%2F" + this.repo
|
||||
+ "/repository/commits?path=" + this.path + "&ref_name=" + this.ref;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ZonedDateTime parseJsonBody(String body) throws JSONException {
|
||||
JSONArray commitArray = new JSONArray(body);
|
||||
int nbrOfCommits = commitArray.length();
|
||||
ZonedDateTime date = null;
|
||||
for (int i = 0; i < nbrOfCommits && date == null; i++) {
|
||||
JSONObject commitItemObject = commitArray.getJSONObject(i);
|
||||
String dateString = commitItemObject.getString("committed_date");
|
||||
try {
|
||||
date = ZonedDateTime.parse(dateString);
|
||||
} catch (DateTimeParseException exception) {
|
||||
Timber.w(exception, "Failed to parse commit date: " + dateString + ".");
|
||||
}
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.model.root;
|
||||
|
||||
class CommandException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -4014185620880841310L;
|
||||
|
||||
CommandException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/org/adaway/model/root/MountType.java
Normal file
31
app/src/main/java/org/adaway/model/root/MountType.java
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package org.adaway.model.root;
|
||||
|
||||
/**
|
||||
* This class is an enum to define mount type.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum MountType {
|
||||
/**
|
||||
* Mount as read only.
|
||||
*/
|
||||
READ_ONLY("ro"),
|
||||
/**
|
||||
* Mount as read/write.
|
||||
*/
|
||||
READ_WRITE("rw");
|
||||
|
||||
private final String option;
|
||||
|
||||
MountType(String option) {
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related command line option.
|
||||
* @return The related command line option.
|
||||
*/
|
||||
public String getOption() {
|
||||
return this.option;
|
||||
}
|
||||
}
|
||||
319
app/src/main/java/org/adaway/model/root/RootModel.java
Normal file
319
app/src/main/java/org/adaway/model/root/RootModel.java
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
package org.adaway.model.root;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.ROOT;
|
||||
import static org.adaway.model.error.HostError.COPY_FAIL;
|
||||
import static org.adaway.model.error.HostError.NOT_ENOUGH_SPACE;
|
||||
import static org.adaway.model.error.HostError.PRIVATE_FILE_FAILED;
|
||||
import static org.adaway.model.error.HostError.REVERT_FAIL;
|
||||
import static org.adaway.model.root.ShellUtils.isWritable;
|
||||
import static org.adaway.util.Constants.ANDROID_SYSTEM_ETC_HOSTS;
|
||||
import static org.adaway.util.Constants.COMMAND_CHMOD_644;
|
||||
import static org.adaway.util.Constants.COMMAND_CHOWN;
|
||||
import static org.adaway.util.Constants.DEFAULT_HOSTS_FILENAME;
|
||||
import static org.adaway.util.Constants.HOSTS_FILENAME;
|
||||
import static org.adaway.util.Constants.LINE_SEPARATOR;
|
||||
import static org.adaway.util.Constants.LOCALHOST_HOSTNAME;
|
||||
import static org.adaway.util.Constants.LOCALHOST_IPV4;
|
||||
import static org.adaway.util.Constants.LOCALHOST_IPV6;
|
||||
import static org.adaway.model.root.MountType.READ_ONLY;
|
||||
import static org.adaway.model.root.MountType.READ_WRITE;
|
||||
import static org.adaway.model.root.ShellUtils.mergeAllLines;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostEntryDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
import org.adaway.model.adblocking.AdBlockMethod;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.util.AppExecutors;
|
||||
import org.adaway.util.WebServerUtils;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is the model to represent hosts file installation.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class RootModel extends AdBlockModel {
|
||||
private static final String HEADER1 = "# This hosts file has been generated by AdAway on: ";
|
||||
private static final String HEADER2 = "# Please do not modify it directly, it will be overwritten when AdAway is applied again.";
|
||||
private static final String HEADER_SOURCES = "# This file is generated from the following sources:";
|
||||
private final HostsSourceDao hostsSourceDao;
|
||||
private final HostEntryDao hostEntryDao;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public RootModel(Context context) {
|
||||
super(context);
|
||||
// Get DOA
|
||||
AppDatabase database = AppDatabase.getInstance(this.context);
|
||||
this.hostsSourceDao = database.hostsSourceDao();
|
||||
this.hostEntryDao = database.hostEntryDao();
|
||||
// Check if host list is applied
|
||||
Executor executor = AppExecutors.getInstance().diskIO();
|
||||
executor.execute(this::checkApplied);
|
||||
executor.execute(() -> syncPreferences(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdBlockMethod getMethod() {
|
||||
return ROOT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() throws HostErrorException {
|
||||
setState(R.string.status_apply_sources);
|
||||
setState(R.string.status_create_new_hosts);
|
||||
createNewHostsFile();
|
||||
setState(R.string.status_copy_new_hosts);
|
||||
copyNewHostsFile();
|
||||
setState(R.string.status_check_copy);
|
||||
setState(R.string.status_hosts_updated);
|
||||
this.applied.postValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to the default hosts file.
|
||||
*
|
||||
* @throws HostErrorException If the hosts file could not be reverted.
|
||||
*/
|
||||
@Override
|
||||
public void revert() throws HostErrorException {
|
||||
// Update status
|
||||
setState(R.string.status_revert);
|
||||
try {
|
||||
// Revert hosts file
|
||||
revertHostFile();
|
||||
setState(R.string.status_revert_done);
|
||||
this.applied.postValue(false);
|
||||
} catch (IOException exception) {
|
||||
throw new HostErrorException(REVERT_FAIL, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecordingLogs() {
|
||||
return TcpdumpUtils.isTcpdumpRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordingLogs(boolean recording) {
|
||||
if (recording) {
|
||||
TcpdumpUtils.startTcpdump(this.context);
|
||||
} else {
|
||||
TcpdumpUtils.stopTcpdump();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLogs() {
|
||||
return TcpdumpUtils.getLogs(this.context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearLogs() {
|
||||
TcpdumpUtils.clearLogFile(this.context);
|
||||
}
|
||||
|
||||
private void checkApplied() {
|
||||
boolean applied = false;
|
||||
|
||||
Shell.Result result = Shell.cmd("head -n 1 " + ANDROID_SYSTEM_ETC_HOSTS).exec();
|
||||
if (!result.isSuccess()) {
|
||||
Timber.e("Failed to read first line of hosts file. Error code: %s", result.getCode());
|
||||
} else {
|
||||
applied = mergeAllLines(result.getOut()).startsWith(HEADER1);
|
||||
}
|
||||
|
||||
this.applied.postValue(applied);
|
||||
}
|
||||
|
||||
private void syncPreferences(Context context) {
|
||||
if (PreferenceHelper.getWebServerEnabled(context) && !WebServerUtils.isWebServerRunning()) {
|
||||
WebServerUtils.startWebServer(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteNewHostsFile() {
|
||||
// delete generated hosts file from private storage
|
||||
this.context.deleteFile(HOSTS_FILENAME);
|
||||
}
|
||||
|
||||
private void copyNewHostsFile() throws HostErrorException {
|
||||
try {
|
||||
copyHostsFile(HOSTS_FILENAME);
|
||||
} catch (CommandException exception) {
|
||||
throw new HostErrorException(COPY_FAIL, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new hosts files in a private file from downloaded hosts sources.
|
||||
*
|
||||
* @throws HostErrorException If the new hosts file could not be created.
|
||||
*/
|
||||
private void createNewHostsFile() throws HostErrorException {
|
||||
deleteNewHostsFile();
|
||||
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(this.context.openFileOutput(HOSTS_FILENAME, MODE_PRIVATE)))) {
|
||||
writeHostsHeader(writer);
|
||||
writeLoopbackToHosts(writer);
|
||||
writeHosts(writer);
|
||||
} catch (IOException exception) {
|
||||
throw new HostErrorException(PRIVATE_FILE_FAILED, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeHostsHeader(BufferedWriter writer) throws IOException {
|
||||
// Format current date
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
|
||||
Date now = new Date();
|
||||
String date = formatter.format(now);
|
||||
// Write header
|
||||
writer.write(HEADER1);
|
||||
writer.write(date);
|
||||
writer.newLine();
|
||||
writer.write(HEADER2);
|
||||
writer.newLine();
|
||||
// Write hosts source
|
||||
writer.write(HEADER_SOURCES);
|
||||
writer.newLine();
|
||||
for (HostsSource hostsSource : this.hostsSourceDao.getEnabled()) {
|
||||
writer.write("# - " + hostsSource.getLabel() + ":" + hostsSource.getUrl());
|
||||
writer.newLine();
|
||||
}
|
||||
// Write empty line separator
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
private void writeLoopbackToHosts(BufferedWriter writer) throws IOException {
|
||||
writer.write(LOCALHOST_IPV4 + " " + LOCALHOST_HOSTNAME);
|
||||
writer.newLine();
|
||||
writer.write(LOCALHOST_IPV6 + " " + LOCALHOST_HOSTNAME);
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
private void writeHosts(BufferedWriter writer) throws IOException {
|
||||
// Get user preferences
|
||||
String redirectionIpv4 = PreferenceHelper.getRedirectionIpv4(this.context);
|
||||
String redirectionIpv6 = PreferenceHelper.getRedirectionIpv6(this.context);
|
||||
boolean enableIpv6 = PreferenceHelper.getEnableIpv6(this.context);
|
||||
// Write each hostname
|
||||
for (HostEntry entry : this.hostEntryDao.getAll()) {
|
||||
String hostname = entry.getHost();
|
||||
if (entry.getType() == REDIRECTED) {
|
||||
writer.write(entry.getRedirection() + " " + hostname);
|
||||
writer.newLine();
|
||||
} else {
|
||||
writer.write(redirectionIpv4 + " " + hostname);
|
||||
writer.newLine();
|
||||
if (enableIpv6) {
|
||||
writer.write(redirectionIpv6 + " " + hostname);
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to default hosts file.
|
||||
*
|
||||
* @throws IOException If the hosts file could not be reverted.
|
||||
*/
|
||||
private void revertHostFile() throws IOException {
|
||||
// Create private file
|
||||
try (FileOutputStream fos = this.context.openFileOutput(DEFAULT_HOSTS_FILENAME, MODE_PRIVATE)) {
|
||||
// Write default localhost as hosts file
|
||||
String localhost = LOCALHOST_IPV4 + " " + LOCALHOST_HOSTNAME + LINE_SEPARATOR +
|
||||
LOCALHOST_IPV6 + " " + LOCALHOST_HOSTNAME + LINE_SEPARATOR;
|
||||
fos.write(localhost.getBytes());
|
||||
// Copy generated hosts file to target location
|
||||
copyHostsFile(DEFAULT_HOSTS_FILENAME);
|
||||
// Delete generated hosts file after applying it
|
||||
this.context.deleteFile(DEFAULT_HOSTS_FILENAME);
|
||||
} catch (Exception exception) {
|
||||
throw new IOException("Unable to revert hosts file.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy source file from private storage of AdAway to hosts file target using root commands.
|
||||
*/
|
||||
private void copyHostsFile(String source) throws HostErrorException, CommandException {
|
||||
String privateDir = this.context.getFilesDir().getAbsolutePath();
|
||||
String privateFile = privateDir + File.separator + source;
|
||||
|
||||
// if the target has a trailing slash, it is not a valid target!
|
||||
String target = ANDROID_SYSTEM_ETC_HOSTS;
|
||||
File targetFile = new File(target);
|
||||
|
||||
/* check for space on partition */
|
||||
long size = new File(privateFile).length();
|
||||
Timber.i("Size of hosts file: %s.", size);
|
||||
if (!hasEnoughSpaceOnPartition(targetFile, size)) {
|
||||
throw new HostErrorException(NOT_ENOUGH_SPACE);
|
||||
}
|
||||
|
||||
/* Execute commands */
|
||||
boolean writable = isWritable(targetFile);
|
||||
try {
|
||||
if (!writable) {
|
||||
// remount for write access
|
||||
Timber.i("Remounting for RW…");
|
||||
if (!ShellUtils.remountPartition(targetFile, READ_WRITE)) {
|
||||
throw new CommandException("Failed to remount hosts file partition as read-write.");
|
||||
}
|
||||
}
|
||||
// Copy hosts file then set owner and permissions
|
||||
Shell.Result result = Shell.cmd(
|
||||
"dd if=" + privateFile + " of=" + target,
|
||||
COMMAND_CHOWN + " " + target,
|
||||
COMMAND_CHMOD_644 + " " + target
|
||||
).exec();
|
||||
if (!result.isSuccess()) {
|
||||
throw new CommandException("Failed to copy hosts file: " + mergeAllLines(result.getErr()));
|
||||
}
|
||||
} finally {
|
||||
if (!writable) {
|
||||
// after all remount target back as read only
|
||||
ShellUtils.remountPartition(targetFile, READ_ONLY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is enough space on partition where target is located
|
||||
*
|
||||
* @param size size of file to put on partition
|
||||
* @param target path where to put the file
|
||||
* @return true if it will fit on partition of target, false if it will not fit.
|
||||
*/
|
||||
private boolean hasEnoughSpaceOnPartition(File target, long size) {
|
||||
long freeSpace = target.getFreeSpace();
|
||||
return (freeSpace == 0 || freeSpace > size);
|
||||
}
|
||||
}
|
||||
99
app/src/main/java/org/adaway/model/root/ShellUtils.java
Normal file
99
app/src/main/java/org/adaway/model/root/ShellUtils.java
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package org.adaway.model.root;
|
||||
|
||||
import static com.topjohnwu.superuser.ShellUtils.escapedString;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an utility class to help with shell commands.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class ShellUtils {
|
||||
private static final String EXECUTABLE_PREFIX = "lib";
|
||||
private static final String EXECUTABLE_SUFFIX = "_exec.so";
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private ShellUtils() {
|
||||
|
||||
}
|
||||
|
||||
public static String mergeAllLines(List<String> lines) {
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
public static boolean isBundledExecutableRunning(String executable) {
|
||||
return Shell.cmd("ps -A | grep " + EXECUTABLE_PREFIX + executable + EXECUTABLE_SUFFIX).exec().isSuccess();
|
||||
}
|
||||
|
||||
public static boolean runBundledExecutable(Context context, String executable, String parameters) {
|
||||
String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
|
||||
String command = "LD_LIBRARY_PATH=" + nativeLibraryDir + " " +
|
||||
nativeLibraryDir + File.separator + EXECUTABLE_PREFIX + executable + EXECUTABLE_SUFFIX + " " +
|
||||
parameters + " &";
|
||||
return Shell.cmd(command).exec().isSuccess();
|
||||
}
|
||||
|
||||
public static void killBundledExecutable(String executable) {
|
||||
Shell.cmd("killall " + EXECUTABLE_PREFIX + executable + EXECUTABLE_SUFFIX).exec();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if a path is writable.
|
||||
*
|
||||
* @param file The file to check.
|
||||
* @return <code>true</code> if the path is writable, <code>false</code> otherwise.
|
||||
*/
|
||||
public static boolean isWritable(File file) {
|
||||
// Check first if file can be written without privileges
|
||||
if (file.canWrite()) {
|
||||
return true;
|
||||
}
|
||||
return Shell.cmd("test -w " + escapedString(file.getAbsolutePath()))
|
||||
.exec()
|
||||
.isSuccess();
|
||||
}
|
||||
|
||||
public static boolean remountPartition(File file, MountType type) {
|
||||
Optional<String> partitionOptional = findPartition(file);
|
||||
if (!partitionOptional.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
String partition = partitionOptional.get();
|
||||
Shell.Result result = Shell.cmd("mount -o " + type.getOption() + ",remount " + partition).exec();
|
||||
boolean success = result.isSuccess();
|
||||
if (!success) {
|
||||
Timber.w("Failed to remount partition %s as %s: %s.", partition, type.getOption(), mergeAllLines(result.getErr()));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private static Optional<String> findPartition(File file) {
|
||||
// Get mount points
|
||||
Shell.Result result = Shell.cmd("cat /proc/mounts | cut -d ' ' -f2").exec();
|
||||
List<String> out = result.getOut();
|
||||
// Check file and each parent against mount points
|
||||
while (file != null) {
|
||||
String path = file.getAbsolutePath();
|
||||
for (String mount : out) {
|
||||
if (path.equals(mount)) {
|
||||
return Optional.of(mount);
|
||||
}
|
||||
}
|
||||
file = file.getParentFile();
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
202
app/src/main/java/org/adaway/model/root/TcpdumpUtils.java
Normal file
202
app/src/main/java/org/adaway/model/root/TcpdumpUtils.java
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.model.root;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.adaway.model.root.ShellUtils.isBundledExecutableRunning;
|
||||
import static org.adaway.model.root.ShellUtils.killBundledExecutable;
|
||||
import static org.adaway.model.root.ShellUtils.mergeAllLines;
|
||||
import static org.adaway.model.root.ShellUtils.runBundledExecutable;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
class TcpdumpUtils {
|
||||
private static final String TCPDUMP_EXECUTABLE = "tcpdump";
|
||||
private static final String TCPDUMP_LOG = "dns_log.txt";
|
||||
private static final String TCPDUMP_HOSTNAME_REGEX = "(?:A\\?|AAAA\\?)\\s(\\S+)\\.\\s";
|
||||
private static final Pattern TCPDUMP_HOSTNAME_PATTERN = Pattern.compile(TCPDUMP_HOSTNAME_REGEX);
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private TcpdumpUtils() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if tcpdump is running
|
||||
*
|
||||
* @return true if tcpdump is running
|
||||
*/
|
||||
static boolean isTcpdumpRunning() {
|
||||
return isBundledExecutableRunning(TCPDUMP_EXECUTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tcpdump tool.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @return returns true if starting worked
|
||||
*/
|
||||
static boolean startTcpdump(Context context) {
|
||||
Timber.d("Starting tcpdump...");
|
||||
checkSystemTcpdump();
|
||||
|
||||
File file = getLogFile(context);
|
||||
try {
|
||||
// Create log file before using it with tcpdump if not exists
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "Problem while getting cache directory!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// "-i any": listen on any network interface
|
||||
// "-p": disable promiscuous mode (doesn't work anyway)
|
||||
// "-l": Make stdout line buffered. Useful if you want to see the data while
|
||||
// capturing it.
|
||||
// "-v": verbose
|
||||
// "-t": don't print a timestamp
|
||||
// "-s 0": capture first 512 bit of packet to get DNS content
|
||||
String parameters = "-i any -p -l -v -t -s 512 'udp dst port 53' >> " + file + " 2>&1";
|
||||
|
||||
return runBundledExecutable(context, TCPDUMP_EXECUTABLE, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tcpdump.
|
||||
*/
|
||||
static void stopTcpdump() {
|
||||
killBundledExecutable(TCPDUMP_EXECUTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tcpdump binary in bundled in the system.
|
||||
*/
|
||||
static void checkSystemTcpdump() {
|
||||
try {
|
||||
Shell.Result result = Shell.cmd("tcpdump --version").exec();
|
||||
int exitCode = result.getCode();
|
||||
String output = mergeAllLines(result.getOut());
|
||||
String msg = "Tcpdump " + (
|
||||
exitCode == 0 ?
|
||||
"present" :
|
||||
"missing (" + exitCode + ")"
|
||||
) + "\n" + output;
|
||||
Timber.i(msg);
|
||||
} catch (Exception exception) {
|
||||
Timber.w(exception, "Failed to check system tcpdump binary.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tcpdump log file.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @return The tcpdump log file.
|
||||
*/
|
||||
static File getLogFile(Context context) {
|
||||
return new File(context.getCacheDir(), TCPDUMP_LOG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tcpdump log content.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @return The tcpdump log file content.
|
||||
*/
|
||||
static List<String> getLogs(Context context) {
|
||||
Path logPath = getLogFile(context).toPath();
|
||||
// Check if the log file exists
|
||||
if (!Files.exists(logPath)) {
|
||||
return emptyList();
|
||||
}
|
||||
try (Stream<String> lines = Files.lines(logPath)) {
|
||||
return lines
|
||||
.map(TcpdumpUtils::getTcpdumpHostname)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
} catch (IOException exception) {
|
||||
Timber.e(exception, "Can not get cache directory.");
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete log file of tcpdump.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
static boolean clearLogFile(Context context) {
|
||||
// Get the log file
|
||||
File file = getLogFile(context);
|
||||
// Check if file exists
|
||||
if (!file.exists()) {
|
||||
return true;
|
||||
}
|
||||
// Truncate the file content
|
||||
try (FileOutputStream outputStream = new FileOutputStream(file, false)) {
|
||||
// Only truncate the file
|
||||
outputStream.close(); // Useless but help lint
|
||||
} catch (IOException exception) {
|
||||
Timber.e(exception, "Error while truncating the tcpdump file!");
|
||||
// Return failed to clear the log file
|
||||
return false;
|
||||
}
|
||||
// Return successfully clear the log file
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets hostname out of tcpdump log line.
|
||||
*
|
||||
* @param input One line from dns log.
|
||||
* @return A hostname or {code null} if no DNS query in the input.
|
||||
*/
|
||||
private static String getTcpdumpHostname(String input) {
|
||||
Matcher tcpdumpHostnameMatcher = TCPDUMP_HOSTNAME_PATTERN.matcher(input);
|
||||
if (tcpdumpHostnameMatcher.find()) {
|
||||
return tcpdumpHostnameMatcher.group(1);
|
||||
} else {
|
||||
Timber.d("Does not find: %s.", input);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* The root ad-blocker related implementations.
|
||||
*
|
||||
* It relies on libsu from John Wu and shell commands.
|
||||
* Supported shell commands are described here: https://chromium.googlesource.com/aosp/platform/system/core/+/upstream/shell_and_utilities/#android-8_0-oreo
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
package org.adaway.model.root;
|
||||
267
app/src/main/java/org/adaway/model/source/SourceLoader.java
Normal file
267
app/src/main/java/org/adaway/model/source/SourceLoader.java
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
package org.adaway.model.source;
|
||||
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.adaway.util.Constants.BOGUS_IPV4;
|
||||
import static org.adaway.util.Constants.LOCALHOST_HOSTNAME;
|
||||
import static org.adaway.util.Constants.LOCALHOST_IPV4;
|
||||
import static org.adaway.util.Constants.LOCALHOST_IPV6;
|
||||
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.db.entity.ListType;
|
||||
import org.adaway.util.RegexUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an {@link HostsSource} loader.<br>
|
||||
* It parses a source and loads it to database.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class SourceLoader {
|
||||
private static final String TAG = "SourceLoader";
|
||||
private static final String END_OF_QUEUE_MARKER = "#EndOfQueueMarker";
|
||||
private static final int INSERT_BATCH_SIZE = 100;
|
||||
private static final String HOSTS_PARSER = "^\\s*([^#\\s]+)\\s+([^#\\s]+).*$";
|
||||
static final Pattern HOSTS_PARSER_PATTERN = Pattern.compile(HOSTS_PARSER);
|
||||
|
||||
private final HostsSource source;
|
||||
|
||||
SourceLoader(HostsSource hostsSource) {
|
||||
this.source = hostsSource;
|
||||
}
|
||||
|
||||
void parse(BufferedReader reader, HostListItemDao hostListItemDao) {
|
||||
// Clear current hosts
|
||||
hostListItemDao.clearSourceHosts(this.source.getId());
|
||||
// Create batch
|
||||
int parserCount = 3;
|
||||
LinkedBlockingQueue<String> hostsLineQueue = new LinkedBlockingQueue<>();
|
||||
LinkedBlockingQueue<HostListItem> hostsListItemQueue = new LinkedBlockingQueue<>();
|
||||
SourceReader sourceReader = new SourceReader(reader, hostsLineQueue, parserCount);
|
||||
ItemInserter inserter = new ItemInserter(hostsListItemQueue, hostListItemDao, parserCount);
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(
|
||||
parserCount + 2,
|
||||
r -> new Thread(r, TAG)
|
||||
);
|
||||
executorService.execute(sourceReader);
|
||||
for (int i = 0; i < parserCount; i++) {
|
||||
executorService.execute(new HostListItemParser(this.source, hostsLineQueue, hostsListItemQueue));
|
||||
}
|
||||
Future<Integer> inserterFuture = executorService.submit(inserter);
|
||||
try {
|
||||
Integer inserted = inserterFuture.get();
|
||||
Timber.i("%s host list items inserted.", inserted);
|
||||
} catch (ExecutionException e) {
|
||||
Timber.w(e, "Failed to parse hosts sources.");
|
||||
} catch (InterruptedException e) {
|
||||
Timber.w(e, "Interrupted while parsing sources.");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
private static class SourceReader implements Runnable {
|
||||
private final BufferedReader reader;
|
||||
private final BlockingQueue<String> queue;
|
||||
private final int parserCount;
|
||||
|
||||
private SourceReader(BufferedReader reader, BlockingQueue<String> queue, int parserCount) {
|
||||
this.reader = reader;
|
||||
this.queue = queue;
|
||||
this.parserCount = parserCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
this.reader.lines().forEach(this.queue::add);
|
||||
} catch (Throwable t) {
|
||||
Timber.w(t, "Failed to read hosts source.");
|
||||
} finally {
|
||||
// Send end of queue marker to parsers
|
||||
for (int i = 0; i < this.parserCount; i++) {
|
||||
this.queue.add(END_OF_QUEUE_MARKER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class HostListItemParser implements Runnable {
|
||||
private final HostsSource source;
|
||||
private final BlockingQueue<String> lineQueue;
|
||||
private final BlockingQueue<HostListItem> itemQueue;
|
||||
|
||||
private HostListItemParser(HostsSource source, BlockingQueue<String> lineQueue, BlockingQueue<HostListItem> itemQueue) {
|
||||
this.source = source;
|
||||
this.lineQueue = lineQueue;
|
||||
this.itemQueue = itemQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean allowedList = this.source.isAllowEnabled();
|
||||
boolean endOfSource = false;
|
||||
while (!endOfSource) {
|
||||
try {
|
||||
String line = this.lineQueue.take();
|
||||
// Check end of queue marker
|
||||
//noinspection StringEquality
|
||||
if (line == END_OF_QUEUE_MARKER) {
|
||||
endOfSource = true;
|
||||
// Send end of queue marker to inserter
|
||||
HostListItem endItem = new HostListItem();
|
||||
endItem.setHost(line);
|
||||
this.itemQueue.add(endItem);
|
||||
} // Check comments
|
||||
else if (line.isEmpty() || line.charAt(0) == '#') {
|
||||
Timber.d("Skip comment: %s.", line);
|
||||
} else {
|
||||
HostListItem item = allowedList ? parseAllowListItem(line) : parseHostListItem(line);
|
||||
if (item != null && isRedirectionValid(item) && isHostValid(item)) {
|
||||
this.itemQueue.add(item);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Timber.w(e, "Interrupted while parsing hosts list item.");
|
||||
endOfSource = true;
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HostListItem parseHostListItem(String line) {
|
||||
Matcher matcher = HOSTS_PARSER_PATTERN.matcher(line);
|
||||
if (!matcher.matches()) {
|
||||
Timber.d("Does not match: %s.", line);
|
||||
return null;
|
||||
}
|
||||
// Check IP address validity or while list entry (if allowed)
|
||||
String ip = matcher.group(1);
|
||||
String hostname = matcher.group(2);
|
||||
assert hostname != null;
|
||||
// Skip localhost name
|
||||
if (LOCALHOST_HOSTNAME.equals(hostname)) {
|
||||
return null;
|
||||
}
|
||||
// check if ip is 127.0.0.1 or 0.0.0.0
|
||||
ListType type;
|
||||
if (LOCALHOST_IPV4.equals(ip)
|
||||
|| BOGUS_IPV4.equals(ip)
|
||||
|| LOCALHOST_IPV6.equals(ip)) {
|
||||
type = BLOCKED;
|
||||
} else if (this.source.isRedirectEnabled()) {
|
||||
type = REDIRECTED;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
HostListItem item = new HostListItem();
|
||||
item.setType(type);
|
||||
item.setHost(hostname);
|
||||
item.setEnabled(true);
|
||||
if (type == REDIRECTED) {
|
||||
item.setRedirection(ip);
|
||||
}
|
||||
item.setSourceId(this.source.getId());
|
||||
return item;
|
||||
}
|
||||
|
||||
private HostListItem parseAllowListItem(String line) {
|
||||
// Extract hostname
|
||||
int indexOf = line.indexOf('#');
|
||||
if (indexOf == 1) {
|
||||
line = line.substring(0, indexOf);
|
||||
}
|
||||
line = line.trim();
|
||||
// Create item
|
||||
HostListItem item = new HostListItem();
|
||||
item.setType(ALLOWED);
|
||||
item.setHost(line);
|
||||
item.setEnabled(true);
|
||||
item.setSourceId(this.source.getId());
|
||||
return item;
|
||||
}
|
||||
|
||||
private boolean isRedirectionValid(HostListItem item) {
|
||||
return item.getType() != REDIRECTED || RegexUtils.isValidIP(item.getRedirection());
|
||||
}
|
||||
|
||||
private boolean isHostValid(HostListItem item) {
|
||||
String hostname = item.getHost();
|
||||
if (item.getType() == BLOCKED) {
|
||||
if (hostname.indexOf('?') != -1 || hostname.indexOf('*') != -1) {
|
||||
return false;
|
||||
}
|
||||
return RegexUtils.isValidHostname(hostname);
|
||||
}
|
||||
return RegexUtils.isValidWildcardHostname(hostname);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ItemInserter implements Callable<Integer> {
|
||||
private final BlockingQueue<HostListItem> hostListItemQueue;
|
||||
private final HostListItemDao hostListItemDao;
|
||||
private final int parserCount;
|
||||
|
||||
private ItemInserter(BlockingQueue<HostListItem> itemQueue, HostListItemDao hostListItemDao, int parserCount) {
|
||||
this.hostListItemQueue = itemQueue;
|
||||
this.hostListItemDao = hostListItemDao;
|
||||
this.parserCount = parserCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
int inserted = 0;
|
||||
int workerStopped = 0;
|
||||
HostListItem[] batch = new HostListItem[INSERT_BATCH_SIZE];
|
||||
int cacheSize = 0;
|
||||
boolean queueEmptied = false;
|
||||
while (!queueEmptied) {
|
||||
try {
|
||||
HostListItem item = this.hostListItemQueue.take();
|
||||
// Check end of queue marker
|
||||
//noinspection StringEquality
|
||||
if (item.getHost() == END_OF_QUEUE_MARKER) {
|
||||
workerStopped++;
|
||||
if (workerStopped >= this.parserCount) {
|
||||
queueEmptied = true;
|
||||
}
|
||||
} else {
|
||||
batch[cacheSize++] = item;
|
||||
if (cacheSize >= batch.length) {
|
||||
this.hostListItemDao.insert(batch);
|
||||
inserted += cacheSize;
|
||||
cacheSize = 0;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Timber.w(e, "Interrupted while inserted hosts list item.");
|
||||
queueEmptied = true;
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
// Flush current batch
|
||||
HostListItem[] remaining = new HostListItem[cacheSize];
|
||||
System.arraycopy(batch, 0, remaining, 0, remaining.length);
|
||||
this.hostListItemDao.insert(remaining);
|
||||
inserted += cacheSize;
|
||||
// Return number of inserted items
|
||||
return inserted;
|
||||
}
|
||||
}
|
||||
}
|
||||
508
app/src/main/java/org/adaway/model/source/SourceModel.java
Normal file
508
app/src/main/java/org/adaway/model/source/SourceModel.java
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
package org.adaway.model.source;
|
||||
|
||||
import static android.content.Context.CONNECTIVITY_SERVICE;
|
||||
import static android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED;
|
||||
import static org.adaway.model.error.HostError.DOWNLOAD_FAILED;
|
||||
import static org.adaway.model.error.HostError.NO_CONNECTION;
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
|
||||
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||
import static java.time.format.FormatStyle.MEDIUM;
|
||||
import static java.time.temporal.ChronoUnit.WEEKS;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.converter.ZonedDateTimeConverter;
|
||||
import org.adaway.db.dao.HostEntryDao;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.model.git.GitHostsSource;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is the model to represent hosts source management.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class SourceModel {
|
||||
/**
|
||||
* The HTTP client cache size (100Mo).
|
||||
*/
|
||||
private static final long CACHE_SIZE = 100L * 1024L * 1024L;
|
||||
private static final String LAST_MODIFIED_HEADER = "Last-Modified";
|
||||
private static final String IF_NONE_MATCH_HEADER = "If-None-Match";
|
||||
private static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since";
|
||||
private static final String ENTITY_TAG_HEADER = "ETag";
|
||||
private static final String WEAK_ENTITY_TAG_PREFIX = "W/";
|
||||
/**
|
||||
* The application context.
|
||||
*/
|
||||
private final Context context;
|
||||
/**
|
||||
* The {@link HostsSource} DAO.
|
||||
*/
|
||||
private final HostsSourceDao hostsSourceDao;
|
||||
/**
|
||||
* The {@link HostListItem} DAO.
|
||||
*/
|
||||
private final HostListItemDao hostListItemDao;
|
||||
/**
|
||||
* The {@link HostEntry} DAO.
|
||||
*/
|
||||
private final HostEntryDao hostEntryDao;
|
||||
/**
|
||||
* The update available status.
|
||||
*/
|
||||
private final MutableLiveData<Boolean> updateAvailable;
|
||||
/**
|
||||
* The model state.
|
||||
*/
|
||||
private final MutableLiveData<String> state;
|
||||
/**
|
||||
* The HTTP client to download hosts sources ({@code null} until initialized by {@link #getHttpClient()}).
|
||||
*/
|
||||
private OkHttpClient cachedHttpClient;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public SourceModel(Context context) {
|
||||
this.context = context;
|
||||
AppDatabase database = AppDatabase.getInstance(this.context);
|
||||
this.hostsSourceDao = database.hostsSourceDao();
|
||||
this.hostListItemDao = database.hostsListItemDao();
|
||||
this.hostEntryDao = database.hostEntryDao();
|
||||
this.state = new MutableLiveData<>("");
|
||||
this.updateAvailable = new MutableLiveData<>();
|
||||
this.updateAvailable.setValue(false);
|
||||
SourceUpdateService.syncPreferences(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model state.
|
||||
*
|
||||
* @return The model state.
|
||||
*/
|
||||
public LiveData<String> getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the update available status.
|
||||
*
|
||||
* @return {@code true} if source update is available, {@code false} otherwise.
|
||||
*/
|
||||
public LiveData<Boolean> isUpdateAvailable() {
|
||||
return this.updateAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is update available for hosts sources.
|
||||
*
|
||||
* @throws HostErrorException If the hosts sources could not be checked.
|
||||
*/
|
||||
public boolean checkForUpdate() throws HostErrorException {
|
||||
// Check current connection
|
||||
if (isDeviceOffline()) {
|
||||
throw new HostErrorException(NO_CONNECTION);
|
||||
}
|
||||
// Initialize update status
|
||||
boolean updateAvailable = false;
|
||||
// Get enabled hosts sources
|
||||
List<HostsSource> sources = this.hostsSourceDao.getEnabled();
|
||||
if (sources.isEmpty()) {
|
||||
// Return no update as no source
|
||||
this.updateAvailable.postValue(false);
|
||||
return false;
|
||||
}
|
||||
// Update state
|
||||
setState(R.string.status_check);
|
||||
// Check each source
|
||||
for (HostsSource source : sources) {
|
||||
// Get URL and lastModified from db
|
||||
ZonedDateTime lastModifiedLocal = source.getLocalModificationDate();
|
||||
// Update state
|
||||
setState(R.string.status_check_source, source.getLabel());
|
||||
// Get hosts source last update
|
||||
ZonedDateTime lastModifiedOnline = getHostsSourceLastUpdate(source);
|
||||
// Some help with debug here
|
||||
Timber.d("lastModifiedLocal: %s", dateToString(lastModifiedLocal));
|
||||
Timber.d("lastModifiedOnline: %s", dateToString(lastModifiedOnline));
|
||||
// Save last modified online
|
||||
this.hostsSourceDao.updateOnlineModificationDate(source.getId(), lastModifiedOnline);
|
||||
// Check if last modified online retrieved
|
||||
if (lastModifiedOnline == null) {
|
||||
// If not, consider update is available if install is older than a week
|
||||
ZonedDateTime lastWeek = ZonedDateTime.now().minus(1, WEEKS);
|
||||
if (lastModifiedLocal != null && lastModifiedLocal.isBefore(lastWeek)) {
|
||||
updateAvailable = true;
|
||||
}
|
||||
} else {
|
||||
// Check if source was never installed or installed before the last update
|
||||
if (lastModifiedLocal == null || lastModifiedOnline.isAfter(lastModifiedLocal)) {
|
||||
updateAvailable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update statuses
|
||||
Timber.d("Update check result: %s.", updateAvailable);
|
||||
if (updateAvailable) {
|
||||
setState(R.string.status_update_available);
|
||||
} else {
|
||||
setState(R.string.status_no_update_found);
|
||||
}
|
||||
this.updateAvailable.postValue(updateAvailable);
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format {@link ZonedDateTime} for printing.
|
||||
*
|
||||
* @param zonedDateTime The date to format.
|
||||
* @return The formatted date string.
|
||||
*/
|
||||
private String dateToString(ZonedDateTime zonedDateTime) {
|
||||
if (zonedDateTime == null) {
|
||||
return "not defined";
|
||||
} else {
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(MEDIUM);
|
||||
return zonedDateTime + " (" + zonedDateTime.format(dateTimeFormatter) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if device is offline.
|
||||
*
|
||||
* @return returns {@code true} if device is offline, {@code false} otherwise.
|
||||
*/
|
||||
private boolean isDeviceOffline() {
|
||||
ConnectivityManager connectivityManager = (ConnectivityManager) this.context.getSystemService(CONNECTIVITY_SERVICE);
|
||||
if (connectivityManager == null) {
|
||||
return false;
|
||||
}
|
||||
NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
|
||||
return netInfo == null || !netInfo.isConnectedOrConnecting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hosts source last online update.
|
||||
*
|
||||
* @param source The hosts source to get last online update.
|
||||
* @return The last online date, {@code null} if the date could not be retrieved.
|
||||
*/
|
||||
@Nullable
|
||||
private ZonedDateTime getHostsSourceLastUpdate(HostsSource source) {
|
||||
switch (source.getType()) {
|
||||
case URL:
|
||||
return getUrlLastUpdate(source);
|
||||
case FILE:
|
||||
Uri fileUri = Uri.parse(source.getUrl());
|
||||
return getFileLastUpdate(fileUri);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url last online update.
|
||||
*
|
||||
* @param source The source to get last online update.
|
||||
* @return The last online date, {@code null} if the date could not be retrieved.
|
||||
*/
|
||||
private ZonedDateTime getUrlLastUpdate(HostsSource source) {
|
||||
String url = source.getUrl();
|
||||
Timber.v("Checking url last update for source: %s.", url);
|
||||
// Check Git hosting
|
||||
if (GitHostsSource.isHostedOnGit(url)) {
|
||||
try {
|
||||
return GitHostsSource.getSource(url).getLastUpdate();
|
||||
} catch (MalformedURLException e) {
|
||||
Timber.w(e, "Failed to get Git last commit for url %s.", url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Default hosting
|
||||
Request request = getRequestFor(source).head().build();
|
||||
try (Response response = getHttpClient().newCall(request).execute()) {
|
||||
String lastModified = response.header(LAST_MODIFIED_HEADER);
|
||||
if (lastModified == null) {
|
||||
return response.code() == HTTP_NOT_MODIFIED ?
|
||||
source.getOnlineModificationDate() : null;
|
||||
}
|
||||
return ZonedDateTime.parse(lastModified, RFC_1123_DATE_TIME);
|
||||
} catch (IOException | DateTimeParseException e) {
|
||||
Timber.e(e, "Exception while fetching last modified date of source %s.", url);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file last modified date.
|
||||
*
|
||||
* @param fileUri The file uri to get last modified date.
|
||||
* @return The file last modified date, {@code null} if date could not be retrieved.
|
||||
*/
|
||||
private ZonedDateTime getFileLastUpdate(Uri fileUri) {
|
||||
ContentResolver contentResolver = this.context.getContentResolver();
|
||||
try (Cursor cursor = contentResolver.query(fileUri, null, null, null, null)) {
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
Timber.w("The content resolver could not find %s.", fileUri);
|
||||
return null;
|
||||
}
|
||||
int columnIndex = cursor.getColumnIndex(COLUMN_LAST_MODIFIED);
|
||||
if (columnIndex == -1) {
|
||||
Timber.w("The content resolver does not support last modified column %s.", fileUri);
|
||||
return null;
|
||||
}
|
||||
return ZonedDateTimeConverter.fromTimestamp(cursor.getLong(columnIndex));
|
||||
} catch (SecurityException e) {
|
||||
Timber.i(e, "The SAF permission was removed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all hosts sources files to copy into a private local file.
|
||||
*
|
||||
* @throws HostErrorException If the hosts sources could not be downloaded.
|
||||
*/
|
||||
public void retrieveHostsSources() throws HostErrorException {
|
||||
// Check connection status
|
||||
if (isDeviceOffline()) {
|
||||
throw new HostErrorException(NO_CONNECTION);
|
||||
}
|
||||
// Update state to downloading
|
||||
setState(R.string.status_retrieve);
|
||||
// Initialize copy counters
|
||||
int numberOfCopies = 0;
|
||||
int numberOfFailedCopies = 0;
|
||||
// Compute current date in UTC timezone
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
// Get each hosts source
|
||||
for (HostsSource source : this.hostsSourceDao.getAll()) {
|
||||
int sourceId = source.getId();
|
||||
// Clear disabled source
|
||||
if (!source.isEnabled()) {
|
||||
this.hostListItemDao.clearSourceHosts(sourceId);
|
||||
this.hostsSourceDao.clearProperties(sourceId);
|
||||
continue;
|
||||
}
|
||||
// Get hosts source last update
|
||||
ZonedDateTime onlineModificationDate = getHostsSourceLastUpdate(source);
|
||||
if (onlineModificationDate == null) {
|
||||
onlineModificationDate = now;
|
||||
}
|
||||
// Check if update available
|
||||
ZonedDateTime localModificationDate = source.getLocalModificationDate();
|
||||
if (localModificationDate != null && localModificationDate.isAfter(onlineModificationDate)) {
|
||||
Timber.i("Skip source %s: no update.", source.getLabel());
|
||||
continue;
|
||||
}
|
||||
// Increment number of copy
|
||||
numberOfCopies++;
|
||||
try {
|
||||
// Check hosts source type
|
||||
switch (source.getType()) {
|
||||
case URL:
|
||||
downloadHostSource(source);
|
||||
break;
|
||||
case FILE:
|
||||
readSourceFile(source);
|
||||
break;
|
||||
default:
|
||||
Timber.w("Hosts source type is not supported.");
|
||||
}
|
||||
// Update local and online modification dates to now
|
||||
localModificationDate = onlineModificationDate.isAfter(now) ? onlineModificationDate : now;
|
||||
this.hostsSourceDao.updateModificationDates(sourceId, localModificationDate, onlineModificationDate);
|
||||
// Update size
|
||||
this.hostsSourceDao.updateSize(sourceId);
|
||||
} catch (IOException e) {
|
||||
Timber.w(e, "Failed to retrieve host source %s.", source.getUrl());
|
||||
// Increment number of failed copy
|
||||
numberOfFailedCopies++;
|
||||
}
|
||||
}
|
||||
// Check if all copies failed
|
||||
if (numberOfCopies == numberOfFailedCopies && numberOfCopies != 0) {
|
||||
throw new HostErrorException(DOWNLOAD_FAILED);
|
||||
}
|
||||
// Synchronize hosts entries
|
||||
syncHostEntries();
|
||||
// Mark no update available
|
||||
this.updateAvailable.postValue(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize hosts entries from current source states.
|
||||
*/
|
||||
public void syncHostEntries() {
|
||||
setState(R.string.status_sync_database);
|
||||
this.hostEntryDao.sync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP client to download hosts sources.
|
||||
*
|
||||
* @return The HTTP client to download hosts sources.
|
||||
*/
|
||||
@NonNull
|
||||
private OkHttpClient getHttpClient() {
|
||||
if (this.cachedHttpClient == null) {
|
||||
this.cachedHttpClient = new OkHttpClient.Builder()
|
||||
.cache(new Cache(this.context.getCacheDir(), CACHE_SIZE))
|
||||
.build();
|
||||
}
|
||||
return this.cachedHttpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request builder for an hosts source.
|
||||
* All cache data available are filled into the headers.
|
||||
*
|
||||
* @param source The hosts source to get request builder.
|
||||
* @return The hosts source request builder.
|
||||
*/
|
||||
private Request.Builder getRequestFor(HostsSource source) {
|
||||
Request.Builder request = new Request.Builder().url(source.getUrl());
|
||||
if (source.getEntityTag() != null) {
|
||||
request = request.header(IF_NONE_MATCH_HEADER, source.getEntityTag());
|
||||
}
|
||||
if (source.getOnlineModificationDate() != null) {
|
||||
String lastModified = source.getOnlineModificationDate().format(RFC_1123_DATE_TIME);
|
||||
request = request.header(IF_MODIFIED_SINCE_HEADER, lastModified);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an hosts source file and append it to the database.
|
||||
*
|
||||
* @param source The hosts source to download.
|
||||
* @throws IOException If the hosts source could not be downloaded.
|
||||
*/
|
||||
private void downloadHostSource(HostsSource source) throws IOException {
|
||||
// Get hosts file URL
|
||||
String hostsFileUrl = source.getUrl();
|
||||
Timber.v("Downloading hosts file: %s.", hostsFileUrl);
|
||||
// Set state to downloading hosts source
|
||||
setState(R.string.status_download_source, hostsFileUrl);
|
||||
// Create request
|
||||
Request request = getRequestFor(source).build();
|
||||
// Request hosts file and open byte stream
|
||||
try (Response response = getHttpClient().newCall(request).execute();
|
||||
Reader reader = requireNonNull(response.body()).charStream();
|
||||
BufferedReader bufferedReader = new BufferedReader(reader)) {
|
||||
// Skip source parsing if not modified
|
||||
if (response.code() == HTTP_NOT_MODIFIED) {
|
||||
Timber.d("Source %s was not updated since last fetch.", source.getUrl());
|
||||
return;
|
||||
}
|
||||
// Extract ETag if present
|
||||
String entityTag = response.header(ENTITY_TAG_HEADER);
|
||||
if (entityTag != null) {
|
||||
if (entityTag.startsWith(WEAK_ENTITY_TAG_PREFIX)) {
|
||||
entityTag = entityTag.substring(WEAK_ENTITY_TAG_PREFIX.length());
|
||||
}
|
||||
this.hostsSourceDao.updateEntityTag(source.getId(), entityTag);
|
||||
}
|
||||
// Parse source
|
||||
parseSourceInputStream(source, bufferedReader);
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Exception while downloading hosts file from " + hostsFileUrl + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a hosts source file and append it to the database.
|
||||
*
|
||||
* @param hostsSource The hosts source to copy.
|
||||
* @throws IOException If the hosts source could not be copied.
|
||||
*/
|
||||
private void readSourceFile(HostsSource hostsSource) throws IOException {
|
||||
// Get hosts file URI
|
||||
String hostsFileUrl = hostsSource.getUrl();
|
||||
Uri fileUri = Uri.parse(hostsFileUrl);
|
||||
Timber.v("Reading hosts source file: %s.", hostsFileUrl);
|
||||
// Set state to copying hosts source
|
||||
setState(R.string.status_read_source, hostsFileUrl);
|
||||
try (InputStream inputStream = this.context.getContentResolver().openInputStream(fileUri);
|
||||
InputStreamReader reader = new InputStreamReader(inputStream);
|
||||
BufferedReader bufferedReader = new BufferedReader(reader)) {
|
||||
parseSourceInputStream(hostsSource, bufferedReader);
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Error while reading hosts file from " + hostsFileUrl + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a source from its input stream to store it into database.
|
||||
*
|
||||
* @param hostsSource The host source to parse.
|
||||
* @param reader The host source reader.
|
||||
*/
|
||||
private void parseSourceInputStream(HostsSource hostsSource, BufferedReader reader) {
|
||||
setState(R.string.status_parse_source, hostsSource.getLabel());
|
||||
long startTime = System.currentTimeMillis();
|
||||
new SourceLoader(hostsSource).parse(reader, this.hostListItemDao);
|
||||
long endTime = System.currentTimeMillis();
|
||||
Timber.i("Parsed " + hostsSource.getUrl() + " in " + (endTime - startTime) / 1000 + "s");
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable all hosts sources.
|
||||
*
|
||||
* @return {@code true} if at least one source was updated, {@code false} otherwise.
|
||||
*/
|
||||
public boolean enableAllSources() {
|
||||
boolean updated = false;
|
||||
for (HostsSource source : this.hostsSourceDao.getAll()) {
|
||||
if (!source.isEnabled()) {
|
||||
this.hostsSourceDao.toggleEnabled(source);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
private void setState(@StringRes int stateResId, Object... details) {
|
||||
String state = this.context.getString(stateResId, details);
|
||||
Timber.d("Source model state: %s.", state);
|
||||
this.state.postValue(state);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package org.adaway.model.source;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
import org.adaway.helper.NotificationHelper;
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
|
||||
import static androidx.work.ExistingPeriodicWorkPolicy.KEEP;
|
||||
import static androidx.work.ExistingPeriodicWorkPolicy.UPDATE;
|
||||
import static androidx.work.ListenableWorker.Result.failure;
|
||||
import static androidx.work.ListenableWorker.Result.retry;
|
||||
import static androidx.work.ListenableWorker.Result.success;
|
||||
import static java.util.concurrent.TimeUnit.HOURS;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a service to check for hosts sources update.<br/>
|
||||
* It could be enabled or disabled for periodic check.<br>
|
||||
* The implementation is based on WorkManager from Android X.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class SourceUpdateService {
|
||||
/**
|
||||
* The name of the periodic work.
|
||||
*/
|
||||
private static final String WORK_NAME = "HostsUpdateWork";
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private SourceUpdateService() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable update service.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param unmeteredNetworkOnly <code>true</code> if the update should be done on unmetered network only, <code>false</code> otherwise.
|
||||
*/
|
||||
public static void enable(Context context, boolean unmeteredNetworkOnly) {
|
||||
enqueueWork(context, UPDATE, unmeteredNetworkOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable update service.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void disable(Context context) {
|
||||
// Cancel previous work
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync service on user preferences.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
static void syncPreferences(Context context) {
|
||||
if (PreferenceHelper.getUpdateCheckHostsDaily(context)) {
|
||||
enqueueWork(context, KEEP, PreferenceHelper.getUpdateOnlyOnWifi(context));
|
||||
} else {
|
||||
disable(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void enqueueWork(Context context, ExistingPeriodicWorkPolicy workPolicy, boolean unmeteredNetworkOnly) {
|
||||
// Create work request
|
||||
PeriodicWorkRequest workRequest = getWorkRequest(unmeteredNetworkOnly);
|
||||
// Enqueue work request
|
||||
WorkManager workManager = WorkManager.getInstance(context);
|
||||
workManager.enqueueUniquePeriodicWork(WORK_NAME, workPolicy, workRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create source update work request.
|
||||
*
|
||||
* @param unmeteredNetworkOnly <code>true</code> if the update should be done on unmetered network only, <code>false</code> otherwise.
|
||||
* @return The source update work request to queue.
|
||||
*/
|
||||
private static PeriodicWorkRequest getWorkRequest(boolean unmeteredNetworkOnly) {
|
||||
// Create worker constraints
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(unmeteredNetworkOnly ? NetworkType.UNMETERED : NetworkType.CONNECTED)
|
||||
.setRequiresStorageNotLow(true)
|
||||
.build();
|
||||
// Create work request
|
||||
return new PeriodicWorkRequest.Builder(HostsSourcesUpdateWorker.class, 6, HOURS)
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(3, HOURS)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a {@link Worker} to fetch hosts sources updates and install them if needed.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public static class HostsSourcesUpdateWorker extends Worker {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param workerParams The parameters to setup this worker.
|
||||
*/
|
||||
public HostsSourcesUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Timber.i("Starting update worker");
|
||||
// Create model
|
||||
AdAwayApplication application = (AdAwayApplication) getApplicationContext();
|
||||
SourceModel model = application.getSourceModel();
|
||||
// Check for update
|
||||
boolean hasUpdate;
|
||||
try {
|
||||
hasUpdate = model.checkForUpdate();
|
||||
} catch (HostErrorException exception) {
|
||||
// An error occurred, check will be retried
|
||||
Timber.e(exception, "Failed to check for update. Will retry later.");
|
||||
return retry();
|
||||
}
|
||||
if (hasUpdate) {
|
||||
// Do update
|
||||
try {
|
||||
doUpdate(application);
|
||||
} catch (HostErrorException exception) {
|
||||
// Installation failed. Worker failed.
|
||||
Timber.e(exception, "Failed to apply hosts file during background update.");
|
||||
return failure();
|
||||
}
|
||||
}
|
||||
// Return as success
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update according user preferences.
|
||||
*
|
||||
* @param application The application.
|
||||
* @throws HostErrorException If the update could not be handled.
|
||||
*/
|
||||
private void doUpdate(AdAwayApplication application) throws HostErrorException {
|
||||
// Check if automatic update are enabled
|
||||
if (PreferenceHelper.getAutomaticUpdateDaily(application)) {
|
||||
// Retrieve source updates
|
||||
SourceModel sourceModel = application.getSourceModel();
|
||||
sourceModel.retrieveHostsSources();
|
||||
// Apply source updates
|
||||
AdBlockModel adBlockModel = application.getAdBlockModel();
|
||||
adBlockModel.apply();
|
||||
} else {
|
||||
// Display update notification
|
||||
NotificationHelper.showUpdateHostsNotification(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package org.adaway.model.update;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import static android.content.Intent.ACTION_INSTALL_PACKAGE;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a {@link BroadcastReceiver} to install downloaded application updates.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class ApkDownloadReceiver extends BroadcastReceiver {
|
||||
private final long downloadId;
|
||||
|
||||
public ApkDownloadReceiver(long downloadId) {
|
||||
this.downloadId = downloadId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
//Fetching the download id received with the broadcast
|
||||
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
|
||||
//Checking if the received broadcast is for our enqueued download by matching download id
|
||||
if (this.downloadId == id) {
|
||||
DownloadManager downloadManager = context.getSystemService(DownloadManager.class);
|
||||
Uri apkUri = downloadManager.getUriForDownloadedFile(id);
|
||||
if (apkUri == null) {
|
||||
Timber.w("Failed to download id: %s.", id);
|
||||
} else {
|
||||
installApk(context, apkUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void installApk(Context context, Uri apkUri) {
|
||||
Intent install = new Intent(ACTION_INSTALL_PACKAGE);
|
||||
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
install.setData(apkUri);
|
||||
context.startActivity(install);
|
||||
}
|
||||
}
|
||||
112
app/src/main/java/org/adaway/model/update/ApkUpdateService.java
Normal file
112
app/src/main/java/org/adaway/model/update/ApkUpdateService.java
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package org.adaway.model.update;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
import org.adaway.helper.NotificationHelper;
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
|
||||
import static androidx.work.ExistingPeriodicWorkPolicy.KEEP;
|
||||
import static androidx.work.ExistingPeriodicWorkPolicy.UPDATE;
|
||||
import static androidx.work.ListenableWorker.Result.success;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a service to check for application update.<br/>
|
||||
* It could be enabled or disabled for periodic check.<br>
|
||||
* The implementation is based on WorkManager from Android X.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class ApkUpdateService {
|
||||
/**
|
||||
* The name of the periodic work.
|
||||
*/
|
||||
private static final String WORK_NAME = "ApkUpdateWork";
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private ApkUpdateService() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable update service.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void enable(Context context) {
|
||||
enqueueWork(context, UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable update service.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void disable(Context context) {
|
||||
// Cancel previous work
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME);
|
||||
}
|
||||
|
||||
static void syncPreferences(Context context) {
|
||||
if (PreferenceHelper.getUpdateCheckAppDaily(context)) {
|
||||
enqueueWork(context, KEEP);
|
||||
} else {
|
||||
disable(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void enqueueWork(Context context, ExistingPeriodicWorkPolicy workPolicy) {
|
||||
// Create work request
|
||||
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(ApkUpdateWorker.class, 1, DAYS).build();
|
||||
// Enqueue work request
|
||||
WorkManager workManager = WorkManager.getInstance(context);
|
||||
workManager.enqueueUniquePeriodicWork(WORK_NAME, workPolicy, workRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a {@link Worker} to check for application update and notify them if needed.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public static class ApkUpdateWorker extends Worker {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param workerParams The parameters to setup this worker.
|
||||
*/
|
||||
public ApkUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
||||
super(context, workerParams);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
Timber.i("Starting update worker");
|
||||
// Create model
|
||||
AdAwayApplication application = (AdAwayApplication) getApplicationContext();
|
||||
UpdateModel model = application.getUpdateModel();
|
||||
// Check for update
|
||||
model.checkForUpdate();
|
||||
Manifest manifest = model.getManifest().getValue();
|
||||
if (manifest != null && manifest.updateAvailable) {
|
||||
// Display update notification
|
||||
NotificationHelper.showUpdateApplicationNotification(application);
|
||||
}
|
||||
// Return as success
|
||||
return success();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/org/adaway/model/update/Manifest.java
Normal file
24
app/src/main/java/org/adaway/model/update/Manifest.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package org.adaway.model.update;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* This class is represent an application manifest.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class Manifest {
|
||||
public final String version;
|
||||
public final int versionCode;
|
||||
public final String changelog;
|
||||
public final boolean updateAvailable;
|
||||
|
||||
public Manifest(String manifest, long currentVersionCode) throws JSONException {
|
||||
JSONObject manifestObject = new JSONObject(manifest);
|
||||
this.version = manifestObject.getString("version");
|
||||
this.versionCode = manifestObject.getInt("versionCode");
|
||||
this.changelog = manifestObject.getString("changelog");
|
||||
this.updateAvailable = this.versionCode > currentVersionCode;
|
||||
}
|
||||
}
|
||||
203
app/src/main/java/org/adaway/model/update/UpdateModel.java
Normal file
203
app/src/main/java/org/adaway/model/update/UpdateModel.java
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package org.adaway.model.update;
|
||||
|
||||
import static android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static org.adaway.model.update.UpdateStore.getApkStore;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is the model in charge of updating the application.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class UpdateModel {
|
||||
private static final String MANIFEST_URL = "https://app.adaway.org/manifest.json";
|
||||
private static final String DOWNLOAD_URL = "https://app.adaway.org/adaway.apk?versionCode=";
|
||||
private final Context context;
|
||||
private final VersionInfo versionInfo;
|
||||
private final OkHttpClient client;
|
||||
private final MutableLiveData<Manifest> manifest;
|
||||
private ApkDownloadReceiver receiver;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public UpdateModel(Context context) {
|
||||
this.context = context;
|
||||
this.versionInfo = VersionInfo.get(context);
|
||||
this.manifest = new MutableLiveData<>();
|
||||
this.client = buildHttpClient();
|
||||
ApkUpdateService.syncPreferences(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current version code.
|
||||
*
|
||||
* @return The current version code.
|
||||
*/
|
||||
public int getVersionCode() {
|
||||
return this.versionInfo.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current version name.
|
||||
*
|
||||
* @return The current version name.
|
||||
*/
|
||||
public String getVersionName() {
|
||||
return this.versionInfo.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last version manifest.
|
||||
*
|
||||
* @return The last version manifest.
|
||||
*/
|
||||
public LiveData<Manifest> getManifest() {
|
||||
return this.manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application update store.
|
||||
*
|
||||
* @return The application update store.
|
||||
*/
|
||||
public UpdateStore getStore() {
|
||||
return getApkStore(this.context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application update channel.
|
||||
*
|
||||
* @return The application update channel.
|
||||
*/
|
||||
public String getChannel() {
|
||||
return PreferenceHelper.getIncludeBetaReleases(this.context) ? "beta" : "stable";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is an update available.
|
||||
*/
|
||||
public void checkForUpdate() {
|
||||
Manifest manifest = downloadManifest();
|
||||
// Notify update
|
||||
if (manifest != null) {
|
||||
this.manifest.postValue(manifest);
|
||||
}
|
||||
}
|
||||
|
||||
private OkHttpClient buildHttpClient() {
|
||||
return new OkHttpClient.Builder().build();
|
||||
}
|
||||
|
||||
private Manifest downloadManifest() {
|
||||
if (!this.versionInfo.isValid()) {
|
||||
return null;
|
||||
}
|
||||
HttpUrl httpUrl = requireNonNull(HttpUrl.parse(MANIFEST_URL), "Failed to parse manifest URL")
|
||||
.newBuilder()
|
||||
.addQueryParameter("versionCode", Integer.toString(this.versionInfo.code))
|
||||
.addQueryParameter("sdkCode", Integer.toString(SDK_INT))
|
||||
.addQueryParameter("channel", getChannel())
|
||||
.addQueryParameter("store", getStore().getName())
|
||||
.build();
|
||||
Request request = new Request.Builder()
|
||||
.url(httpUrl)
|
||||
.build();
|
||||
try (Response execute = this.client.newCall(request).execute();
|
||||
ResponseBody body = execute.body()) {
|
||||
if (execute.isSuccessful() && body != null) {
|
||||
return new Manifest(body.string(), this.versionInfo.code);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException | JSONException exception) {
|
||||
Timber.e(exception, "Unable to download manifest.");
|
||||
// Return failed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the application to the latest version.
|
||||
*
|
||||
* @return The download identifier ({@code -1} if download was not started).
|
||||
*/
|
||||
public long update() {
|
||||
// Check manifest
|
||||
Manifest manifest = this.manifest.getValue();
|
||||
if (manifest == null) {
|
||||
return -1;
|
||||
}
|
||||
// Check previous broadcast receiver
|
||||
if (this.receiver != null) {
|
||||
this.context.unregisterReceiver(this.receiver);
|
||||
}
|
||||
// Queue download
|
||||
long downloadId = download(manifest);
|
||||
// Register new broadcast receiver
|
||||
this.receiver = new ApkDownloadReceiver(downloadId);
|
||||
this.context.registerReceiver(this.receiver, new IntentFilter(ACTION_DOWNLOAD_COMPLETE));
|
||||
// Return download identifier
|
||||
return downloadId;
|
||||
}
|
||||
|
||||
private long download(Manifest manifest) {
|
||||
Timber.i("Downloading " + manifest.version + ".");
|
||||
Uri uri = Uri.parse(DOWNLOAD_URL + manifest.versionCode);
|
||||
DownloadManager.Request request = new DownloadManager.Request(uri)
|
||||
.setTitle("AdAway " + manifest.version)
|
||||
.setDescription(this.context.getString(R.string.update_notification_description));
|
||||
DownloadManager downloadManager = this.context.getSystemService(DownloadManager.class);
|
||||
return downloadManager.enqueue(request);
|
||||
}
|
||||
|
||||
private static class VersionInfo {
|
||||
private final int code;
|
||||
private final String name;
|
||||
|
||||
private VersionInfo(int code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static VersionInfo get(Context context) {
|
||||
try {
|
||||
PackageInfo packageInfo = context.getPackageManager()
|
||||
.getPackageInfo(context.getPackageName(), 0);
|
||||
return new VersionInfo(packageInfo.versionCode, packageInfo.versionName);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return new VersionInfo(0, "development");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return this.code > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
120
app/src/main/java/org/adaway/model/update/UpdateStore.java
Normal file
120
app/src/main/java/org/adaway/model/update/UpdateStore.java
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package org.adaway.model.update;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import static android.content.pm.PackageManager.GET_SIGNATURES;
|
||||
import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.Build.VERSION_CODES.P;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This enumerates represents the stores to get AdAway updates.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public enum UpdateStore {
|
||||
/**
|
||||
* The official store (usually GitHub releases) with AdAway signing key.
|
||||
*/
|
||||
ADAWAY("adaway", "D647FDAC42961502AC78F99919B8E1901747E8DA78FE13E1EABA688FECC4C99E"),
|
||||
/**
|
||||
* The F-Droid store with F-Droid signing key.
|
||||
*/
|
||||
F_DROID("fdroid", "42203F1AC857426D1496E971DB96FBE1F88C25C9E1F895A5C98D703891292277"),
|
||||
/**
|
||||
* An unknown store.
|
||||
*/
|
||||
UNKNOWN("unknown", "");
|
||||
/**
|
||||
* The store name.
|
||||
*/
|
||||
public final String storeName;
|
||||
/**
|
||||
* The store singing certificate digest.
|
||||
*/
|
||||
public final String sign;
|
||||
|
||||
UpdateStore(String name, String sign) {
|
||||
this.storeName = name;
|
||||
this.sign = sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store of the running application.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @return The application store, {@link #UNKNOWN} if store can't be defined.
|
||||
*/
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
public static UpdateStore getApkStore(Context context) {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
String packageName = context.getPackageName();
|
||||
Signature[] signatures;
|
||||
try {
|
||||
if (SDK_INT >= P) {
|
||||
signatures = packageManager.getPackageInfo(
|
||||
packageName,
|
||||
GET_SIGNING_CERTIFICATES
|
||||
).signingInfo.getSigningCertificateHistory();
|
||||
} else {
|
||||
// Signatures are not used for security reason. Only to guess the flavor of the app.
|
||||
signatures = packageManager.getPackageInfo(
|
||||
packageName,
|
||||
GET_SIGNATURES
|
||||
).signatures;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Timber.w(e, "Failed to get application package info.");
|
||||
return UpdateStore.UNKNOWN;
|
||||
}
|
||||
return UpdateStore.getFromSigns(signatures);
|
||||
}
|
||||
|
||||
private static UpdateStore getFromSigns(Signature[] signatures) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
for (Signature signature : signatures) {
|
||||
md.update(signature.toByteArray());
|
||||
String sign = bytesToHex(md.digest());
|
||||
for (UpdateStore store : UpdateStore.values()) {
|
||||
if (store.sign.equals(sign)) {
|
||||
return store;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Timber.w(e, "SHA-256 algorithm is no supported.");
|
||||
}
|
||||
return UpdateStore.UNKNOWN;
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
|
||||
'9', 'A', 'B', 'C', 'D', 'E', 'F'};
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
int v;
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store name.
|
||||
*
|
||||
* @return The store name.
|
||||
*/
|
||||
public String getName() {
|
||||
return this.storeName;
|
||||
}
|
||||
}
|
||||
124
app/src/main/java/org/adaway/model/vpn/VpnModel.java
Normal file
124
app/src/main/java/org/adaway/model/vpn/VpnModel.java
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package org.adaway.model.vpn;
|
||||
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.VPN;
|
||||
import static org.adaway.model.error.HostError.ENABLE_VPN_FAIL;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.LruCache;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostEntryDao;
|
||||
import org.adaway.db.entity.HostEntry;
|
||||
import org.adaway.model.adblocking.AdBlockMethod;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.vpn.VpnServiceControls;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is the model to represent VPN service configuration.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class VpnModel extends AdBlockModel {
|
||||
private final HostEntryDao hostEntryDao;
|
||||
private final LruCache<String, HostEntry> blockCache;
|
||||
private final LinkedHashSet<String> logs;
|
||||
private boolean recordingLogs;
|
||||
private int requestCount;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public VpnModel(Context context) {
|
||||
super(context);
|
||||
AppDatabase database = AppDatabase.getInstance(context);
|
||||
this.hostEntryDao = database.hostEntryDao();
|
||||
this.blockCache = new LruCache<String, HostEntry>(4 * 1024) {
|
||||
@Override
|
||||
protected HostEntry create(String key) {
|
||||
return VpnModel.this.hostEntryDao.getEntry(key);
|
||||
}
|
||||
};
|
||||
this.logs = new LinkedHashSet<>();
|
||||
this.recordingLogs = false;
|
||||
this.requestCount = 0;
|
||||
this.applied.postValue(VpnServiceControls.isRunning(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdBlockMethod getMethod() {
|
||||
return VPN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() throws HostErrorException {
|
||||
// Clear cache
|
||||
this.blockCache.evictAll();
|
||||
// Start VPN
|
||||
boolean started = VpnServiceControls.start(this.context);
|
||||
this.applied.postValue(started);
|
||||
if (!started) {
|
||||
throw new HostErrorException(ENABLE_VPN_FAIL);
|
||||
}
|
||||
setState(R.string.status_vpn_configuration_updated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revert() {
|
||||
VpnServiceControls.stop(this.context);
|
||||
this.applied.postValue(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecordingLogs() {
|
||||
return this.recordingLogs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRecordingLogs(boolean recording) {
|
||||
this.recordingLogs = recording;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLogs() {
|
||||
return new ArrayList<>(this.logs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearLogs() {
|
||||
this.logs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks host entry related to an host name.
|
||||
*
|
||||
* @param host A hostname to check.
|
||||
* @return The related host entry.
|
||||
*/
|
||||
public HostEntry getEntry(String host) {
|
||||
// Compute miss rate periodically
|
||||
this.requestCount++;
|
||||
if (this.requestCount >= 1000) {
|
||||
int hits = this.blockCache.hitCount();
|
||||
int misses = this.blockCache.missCount();
|
||||
double missRate = 100D * (hits + misses) / misses;
|
||||
Timber.d("Host cache miss rate: %s.", missRate);
|
||||
this.requestCount = 0;
|
||||
}
|
||||
// Add host to logs
|
||||
if (this.recordingLogs) {
|
||||
this.logs.add(host);
|
||||
}
|
||||
// Check cache
|
||||
return this.blockCache.get(host);
|
||||
}
|
||||
}
|
||||
81
app/src/main/java/org/adaway/tile/AdBlockingTileService.java
Normal file
81
app/src/main/java/org/adaway/tile/AdBlockingTileService.java
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package org.adaway.tile;
|
||||
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static android.service.quicksettings.Tile.STATE_ACTIVE;
|
||||
import static android.service.quicksettings.Tile.STATE_INACTIVE;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is a {@link TileService} to toggle ad-blocking.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AdBlockingTileService extends TileService {
|
||||
private final AtomicBoolean toggling = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void onTileAdded() {
|
||||
boolean adBlocked = getModel().isApplied().getValue() == Boolean.TRUE;
|
||||
updateTile(adBlocked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartListening() {
|
||||
LiveData<Boolean> applied = getModel().isApplied();
|
||||
applied.observeForever(this::updateTile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopListening() {
|
||||
LiveData<Boolean> applied = getModel().isApplied();
|
||||
applied.removeObserver(this::updateTile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
AppExecutors.getInstance()
|
||||
.diskIO()
|
||||
.execute(this::toggleAdBlocking);
|
||||
}
|
||||
|
||||
private void updateTile(boolean adBlocked) {
|
||||
Tile tile = getQsTile();
|
||||
tile.setState(adBlocked ? STATE_ACTIVE : STATE_INACTIVE);
|
||||
tile.updateTile();
|
||||
}
|
||||
|
||||
private void toggleAdBlocking() {
|
||||
if (this.toggling.get()) {
|
||||
return;
|
||||
}
|
||||
AdBlockModel model = getModel();
|
||||
try {
|
||||
this.toggling.set(true);
|
||||
if (model.isApplied().getValue() == Boolean.TRUE) {
|
||||
model.revert();
|
||||
} else {
|
||||
model.apply();
|
||||
}
|
||||
} catch (HostErrorException e) {
|
||||
Timber.w(e, "Failed to toggle ad-blocking.");
|
||||
} finally {
|
||||
this.toggling.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private AdBlockModel getModel() {
|
||||
return ((AdAwayApplication) getApplication()).getAdBlockModel();
|
||||
}
|
||||
}
|
||||
104
app/src/main/java/org/adaway/ui/Animations.java
Normal file
104
app/src/main/java/org/adaway/ui/Animations.java
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package org.adaway.ui;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.view.View;
|
||||
|
||||
import static android.view.View.ALPHA;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
/**
|
||||
* This class is an utility class to animate views.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public final class Animations {
|
||||
|
||||
private Animations() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate view to be shown.
|
||||
*
|
||||
* @param view The view to animate.
|
||||
*/
|
||||
public static void showView(View view) {
|
||||
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, ALPHA, 1F);
|
||||
objectAnimator.setAutoCancel(true);
|
||||
objectAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
view.setVisibility(VISIBLE);
|
||||
}
|
||||
});
|
||||
objectAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate view to be hidden.
|
||||
*
|
||||
* @param view The view to animate.
|
||||
*/
|
||||
public static void hideView(View view) {
|
||||
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, ALPHA, 0F);
|
||||
objectAnimator.setAutoCancel(true);
|
||||
objectAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(INVISIBLE);
|
||||
}
|
||||
});
|
||||
objectAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate view to be removed.
|
||||
*
|
||||
* @param view The view to animate.
|
||||
*/
|
||||
public static void removeView(View view) {
|
||||
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, ALPHA, 0F);
|
||||
objectAnimator.setAutoCancel(true);
|
||||
objectAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(GONE);
|
||||
}
|
||||
});
|
||||
objectAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately set view to shown state.
|
||||
*
|
||||
* @param view The view to set.
|
||||
*/
|
||||
public static void setShown(View view) {
|
||||
view.setVisibility(VISIBLE);
|
||||
view.setAlpha(1f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately set view to hidden state.
|
||||
*
|
||||
* @param view The view to set.
|
||||
*/
|
||||
public static void setHidden(View view) {
|
||||
view.setVisibility(INVISIBLE);
|
||||
view.setAlpha(0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately set view to gone state.
|
||||
*
|
||||
* @param view The view to set.
|
||||
*/
|
||||
public static void setRemoved(View view) {
|
||||
view.setVisibility(GONE);
|
||||
view.setAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package org.adaway.ui.adblocking;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
import org.adaway.R;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.model.source.SourceModel;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
|
||||
|
||||
/**
|
||||
* This class is a {@link Snackbar} to notify about adblock model new configuration to apply.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class ApplyConfigurationSnackbar {
|
||||
/**
|
||||
* The view to bind the snackbar to.
|
||||
*/
|
||||
private final View view;
|
||||
/**
|
||||
* The notify snackbar when hosts update available.
|
||||
*/
|
||||
private final Snackbar notifySnackbar;
|
||||
/**
|
||||
* The wait snackbar during hosts install.
|
||||
*/
|
||||
private final Snackbar waitSnackbar;
|
||||
/**
|
||||
* To synchronize sources before installing or not.
|
||||
*/
|
||||
private final boolean syncSources;
|
||||
/**
|
||||
* The current hosts update available status ({@code true} if update available, {@code false} otherwise).
|
||||
*/
|
||||
private boolean update;
|
||||
/**
|
||||
* Whether or not ignore the next update event ({@code true} to ignore, {@code false} otherwise).
|
||||
*/
|
||||
private boolean skipUpdate;
|
||||
/**
|
||||
* Whether or not ignore update events during the install ({@code true} to ignore, {@code false} otherwise).
|
||||
*/
|
||||
private boolean ignoreEventDuringInstall;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param view The view to bind the snackbar to.
|
||||
* @param syncSources To synchronize sources before installing or not.
|
||||
* @param ignoreEventDuringInstall {@code true} to ignore events, {@code false} otherwise.
|
||||
*/
|
||||
public ApplyConfigurationSnackbar(@NonNull View view, boolean syncSources, boolean ignoreEventDuringInstall) {
|
||||
this.view = view;
|
||||
this.notifySnackbar = Snackbar.make(this.view, R.string.notification_configuration_changed, LENGTH_INDEFINITE)
|
||||
.setAction(R.string.notification_configuration_changed_action, v -> apply());
|
||||
this.waitSnackbar = Snackbar.make(this.view, R.string.notification_configuration_installing, LENGTH_INDEFINITE);
|
||||
appendViewToSnackbar(this.waitSnackbar, new ProgressBar(this.view.getContext()));
|
||||
this.syncSources = syncSources;
|
||||
this.ignoreEventDuringInstall = ignoreEventDuringInstall;
|
||||
this.update = false;
|
||||
this.skipUpdate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link Observer} which ignores first (initialization) event.
|
||||
*
|
||||
* @param <T> The type of data to observe.
|
||||
* @return The observer instance.
|
||||
*/
|
||||
public <T> Observer<T> createObserver() {
|
||||
return new Observer<T>() {
|
||||
boolean firstUpdate = true;
|
||||
|
||||
@Override
|
||||
public void onChanged(@Nullable T t) {
|
||||
// Check new data
|
||||
if (t == null || (t instanceof Collection && ((Collection<?>) t).isEmpty())) {
|
||||
return;
|
||||
}
|
||||
// First update
|
||||
if (this.firstUpdate) {
|
||||
this.firstUpdate = false;
|
||||
return;
|
||||
}
|
||||
ApplyConfigurationSnackbar.this.notifyUpdateAvailable();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify update available.
|
||||
*/
|
||||
public void notifyUpdateAvailable() {
|
||||
// Check if notify snackbar is already displayed
|
||||
if (this.notifySnackbar.isShown()) {
|
||||
return;
|
||||
}
|
||||
// Check if wait snackbar is displayed
|
||||
if (this.waitSnackbar.isShown()) {
|
||||
// Mark update available
|
||||
this.update = true;
|
||||
return;
|
||||
}
|
||||
// Check if update event should be skipped
|
||||
if (this.skipUpdate) {
|
||||
this.skipUpdate = false;
|
||||
return;
|
||||
}
|
||||
// Show notify snackbar
|
||||
this.notifySnackbar.show();
|
||||
// Mark update as notified
|
||||
this.update = false;
|
||||
}
|
||||
|
||||
private void apply() {
|
||||
showLoading();
|
||||
AppExecutors.getInstance().diskIO().execute(() -> {
|
||||
AdAwayApplication application = (AdAwayApplication) this.view.getContext().getApplicationContext();
|
||||
SourceModel sourceModel = application.getSourceModel();
|
||||
AdBlockModel adBlockModel = application.getAdBlockModel();
|
||||
try {
|
||||
if (this.syncSources) {
|
||||
sourceModel.retrieveHostsSources();
|
||||
} else {
|
||||
sourceModel.syncHostEntries();
|
||||
}
|
||||
adBlockModel.apply();
|
||||
endLoading(true);
|
||||
} catch (HostErrorException exception) {
|
||||
endLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showLoading() {
|
||||
// Clear notify snackbar
|
||||
this.notifySnackbar.dismiss();
|
||||
// Show wait snackbar
|
||||
this.waitSnackbar.show();
|
||||
}
|
||||
|
||||
private void endLoading(boolean successfulInstall) {
|
||||
// Ensure the snackbar has time to display
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
// Clear snackbars
|
||||
this.waitSnackbar.dismiss();
|
||||
// Check install failure
|
||||
if (!successfulInstall) {
|
||||
Snackbar failureSnackbar = Snackbar.make(this.view, R.string.notification_configuration_failed, LENGTH_LONG);
|
||||
ImageView view = new ImageView(this.view.getContext());
|
||||
view.setImageResource(R.drawable.ic_error_outline_24dp);
|
||||
appendViewToSnackbar(failureSnackbar, view);
|
||||
failureSnackbar.show();
|
||||
}
|
||||
// Check pending update notification
|
||||
else if (this.update) {
|
||||
// Ignore next update event if events should be ignored
|
||||
if (this.ignoreEventDuringInstall) {
|
||||
this.skipUpdate = true;
|
||||
} else {
|
||||
// Otherwise display update notification
|
||||
notifyUpdateAvailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendViewToSnackbar(Snackbar snackbar, View view) {
|
||||
ViewGroup viewGroup = (ViewGroup) snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_text).getParent();
|
||||
viewGroup.addView(view);
|
||||
}
|
||||
}
|
||||
145
app/src/main/java/org/adaway/ui/adware/AdwareFragment.java
Normal file
145
app/src/main/java/org/adaway/ui/adware/AdwareFragment.java
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.adware;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.adaway.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class is a {@link Fragment} to scan and uninstall adware.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AdwareFragment extends Fragment {
|
||||
/**
|
||||
* The adware install list view.
|
||||
*/
|
||||
private ListView mListView;
|
||||
/**
|
||||
* The status text.
|
||||
*/
|
||||
private TextView mStatusText;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
// Create fragment view
|
||||
View view = inflater.inflate(R.layout.adware_fragment, container, false);
|
||||
// Get list view
|
||||
this.mListView = view.findViewById(R.id.adware_list);
|
||||
// Bind list onclick listener
|
||||
this.mListView.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
// Get clicked adware
|
||||
AdwareInstall adwareInstall = (AdwareInstall) parent.getItemAtPosition(position);
|
||||
// Uninstall adware
|
||||
AdwareFragment.this.uninstallAdware(adwareInstall);
|
||||
});
|
||||
// Get status text
|
||||
this.mStatusText = view.findViewById(R.id.adware_status_text);
|
||||
/*
|
||||
* Get model and bind it to view.
|
||||
*/
|
||||
// Get the model scope
|
||||
FragmentActivity activity = requireActivity();
|
||||
// Get the model
|
||||
AdwareViewModel model = new ViewModelProvider(activity).get(AdwareViewModel.class);
|
||||
// Bind model to views
|
||||
model.getAdware().observe(getViewLifecycleOwner(), data -> {
|
||||
if (data == null) {
|
||||
this.displayStatusText(R.string.adware_scanning);
|
||||
} else if (data.isEmpty()) {
|
||||
this.displayStatusText(R.string.adware_empty);
|
||||
} else {
|
||||
this.displayAdware(data);
|
||||
}
|
||||
});
|
||||
// Return created view
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a status text.
|
||||
*
|
||||
* @param text The status text to display.
|
||||
*/
|
||||
private void displayStatusText(int text) {
|
||||
// Set text
|
||||
this.mStatusText.setText(text);
|
||||
// Show the text
|
||||
this.mStatusText.setVisibility(View.VISIBLE);
|
||||
this.mListView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the installed adware.
|
||||
*
|
||||
* @param data The adware to show.
|
||||
*/
|
||||
private void displayAdware(List<AdwareInstall> data) {
|
||||
// Create adapter
|
||||
String[] from = new String[]{
|
||||
AdwareInstall.APPLICATION_NAME_KEY,
|
||||
AdwareInstall.PACKAGE_NAME_KEY
|
||||
};
|
||||
int[] to = new int[]{
|
||||
R.id.checkbox_list_text,
|
||||
R.id.checkbox_list_subtext
|
||||
};
|
||||
SimpleAdapter adapter = new SimpleAdapter(this.getContext(),
|
||||
data,
|
||||
R.layout.list_two_entries,
|
||||
from,
|
||||
to
|
||||
);
|
||||
// Update list
|
||||
adapter.notifyDataSetChanged();
|
||||
// Show the list
|
||||
this.mListView.setAdapter(adapter);
|
||||
this.mStatusText.setVisibility(View.GONE);
|
||||
this.mListView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall adware.
|
||||
*
|
||||
* @param adwareInstall The adware to uninstall.
|
||||
*/
|
||||
private void uninstallAdware(AdwareInstall adwareInstall) {
|
||||
Intent intent = new Intent(Intent.ACTION_DELETE);
|
||||
intent.setData(Uri.parse("package:" + adwareInstall.get(AdwareInstall.PACKAGE_NAME_KEY)));
|
||||
this.startActivity(intent);
|
||||
}
|
||||
}
|
||||
43
app/src/main/java/org/adaway/ui/adware/AdwareInstall.java
Normal file
43
app/src/main/java/org/adaway/ui/adware/AdwareInstall.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package org.adaway.ui.adware;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* This class is a POJO to represent an installed adware.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class AdwareInstall extends HashMap<String, String> implements Comparable<AdwareInstall> {
|
||||
/**
|
||||
* The adware application name.
|
||||
*/
|
||||
final static String APPLICATION_NAME_KEY = "app_name";
|
||||
/**
|
||||
* The adware package name.
|
||||
*/
|
||||
final static String PACKAGE_NAME_KEY = "package_name";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param applicationName The adware application name.
|
||||
* @param packageName The adware package name.
|
||||
*/
|
||||
AdwareInstall(String applicationName, String packageName) {
|
||||
super(2);
|
||||
this.put(AdwareInstall.APPLICATION_NAME_KEY, applicationName);
|
||||
this.put(AdwareInstall.PACKAGE_NAME_KEY, packageName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull AdwareInstall other) {
|
||||
int nameComparison = this.get(AdwareInstall.APPLICATION_NAME_KEY).compareTo(other.get(AdwareInstall.APPLICATION_NAME_KEY));
|
||||
if (nameComparison == 0) {
|
||||
return this.get(AdwareInstall.PACKAGE_NAME_KEY).compareTo(other.get(AdwareInstall.PACKAGE_NAME_KEY));
|
||||
} else {
|
||||
return nameComparison;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
app/src/main/java/org/adaway/ui/adware/AdwareLiveData.java
Normal file
154
app/src/main/java/org/adaway/ui/adware/AdwareLiveData.java
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package org.adaway.ui.adware;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is {@link LiveData} to represents installed adware on device.
|
||||
*/
|
||||
class AdwareLiveData extends LiveData<List<AdwareInstall>> {
|
||||
/**
|
||||
* The adware package prefixes.
|
||||
*/
|
||||
private static final String[] AD_PACKAGE_PREFIXES = {
|
||||
"com.airpush.",
|
||||
"com.adnotify.",
|
||||
"com.appbucks.sdk.",
|
||||
"com.appenda.",
|
||||
"com.applovin.",
|
||||
"com.iac.notification.",
|
||||
"com.inmobi.",
|
||||
"com.Leadbolt.",
|
||||
"com.sellaring.",
|
||||
"com.senddroid.",
|
||||
"com.tapjoy.",
|
||||
"cn.kuguo."
|
||||
};
|
||||
|
||||
/**
|
||||
* The application context.
|
||||
*/
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
AdwareLiveData(Context context) {
|
||||
this.context = context;
|
||||
AppExecutors.getInstance().diskIO().execute(this::loadData);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void loadData() {
|
||||
// Get the package manager
|
||||
PackageManager pm = this.context.getPackageManager();
|
||||
// Get the adware packages
|
||||
List<PackageInfo> adwarePackages = this.getAdwarePackages(pm);
|
||||
// Create related adware installs
|
||||
List<AdwareInstall> adwareInstalls = adwarePackages.stream()
|
||||
.map(this::createInstallFromPackageInfo)
|
||||
.sorted()
|
||||
.collect(toList());
|
||||
// Post loaded adware installs
|
||||
this.postValue(adwareInstalls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all installed packages that look like they include a known ad framework
|
||||
*
|
||||
* @param pm The package manager.
|
||||
* @return The found adware package information.
|
||||
*/
|
||||
private List<PackageInfo> getAdwarePackages(PackageManager pm) {
|
||||
List<PackageInfo> adPackages = new ArrayList<>();
|
||||
// It'd be simpler to just use pm.getInstalledPackages here, but apparently it's broken
|
||||
List<ApplicationInfo> applicationInfoList = pm.getInstalledApplications(0);
|
||||
for (ApplicationInfo applicationInfo : applicationInfoList) {
|
||||
try {
|
||||
// Retrieve package information
|
||||
PackageInfo packageInfo = pm.getPackageInfo(
|
||||
applicationInfo.packageName,
|
||||
PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS | PackageManager.GET_SERVICES
|
||||
);
|
||||
if (this.isAdware(packageInfo)) {
|
||||
adPackages.add(packageInfo);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Timber.e(exception, "An error occurred while scanning applications for adware");
|
||||
}
|
||||
}
|
||||
return adPackages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if application is an adware.
|
||||
*
|
||||
* @param info The application package information.
|
||||
* @return <code>true</code> if the application is an adware, <code>false</code> otherwise.
|
||||
*/
|
||||
private boolean isAdware(PackageInfo info) {
|
||||
// Get package name
|
||||
String packageName = info.packageName;
|
||||
Timber.v("Scanning package %s", packageName);
|
||||
// Check package components
|
||||
boolean matchActivity = info.activities != null && checkComponent(packageName, "activity", info.activities);
|
||||
boolean matchReceiver = info.receivers != null && checkComponent(packageName, "receiver", info.receivers);
|
||||
boolean matchService = info.services != null && checkComponent(packageName, "service", info.services);
|
||||
return matchActivity || matchReceiver || matchService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if an application component match the adware signature.
|
||||
*
|
||||
* @param packageName The application package name.
|
||||
* @param type The component type.
|
||||
* @param info The application components to check.
|
||||
* @return <code>true</code> if a component matches adware signature, <code>false</code> otherwise.
|
||||
*/
|
||||
private boolean checkComponent(String packageName, String type, ComponentInfo[] info) {
|
||||
for (ComponentInfo componentInfo : info) {
|
||||
String componentName = componentInfo.name;
|
||||
Timber.v("[%s] %s", type, componentName);
|
||||
for (String adPackagePrefix : AD_PACKAGE_PREFIXES) {
|
||||
if (componentName.startsWith(adPackagePrefix)) {
|
||||
Timber.i("Detected ad framework prefix %s in package %s as %s %s", adPackagePrefix, packageName, type, componentName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link AdwareInstall} from {@link PackageInfo}.
|
||||
*
|
||||
* @param packageInfo The package info to convert.
|
||||
* @return The related adware install.
|
||||
*/
|
||||
private AdwareInstall createInstallFromPackageInfo(PackageInfo packageInfo) {
|
||||
// Get the package manager
|
||||
PackageManager pm = this.context.getPackageManager();
|
||||
// Retrieve application name
|
||||
String applicationName = pm.getApplicationLabel(packageInfo.applicationInfo).toString();
|
||||
// Add adware install
|
||||
return new AdwareInstall(applicationName, packageInfo.packageName);
|
||||
}
|
||||
}
|
||||
37
app/src/main/java/org/adaway/ui/adware/AdwareViewModel.java
Normal file
37
app/src/main/java/org/adaway/ui/adware/AdwareViewModel.java
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package org.adaway.ui.adware;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* This class is a {@link androidx.lifecycle.ViewModel} for adware UI.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AdwareViewModel extends AndroidViewModel {
|
||||
/**
|
||||
* The install adware.
|
||||
*/
|
||||
private final AdwareLiveData adware;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param application The application context.
|
||||
*/
|
||||
public AdwareViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
this.adware = new AdwareLiveData(application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the installed adware.
|
||||
*
|
||||
* @return The installed adware.
|
||||
*/
|
||||
public AdwareLiveData getAdware() {
|
||||
return this.adware;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package org.adaway.ui.dialog;
|
||||
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.arch.core.util.Function;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.widget.Button;
|
||||
|
||||
/**
|
||||
* This class is a {@link TextWatcher} to validate an alert dialog field.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AlertDialogValidator implements TextWatcher {
|
||||
/**
|
||||
* The button to change status.
|
||||
*/
|
||||
private final Button mButton;
|
||||
/**
|
||||
* The field validator.
|
||||
*/
|
||||
private final Function<String, Boolean> validator;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param dialog The button to change status.
|
||||
* @param validator The field validator.
|
||||
* @param initialState The validation initial state.
|
||||
*/
|
||||
public AlertDialogValidator(AlertDialog dialog, Function<String, Boolean> validator, boolean initialState) {
|
||||
this.mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
this.mButton.setEnabled(initialState);
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String url = s.toString();
|
||||
this.mButton.setEnabled(this.validator.apply(url));
|
||||
}
|
||||
}
|
||||
110
app/src/main/java/org/adaway/ui/dialog/MissingAppDialog.java
Normal file
110
app/src/main/java/org/adaway/ui/dialog/MissingAppDialog.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.dialog;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.adaway.R;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an utility class to help install missing applications.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class MissingAppDialog {
|
||||
/**
|
||||
* Show a dialog to install a text editor.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void showTextEditorMissingDialog(Context context) {
|
||||
showMissingAppDialog(
|
||||
context,
|
||||
R.string.no_text_editor_title,
|
||||
R.string.no_text_editor,
|
||||
"market://details?id=jp.sblo.pandora.jota",
|
||||
"Text Edit"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a dialog to install a file manager.
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
public static void showFileManagerMissingDialog(Context context) {
|
||||
showMissingAppDialog(
|
||||
context,
|
||||
R.string.no_file_manager_title,
|
||||
R.string.no_file_manager,
|
||||
"market://details?id=org.openintents.filemanager",
|
||||
"OI File Manager"
|
||||
);
|
||||
}
|
||||
|
||||
private static void showMissingAppDialog(
|
||||
Context context,
|
||||
@StringRes int title,
|
||||
@StringRes int message,
|
||||
String appGooglePlayUri,
|
||||
String appFdroidQuery
|
||||
) {
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(R.string.button_yes, (dialog, id) -> {
|
||||
Intent intentGooglePlay = new Intent(Intent.ACTION_VIEW);
|
||||
intentGooglePlay.setData(Uri.parse(appGooglePlayUri));
|
||||
|
||||
try {
|
||||
context.startActivity(intentGooglePlay);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Timber.e(e, "No Google Play Store installed!, Trying FDroid...");
|
||||
|
||||
Intent intentFDroid = new Intent(Intent.ACTION_SEARCH);
|
||||
intentFDroid.setComponent(new ComponentName("org.fdroid.fdroid",
|
||||
"org.fdroid.fdroid.SearchResults"));
|
||||
intentFDroid.putExtra(SearchManager.QUERY, appFdroidQuery);
|
||||
|
||||
try {
|
||||
context.startActivity(intentFDroid);
|
||||
} catch (ActivityNotFoundException e2) {
|
||||
Timber.e(e2, "No FDroid installed!");
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.button_no, (dialog, id) -> dialog.dismiss())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
108
app/src/main/java/org/adaway/ui/help/HelpActivity.java
Normal file
108
app/src/main/java/org/adaway/ui/help/HelpActivity.java
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.help;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.helper.ThemeHelper;
|
||||
|
||||
public class HelpActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.applyTheme(this);
|
||||
setContentView(R.layout.help_activity);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
ViewPager2 viewPager = findViewById(R.id.pager);
|
||||
viewPager.setAdapter(new TabsAdapter(this));
|
||||
|
||||
TabLayout tabLayout = findViewById(R.id.tabLayout);
|
||||
|
||||
new TabLayoutMediator(
|
||||
tabLayout,
|
||||
viewPager,
|
||||
(tab, position) -> tab.setText(getTabName(position))
|
||||
).attach();
|
||||
}
|
||||
|
||||
private @StringRes
|
||||
int getTabName(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return R.string.help_tab_faq;
|
||||
case 1:
|
||||
return R.string.help_tab_problems;
|
||||
case 2:
|
||||
return R.string.help_tab_s_on_s_off;
|
||||
default:
|
||||
throw new IllegalStateException("Position " + position + " is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private static class TabsAdapter extends FragmentStateAdapter {
|
||||
private final Fragment faqFragment = HelpFragmentHtml.newInstance(R.raw.help_faq);
|
||||
private final Fragment problemsFragment = HelpFragmentHtml.newInstance(R.raw.help_problems);
|
||||
private final Fragment sonSofFragment = HelpFragmentHtml.newInstance(R.raw.help_s_on_s_off);
|
||||
|
||||
TabsAdapter(@NonNull FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return this.faqFragment;
|
||||
case 1:
|
||||
return this.problemsFragment;
|
||||
case 2:
|
||||
return this.sonSofFragment;
|
||||
default:
|
||||
throw new IllegalStateException("Position " + position + " is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
app/src/main/java/org/adaway/ui/help/HelpFragmentHtml.java
Normal file
96
app/src/main/java/org/adaway/ui/help/HelpFragmentHtml.java
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.help;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RawRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.adaway.R;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_LEGACY;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class HelpFragmentHtml extends Fragment {
|
||||
private static final String TAG = "Help";
|
||||
private static final String ARG_HTML_FILE = "htmlFile";
|
||||
|
||||
/**
|
||||
* Create a new instance of HelpFragmentHtml, providing "htmlFile" as an argument.
|
||||
*/
|
||||
static HelpFragmentHtml newInstance(@RawRes int htmlFile) {
|
||||
HelpFragmentHtml instance = new HelpFragmentHtml();
|
||||
|
||||
// Supply html raw file input as an argument.
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(ARG_HTML_FILE, htmlFile);
|
||||
instance.setArguments(args);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Spanned spanned = new SpannableString("");
|
||||
if (getArguments() != null) {
|
||||
int htmlFile = getArguments().getInt(ARG_HTML_FILE);
|
||||
try {
|
||||
spanned = Html.fromHtml(readHtmlRawFile(htmlFile), FROM_HTML_MODE_LEGACY);
|
||||
} catch (IOException e) {
|
||||
Timber.w("Failed to read help file.");
|
||||
}
|
||||
}
|
||||
|
||||
View view = inflater.inflate(R.layout.help_fragment, container, false);
|
||||
TextView helpTextView = view.findViewById(R.id.helpTextView);
|
||||
helpTextView.setText(spanned);
|
||||
helpTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
return view;
|
||||
}
|
||||
|
||||
private String readHtmlRawFile(@RawRes int resourceId) throws IOException {
|
||||
try (InputStream inputStream = getResources().openRawResource(resourceId);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
content.append(line);
|
||||
}
|
||||
return content.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
366
app/src/main/java/org/adaway/ui/home/HomeActivity.java
Normal file
366
app/src/main/java/org/adaway/ui/home/HomeActivity.java
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
package org.adaway.ui.home;
|
||||
|
||||
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED;
|
||||
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.UNDEFINED;
|
||||
import static org.adaway.model.adblocking.AdBlockMethod.VPN;
|
||||
import static org.adaway.ui.Animations.removeView;
|
||||
import static org.adaway.ui.Animations.showView;
|
||||
import static org.adaway.ui.lists.ListsActivity.ALLOWED_HOSTS_TAB;
|
||||
import static org.adaway.ui.lists.ListsActivity.BLOCKED_HOSTS_TAB;
|
||||
import static org.adaway.ui.lists.ListsActivity.REDIRECTED_HOSTS_TAB;
|
||||
import static org.adaway.ui.lists.ListsActivity.TAB;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.net.VpnService;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.databinding.HomeActivityBinding;
|
||||
import org.adaway.helper.NotificationHelper;
|
||||
import org.adaway.helper.PreferenceHelper;
|
||||
import org.adaway.helper.ThemeHelper;
|
||||
import org.adaway.model.adblocking.AdBlockMethod;
|
||||
import org.adaway.model.error.HostError;
|
||||
import org.adaway.ui.help.HelpActivity;
|
||||
import org.adaway.ui.hosts.HostsSourcesActivity;
|
||||
import org.adaway.ui.lists.ListsActivity;
|
||||
import org.adaway.ui.log.LogActivity;
|
||||
import org.adaway.ui.prefs.PrefsActivity;
|
||||
import org.adaway.ui.support.SupportActivity;
|
||||
import org.adaway.ui.update.UpdateActivity;
|
||||
import org.adaway.ui.welcome.WelcomeActivity;
|
||||
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is the application main activity.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HomeActivity extends AppCompatActivity {
|
||||
/**
|
||||
* The project link.
|
||||
*/
|
||||
private static final String PROJECT_LINK = "https://github.com/AdAway/AdAway";
|
||||
|
||||
private HomeActivityBinding binding;
|
||||
private BottomSheetBehavior<View> drawerBehavior;
|
||||
private OnBackPressedCallback onBackPressedCallback;
|
||||
private HomeViewModel homeViewModel;
|
||||
private ActivityResultLauncher<Intent> prepareVpnLauncher;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.applyTheme(this);
|
||||
NotificationHelper.clearUpdateNotifications(this);
|
||||
Timber.i("Starting main activity");
|
||||
this.binding = HomeActivityBinding.inflate(getLayoutInflater());
|
||||
setContentView(this.binding.getRoot());
|
||||
|
||||
this.homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
|
||||
this.homeViewModel.isAdBlocked().observe(this, this::notifyAdBlocked);
|
||||
this.homeViewModel.getError().observe(this, this::notifyError);
|
||||
|
||||
applyActionBar();
|
||||
bindAppVersion();
|
||||
bindHostCounter();
|
||||
bindSourceCounter();
|
||||
bindPending();
|
||||
bindState();
|
||||
bindClickListeners();
|
||||
setUpBottomDrawer();
|
||||
bindFab();
|
||||
|
||||
this.binding.navigationView.setNavigationItemSelectedListener(item -> {
|
||||
if (showFragment(item.getItemId())) {
|
||||
this.drawerBehavior.setState(STATE_HIDDEN);
|
||||
}
|
||||
return false; // TODO Handle selection
|
||||
});
|
||||
|
||||
this.prepareVpnLauncher = registerForActivityResult(new StartActivityForResult(), result -> {
|
||||
|
||||
});
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
checkUpdateAtStartup();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
checkFirstStep();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
return showFragment(item.getItemId());
|
||||
}
|
||||
|
||||
private void checkFirstStep() {
|
||||
AdBlockMethod adBlockMethod = PreferenceHelper.getAdBlockMethod(this);
|
||||
Intent prepareIntent;
|
||||
if (adBlockMethod == UNDEFINED) {
|
||||
// Start welcome activity
|
||||
startActivity(new Intent(this, WelcomeActivity.class));
|
||||
finish();
|
||||
} else if (adBlockMethod == VPN && (prepareIntent = VpnService.prepare(this)) != null) {
|
||||
// Prepare VPN
|
||||
this.prepareVpnLauncher.launch(prepareIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkUpdateAtStartup() {
|
||||
boolean checkAppUpdateAtStartup = PreferenceHelper.getUpdateCheckAppStartup(this);
|
||||
if (checkAppUpdateAtStartup) {
|
||||
this.homeViewModel.checkForAppUpdate();
|
||||
}
|
||||
boolean checkUpdateAtStartup = PreferenceHelper.getUpdateCheck(this);
|
||||
if (checkUpdateAtStartup) {
|
||||
this.homeViewModel.update();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyActionBar() {
|
||||
setSupportActionBar(this.binding.bar);
|
||||
}
|
||||
|
||||
private void bindAppVersion() {
|
||||
TextView versionTextView = this.binding.content.versionTextView;
|
||||
versionTextView.setText(this.homeViewModel.getVersionName());
|
||||
versionTextView.setOnClickListener(this::showUpdate);
|
||||
|
||||
this.homeViewModel.getAppManifest().observe(
|
||||
this,
|
||||
manifest -> {
|
||||
if (manifest.updateAvailable) {
|
||||
versionTextView.setTypeface(versionTextView.getTypeface(), Typeface.BOLD);
|
||||
versionTextView.setText(R.string.update_available);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void bindHostCounter() {
|
||||
Function1<Integer, CharSequence> stringMapper = count -> Integer.toString(count);
|
||||
|
||||
TextView blockedHostCountTextView = this.binding.content.blockedHostCounterTextView;
|
||||
LiveData<Integer> blockedHostCount = this.homeViewModel.getBlockedHostCount();
|
||||
Transformations.map(blockedHostCount, stringMapper).observe(this, blockedHostCountTextView::setText);
|
||||
|
||||
TextView allowedHostCountTextView = this.binding.content.allowedHostCounterTextView;
|
||||
LiveData<Integer> allowedHostCount = this.homeViewModel.getAllowedHostCount();
|
||||
Transformations.map(allowedHostCount, stringMapper).observe(this, allowedHostCountTextView::setText);
|
||||
|
||||
TextView redirectHostCountTextView = this.binding.content.redirectHostCounterTextView;
|
||||
LiveData<Integer> redirectHostCount = this.homeViewModel.getRedirectHostCount();
|
||||
Transformations.map(redirectHostCount, stringMapper).observe(this, redirectHostCountTextView::setText);
|
||||
}
|
||||
|
||||
private void bindSourceCounter() {
|
||||
Resources resources = getResources();
|
||||
|
||||
TextView upToDateSourcesTextView = this.binding.content.upToDateSourcesTextView;
|
||||
LiveData<Integer> upToDateSourceCount = this.homeViewModel.getUpToDateSourceCount();
|
||||
upToDateSourceCount.observe(this, count ->
|
||||
upToDateSourcesTextView.setText(resources.getQuantityString(R.plurals.up_to_date_source_label, count, count))
|
||||
);
|
||||
|
||||
TextView outdatedSourcesTextView = this.binding.content.outdatedSourcesTextView;
|
||||
LiveData<Integer> outdatedSourceCount = this.homeViewModel.getOutdatedSourceCount();
|
||||
outdatedSourceCount.observe(this, count ->
|
||||
outdatedSourcesTextView.setText(resources.getQuantityString(R.plurals.outdated_source_label, count, count))
|
||||
);
|
||||
}
|
||||
|
||||
private void bindPending() {
|
||||
this.homeViewModel.getPending().observe(this, pending -> {
|
||||
if (pending) {
|
||||
showView(this.binding.content.sourcesProgressBar);
|
||||
showView(this.binding.content.stateTextView);
|
||||
} else {
|
||||
removeView(this.binding.content.sourcesProgressBar);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindState() {
|
||||
this.homeViewModel.getState().observe(this, text -> {
|
||||
this.binding.content.stateTextView.setText(text);
|
||||
if (text.isEmpty()) {
|
||||
removeView(this.binding.content.stateTextView);
|
||||
} else {
|
||||
showView(this.binding.content.stateTextView);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindClickListeners() {
|
||||
this.binding.content.blockedHostCardView.setOnClickListener(v -> startHostListActivity(BLOCKED_HOSTS_TAB));
|
||||
this.binding.content.allowedHostCardView.setOnClickListener(v -> startHostListActivity(ALLOWED_HOSTS_TAB));
|
||||
this.binding.content.redirectHostCardView.setOnClickListener(v -> startHostListActivity(REDIRECTED_HOSTS_TAB));
|
||||
this.binding.content.sourcesCardView.setOnClickListener(this::startHostsSourcesActivity);
|
||||
this.binding.content.checkForUpdateImageView.setOnClickListener(v -> this.homeViewModel.update());
|
||||
this.binding.content.updateImageView.setOnClickListener(v -> this.homeViewModel.sync());
|
||||
this.binding.content.logCardView.setOnClickListener(this::startDnsLogActivity);
|
||||
this.binding.content.helpCardView.setOnClickListener(this::startHelpActivity);
|
||||
this.binding.content.supportCardView.setOnClickListener(this::showSupportActivity);
|
||||
}
|
||||
|
||||
private void setUpBottomDrawer() {
|
||||
this.drawerBehavior = BottomSheetBehavior.from(this.binding.bottomDrawer);
|
||||
this.drawerBehavior.setState(STATE_HIDDEN);
|
||||
|
||||
this.onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
// Hide drawer if expanded
|
||||
HomeActivity.this.drawerBehavior.setState(STATE_HIDDEN);
|
||||
HomeActivity.this.onBackPressedCallback.setEnabled(false);
|
||||
}
|
||||
};
|
||||
getOnBackPressedDispatcher().addCallback(this.onBackPressedCallback);
|
||||
|
||||
this.binding.bar.setNavigationOnClickListener(v -> {
|
||||
this.drawerBehavior.setState(STATE_HALF_EXPANDED);
|
||||
this.onBackPressedCallback.setEnabled(true);
|
||||
});
|
||||
// this.binding.bar.setNavigationIcon(R.drawable.ic_menu_24dp);
|
||||
// this.binding.bar.replaceMenu(R.menu.next_actions);
|
||||
}
|
||||
|
||||
private void bindFab() {
|
||||
this.binding.fab.setOnClickListener(v -> this.homeViewModel.toggleAdBlocking());
|
||||
}
|
||||
|
||||
private boolean showFragment(@IdRes int actionId) {
|
||||
if (actionId == R.id.drawer_preferences) {
|
||||
startPrefsActivity();
|
||||
this.drawerBehavior.setState(STATE_HIDDEN);
|
||||
return true;
|
||||
} else if (actionId == R.id.drawer_github_project) {
|
||||
showProjectPage();
|
||||
this.drawerBehavior.setState(STATE_HIDDEN);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start hosts lists activity.
|
||||
*
|
||||
* @param tab The tab to show.
|
||||
*/
|
||||
private void startHostListActivity(int tab) {
|
||||
Intent intent = new Intent(this, ListsActivity.class);
|
||||
intent.putExtra(TAB, tab);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start hosts source activity.
|
||||
*
|
||||
* @param view The event source view.
|
||||
*/
|
||||
private void startHostsSourcesActivity(View view) {
|
||||
startActivity(new Intent(this, HostsSourcesActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start help activity.
|
||||
*
|
||||
* @param view The source event view.
|
||||
*/
|
||||
private void startHelpActivity(View view) {
|
||||
startActivity(new Intent(this, HelpActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show development project page.
|
||||
*/
|
||||
private void showProjectPage() {
|
||||
// Show development page
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(PROJECT_LINK));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show support activity.
|
||||
*
|
||||
* @param view The source event view.
|
||||
*/
|
||||
private void showSupportActivity(View view) {
|
||||
startActivity(new Intent(this, SupportActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start preferences activity.
|
||||
*/
|
||||
private void startPrefsActivity() {
|
||||
startActivity(new Intent(this, PrefsActivity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start DNS log activity.
|
||||
*
|
||||
* @param view The source event view.
|
||||
*/
|
||||
private void startDnsLogActivity(View view) {
|
||||
startActivity(new Intent(this, LogActivity.class));
|
||||
}
|
||||
|
||||
private void notifyAdBlocked(boolean adBlocked) {
|
||||
int color = adBlocked ? getResources().getColor(R.color.primary, null) : Color.GRAY;
|
||||
this.binding.content.headerFrameLayout.setBackgroundColor(color);
|
||||
this.binding.fab.setImageResource(adBlocked ? R.drawable.ic_pause_24dp : R.drawable.logo);
|
||||
}
|
||||
|
||||
private void notifyError(HostError error) {
|
||||
removeView(this.binding.content.stateTextView);
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = getString(error.getDetailsKey()) + "\n\n" + getString(R.string.error_dialog_help);
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setTitle(error.getMessageKey())
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.button_close, (dialog, id) -> dialog.dismiss())
|
||||
.setNegativeButton(R.string.button_help, (dialog, id) -> {
|
||||
dialog.dismiss();
|
||||
startActivity(new Intent(this, HelpActivity.class));
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showUpdate(View view) {
|
||||
Intent intent = new Intent(this, UpdateActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
182
app/src/main/java/org/adaway/ui/home/HomeViewModel.java
Normal file
182
app/src/main/java/org/adaway/ui/home/HomeViewModel.java
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package org.adaway.ui.home;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.adaway.AdAwayApplication;
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.model.adblocking.AdBlockModel;
|
||||
import org.adaway.model.error.HostError;
|
||||
import org.adaway.model.error.HostErrorException;
|
||||
import org.adaway.model.source.SourceModel;
|
||||
import org.adaway.model.update.Manifest;
|
||||
import org.adaway.model.update.UpdateModel;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is an {@link AndroidViewModel} for the {@link HomeActivity} cards.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HomeViewModel extends AndroidViewModel {
|
||||
private static final AppExecutors EXECUTORS = AppExecutors.getInstance();
|
||||
|
||||
private final SourceModel sourceModel;
|
||||
private final AdBlockModel adBlockModel;
|
||||
private final UpdateModel updateModel;
|
||||
|
||||
private final HostsSourceDao hostsSourceDao;
|
||||
private final HostListItemDao hostListItemDao;
|
||||
|
||||
private final MutableLiveData<Boolean> pending;
|
||||
private final MediatorLiveData<String> state;
|
||||
private final MutableLiveData<HostError> error;
|
||||
|
||||
public HomeViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
AdAwayApplication awayApplication = (AdAwayApplication) application;
|
||||
this.sourceModel = awayApplication.getSourceModel();
|
||||
this.adBlockModel = awayApplication.getAdBlockModel();
|
||||
this.updateModel = awayApplication.getUpdateModel();
|
||||
|
||||
AppDatabase database = AppDatabase.getInstance(application);
|
||||
this.hostsSourceDao = database.hostsSourceDao();
|
||||
this.hostListItemDao = database.hostsListItemDao();
|
||||
|
||||
this.pending = new MutableLiveData<>(false);
|
||||
this.state = new MediatorLiveData<>();
|
||||
this.state.addSource(this.sourceModel.getState(), this.state::setValue);
|
||||
this.state.addSource(this.adBlockModel.getState(), this.state::setValue);
|
||||
this.error = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
private static boolean isTrue(LiveData<Boolean> liveData) {
|
||||
Boolean value = liveData.getValue();
|
||||
return value != null && value;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isAdBlocked() {
|
||||
return this.adBlockModel.isApplied();
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isUpdateAvailable() {
|
||||
return this.sourceModel.isUpdateAvailable();
|
||||
}
|
||||
|
||||
public String getVersionName() {
|
||||
return this.updateModel.getVersionName();
|
||||
}
|
||||
|
||||
public LiveData<Manifest> getAppManifest() {
|
||||
return this.updateModel.getManifest();
|
||||
}
|
||||
|
||||
public LiveData<Integer> getBlockedHostCount() {
|
||||
return this.hostListItemDao.getBlockedHostCount();
|
||||
}
|
||||
|
||||
public LiveData<Integer> getAllowedHostCount() {
|
||||
return this.hostListItemDao.getAllowedHostCount();
|
||||
}
|
||||
|
||||
public LiveData<Integer> getRedirectHostCount() {
|
||||
return this.hostListItemDao.getRedirectHostCount();
|
||||
}
|
||||
|
||||
public LiveData<Integer> getUpToDateSourceCount() {
|
||||
return this.hostsSourceDao.countUpToDate();
|
||||
}
|
||||
|
||||
public LiveData<Integer> getOutdatedSourceCount() {
|
||||
return this.hostsSourceDao.countOutdated();
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getPending() {
|
||||
return this.pending;
|
||||
}
|
||||
|
||||
public LiveData<String> getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public LiveData<HostError> getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public void checkForAppUpdate() {
|
||||
EXECUTORS.networkIO().execute(this.updateModel::checkForUpdate);
|
||||
}
|
||||
|
||||
public void toggleAdBlocking() {
|
||||
if (isTrue(this.pending)) {
|
||||
return;
|
||||
}
|
||||
EXECUTORS.diskIO().execute(() -> {
|
||||
try {
|
||||
this.pending.postValue(true);
|
||||
if (isTrue(this.adBlockModel.isApplied())) {
|
||||
this.adBlockModel.revert();
|
||||
} else {
|
||||
this.adBlockModel.apply();
|
||||
}
|
||||
} catch (HostErrorException exception) {
|
||||
Timber.w(exception, "Failed to toggle ad blocking.");
|
||||
this.error.postValue(exception.getError());
|
||||
} finally {
|
||||
this.pending.postValue(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void update() {
|
||||
if (isTrue(this.pending)) {
|
||||
return;
|
||||
}
|
||||
EXECUTORS.networkIO().execute(() -> {
|
||||
try {
|
||||
this.pending.postValue(true);
|
||||
this.sourceModel.checkForUpdate();
|
||||
} catch (HostErrorException exception) {
|
||||
Timber.w(exception, "Failed to update.");
|
||||
this.error.postValue(exception.getError());
|
||||
} finally {
|
||||
this.pending.postValue(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void sync() {
|
||||
if (isTrue(this.pending)) {
|
||||
return;
|
||||
}
|
||||
EXECUTORS.networkIO().execute(() -> {
|
||||
try {
|
||||
this.pending.postValue(true);
|
||||
this.sourceModel.retrieveHostsSources();
|
||||
this.adBlockModel.apply();
|
||||
} catch (HostErrorException exception) {
|
||||
Timber.w(exception, "Failed to sync.");
|
||||
this.error.postValue(exception.getError());
|
||||
} finally {
|
||||
this.pending.postValue(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void enableAllSources() {
|
||||
EXECUTORS.diskIO().execute(() -> {
|
||||
if (this.sourceModel.enableAllSources()) {
|
||||
sync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package org.adaway.ui.hosts;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.helper.ThemeHelper;
|
||||
|
||||
/**
|
||||
* This activity display hosts list items.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HostsSourcesActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.applyTheme(this);
|
||||
/*
|
||||
* Create fragment
|
||||
*/
|
||||
HostsSourcesFragment fragment = new HostsSourcesFragment();
|
||||
/*
|
||||
* Set view content.
|
||||
*/
|
||||
setContentView(R.layout.hosts_sources_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.hosts_sources_container, fragment)
|
||||
.commit();
|
||||
/*
|
||||
* Configure actionbar.
|
||||
*/
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
208
app/src/main/java/org/adaway/ui/hosts/HostsSourcesAdapter.java
Normal file
208
app/src/main/java/org/adaway/ui/hosts/HostsSourcesAdapter.java
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package org.adaway.ui.hosts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/**
|
||||
* This class is a the {@link RecyclerView.Adapter} for the hosts sources view.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class HostsSourcesAdapter extends ListAdapter<HostsSource, HostsSourcesAdapter.ViewHolder> {
|
||||
/**
|
||||
* This callback is use to compare hosts sources.
|
||||
*/
|
||||
private static final DiffUtil.ItemCallback<HostsSource> DIFF_CALLBACK =
|
||||
new DiffUtil.ItemCallback<HostsSource>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull HostsSource oldSource, @NonNull HostsSource newSource) {
|
||||
return oldSource.getUrl().equals(newSource.getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull HostsSource oldSource, @NonNull HostsSource newSource) {
|
||||
// NOTE: if you use equals, your object must properly override Object#equals()
|
||||
// Incorrectly returning false here will result in too many animations.
|
||||
return oldSource.equals(newSource);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This callback is use to call view actions.
|
||||
*/
|
||||
@NonNull
|
||||
private final HostsSourcesViewCallback viewCallback;
|
||||
private static final String[] QUANTITY_PREFIXES = new String[]{"k", "M", "G"};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param viewCallback The view callback.
|
||||
*/
|
||||
HostsSourcesAdapter(@NonNull HostsSourcesViewCallback viewCallback) {
|
||||
super(DIFF_CALLBACK);
|
||||
this.viewCallback = viewCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the approximate delay from a date to now.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param from The date from which computes the delay.
|
||||
* @return The approximate delay.
|
||||
*/
|
||||
private static String getApproximateDelay(Context context, ZonedDateTime from) {
|
||||
// Get resource for plurals
|
||||
Resources resources = context.getResources();
|
||||
// Get current date in UTC timezone
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
// Get delay between from and now in minutes
|
||||
long delay = Duration.between(from, now).toMinutes();
|
||||
// Check if delay is lower than an hour
|
||||
if (delay < 60) {
|
||||
return resources.getString(R.string.hosts_source_few_minutes);
|
||||
}
|
||||
// Get delay in hours
|
||||
delay /= 60;
|
||||
// Check if delay is lower than a day
|
||||
if (delay < 24) {
|
||||
int hours = (int) delay;
|
||||
return resources.getQuantityString(R.plurals.hosts_source_hours, hours, hours);
|
||||
}
|
||||
// Get delay in days
|
||||
delay /= 24;
|
||||
// Check if delay is lower than a month
|
||||
if (delay < 30) {
|
||||
int days = (int) delay;
|
||||
return resources.getQuantityString(R.plurals.hosts_source_days, days, days);
|
||||
}
|
||||
// Get delay in months
|
||||
int months = (int) delay / 30;
|
||||
return resources.getQuantityString(R.plurals.hosts_source_months, months, months);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public HostsSourcesAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
|
||||
View view = layoutInflater.inflate(R.layout.hosts_sources_card, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
HostsSource source = this.getItem(position);
|
||||
holder.enabledCheckBox.setChecked(source.isEnabled());
|
||||
holder.enabledCheckBox.setOnClickListener(view -> viewCallback.toggleEnabled(source));
|
||||
holder.labelTextView.setText(source.getLabel());
|
||||
holder.urlTextView.setText(source.getUrl());
|
||||
holder.updateTextView.setText(getUpdateText(source));
|
||||
holder.sizeTextView.setText(getHostCount(source));
|
||||
holder.itemView.setOnClickListener(view -> viewCallback.edit(source));
|
||||
}
|
||||
|
||||
private String getUpdateText(HostsSource source) {
|
||||
// Get context
|
||||
Context context = this.viewCallback.getContext();
|
||||
// Check if source is enabled
|
||||
if (!source.isEnabled()) {
|
||||
return context.getString(R.string.hosts_source_disabled);
|
||||
}
|
||||
// Check modification dates
|
||||
boolean lastOnlineModificationDefined = source.getOnlineModificationDate() != null;
|
||||
boolean lastLocalModificationDefined = source.getLocalModificationDate() != null;
|
||||
// Declare update text
|
||||
String updateText;
|
||||
// Check if last online modification date is known
|
||||
if (lastOnlineModificationDefined) {
|
||||
// Get last online modification delay
|
||||
String approximateDelay = getApproximateDelay(context, source.getOnlineModificationDate());
|
||||
if (!lastLocalModificationDefined) {
|
||||
updateText = context.getString(R.string.hosts_source_last_update, approximateDelay);
|
||||
} else if (source.getOnlineModificationDate().isAfter(source.getLocalModificationDate())) {
|
||||
updateText = context.getString(R.string.hosts_source_need_update, approximateDelay);
|
||||
} else {
|
||||
updateText = context.getString(R.string.hosts_source_up_to_date, approximateDelay);
|
||||
}
|
||||
} else {
|
||||
if (lastLocalModificationDefined) {
|
||||
String approximateDelay = getApproximateDelay(context, source.getLocalModificationDate());
|
||||
updateText = context.getString(R.string.hosts_source_installed, approximateDelay);
|
||||
} else {
|
||||
updateText = context.getString(R.string.hosts_source_unknown_status);
|
||||
}
|
||||
}
|
||||
return updateText;
|
||||
}
|
||||
|
||||
private String getHostCount(HostsSource source) {
|
||||
// Note: NumberFormat.getCompactNumberInstance is Java 12 only
|
||||
// Check empty source
|
||||
int size = source.getSize();
|
||||
if (size <= 0 || !source.isEnabled()) {
|
||||
return "";
|
||||
}
|
||||
// Compute size decimal length
|
||||
int length = 1;
|
||||
while (size > 10) {
|
||||
size /= 10;
|
||||
length++;
|
||||
}
|
||||
// Compute prefix to use
|
||||
int prefixIndex = (length - 1) / 3 - 1;
|
||||
// Return formatted count
|
||||
Context context = this.viewCallback.getContext();
|
||||
size = source.getSize();
|
||||
if (prefixIndex < 0) {
|
||||
return context.getString(R.string.hosts_count, Integer.toString(size));
|
||||
} else if (prefixIndex >= QUANTITY_PREFIXES.length) {
|
||||
prefixIndex = QUANTITY_PREFIXES.length - 1;
|
||||
size = 13;
|
||||
}
|
||||
size = Math.toIntExact(Math.round(size / Math.pow(10, (prefixIndex + 1) * 3D)));
|
||||
return context.getString(R.string.hosts_count, size + QUANTITY_PREFIXES[prefixIndex]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a the {@link RecyclerView.ViewHolder} for the hosts sources view.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final CheckBox enabledCheckBox;
|
||||
final TextView labelTextView;
|
||||
final TextView urlTextView;
|
||||
final TextView updateTextView;
|
||||
final TextView sizeTextView;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param itemView The hosts sources view.
|
||||
*/
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.enabledCheckBox = itemView.findViewById(R.id.sourceEnabledCheckBox);
|
||||
this.labelTextView = itemView.findViewById(R.id.sourceLabelTextView);
|
||||
this.urlTextView = itemView.findViewById(R.id.sourceUrlTextView);
|
||||
this.updateTextView = itemView.findViewById(R.id.sourceUpdateTextView);
|
||||
this.sizeTextView = itemView.findViewById(R.id.sourceSizeTextView);
|
||||
}
|
||||
}
|
||||
}
|
||||
119
app/src/main/java/org/adaway/ui/hosts/HostsSourcesFragment.java
Normal file
119
app/src/main/java/org/adaway/ui/hosts/HostsSourcesFragment.java
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.hosts;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.ui.adblocking.ApplyConfigurationSnackbar;
|
||||
import org.adaway.ui.source.SourceEditActivity;
|
||||
|
||||
import static org.adaway.ui.source.SourceEditActivity.SOURCE_ID;
|
||||
|
||||
/**
|
||||
* This class is a {@link Fragment} to display and manage hosts sources.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HostsSourcesFragment extends Fragment implements HostsSourcesViewCallback {
|
||||
/**
|
||||
* The view model (<code>null</code> if view is not created).
|
||||
*/
|
||||
private HostsSourcesViewModel mViewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
// Get activity
|
||||
Activity activity = requireActivity();
|
||||
// Initialize view model
|
||||
this.mViewModel = new ViewModelProvider(this).get(HostsSourcesViewModel.class);
|
||||
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
|
||||
// Create fragment view
|
||||
View view = inflater.inflate(R.layout.hosts_sources_fragment, container, false);
|
||||
/*
|
||||
* Configure snackbar.
|
||||
*/
|
||||
// Get lists layout to attached snackbar to
|
||||
CoordinatorLayout coordinatorLayout = view.findViewById(R.id.coordinator);
|
||||
// Create apply snackbar
|
||||
ApplyConfigurationSnackbar applySnackbar = new ApplyConfigurationSnackbar(coordinatorLayout, true, true);
|
||||
// Bind snakbar to view models
|
||||
this.mViewModel.getHostsSources().observe(lifecycleOwner, applySnackbar.createObserver());
|
||||
/*
|
||||
* Configure recycler view.
|
||||
*/
|
||||
// Store recycler view
|
||||
RecyclerView recyclerView = view.findViewById(R.id.hosts_sources_list);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
// Defile recycler layout
|
||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(activity);
|
||||
recyclerView.setLayoutManager(linearLayoutManager);
|
||||
// Create recycler adapter
|
||||
HostsSourcesAdapter adapter = new HostsSourcesAdapter(this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
// Bind adapter to view model
|
||||
this.mViewModel.getHostsSources().observe(lifecycleOwner, adapter::submitList);
|
||||
/*
|
||||
* Add floating action button.
|
||||
*/
|
||||
// Get floating action button
|
||||
FloatingActionButton button = view.findViewById(R.id.hosts_sources_add);
|
||||
// Set click listener to display menu add entry
|
||||
button.setOnClickListener(actionButton -> startSourceEdition(null));
|
||||
// Return fragment view
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggleEnabled(HostsSource source) {
|
||||
this.mViewModel.toggleSourceEnabled(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void edit(HostsSource source) {
|
||||
startSourceEdition(source);
|
||||
}
|
||||
|
||||
private void startSourceEdition(@Nullable HostsSource source) {
|
||||
Intent intent = new Intent(requireContext(), SourceEditActivity.class);
|
||||
if (source != null) {
|
||||
intent.putExtra(SOURCE_ID, source.getId());
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.adaway.ui.hosts;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
|
||||
/**
|
||||
* This class is represents the {@link HostsSourcesFragment} callback.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public interface HostsSourcesViewCallback {
|
||||
/**
|
||||
* Get the application context.
|
||||
*
|
||||
* @return The application context.
|
||||
*/
|
||||
Context getContext();
|
||||
|
||||
/**
|
||||
* Toggle host source enable status.
|
||||
*
|
||||
* @param source The hosts source to toggle status.
|
||||
*/
|
||||
void toggleEnabled(HostsSource source);
|
||||
|
||||
/**
|
||||
* Start an action.
|
||||
*
|
||||
* @param source The hosts source to start the action.
|
||||
*/
|
||||
void edit(HostsSource source);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.adaway.ui.hosts;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostsSourceDao;
|
||||
import org.adaway.db.entity.HostsSource;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* This class is an {@link AndroidViewModel} for the {@link HostsSourcesFragment}.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class HostsSourcesViewModel extends AndroidViewModel {
|
||||
private static final Executor EXECUTOR = AppExecutors.getInstance().diskIO();
|
||||
private final HostsSourceDao hostsSourceDao;
|
||||
|
||||
public HostsSourcesViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
this.hostsSourceDao = AppDatabase.getInstance(application).hostsSourceDao();
|
||||
}
|
||||
|
||||
public LiveData<List<HostsSource>> getHostsSources() {
|
||||
return this.hostsSourceDao.loadAll();
|
||||
}
|
||||
|
||||
public void toggleSourceEnabled(HostsSource source) {
|
||||
EXECUTOR.execute(() -> this.hostsSourceDao.toggleEnabled(source));
|
||||
}
|
||||
}
|
||||
185
app/src/main/java/org/adaway/ui/lists/ListsActivity.java
Normal file
185
app/src/main/java/org/adaway/ui/lists/ListsActivity.java
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package org.adaway.ui.lists;
|
||||
|
||||
import static android.content.Intent.ACTION_SEARCH;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.SearchView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.helper.ThemeHelper;
|
||||
import org.adaway.ui.adblocking.ApplyConfigurationSnackbar;
|
||||
|
||||
/**
|
||||
* This activity display hosts list items.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class ListsActivity extends AppCompatActivity {
|
||||
/**
|
||||
* The tab to display argument.
|
||||
*/
|
||||
public static final String TAB = "org.adaway.lists.tab";
|
||||
/**
|
||||
* The blocked hosts tab index.
|
||||
*/
|
||||
public static final int BLOCKED_HOSTS_TAB = 0;
|
||||
/**
|
||||
* The allowed hosts tab index.
|
||||
*/
|
||||
public static final int ALLOWED_HOSTS_TAB = 1;
|
||||
/**
|
||||
* The redirected hosts tab index.
|
||||
*/
|
||||
public static final int REDIRECTED_HOSTS_TAB = 2;
|
||||
/**
|
||||
* The view model.
|
||||
*/
|
||||
private ListsViewModel listsViewModel;
|
||||
/**
|
||||
* The back press callback.
|
||||
*/
|
||||
private OnBackPressedCallback onBackPressedCallback;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.applyTheme(this);
|
||||
/*
|
||||
* Set view content.
|
||||
*/
|
||||
setContentView(R.layout.lists_fragment);
|
||||
/*
|
||||
* Configure actionbar.
|
||||
*/
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
/*
|
||||
* Configure back press callback.
|
||||
*/
|
||||
this.onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
ListsActivity.this.listsViewModel.clearSearch();
|
||||
ListsActivity.this.onBackPressedCallback.setEnabled(false);
|
||||
}
|
||||
};
|
||||
getOnBackPressedDispatcher().addCallback(this.onBackPressedCallback);
|
||||
/*
|
||||
* Configure tabs.
|
||||
*/
|
||||
// Get view pager
|
||||
ViewPager2 viewPager = findViewById(R.id.lists_view_pager);
|
||||
// Create pager adapter
|
||||
ListsFragmentPagerAdapter pagerAdapter = new ListsFragmentPagerAdapter(this);
|
||||
// Set view pager adapter
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
// Get navigation view
|
||||
BottomNavigationView navigationView = findViewById(R.id.navigation);
|
||||
// Add view pager on page listener to set selected tab according the selected page
|
||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
navigationView.getMenu().getItem(position).setChecked(true);
|
||||
pagerAdapter.ensureActionModeCanceled();
|
||||
}
|
||||
});
|
||||
// Add navigation view item selected listener to change view pager current item
|
||||
navigationView.setOnItemSelectedListener(item -> {
|
||||
if (item.getItemId() == R.id.lists_navigation_blocked) {
|
||||
viewPager.setCurrentItem(0);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.lists_navigation_allowed) {
|
||||
viewPager.setCurrentItem(1);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.lists_navigation_redirected) {
|
||||
viewPager.setCurrentItem(2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// Display requested tab
|
||||
Intent intent = getIntent();
|
||||
int tab = intent.getIntExtra(TAB, BLOCKED_HOSTS_TAB);
|
||||
viewPager.setCurrentItem(tab);
|
||||
/*
|
||||
* Configure add action button.
|
||||
*/
|
||||
// Get the add action button
|
||||
FloatingActionButton addActionButton = findViewById(R.id.lists_add);
|
||||
// Set add action button listener
|
||||
addActionButton.setOnClickListener(clickedView -> {
|
||||
// Get current fragment position
|
||||
int currentItemPosition = viewPager.getCurrentItem();
|
||||
// Add item to the current fragment
|
||||
pagerAdapter.addItem(currentItemPosition);
|
||||
});
|
||||
/*
|
||||
* Configure snackbar.
|
||||
*/
|
||||
// Get lists layout to attached snackbar to
|
||||
CoordinatorLayout coordinatorLayout = findViewById(R.id.coordinator);
|
||||
// Create apply snackbar
|
||||
ApplyConfigurationSnackbar applySnackbar = new ApplyConfigurationSnackbar(coordinatorLayout, false, false);
|
||||
// Bind snackbar to view models
|
||||
this.listsViewModel = new ViewModelProvider(this).get(ListsViewModel.class);
|
||||
this.listsViewModel.getModelChanged().observe(this, applySnackbar.createObserver());
|
||||
// Get the intent, verify the action and get the query
|
||||
handleQuery(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleQuery(intent);
|
||||
}
|
||||
|
||||
private void handleQuery(Intent intent) {
|
||||
if (ACTION_SEARCH.equals(intent.getAction())) {
|
||||
String query = intent.getStringExtra(SearchManager.QUERY);
|
||||
this.listsViewModel.search(query);
|
||||
this.onBackPressedCallback.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.list_menu, menu);
|
||||
// Get the SearchView and set the searchable configuration
|
||||
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
|
||||
searchView.setIconifiedByDefault(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_toggle_source) {
|
||||
this.listsViewModel.toggleSources();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/org/adaway/ui/lists/ListsFilter.java
Normal file
33
app/src/main/java/org/adaway/ui/lists/ListsFilter.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package org.adaway.ui.lists;
|
||||
|
||||
/**
|
||||
* This class represents the filter to apply to host lists.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class ListsFilter {
|
||||
public static final ListsFilter ALL = new ListsFilter(true, "");
|
||||
/**
|
||||
* Whether included hosts from sources or not.
|
||||
*/
|
||||
public final boolean sourcesIncluded;
|
||||
/**
|
||||
* The query filter to apply to hosts name (wildcard based).
|
||||
*/
|
||||
public final String query;
|
||||
/**
|
||||
* The query filter to apply to hosts name (sql like format).
|
||||
*/
|
||||
public final String sqlQuery;
|
||||
|
||||
public ListsFilter(boolean sourcesIncluded, String query) {
|
||||
this.sourcesIncluded = sourcesIncluded;
|
||||
this.query = query;
|
||||
this.sqlQuery = convertToLikeQuery(query);
|
||||
}
|
||||
|
||||
private static String convertToLikeQuery(String query) {
|
||||
return "%" + query.replace("*", "%")
|
||||
.replace("?", "_") + "%";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package org.adaway.ui.lists;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import org.adaway.ui.lists.type.AbstractListFragment;
|
||||
import org.adaway.ui.lists.type.AllowedHostsFragment;
|
||||
import org.adaway.ui.lists.type.BlockedHostsFragment;
|
||||
import org.adaway.ui.lists.type.RedirectedHostsFragment;
|
||||
|
||||
import static org.adaway.ui.lists.ListsActivity.BLOCKED_HOSTS_TAB;
|
||||
import static org.adaway.ui.lists.ListsActivity.REDIRECTED_HOSTS_TAB;
|
||||
import static org.adaway.ui.lists.ListsActivity.ALLOWED_HOSTS_TAB;
|
||||
|
||||
/**
|
||||
* This class is a {@link FragmentStateAdapter} to store lists tab fragments.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class ListsFragmentPagerAdapter extends FragmentStateAdapter {
|
||||
/**
|
||||
* The number of fragment.
|
||||
*/
|
||||
private static final int FRAGMENT_COUNT = 3;
|
||||
/**
|
||||
* The blacklist fragment (<code>null</code> until first retrieval).
|
||||
*/
|
||||
private final AbstractListFragment blacklistFragment;
|
||||
/**
|
||||
* The whitelist fragment (<code>null</code> until first retrieval).
|
||||
*/
|
||||
private final AbstractListFragment whitelistFragment;
|
||||
/**
|
||||
* The redirection list fragment (<code>null</code> until first retrieval).
|
||||
*/
|
||||
private final AbstractListFragment redirectionListFragment;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
*/
|
||||
ListsFragmentPagerAdapter(FragmentActivity fragmentActivity) {
|
||||
super(fragmentActivity);
|
||||
this.blacklistFragment = new BlockedHostsFragment();
|
||||
this.whitelistFragment = new AllowedHostsFragment();
|
||||
this.redirectionListFragment = new RedirectedHostsFragment();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case BLOCKED_HOSTS_TAB:
|
||||
return this.blacklistFragment;
|
||||
case ALLOWED_HOSTS_TAB:
|
||||
return this.whitelistFragment;
|
||||
case REDIRECTED_HOSTS_TAB:
|
||||
return this.redirectionListFragment;
|
||||
default:
|
||||
throw new IllegalStateException("Position " + position + " is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return FRAGMENT_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure action mode is cancelled.
|
||||
*/
|
||||
void ensureActionModeCanceled() {
|
||||
if (this.blacklistFragment != null) {
|
||||
this.blacklistFragment.ensureActionModeCanceled();
|
||||
}
|
||||
if (this.whitelistFragment != null) {
|
||||
this.whitelistFragment.ensureActionModeCanceled();
|
||||
}
|
||||
if (this.redirectionListFragment != null) {
|
||||
this.redirectionListFragment.ensureActionModeCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item into the requested fragment.
|
||||
*
|
||||
* @param position The fragment position.
|
||||
*/
|
||||
void addItem(int position) {
|
||||
switch (position) {
|
||||
case BLOCKED_HOSTS_TAB:
|
||||
if (this.blacklistFragment != null) {
|
||||
this.blacklistFragment.addItem();
|
||||
}
|
||||
break;
|
||||
case ALLOWED_HOSTS_TAB:
|
||||
if (this.whitelistFragment != null) {
|
||||
this.whitelistFragment.addItem();
|
||||
}
|
||||
break;
|
||||
case REDIRECTED_HOSTS_TAB:
|
||||
if (this.redirectionListFragment != null) {
|
||||
this.redirectionListFragment.addItem();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/src/main/java/org/adaway/ui/lists/ListsViewCallback.java
Normal file
36
app/src/main/java/org/adaway/ui/lists/ListsViewCallback.java
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package org.adaway.ui.lists;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.ui.lists.type.AbstractListFragment;
|
||||
|
||||
/**
|
||||
* This class is represents the {@link AbstractListFragment} callback.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public interface ListsViewCallback {
|
||||
/**
|
||||
* Toggle item enable status.
|
||||
*
|
||||
* @param item The list to toggle status.
|
||||
*/
|
||||
void toggleItemEnabled(HostListItem item);
|
||||
|
||||
/**
|
||||
* Start an action.
|
||||
*
|
||||
* @param item The list to start the action.
|
||||
* @param sourceView The list related view.
|
||||
* @return <code>true</code> if the action was started, <code>false</code> otherwise.
|
||||
*/
|
||||
boolean startAction(HostListItem item, View sourceView);
|
||||
|
||||
/**
|
||||
* Copy an hosts into clipboard.
|
||||
*
|
||||
* @param item The list to copy hosts.
|
||||
*/
|
||||
boolean copyHostToClipboard(HostListItem item);
|
||||
}
|
||||
160
app/src/main/java/org/adaway/ui/lists/ListsViewModel.java
Normal file
160
app/src/main/java/org/adaway/ui/lists/ListsViewModel.java
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package org.adaway.ui.lists;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.paging.Pager;
|
||||
import androidx.paging.PagingConfig;
|
||||
import androidx.paging.PagingData;
|
||||
|
||||
import org.adaway.db.AppDatabase;
|
||||
import org.adaway.db.dao.HostListItemDao;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.ListType;
|
||||
import org.adaway.ui.lists.type.AbstractListFragment;
|
||||
import org.adaway.util.AppExecutors;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static androidx.lifecycle.Transformations.switchMap;
|
||||
import static androidx.paging.PagingLiveData.getLiveData;
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
import static org.adaway.ui.lists.ListsFilter.ALL;
|
||||
|
||||
/**
|
||||
* This class is an {@link AndroidViewModel} for the {@link AbstractListFragment} implementations.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class ListsViewModel extends AndroidViewModel {
|
||||
private static final Executor EXECUTOR = AppExecutors.getInstance().diskIO();
|
||||
private final HostListItemDao hostListItemDao;
|
||||
private final MutableLiveData<ListsFilter> filter;
|
||||
private final LiveData<PagingData<HostListItem>> blockedListItems;
|
||||
private final LiveData<PagingData<HostListItem>> allowedListItems;
|
||||
private final LiveData<PagingData<HostListItem>> redirectedListItems;
|
||||
private final MutableLiveData<Boolean> modelChanged;
|
||||
|
||||
public ListsViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
this.hostListItemDao = AppDatabase.getInstance(application).hostsListItemDao();
|
||||
this.filter = new MutableLiveData<>(ALL);
|
||||
PagingConfig pagingConfig = new PagingConfig(50, 150, true);
|
||||
this.blockedListItems = switchMap(
|
||||
this.filter,
|
||||
filter -> getLiveData(new Pager<>(pagingConfig, () ->
|
||||
this.hostListItemDao.loadList(BLOCKED.getValue(), filter.sourcesIncluded, filter.sqlQuery)
|
||||
))
|
||||
);
|
||||
this.allowedListItems = switchMap(
|
||||
this.filter,
|
||||
filter -> getLiveData(new Pager<>(pagingConfig, () ->
|
||||
this.hostListItemDao.loadList(ALLOWED.getValue(), filter.sourcesIncluded, filter.sqlQuery)
|
||||
))
|
||||
);
|
||||
this.redirectedListItems = switchMap(
|
||||
this.filter,
|
||||
filter -> getLiveData(new Pager<>(pagingConfig, () ->
|
||||
this.hostListItemDao.loadList(REDIRECTED.getValue(), filter.sourcesIncluded, filter.sqlQuery)
|
||||
))
|
||||
);
|
||||
this.modelChanged = new MutableLiveData<>(false);
|
||||
}
|
||||
|
||||
public LiveData<PagingData<HostListItem>> getBlockedListItems() {
|
||||
return this.blockedListItems;
|
||||
}
|
||||
|
||||
public LiveData<PagingData<HostListItem>> getAllowedListItems() {
|
||||
return this.allowedListItems;
|
||||
}
|
||||
|
||||
public LiveData<PagingData<HostListItem>> getRedirectedListItems() {
|
||||
return this.redirectedListItems;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getModelChanged() {
|
||||
return this.modelChanged;
|
||||
}
|
||||
|
||||
public void toggleItemEnabled(HostListItem item) {
|
||||
item.setEnabled(!item.isEnabled());
|
||||
EXECUTOR.execute(() -> {
|
||||
this.hostListItemDao.update(item);
|
||||
this.modelChanged.postValue(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void addListItem(@NonNull ListType type, @NonNull String host, String redirection) {
|
||||
HostListItem item = new HostListItem();
|
||||
item.setType(type);
|
||||
item.setHost(host);
|
||||
item.setRedirection(redirection);
|
||||
item.setEnabled(true);
|
||||
item.setSourceId(USER_SOURCE_ID);
|
||||
EXECUTOR.execute(() -> {
|
||||
Optional<Integer> id = this.hostListItemDao.getHostId(host);
|
||||
if (id.isPresent()) {
|
||||
item.setId(id.get());
|
||||
this.hostListItemDao.update(item);
|
||||
} else {
|
||||
this.hostListItemDao.insert(item);
|
||||
}
|
||||
this.modelChanged.postValue(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void updateListItem(@NonNull HostListItem item, @NonNull String host, String redirection) {
|
||||
item.setHost(host);
|
||||
item.setRedirection(redirection);
|
||||
EXECUTOR.execute(() -> {
|
||||
this.hostListItemDao.update(item);
|
||||
this.modelChanged.postValue(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void removeListItem(HostListItem list) {
|
||||
EXECUTOR.execute(() -> {
|
||||
this.hostListItemDao.delete(list);
|
||||
this.modelChanged.postValue(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void search(String query) {
|
||||
ListsFilter currentFilter = getFilter();
|
||||
ListsFilter newFilter = new ListsFilter(currentFilter.sourcesIncluded, query);
|
||||
setFilter(newFilter);
|
||||
}
|
||||
|
||||
public boolean isSearching() {
|
||||
return !getFilter().query.isEmpty();
|
||||
}
|
||||
|
||||
public void clearSearch() {
|
||||
ListsFilter currentFilter = getFilter();
|
||||
ListsFilter newFilter = new ListsFilter(currentFilter.sourcesIncluded, "");
|
||||
setFilter(newFilter);
|
||||
}
|
||||
|
||||
public void toggleSources() {
|
||||
ListsFilter currentFilter = getFilter();
|
||||
ListsFilter newFilter = new ListsFilter(!currentFilter.sourcesIncluded, currentFilter.query);
|
||||
setFilter(newFilter);
|
||||
}
|
||||
|
||||
private ListsFilter getFilter() {
|
||||
ListsFilter filter = this.filter.getValue();
|
||||
return filter == null ? ALL : filter;
|
||||
}
|
||||
|
||||
private void setFilter(ListsFilter filter) {
|
||||
this.filter.setValue(filter);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package org.adaway.ui.lists.type;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.paging.PagingData;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.ui.lists.ListsViewCallback;
|
||||
import org.adaway.ui.lists.ListsViewModel;
|
||||
import org.adaway.util.Clipboard;
|
||||
|
||||
/**
|
||||
* This class is a {@link Fragment} to display and manage lists of {@link org.adaway.ui.lists.ListsActivity}.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public abstract class AbstractListFragment extends Fragment implements ListsViewCallback {
|
||||
/**
|
||||
* The view model (<code>null</code> if view is not created).
|
||||
*/
|
||||
protected ListsViewModel mViewModel;
|
||||
/**
|
||||
* The current activity (<code>null</code> if view is not created).
|
||||
*/
|
||||
protected FragmentActivity mActivity;
|
||||
/**
|
||||
* The current action mode when item is selection (<code>null</code> if no action started).
|
||||
*/
|
||||
private ActionMode mActionMode;
|
||||
/**
|
||||
* The action mode callback (<code>null</code> if view is not created).
|
||||
*/
|
||||
private ActionMode.Callback mActionCallback;
|
||||
/**
|
||||
* The hosts list related to the current action (<code>null</code> if view is not created).
|
||||
*/
|
||||
private HostListItem mActionItem;
|
||||
/**
|
||||
* The view related hosts source of the current action (<code>null</code> if view is not created).
|
||||
*/
|
||||
private View mActionSourceView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
// Store activity
|
||||
this.mActivity = requireActivity();
|
||||
// Create fragment view
|
||||
View view = inflater.inflate(R.layout.hosts_lists_fragment, container, false);
|
||||
/*
|
||||
* Configure recycler view.
|
||||
*/
|
||||
// Store recycler view
|
||||
RecyclerView recyclerView = view.findViewById(R.id.hosts_lists_list);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
// Defile recycler layout
|
||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this.mActivity);
|
||||
recyclerView.setLayoutManager(linearLayoutManager);
|
||||
// Create recycler adapter
|
||||
ListsAdapter adapter = new ListsAdapter(this, isTwoRowsItem());
|
||||
recyclerView.setAdapter(adapter);
|
||||
/*
|
||||
* Create action mode.
|
||||
*/
|
||||
// Create action mode callback to display edit/delete menu
|
||||
this.mActionCallback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
// Get menu inflater
|
||||
MenuInflater inflater = actionMode.getMenuInflater();
|
||||
// Set action mode title
|
||||
actionMode.setTitle(R.string.checkbox_list_context_title);
|
||||
// Inflate edit/delete menu
|
||||
inflater.inflate(R.menu.checkbox_list_context, menu);
|
||||
// Return action created
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
||||
// Nothing special to do
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode actionMode, MenuItem item) {
|
||||
// Check action item
|
||||
if (mActionItem == null) {
|
||||
return false;
|
||||
}
|
||||
// Check item identifier
|
||||
if (item.getItemId() == R.id.edit_action) {
|
||||
// Edit action item
|
||||
editItem(mActionItem);
|
||||
// Finish action mode
|
||||
mActionMode.finish();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.delete_action) {
|
||||
// Delete action item
|
||||
deleteItem(mActionItem);
|
||||
// Finish action mode
|
||||
mActionMode.finish();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode actionMode) {
|
||||
// Clear view background color
|
||||
if (mActionSourceView != null) {
|
||||
mActionSourceView.setBackgroundColor(Color.TRANSPARENT);
|
||||
}
|
||||
// Clear current source and its view
|
||||
mActionItem = null;
|
||||
mActionSourceView = null;
|
||||
// Clear action mode
|
||||
mActionMode = null;
|
||||
}
|
||||
};
|
||||
/*
|
||||
* Load data.
|
||||
*/
|
||||
// Get view model and bind it to the list view
|
||||
this.mViewModel = new ViewModelProvider(this.mActivity).get(ListsViewModel.class);
|
||||
getData().observe(getViewLifecycleOwner(), data -> adapter.submitData(getLifecycle(), data));
|
||||
// Return created view
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startAction(HostListItem item, View sourceView) {
|
||||
// Check if there is already a current action
|
||||
if (this.mActionMode != null) {
|
||||
return false;
|
||||
}
|
||||
// Store current source and its view
|
||||
this.mActionItem = item;
|
||||
this.mActionSourceView = sourceView;
|
||||
// Get current item background color
|
||||
int currentItemBackgroundColor = getResources().getColor(R.color.selected_background, null);
|
||||
// Apply background color to view
|
||||
this.mActionSourceView.setBackgroundColor(currentItemBackgroundColor);
|
||||
// Start action mode and store it
|
||||
this.mActionMode = this.mActivity.startActionMode(this.mActionCallback);
|
||||
// Return event consumed
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean copyHostToClipboard(HostListItem item) {
|
||||
Clipboard.copyHostToClipboard(this.mActivity, item.getHost());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure action mode is cancelled.
|
||||
*/
|
||||
public void ensureActionModeCanceled() {
|
||||
if (this.mActionMode != null) {
|
||||
this.mActionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract LiveData<PagingData<HostListItem>> getData();
|
||||
|
||||
protected boolean isTwoRowsItem() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a UI to add an item to the list.
|
||||
*/
|
||||
public abstract void addItem();
|
||||
|
||||
protected abstract void editItem(HostListItem item);
|
||||
|
||||
protected void deleteItem(HostListItem item) {
|
||||
this.mViewModel.removeListItem(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggleItemEnabled(HostListItem item) {
|
||||
this.mViewModel.toggleItemEnabled(item);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.lists.type;
|
||||
|
||||
|
||||
import android.text.Editable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagingData;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.ListType;
|
||||
import org.adaway.ui.dialog.AlertDialogValidator;
|
||||
import org.adaway.util.RegexUtils;
|
||||
|
||||
/**
|
||||
* This class is a {@link AbstractListFragment} to display and manage allowed hosts.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class AllowedHostsFragment extends AbstractListFragment {
|
||||
@Override
|
||||
protected LiveData<PagingData<HostListItem>> getData() {
|
||||
return this.mViewModel.getAllowedListItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addItem() {
|
||||
// Create dialog view
|
||||
LayoutInflater factory = LayoutInflater.from(this.mActivity);
|
||||
View view = factory.inflate(R.layout.lists_allowed_dialog, null);
|
||||
EditText inputEditText = view.findViewById(R.id.list_dialog_hostname);
|
||||
// Create dialog
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this.mActivity)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.list_add_dialog_white)
|
||||
.setView(view)
|
||||
// Setup buttons
|
||||
.setPositiveButton(
|
||||
R.string.button_add,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check hostname validity
|
||||
String hostname = inputEditText.getText().toString();
|
||||
if (RegexUtils.isValidWildcardHostname(hostname)) {
|
||||
// Insert host to whitelist
|
||||
this.mViewModel.addListItem(ListType.ALLOWED, hostname, null);
|
||||
}
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel,
|
||||
(dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
inputEditText.addTextChangedListener(
|
||||
new AlertDialogValidator(alertDialog, RegexUtils::isValidWildcardHostname, false)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void editItem(HostListItem item) {
|
||||
// Create dialog view
|
||||
LayoutInflater factory = LayoutInflater.from(this.mActivity);
|
||||
View view = factory.inflate(R.layout.lists_allowed_dialog, null);
|
||||
// Set hostname
|
||||
EditText inputEditText = view.findViewById(R.id.list_dialog_hostname);
|
||||
inputEditText.setText(item.getHost());
|
||||
// Move cursor to end of EditText
|
||||
Editable inputEditContent = inputEditText.getText();
|
||||
inputEditText.setSelection(inputEditContent.length());
|
||||
// Create dialog builder
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this.mActivity)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.list_edit_dialog_white)
|
||||
.setView(view)
|
||||
// Setup buttons
|
||||
.setPositiveButton(
|
||||
R.string.button_save,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check hostname validity
|
||||
String hostname = inputEditText.getText().toString();
|
||||
if (RegexUtils.isValidWildcardHostname(hostname)) {
|
||||
// Update list item
|
||||
this.mViewModel.updateListItem(item, hostname, null);
|
||||
}
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel,
|
||||
(dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
inputEditText.addTextChangedListener(
|
||||
new AlertDialogValidator(alertDialog, RegexUtils::isValidWildcardHostname, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.lists.type;
|
||||
|
||||
|
||||
import android.text.Editable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagingData;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.ui.dialog.AlertDialogValidator;
|
||||
import org.adaway.util.RegexUtils;
|
||||
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
|
||||
/**
|
||||
* This class is a {@link AbstractListFragment} to display and manage blocked hosts.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class BlockedHostsFragment extends AbstractListFragment {
|
||||
@Override
|
||||
protected LiveData<PagingData<HostListItem>> getData() {
|
||||
return this.mViewModel.getBlockedListItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addItem() {
|
||||
// Create dialog view
|
||||
LayoutInflater factory = LayoutInflater.from(this.mActivity);
|
||||
View view = factory.inflate(R.layout.lists_blocked_dialog, null);
|
||||
EditText inputEditText = view.findViewById(R.id.list_dialog_hostname);
|
||||
// Create dialog
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this.mActivity)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.list_add_dialog_black)
|
||||
.setView(view)
|
||||
// Setup buttons
|
||||
.setPositiveButton(
|
||||
R.string.button_add,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check if hostname is valid
|
||||
String hostname = inputEditText.getText().toString();
|
||||
if (RegexUtils.isValidHostname(hostname)) {
|
||||
// Insert host to black list
|
||||
this.mViewModel.addListItem(BLOCKED, hostname, null);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel,
|
||||
(dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
inputEditText.addTextChangedListener(
|
||||
new AlertDialogValidator(alertDialog, RegexUtils::isValidHostname, false)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void editItem(HostListItem item) {
|
||||
// Create dialog view
|
||||
LayoutInflater factory = LayoutInflater.from(this.mActivity);
|
||||
View view = factory.inflate(R.layout.lists_blocked_dialog, null);
|
||||
// Set hostname
|
||||
EditText inputEditText = view.findViewById(R.id.list_dialog_hostname);
|
||||
inputEditText.setText(item.getHost());
|
||||
// Move cursor to end of EditText
|
||||
Editable inputEditContent = inputEditText.getText();
|
||||
inputEditText.setSelection(inputEditContent.length());
|
||||
// Create dialog
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this.mActivity)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.list_edit_dialog_black)
|
||||
.setView(view)
|
||||
// Setup buttons
|
||||
.setPositiveButton(
|
||||
R.string.button_save,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check hostname validity
|
||||
String hostname = inputEditText.getText().toString();
|
||||
if (RegexUtils.isValidHostname(hostname)) {
|
||||
// Update list item
|
||||
this.mViewModel.updateListItem(item, hostname, null);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel
|
||||
, (dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
inputEditText.addTextChangedListener(
|
||||
new AlertDialogValidator(alertDialog, RegexUtils::isValidHostname, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
131
app/src/main/java/org/adaway/ui/lists/type/ListsAdapter.java
Normal file
131
app/src/main/java/org/adaway/ui/lists/type/ListsAdapter.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package org.adaway.ui.lists.type;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.paging.PagingDataAdapter;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.ui.lists.ListsViewCallback;
|
||||
|
||||
import static org.adaway.db.entity.HostsSource.USER_SOURCE_ID;
|
||||
|
||||
/**
|
||||
* This class is a the {@link RecyclerView.Adapter} for the hosts list view.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class ListsAdapter extends PagingDataAdapter<HostListItem, ListsAdapter.ViewHolder> {
|
||||
/**
|
||||
* This callback is use to compare hosts sources.
|
||||
*/
|
||||
private static final DiffUtil.ItemCallback<HostListItem> DIFF_CALLBACK =
|
||||
new DiffUtil.ItemCallback<HostListItem>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull HostListItem oldItem, @NonNull HostListItem newItem) {
|
||||
return (oldItem.getHost().equals(newItem.getHost()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull HostListItem oldItem, @NonNull HostListItem newItem) {
|
||||
// NOTE: if you use equals, your object must properly override Object#equals()
|
||||
// Incorrectly returning false here will result in too many animations.
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This callback is use to call view actions.
|
||||
*/
|
||||
@NonNull
|
||||
private final ListsViewCallback viewCallback;
|
||||
/**
|
||||
* Whether the list item needs two rows or not.
|
||||
*/
|
||||
private final boolean twoRows;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param viewCallback The view callback.
|
||||
* @param twoRows Whether the list items need two rows or not.
|
||||
*/
|
||||
ListsAdapter(@NonNull ListsViewCallback viewCallback, boolean twoRows) {
|
||||
super(DIFF_CALLBACK);
|
||||
this.viewCallback = viewCallback;
|
||||
this.twoRows = twoRows;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
|
||||
View view = layoutInflater.inflate(
|
||||
this.twoRows ? R.layout.checkbox_list_two_entries : R.layout.checkbox_list_entry,
|
||||
parent,
|
||||
false
|
||||
);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
HostListItem item = getItem(position);
|
||||
if (item == null) { // Data might be null if not loaded yet
|
||||
holder.clear();
|
||||
return;
|
||||
}
|
||||
boolean editable = item.getSourceId() == USER_SOURCE_ID;
|
||||
holder.enabledCheckBox.setEnabled(editable);
|
||||
holder.enabledCheckBox.setChecked(item.isEnabled());
|
||||
holder.enabledCheckBox.setOnClickListener(editable ? view -> this.viewCallback.toggleItemEnabled(item) : null);
|
||||
holder.hostTextView.setText(item.getHost());
|
||||
if (this.twoRows) {
|
||||
holder.redirectionTextView.setText(item.getRedirection());
|
||||
}
|
||||
holder.itemView.setOnLongClickListener(editable ?
|
||||
view -> this.viewCallback.startAction(item, holder.itemView) :
|
||||
view -> this.viewCallback.copyHostToClipboard(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a the {@link RecyclerView.ViewHolder} for the hosts list view.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final CheckBox enabledCheckBox;
|
||||
final TextView hostTextView;
|
||||
final TextView redirectionTextView;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param itemView The hosts sources view.
|
||||
*/
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.enabledCheckBox = itemView.findViewById(R.id.checkbox_list_checkbox);
|
||||
this.hostTextView = itemView.findViewById(R.id.checkbox_list_text);
|
||||
this.redirectionTextView = itemView.findViewById(R.id.checkbox_list_subtext);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.enabledCheckBox.setChecked(true);
|
||||
this.enabledCheckBox.setEnabled(false);
|
||||
this.enabledCheckBox.setOnClickListener(null);
|
||||
this.hostTextView.setText("");
|
||||
if (this.redirectionTextView != null) {
|
||||
this.redirectionTextView.setText("");
|
||||
}
|
||||
this.itemView.setOnLongClickListener(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.lists.type;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagingData;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.db.entity.HostListItem;
|
||||
import org.adaway.db.entity.ListType;
|
||||
import org.adaway.ui.dialog.AlertDialogValidator;
|
||||
import org.adaway.util.RegexUtils;
|
||||
|
||||
/**
|
||||
* This class is a {@link AbstractListFragment} to display and manage redirected hosts.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class RedirectedHostsFragment extends AbstractListFragment {
|
||||
@Override
|
||||
protected boolean isTwoRowsItem() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LiveData<PagingData<HostListItem>> getData() {
|
||||
return this.mViewModel.getRedirectedListItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addItem() {
|
||||
// Create dialog view
|
||||
LayoutInflater factory = LayoutInflater.from(this.mActivity);
|
||||
View view = factory.inflate(R.layout.lists_redirected_dialog, null);
|
||||
EditText hostnameEditText = view.findViewById(R.id.list_dialog_hostname);
|
||||
EditText ipEditText = view.findViewById(R.id.list_dialog_ip);
|
||||
// Create dialog
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this.mActivity)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.list_add_dialog_redirect)
|
||||
.setView(view)
|
||||
// Setup buttons
|
||||
.setPositiveButton(
|
||||
R.string.button_add,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check if hostname and IP are valid
|
||||
String hostname = hostnameEditText.getText().toString();
|
||||
String ip = ipEditText.getText().toString();
|
||||
if (RegexUtils.isValidHostname(hostname) && RegexUtils.isValidIP(ip)) {
|
||||
// Insert host to redirection list
|
||||
this.mViewModel.addListItem(ListType.REDIRECTED, hostname, ip);
|
||||
}
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel,
|
||||
(dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
AlertDialogValidator validator = new AlertDialogValidator(
|
||||
alertDialog,
|
||||
input -> {
|
||||
String hostname = hostnameEditText.getText().toString();
|
||||
String ip = ipEditText.getText().toString();
|
||||
return RegexUtils.isValidHostname(hostname) && RegexUtils.isValidIP(ip);
|
||||
},
|
||||
false
|
||||
);
|
||||
hostnameEditText.addTextChangedListener(validator);
|
||||
ipEditText.addTextChangedListener(validator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void editItem(HostListItem item) {
|
||||
// Create dialog view
|
||||
LayoutInflater factory = LayoutInflater.from(this.mActivity);
|
||||
View view = factory.inflate(R.layout.lists_redirected_dialog, null);
|
||||
// Set hostname and IP
|
||||
EditText hostnameEditText = view.findViewById(R.id.list_dialog_hostname);
|
||||
EditText ipEditText = view.findViewById(R.id.list_dialog_ip);
|
||||
hostnameEditText.setText(item.getHost());
|
||||
ipEditText.setText(item.getRedirection());
|
||||
// Move cursor to end of EditText
|
||||
Editable hostnameEditContent = hostnameEditText.getText();
|
||||
hostnameEditText.setSelection(hostnameEditContent.length());
|
||||
// Create dialog
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this.mActivity)
|
||||
.setCancelable(true)
|
||||
.setTitle(getString(R.string.list_edit_dialog_redirect))
|
||||
.setView(view)
|
||||
// Set buttons
|
||||
.setPositiveButton(R.string.button_save,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check hostname and IP validity
|
||||
String hostname = hostnameEditText.getText().toString();
|
||||
String ip = ipEditText.getText().toString();
|
||||
if (RegexUtils.isValidHostname(hostname) && RegexUtils.isValidIP(ip)) {
|
||||
// Update list item
|
||||
this.mViewModel.updateListItem(item, hostname, ip);
|
||||
}
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel,
|
||||
(dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
AlertDialogValidator validator = new AlertDialogValidator(
|
||||
alertDialog,
|
||||
input -> {
|
||||
String hostname = hostnameEditText.getText().toString();
|
||||
String ip = ipEditText.getText().toString();
|
||||
return RegexUtils.isValidHostname(hostname) && RegexUtils.isValidIP(ip);
|
||||
},
|
||||
true
|
||||
);
|
||||
hostnameEditText.addTextChangedListener(validator);
|
||||
ipEditText.addTextChangedListener(validator);
|
||||
}
|
||||
}
|
||||
235
app/src/main/java/org/adaway/ui/log/LogActivity.java
Normal file
235
app/src/main/java/org/adaway/ui/log/LogActivity.java
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (C) 2011-2012 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* This file is part of AdAway.
|
||||
*
|
||||
* AdAway is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AdAway is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AdAway. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.adaway.ui.log;
|
||||
|
||||
import static org.adaway.ui.Animations.hideView;
|
||||
import static org.adaway.ui.Animations.showView;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.databinding.LogActivityBinding;
|
||||
import org.adaway.databinding.LogRedirectDialogBinding;
|
||||
import org.adaway.db.entity.ListType;
|
||||
import org.adaway.helper.ThemeHelper;
|
||||
import org.adaway.ui.adblocking.ApplyConfigurationSnackbar;
|
||||
import org.adaway.ui.dialog.AlertDialogValidator;
|
||||
import org.adaway.util.Clipboard;
|
||||
import org.adaway.util.RegexUtils;
|
||||
|
||||
/**
|
||||
* This class is an {@link android.app.Activity} to show DNS request log entries.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
public class LogActivity extends AppCompatActivity implements LogViewCallback {
|
||||
private LogActivityBinding binding;
|
||||
/**
|
||||
* The view model (<code>null</code> if activity is not created).
|
||||
*/
|
||||
private LogViewModel mViewModel;
|
||||
/**
|
||||
* The snackbar notification (<code>null</code> if activity is not created).
|
||||
*/
|
||||
private ApplyConfigurationSnackbar mApplySnackbar;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
/*
|
||||
* Create activity.
|
||||
*/
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.applyTheme(this);
|
||||
this.binding = LogActivityBinding.inflate(getLayoutInflater());
|
||||
setContentView(this.binding.getRoot());
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
// Get view model
|
||||
this.mViewModel = new ViewModelProvider(this).get(LogViewModel.class);
|
||||
/*
|
||||
* Configure swipe layout.
|
||||
*/
|
||||
this.binding.swipeRefresh.setOnRefreshListener(this.mViewModel::updateLogs);
|
||||
/*
|
||||
* Configure empty view.
|
||||
*/
|
||||
if (this.mViewModel.areBlockedRequestsIgnored()) {
|
||||
this.binding.emptyTextView.append(getString(R.string.log_blocked_requests_ignored));
|
||||
}
|
||||
/*
|
||||
* Configure recycler view.
|
||||
*/
|
||||
// Get recycler view
|
||||
this.binding.logList.setHasFixedSize(true);
|
||||
// Defile recycler layout
|
||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
|
||||
this.binding.logList.setLayoutManager(linearLayoutManager);
|
||||
// Create recycler adapter
|
||||
LogAdapter adapter = new LogAdapter(this);
|
||||
this.binding.logList.setAdapter(adapter);
|
||||
/*
|
||||
* Configure fab.
|
||||
*/
|
||||
this.binding.toggleLogRecording.setOnClickListener(v -> this.mViewModel.toggleRecording());
|
||||
this.mViewModel.isRecording().observe(this, recoding ->
|
||||
this.binding.toggleLogRecording.setImageResource(TRUE.equals(recoding) ?
|
||||
R.drawable.ic_pause_24dp :
|
||||
R.drawable.ic_record_24dp
|
||||
)
|
||||
);
|
||||
/*
|
||||
* Configure snackbar.
|
||||
*/
|
||||
// Create apply snackbar
|
||||
this.mApplySnackbar = new ApplyConfigurationSnackbar(this.binding.swipeRefresh, false, false);
|
||||
/*
|
||||
* Load data.
|
||||
*/
|
||||
// Bind view model to the list view
|
||||
this.mViewModel.getLogs().observe(this, logEntries -> {
|
||||
if (logEntries.isEmpty()) {
|
||||
showView(this.binding.emptyTextView);
|
||||
} else {
|
||||
hideView(this.binding.emptyTextView);
|
||||
}
|
||||
adapter.submitList(logEntries);
|
||||
this.binding.swipeRefresh.setRefreshing(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// Mark as loading data
|
||||
this.binding.swipeRefresh.setRefreshing(true);
|
||||
// Load initial data
|
||||
this.mViewModel.updateLogs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater menuInflater = getMenuInflater();
|
||||
menuInflater.inflate(R.menu.log_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.sort) {
|
||||
this.mViewModel.toggleSort();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.delete) {
|
||||
this.mViewModel.clearLogs();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListItem(@NonNull String hostName, @NonNull ListType type) {
|
||||
// Check view model and snackbar notification
|
||||
if (this.mViewModel == null || this.mApplySnackbar == null) {
|
||||
return;
|
||||
}
|
||||
// Check type other than redirection
|
||||
if (type != ListType.REDIRECTED) {
|
||||
// Insert list item
|
||||
this.mViewModel.addListItem(hostName, type, null);
|
||||
// Display snackbar notification
|
||||
this.mApplySnackbar.notifyUpdateAvailable();
|
||||
} else {
|
||||
// Create dialog view
|
||||
LayoutInflater inflater = LayoutInflater.from(this);
|
||||
LogRedirectDialogBinding redirectBinding = LogRedirectDialogBinding.inflate(inflater);
|
||||
// Create dialog
|
||||
AlertDialog alertDialog = new MaterialAlertDialogBuilder(this)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.log_redirect_dialog_title)
|
||||
.setView(redirectBinding.getRoot())
|
||||
// Setup buttons
|
||||
.setPositiveButton(
|
||||
R.string.button_add,
|
||||
(dialog, which) -> {
|
||||
// Close dialog
|
||||
dialog.dismiss();
|
||||
// Check IP is valid
|
||||
String ip = redirectBinding.redirectIp.getText().toString();
|
||||
if (RegexUtils.isValidIP(ip)) {
|
||||
// Insert list item
|
||||
this.mViewModel.addListItem(hostName, type, ip);
|
||||
// Display snackbar notification
|
||||
this.mApplySnackbar.notifyUpdateAvailable();
|
||||
}
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
R.string.button_cancel,
|
||||
(dialog, which) -> dialog.dismiss()
|
||||
)
|
||||
.create();
|
||||
// Show dialog
|
||||
alertDialog.show();
|
||||
// Set button validation behavior
|
||||
redirectBinding.redirectIp.addTextChangedListener(
|
||||
new AlertDialogValidator(alertDialog, RegexUtils::isValidIP, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListItem(@NonNull String hostName) {
|
||||
if (this.mViewModel != null && this.mApplySnackbar != null) {
|
||||
this.mViewModel.removeListItem(hostName);
|
||||
this.mApplySnackbar.notifyUpdateAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openHostInBrowser(@NonNull String hostName) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("http://" + hostName));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyHostToClipboard(@NonNull String hostName) {
|
||||
Clipboard.copyHostToClipboard(this, hostName);
|
||||
}
|
||||
}
|
||||
107
app/src/main/java/org/adaway/ui/log/LogAdapter.java
Normal file
107
app/src/main/java/org/adaway/ui/log/LogAdapter.java
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package org.adaway.ui.log;
|
||||
|
||||
import static android.graphics.PorterDuff.Mode.MULTIPLY;
|
||||
import static org.adaway.db.entity.ListType.ALLOWED;
|
||||
import static org.adaway.db.entity.ListType.BLOCKED;
|
||||
import static org.adaway.db.entity.ListType.REDIRECTED;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.adaway.R;
|
||||
import org.adaway.databinding.LogEntryBinding;
|
||||
import org.adaway.db.entity.ListType;
|
||||
|
||||
/**
|
||||
* This class is a the {@link RecyclerView.Adapter} for the DNS request log view.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
class LogAdapter extends ListAdapter<LogEntry, LogAdapter.ViewHolder> {
|
||||
/**
|
||||
* This callback is use to compare hosts sources.
|
||||
*/
|
||||
private static final DiffUtil.ItemCallback<LogEntry> DIFF_CALLBACK =
|
||||
new DiffUtil.ItemCallback<LogEntry>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull LogEntry oldEntry, @NonNull LogEntry newEntry) {
|
||||
return oldEntry.getHost().equals(newEntry.getHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull LogEntry oldEntry, @NonNull LogEntry newEntry) {
|
||||
return oldEntry.equals(newEntry);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The activity view callback.
|
||||
*/
|
||||
private final LogViewCallback callback;
|
||||
|
||||
LogAdapter(LogViewCallback callback) {
|
||||
super(DIFF_CALLBACK);
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
|
||||
LogEntryBinding binding = LogEntryBinding.inflate(layoutInflater, parent, false);
|
||||
return new ViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
// Get log entry
|
||||
LogEntry entry = getItem(position);
|
||||
// Set host name
|
||||
holder.binding.hostnameTextView.setText(entry.getHost());
|
||||
holder.binding.hostnameTextView.setOnClickListener(v -> this.callback.openHostInBrowser(entry.getHost()));
|
||||
holder.binding.hostnameTextView.setOnLongClickListener(v -> {
|
||||
this.callback.copyHostToClipboard(entry.getHost());
|
||||
return true;
|
||||
});
|
||||
// Set type status
|
||||
bindImageView(holder.binding.blockImageView, BLOCKED, entry);
|
||||
bindImageView(holder.binding.allowImageView, ALLOWED, entry);
|
||||
bindImageView(holder.binding.redirectionImageView, REDIRECTED, entry);
|
||||
}
|
||||
|
||||
private void bindImageView(ImageView imageView, ListType type, LogEntry entry) {
|
||||
if (type == entry.getType()) {
|
||||
int primaryColor = this.callback.getColor(R.color.primary);
|
||||
imageView.setColorFilter(primaryColor, MULTIPLY);
|
||||
imageView.setOnClickListener(v -> this.callback.removeListItem(entry.getHost()));
|
||||
} else {
|
||||
imageView.clearColorFilter();
|
||||
imageView.setOnClickListener(v -> this.callback.addListItem(entry.getHost(), type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a the {@link RecyclerView.ViewHolder} for the log entry view.
|
||||
*
|
||||
* @author Bruce BUJON (bruce.bujon(at)gmail(dot)com)
|
||||
*/
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final org.adaway.databinding.LogEntryBinding binding;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param binding The log entry view binding.
|
||||
*/
|
||||
ViewHolder(LogEntryBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue