Set up project skeleton (milestone 1)
- Architecture and feature overview in docs/ARCHITECTURE.md - Gradle 9.8 / AGP 9.4 / Kotlin 2.4 with version catalog and convention plugins - Modules :app, :core:*, :device-profiles, :feature:* (placeholder screens) - Flavors core (no INTERNET, enforced by a build check) and full - Material You theme, navigation, expert mode and dry-run settings (DataStore) - Pixel 8-10 device detection, unit tests, GPLv3 license, English README with Obtainium badge Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
plugins {
|
||||
alias(libs.plugins.pixelwall.android.application)
|
||||
alias(libs.plugins.pixelwall.android.compose)
|
||||
alias(libs.plugins.pixelwall.android.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.pixelwall.app"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.pixelwall.app"
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
}
|
||||
|
||||
flavorDimensions += "network"
|
||||
productFlavors {
|
||||
// Ohne INTERNET-Berechtigung. Firewall nur als Sperre pro App.
|
||||
create("core") {
|
||||
dimension = "network"
|
||||
isDefault = true
|
||||
}
|
||||
// Mit INTERNET: DNS-Filter, Blocklisten, optionale Regelwerk-Updates.
|
||||
create("full") {
|
||||
dimension = "network"
|
||||
applicationIdSuffix = ".full"
|
||||
versionNameSuffix = "-full"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
// F-Droid / reproducible builds: keine verschlüsselten Abhängigkeits-Metadaten im APK.
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
androidResources {
|
||||
localeFilters += listOf("en", "de")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:domain"))
|
||||
implementation(project(":core:data"))
|
||||
implementation(project(":core:system"))
|
||||
implementation(project(":core:privileged"))
|
||||
implementation(project(":core:ui"))
|
||||
implementation(project(":device-profiles"))
|
||||
implementation(project(":feature:dashboard"))
|
||||
implementation(project(":feature:permissions"))
|
||||
implementation(project(":feature:network"))
|
||||
implementation(project(":feature:system"))
|
||||
implementation(project(":feature:profiles"))
|
||||
implementation(project(":feature:trackers"))
|
||||
implementation(project(":feature:monitor"))
|
||||
implementation(project(":feature:changelog"))
|
||||
implementation(project(":feature:onboarding"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
testImplementation(project(":core:testing"))
|
||||
testImplementation(libs.truth)
|
||||
testImplementation(libs.turbine)
|
||||
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
# Projektspezifische R8-Regeln. Hilt, Compose und DataStore bringen eigene Consumer-Regeln mit.
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Flavor "core": Netzwerkzugriff ist ausgeschlossen, auch wenn eine Bibliothek ihn anfordert. -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Flavor "full": INTERNET nur für DNS-Weiterleitung der Firewall und optionale Regelwerk-Updates. -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name=".PixelwallApplication"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.pixelwall.app
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.pixelwall.app.navigation.PixelwallApp
|
||||
import org.pixelwall.core.ui.theme.PixelwallTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
PixelwallTheme {
|
||||
PixelwallApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.pixelwall.app
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class PixelwallApplication : Application()
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.pixelwall.app.navigation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Apps
|
||||
import androidx.compose.material.icons.outlined.Lan
|
||||
import androidx.compose.material.icons.outlined.MoreHoriz
|
||||
import androidx.compose.material.icons.outlined.Security
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.pixelwall.app.R
|
||||
import org.pixelwall.app.settings.MoreScreen
|
||||
import org.pixelwall.feature.changelog.changelogScreen
|
||||
import org.pixelwall.feature.dashboard.DASHBOARD_ROUTE
|
||||
import org.pixelwall.feature.dashboard.dashboardScreen
|
||||
import org.pixelwall.feature.monitor.monitorScreen
|
||||
import org.pixelwall.feature.network.NETWORK_ROUTE
|
||||
import org.pixelwall.feature.network.networkScreen
|
||||
import org.pixelwall.feature.onboarding.onboardingScreen
|
||||
import org.pixelwall.feature.permissions.PERMISSIONS_ROUTE
|
||||
import org.pixelwall.feature.permissions.permissionsScreen
|
||||
import org.pixelwall.feature.profiles.profilesScreen
|
||||
import org.pixelwall.feature.system.SYSTEM_ROUTE
|
||||
import org.pixelwall.feature.system.systemScreen
|
||||
import org.pixelwall.feature.trackers.trackersScreen
|
||||
|
||||
const val MORE_ROUTE = "more"
|
||||
|
||||
enum class TopLevelDestination(val route: String, @StringRes val label: Int, val icon: ImageVector) {
|
||||
DASHBOARD(DASHBOARD_ROUTE, R.string.nav_dashboard, Icons.Outlined.Security),
|
||||
PERMISSIONS(PERMISSIONS_ROUTE, R.string.nav_permissions, Icons.Outlined.Apps),
|
||||
NETWORK(NETWORK_ROUTE, R.string.nav_network, Icons.Outlined.Lan),
|
||||
SYSTEM(SYSTEM_ROUTE, R.string.nav_system, Icons.Outlined.Settings),
|
||||
MORE(MORE_ROUTE, R.string.nav_more, Icons.Outlined.MoreHoriz),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PixelwallApp(navController: NavHostController = rememberNavController()) {
|
||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = backStackEntry?.destination
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
TopLevelDestination.entries.forEach { dest ->
|
||||
val selected = currentDestination?.hierarchy?.any { it.route == dest.route } == true
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
navController.navigate(dest.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = { Icon(dest.icon, contentDescription = null) },
|
||||
label = { Text(stringResource(dest.label)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = DASHBOARD_ROUTE,
|
||||
modifier = Modifier.padding(padding),
|
||||
) {
|
||||
dashboardScreen()
|
||||
permissionsScreen()
|
||||
networkScreen()
|
||||
systemScreen()
|
||||
profilesScreen()
|
||||
trackersScreen()
|
||||
monitorScreen()
|
||||
changelogScreen()
|
||||
onboardingScreen()
|
||||
composable(MORE_ROUTE) {
|
||||
MoreScreen(onNavigate = { route -> navController.navigate(route) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.pixelwall.app.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ListAlt
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.NotificationsActive
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.pixelwall.app.R
|
||||
import org.pixelwall.feature.changelog.CHANGELOG_ROUTE
|
||||
import org.pixelwall.feature.monitor.MONITOR_ROUTE
|
||||
import org.pixelwall.feature.onboarding.ONBOARDING_ROUTE
|
||||
import org.pixelwall.feature.profiles.PROFILES_ROUTE
|
||||
import org.pixelwall.feature.trackers.TRACKERS_ROUTE
|
||||
|
||||
private data class MoreEntry(@StringRes val label: Int, val icon: ImageVector, val route: String)
|
||||
|
||||
private val toolEntries = listOf(
|
||||
MoreEntry(R.string.more_profiles, Icons.Outlined.Tune, PROFILES_ROUTE),
|
||||
MoreEntry(R.string.more_trackers, Icons.Outlined.BugReport, TRACKERS_ROUTE),
|
||||
MoreEntry(R.string.more_monitor, Icons.Outlined.NotificationsActive, MONITOR_ROUTE),
|
||||
MoreEntry(R.string.more_changelog, Icons.AutoMirrored.Outlined.ListAlt, CHANGELOG_ROUTE),
|
||||
MoreEntry(R.string.more_onboarding, Icons.Outlined.AdminPanelSettings, ONBOARDING_ROUTE),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MoreScreen(onNavigate: (String) -> Unit, viewModel: MoreViewModel = hiltViewModel()) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text(state.device.model) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(R.string.device_line, state.device.device, state.device.release, state.device.securityPatch) +
|
||||
if (state.isSupportedPixel) "" else "\n" + stringResource(R.string.device_unsupported),
|
||||
)
|
||||
},
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
item { SectionHeader(R.string.more_section_tools) }
|
||||
toolEntries.forEach { entry ->
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(entry.label)) },
|
||||
leadingContent = { Icon(entry.icon, contentDescription = null) },
|
||||
modifier = Modifier.clickable(role = Role.Button) { onNavigate(entry.route) },
|
||||
)
|
||||
}
|
||||
}
|
||||
item { SectionHeader(R.string.more_section_settings) }
|
||||
item {
|
||||
SwitchItem(
|
||||
title = R.string.setting_expert_mode,
|
||||
summary = R.string.setting_expert_mode_summary,
|
||||
checked = state.preferences.expertMode,
|
||||
onCheckedChange = viewModel::setExpertMode,
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchItem(
|
||||
title = R.string.setting_dry_run,
|
||||
summary = R.string.setting_dry_run_summary,
|
||||
checked = state.preferences.dryRun,
|
||||
onCheckedChange = viewModel::setDryRun,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(@StringRes text: Int) {
|
||||
Text(
|
||||
text = stringResource(text),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 8.dp)
|
||||
.semantics { heading() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchItem(@StringRes title: Int, @StringRes summary: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(title)) },
|
||||
supportingContent = { Text(stringResource(summary)) },
|
||||
// Der Switch selbst ist nicht fokussierbar; die ganze Zeile ist der Schalter (TalkBack liest Titel + Zustand).
|
||||
trailingContent = { Switch(checked = checked, onCheckedChange = null) },
|
||||
modifier = Modifier.toggleable(value = checked, role = Role.Switch, onValueChange = onCheckedChange),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.pixelwall.app.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixelwall.core.domain.DeviceInfoProvider
|
||||
import org.pixelwall.core.domain.UserPreferences
|
||||
import org.pixelwall.core.domain.UserPreferencesRepository
|
||||
import org.pixelwall.core.model.DeviceInfo
|
||||
import org.pixelwall.deviceprofiles.DeviceProfiles
|
||||
import org.pixelwall.deviceprofiles.PixelProfile
|
||||
import javax.inject.Inject
|
||||
|
||||
data class MoreUiState(
|
||||
val preferences: UserPreferences = UserPreferences(),
|
||||
val device: DeviceInfo,
|
||||
val isSupportedPixel: Boolean,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class MoreViewModel @Inject constructor(
|
||||
private val preferencesRepository: UserPreferencesRepository,
|
||||
deviceInfoProvider: DeviceInfoProvider,
|
||||
) : ViewModel() {
|
||||
|
||||
private val device = deviceInfoProvider.current()
|
||||
private val supported = DeviceProfiles.resolve(device) == PixelProfile
|
||||
|
||||
val uiState: StateFlow<MoreUiState> = preferencesRepository.preferences
|
||||
.map { MoreUiState(it, device, supported) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), MoreUiState(device = device, isSupportedPixel = supported))
|
||||
|
||||
fun setExpertMode(enabled: Boolean) {
|
||||
viewModelScope.launch { preferencesRepository.setExpertMode(enabled) }
|
||||
}
|
||||
|
||||
fun setDryRun(enabled: Boolean) {
|
||||
viewModelScope.launch { preferencesRepository.setDryRun(enabled) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#FF1F3B63" />
|
||||
</shape>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Schild mit Pixelraster, 108dp-Raster, Inhalt in der 66dp-Sicherheitszone. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M54,28 L76,36 L76,54 C76,67 66,77 54,81 C42,77 32,67 32,54 L32,36 Z" />
|
||||
<path
|
||||
android:fillColor="#FF1F3B63"
|
||||
android:pathData="M44,44h8v8h-8z M56,44h8v8h-8z M44,56h8v8h-8z M56,56h8v8h-8z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="nav_dashboard">Übersicht</string>
|
||||
<string name="nav_permissions">Berechtigungen</string>
|
||||
<string name="nav_network">Netzwerk</string>
|
||||
<string name="nav_system">System</string>
|
||||
<string name="nav_more">Mehr</string>
|
||||
<string name="more_title">Mehr</string>
|
||||
<string name="more_section_tools">Werkzeuge</string>
|
||||
<string name="more_section_settings">Einstellungen</string>
|
||||
<string name="more_profiles">Profile & Automatisierung</string>
|
||||
<string name="more_trackers">Tracker-Analyse</string>
|
||||
<string name="more_monitor">Überwachung</string>
|
||||
<string name="more_changelog">Änderungsprotokoll & Rückgängig</string>
|
||||
<string name="more_onboarding">Zugriffsstufen (Shizuku, ADB)</string>
|
||||
<string name="setting_expert_mode">Expertenmodus</string>
|
||||
<string name="setting_expert_mode_summary">Zeigt die rohen Settings-Keys und Shell-Befehle an, die ausgeführt werden.</string>
|
||||
<string name="setting_dry_run">Probelauf (Dry Run)</string>
|
||||
<string name="setting_dry_run_summary">Zeigt nur an, was sich ändern würde. Es wird nichts angewendet.</string>
|
||||
<string name="device_line">%1$s · Android %2$s · Patch %3$s</string>
|
||||
<string name="device_unsupported">Dieses Gerät ist kein unterstütztes Pixel. Gerätespezifische Funktionen sind ausgegraut.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Pixelwall</string>
|
||||
<string name="nav_dashboard">Overview</string>
|
||||
<string name="nav_permissions">Permissions</string>
|
||||
<string name="nav_network">Network</string>
|
||||
<string name="nav_system">System</string>
|
||||
<string name="nav_more">More</string>
|
||||
<string name="more_title">More</string>
|
||||
<string name="more_section_tools">Tools</string>
|
||||
<string name="more_section_settings">Settings</string>
|
||||
<string name="more_profiles">Profiles & automation</string>
|
||||
<string name="more_trackers">Tracker analysis</string>
|
||||
<string name="more_monitor">Monitoring</string>
|
||||
<string name="more_changelog">Change log & undo</string>
|
||||
<string name="more_onboarding">Access levels (Shizuku, ADB)</string>
|
||||
<string name="setting_expert_mode">Expert mode</string>
|
||||
<string name="setting_expert_mode_summary">Show the raw settings keys and shell commands that will run.</string>
|
||||
<string name="setting_dry_run">Dry run</string>
|
||||
<string name="setting_dry_run_summary">Only show what would change. Nothing is applied.</string>
|
||||
<string name="device_line">%1$s · Android %2$s · patch %3$s</string>
|
||||
<string name="device_unsupported">This device is not a supported Pixel. Device-specific features are greyed out.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Keine Cloud-Sicherung und keine Geräteübertragung von Protokollen und Snapshots. -->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="file" />
|
||||
<exclude domain="database" />
|
||||
<exclude domain="sharedpref" />
|
||||
<exclude domain="external" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" />
|
||||
<exclude domain="file" />
|
||||
<exclude domain="database" />
|
||||
<exclude domain="sharedpref" />
|
||||
<exclude domain="external" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.pixelwall.app.settings
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.pixelwall.core.domain.DeviceInfoProvider
|
||||
import org.pixelwall.core.model.DeviceInfo
|
||||
import org.pixelwall.core.testing.FakeUserPreferencesRepository
|
||||
import org.pixelwall.core.testing.MainDispatcherRule
|
||||
|
||||
class MoreViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private fun device(manufacturer: String, codename: String) = DeviceInfoProvider {
|
||||
DeviceInfo(manufacturer, manufacturer.lowercase(), codename, codename, 36, "16", "2026-10-05")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pixelIsReportedAsSupported() {
|
||||
val vm = MoreViewModel(FakeUserPreferencesRepository(), device("Google", "caiman"))
|
||||
assertThat(vm.uiState.value.isSupportedPixel).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherDeviceIsReportedAsUnsupported() {
|
||||
val vm = MoreViewModel(FakeUserPreferencesRepository(), device("OnePlus", "aston"))
|
||||
assertThat(vm.uiState.value.isSupportedPixel).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun togglingDryRunUpdatesState() = runTest {
|
||||
val vm = MoreViewModel(FakeUserPreferencesRepository(), device("Google", "shiba"))
|
||||
vm.uiState.test {
|
||||
assertThat(awaitItem().preferences.dryRun).isFalse()
|
||||
vm.setDryRun(true)
|
||||
assertThat(expectMostRecentItem().preferences.dryRun).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user