Skip to content

Commit 151dfed

Browse files
committed
feat: add user consent screen
This introduces a mandatory consent screen shown on the first app launch. This screen informs the user about the app's capabilities regarding kernel parameter modifications and requires their agreement before proceeding. Changes include: - Addition of `ConsentActivity` and its layout. - Logic in `StartActivity` to show the consent screen if not yet granted. - New preference to store the consent status. - Updated string resources for the consent screen.
1 parent f6efc4d commit 151dfed

10 files changed

Lines changed: 400 additions & 2 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
<activity
4242
android:name=".ui.start.StartErrorActivity"
4343
android:theme="@style/AppTheme" />
44+
<activity
45+
android:name=".ui.start.ConsentActivity"
46+
android:theme="@style/AppTheme" />
4447
<activity
4548
android:name=".ui.tasker.TaskerPluginActivity"
4649
android:exported="true"
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package com.androidvip.sysctlgui.ui.start
2+
3+
import android.content.Intent
4+
import android.os.Bundle
5+
import androidx.activity.ComponentActivity
6+
import androidx.activity.compose.setContent
7+
import androidx.activity.enableEdgeToEdge
8+
import androidx.compose.foundation.background
9+
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.layout.Arrangement
11+
import androidx.compose.foundation.layout.Box
12+
import androidx.compose.foundation.layout.Column
13+
import androidx.compose.foundation.layout.Row
14+
import androidx.compose.foundation.layout.Spacer
15+
import androidx.compose.foundation.layout.displayCutoutPadding
16+
import androidx.compose.foundation.layout.fillMaxHeight
17+
import androidx.compose.foundation.layout.fillMaxSize
18+
import androidx.compose.foundation.layout.fillMaxWidth
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.foundation.layout.size
21+
import androidx.compose.foundation.layout.systemBarsPadding
22+
import androidx.compose.foundation.rememberScrollState
23+
import androidx.compose.foundation.verticalScroll
24+
import androidx.compose.material3.Button
25+
import androidx.compose.material3.Card
26+
import androidx.compose.material3.Checkbox
27+
import androidx.compose.material3.FilledTonalButton
28+
import androidx.compose.material3.Icon
29+
import androidx.compose.material3.MaterialTheme
30+
import androidx.compose.material3.Text
31+
import androidx.compose.runtime.Composable
32+
import androidx.compose.runtime.getValue
33+
import androidx.compose.runtime.mutableStateOf
34+
import androidx.compose.runtime.remember
35+
import androidx.compose.runtime.setValue
36+
import androidx.compose.ui.Alignment
37+
import androidx.compose.ui.Modifier
38+
import androidx.compose.ui.graphics.vector.ImageVector
39+
import androidx.compose.ui.res.painterResource
40+
import androidx.compose.ui.res.stringResource
41+
import androidx.compose.ui.res.vectorResource
42+
import androidx.compose.ui.text.style.TextAlign
43+
import androidx.compose.ui.tooling.preview.Preview
44+
import androidx.compose.ui.tooling.preview.PreviewLightDark
45+
import androidx.compose.ui.unit.dp
46+
import com.androidvip.sysctlgui.R
47+
import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
48+
import com.androidvip.sysctlgui.design.utils.AnimatedStrikeThroughIcon
49+
import com.androidvip.sysctlgui.design.utils.StatusBarProtection
50+
import com.androidvip.sysctlgui.design.utils.isLandscape
51+
import com.androidvip.sysctlgui.domain.repository.AppPrefs
52+
import org.koin.android.ext.android.inject
53+
54+
class ConsentActivity : ComponentActivity() {
55+
private val prefs by inject<AppPrefs>()
56+
57+
override fun onCreate(savedInstanceState: Bundle?) {
58+
enableEdgeToEdge()
59+
super.onCreate(savedInstanceState)
60+
61+
setContent {
62+
SysctlGuiTheme {
63+
ScreenContent(
64+
onClosePressed = { finish() },
65+
onContinuePressed = { granted ->
66+
if (granted) startMainActivity() else finish()
67+
}
68+
)
69+
StatusBarProtection()
70+
}
71+
}
72+
}
73+
74+
private fun startMainActivity() {
75+
prefs.consentGranted = true
76+
val mainIntent = Intent(this, StartActivity::class.java).apply {
77+
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
78+
putExtras(intent.extras ?: Bundle())
79+
}
80+
startActivity(mainIntent)
81+
finish()
82+
}
83+
84+
@Composable
85+
private fun ScreenContent(
86+
onContinuePressed: (granted: Boolean) -> Unit = {},
87+
onClosePressed: () -> Unit = {}
88+
) {
89+
var consentGranted by remember { mutableStateOf(false) }
90+
91+
Box(
92+
modifier = Modifier
93+
.fillMaxSize()
94+
.background(MaterialTheme.colorScheme.background)
95+
.verticalScroll(rememberScrollState()),
96+
) {
97+
if (isLandscape()) {
98+
Row(
99+
modifier = Modifier
100+
.fillMaxWidth()
101+
.systemBarsPadding()
102+
.displayCutoutPadding()
103+
.padding(24.dp),
104+
horizontalArrangement = Arrangement.spacedBy(16.dp)
105+
) {
106+
Column(
107+
modifier = Modifier.weight(1f),
108+
horizontalAlignment = Alignment.CenterHorizontally
109+
) {
110+
ConsentInfo(consentGranted)
111+
}
112+
113+
Column(
114+
modifier = Modifier.weight(1f),
115+
horizontalAlignment = Alignment.CenterHorizontally
116+
) {
117+
RevertInfoCard()
118+
ConsentActions(
119+
onContinuePressed = { onContinuePressed(consentGranted) },
120+
onConsentChanged = { consentGranted = it },
121+
onClosePressed = onClosePressed
122+
)
123+
}
124+
}
125+
} else {
126+
Column(
127+
modifier = Modifier
128+
.fillMaxHeight()
129+
.systemBarsPadding()
130+
.padding(24.dp),
131+
horizontalAlignment = Alignment.CenterHorizontally
132+
) {
133+
Spacer(modifier = Modifier.size(24.dp))
134+
ConsentInfo(consentGranted)
135+
Spacer(modifier = Modifier.size(24.dp))
136+
RevertInfoCard()
137+
ConsentActions(
138+
onContinuePressed = { onContinuePressed(consentGranted) },
139+
onConsentChanged = { consentGranted = it },
140+
onClosePressed = onClosePressed
141+
)
142+
}
143+
}
144+
}
145+
}
146+
147+
@Composable
148+
private fun RevertInfoCard() {
149+
Card(
150+
modifier = Modifier.fillMaxWidth()
151+
) {
152+
Row(
153+
verticalAlignment = Alignment.CenterVertically,
154+
horizontalArrangement = Arrangement.spacedBy(16.dp),
155+
modifier = Modifier.padding(16.dp)
156+
) {
157+
Icon(
158+
painter = painterResource(R.drawable.ic_restore_settings),
159+
contentDescription = null,
160+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
161+
)
162+
163+
Text(
164+
text = stringResource(R.string.consent_revert_info),
165+
style = MaterialTheme.typography.bodyMedium,
166+
color = MaterialTheme.colorScheme.onSurface,
167+
modifier = Modifier.weight(1f)
168+
)
169+
}
170+
}
171+
}
172+
173+
@Composable
174+
private fun ConsentInfo(consentGranted: Boolean) {
175+
AnimatedStrikeThroughIcon(
176+
icon = ImageVector.vectorResource(R.drawable.ic_edit),
177+
modifier = Modifier.size(92.dp),
178+
isOff = !consentGranted,
179+
tint = MaterialTheme.colorScheme.primary
180+
)
181+
182+
Text(
183+
text = stringResource(R.string.consent_title),
184+
style = MaterialTheme.typography.titleLarge,
185+
color = MaterialTheme.colorScheme.onBackground,
186+
textAlign = TextAlign.Center,
187+
modifier = Modifier.padding(top = 24.dp)
188+
)
189+
190+
Text(
191+
text = stringResource(R.string.consent_message),
192+
style = MaterialTheme.typography.bodyMedium,
193+
color = MaterialTheme.colorScheme.onBackground,
194+
modifier = Modifier.padding(top = 16.dp)
195+
)
196+
}
197+
198+
@Composable
199+
private fun ConsentActions(
200+
modifier: Modifier = Modifier,
201+
onContinuePressed: () -> Unit,
202+
onClosePressed: () -> Unit,
203+
onConsentChanged: (granted: Boolean) -> Unit
204+
) {
205+
var checked by remember { mutableStateOf(false) }
206+
207+
Row(
208+
modifier = modifier
209+
.fillMaxWidth()
210+
.clickable { checked = !checked; onConsentChanged(checked) },
211+
verticalAlignment = Alignment.CenterVertically,
212+
) {
213+
Checkbox(
214+
checked = checked,
215+
onCheckedChange = { checked = it; onConsentChanged(it) }
216+
)
217+
Text(
218+
text = stringResource(R.string.consent_checkbox),
219+
style = MaterialTheme.typography.bodyMedium,
220+
color = MaterialTheme.colorScheme.onBackground,
221+
)
222+
}
223+
224+
Button(
225+
onClick = onContinuePressed,
226+
enabled = checked,
227+
modifier = Modifier.padding(top = 24.dp)
228+
) {
229+
Text(
230+
text = stringResource(R.string.continue_),
231+
style = MaterialTheme.typography.bodyMedium,
232+
)
233+
}
234+
235+
FilledTonalButton(onClick = onClosePressed) {
236+
Text(
237+
text = stringResource(R.string.exit),
238+
style = MaterialTheme.typography.bodyMedium,
239+
)
240+
}
241+
}
242+
243+
@Composable
244+
@PreviewLightDark
245+
@Preview(device = "spec:parent=pixel_5,orientation=landscape", showSystemUi = true)
246+
private fun Preview() {
247+
SysctlGuiTheme {
248+
Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
249+
ScreenContent()
250+
}
251+
StatusBarProtection()
252+
}
253+
}
254+
}

app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartActivity.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,19 @@ class StartActivity : AppCompatActivity() {
2727

2828
override fun onCreate(savedInstanceState: Bundle?) {
2929
val splashScreen = installSplashScreen()
30-
super.onCreate(savedInstanceState)
3130
enableEdgeToEdge()
31+
super.onCreate(savedInstanceState)
3232

3333
splashScreen.setKeepOnScreenCondition { true }
3434
binding = ActivitySplashBinding.inflate(layoutInflater)
3535
setContentView(binding.root)
3636

37+
if (!prefs.consentGranted) {
38+
startActivity(Intent(this, ConsentActivity::class.java))
39+
finish()
40+
return
41+
}
42+
3743
lifecycleScope.launch {
3844
rootUtils.getRootShell()
3945
val isRootAccessGiven = checkRootAccess()
@@ -72,7 +78,11 @@ class StartActivity : AppCompatActivity() {
7278

7379
private fun navigate() {
7480
val shortcutNames = Actions.entries.map { it.name }
75-
val nextIntent = Intent(this, MainActivity::class.java).apply {
81+
val nextIntent = if (prefs.consentGranted) {
82+
Intent(this, MainActivity::class.java)
83+
} else {
84+
Intent(this, ConsentActivity::class.java)
85+
}.apply {
7686
if (intent.action in shortcutNames) {
7787
putExtra(MainActivity.EXTRA_DESTINATION, intent.action)
7888
putExtras(intent.extras ?: Bundle())
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="#FFF"
8+
android:pathData="M12 3C7.03 3 3 7.03 3 12H0L4 16L8 12H5C5 8.13 8.13 5 12 5S19 8.13 19 12 15.87 19 12 19C10.55 19 9.13 18.54 7.94 17.7L6.5 19.14C8.08 20.34 10 21 12 21C16.97 21 21 16.97 21 12S16.97 3 12 3M16.29 13.19L15.29 12.45C15.3 12.3 15.3 12.15 15.29 12C15.31 11.85 15.31 11.7 15.29 11.55L16.29 10.81C16.37 10.74 16.4 10.62 16.35 10.5L15.44 9C15.39 8.89 15.26 8.84 15.15 8.88L14 9.3C13.77 9.12 13.5 8.97 13.24 8.85L13.07 7.67C13.05 7.57 12.96 7.5 12.86 7.5H11.1C11 7.5 10.89 7.58 10.87 7.69L10.7 8.88C10.43 9 10.17 9.13 9.93 9.3L8.81 8.85C8.71 8.81 8.59 8.85 8.53 8.95L7.63 10.5C7.58 10.61 7.6 10.72 7.69 10.79L8.69 11.55C8.66 11.85 8.66 12.15 8.69 12.45L7.69 13.19C7.61 13.26 7.58 13.38 7.63 13.5L8.53 15C8.58 15.11 8.7 15.16 8.81 15.12L9.93 14.67C10.16 14.85 10.42 15 10.69 15.12L10.87 16.3C10.89 16.41 11 16.5 11.1 16.47H12.9C13 16.47 13.11 16.39 13.12 16.28L13.3 15.09C13.56 14.97 13.81 14.83 14.05 14.67L15.18 15.12C15.28 15.12 15.4 15.12 15.46 15L16.36 13.45C16.41 13.35 16.38 13.23 16.29 13.16M12 13.5C11.17 13.5 10.5 12.83 10.5 12S11.17 10.5 12 10.5 13.5 11.17 13.5 12C13.5 12.82 12.84 13.5 12 13.5H12" />
9+
</vector>

app/src/main/res/values-pt-rBR/strings.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,15 @@
105105
<string name="history_deleted">Histórico deletado</string>
106106
<string name="startup_params">Parâmetros da inicialização</string>
107107
<string name="sysctl_help">Sysctl é usado para modificar parâmetros do kernel em tempo de execução. Os parâmetros disponíveis são aqueles listados em /proc/sys e o objetivo principal deste aplicativo é fornecer uma maneira gráfica de editá-los.</string>
108+
<string name="consent_title">Consentimento para editar parâmetros</string>
109+
<string name="consent_message">Precisamos do seu consentimento para prosseguir. Veja o que o aplicativo pode e não pode fazer após o consentimento ser concedido:\n\t
110+
❌ Executar comandos não relacionados à ferramenta sysctl\n\t
111+
❌ Executar comandos sem permisão ou ação iniciada pelo usuário\n\t
112+
❌ Deixar alterações permanentes\n\t
113+
✅ Executar apenas comandos sysctl para os parâmetros escolhidos\n\t
114+
✅ Reverter todas as alterações
115+
</string>
116+
<string name="consent_checkbox">Eu concordo com o declarado acima</string>
117+
<string name="continue_">Continuar</string>
118+
<string name="consent_revert_info">Devido à natureza do kernel, as alterações feitas em tempo de execução podem ser revertidas com uma simples reinicialização do dispositivo. Você encontrará a opção de reversão nas configurações do aplicativo.</string>
108119
</resources>

app/src/main/res/values/strings.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,15 @@
105105
<string name="history_deleted">History deleted</string>
106106
<string name="startup_params">Startup params</string>
107107
<string name="sysctl_help">Sysctl is used to modify kernel parameters at runtime. The parameters available are those listed under /proc/sys and this app\'s main purpose is to provide a graphical way to edit them.</string>
108+
<string name="consent_title">Edit parameters consent</string>
109+
<string name="consent_message">We need your consent before we can proceed. This is what the app can and cannot do, once consent is granted:\n\t
110+
❌ Execute commands unrelated to the sysctl tool\n\t
111+
❌ Execute commands without a user-initiated action or permission\n\t
112+
❌ Leave permanent changes in place\n\t
113+
✅ Execute only sysctl commands for the chosen parameters\n\t
114+
✅ Revert back all changes
115+
</string>
116+
<string name="consent_checkbox">I consent to the above</string>
117+
<string name="continue_">Continue</string>
118+
<string name="consent_revert_info">By the nature of the kernel, changes done at runtime can be reverted by a simple device reboot. You can find the revert back option in the app settings.</string>
108119
</resources>

0 commit comments

Comments
 (0)