Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f0307f1
New translations strings.xml (Portuguese, Brazilian)
sameerasw May 17, 2026
86e1d69
fix: add length validation to SimpleMarkdown parsing and update gradl…
sameerasw May 17, 2026
0df4e23
New translations strings.xml (Indonesian)
sameerasw May 17, 2026
c6100a5
Merge branch 'develop' into l10n_develop
sameerasw May 17, 2026
cf85bdc
New translations strings.xml (French)
sameerasw May 17, 2026
65f945d
New translations strings.xml (Turkish)
sameerasw May 17, 2026
ac17152
New translations strings.xml (Indonesian)
sameerasw May 17, 2026
9a698f6
New translations strings.xml (French)
sameerasw May 17, 2026
00ed235
New translations strings.xml (Spanish)
sameerasw May 17, 2026
399dfbc
New translations strings.xml (Russian)
sameerasw May 18, 2026
e789d4d
New translations strings.xml (Indonesian)
sameerasw May 18, 2026
333b698
New Crowdin updates (#440)
sameerasw May 19, 2026
f867ec7
New translations strings.xml (Japanese)
sameerasw May 19, 2026
33e36fd
New translations strings.xml (Japanese)
sameerasw May 19, 2026
c3ac998
New translations strings.xml (Japanese)
sameerasw May 19, 2026
5c8270f
New translations strings.xml (Dutch)
sameerasw May 19, 2026
2352ae2
New translations strings.xml (Dutch)
sameerasw May 19, 2026
0f14c6d
feat: Screen lock with input simulation for widget
sameerasw May 20, 2026
aecb000
feat: shizuku permission ahndlign for screen off widget
sameerasw May 20, 2026
f3beb36
feat: Double tap to screen off
sameerasw May 20, 2026
854181b
version: Upgraded to v15.1
sameerasw May 20, 2026
d387922
Merge branch 'develop' into l10n_develop
sameerasw May 20, 2026
a1d78f5
New Crowdin updates (#444)
sameerasw May 20, 2026
a4565b9
Merge branch 'main' into develop
sameerasw May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ android {
applicationId = "com.sameerasw.essentials"
minSdk = 26
targetSdk = 37
versionCode = 44
versionName = "15.0"
versionCode = 45
versionName = "15.1"

val whatsNewCounter = 2
buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,9 @@ class FeatureSettingsActivity : AppCompatActivity() {
vibrator = vibrator,
prefs = prefs,
modifier = Modifier.padding(top = 16.dp),
highlightSetting = highlightSetting
highlightSetting = highlightSetting,
onShowPermissionSheet = { showPermissionSheet = it },
onSetChildFeatureForPermissions = { childFeatureForPermissions = it }
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sameerasw.essentials.domain

enum class ScreenOffMethod {
ACCESSIBILITY,
INPUT
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import android.os.SystemClock
import android.os.Vibrator
import android.os.VibratorManager
import android.provider.Settings
import android.view.KeyEvent
import android.widget.RemoteViews
import android.widget.Toast
import androidx.core.content.getSystemService
import com.sameerasw.essentials.R
import com.sameerasw.essentials.domain.HapticFeedbackType
import com.sameerasw.essentials.domain.ScreenOffMethod
import com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService
import com.sameerasw.essentials.utils.HapticUtil
import com.sameerasw.essentials.utils.ShellUtils
import com.sameerasw.essentials.utils.performHapticFeedback

class ScreenOffWidgetProvider : AppWidgetProvider() {

companion object {
private const val DOUBLE_TAP_TIMEOUT = 500L // 500ms
}

override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
Expand All @@ -26,15 +42,85 @@ class ScreenOffWidgetProvider : AppWidgetProvider() {
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (intent.action == "WIDGET_CLICK") {
if (isAccessibilityEnabled(context)) {
val serviceIntent =
Intent(context, ScreenOffAccessibilityService::class.java).apply {
action = "LOCK_SCREEN"
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val isDoubleTapRequired = prefs.getBoolean("screen_off_double_tap", false)

if (isDoubleTapRequired) {
val lastTapTime = prefs.getLong("screen_off_last_tap_time", 0)
val currentTime = SystemClock.elapsedRealtime()

if (currentTime - lastTapTime < DOUBLE_TAP_TIMEOUT) {
// Double tap detected
prefs.edit().putLong("screen_off_last_tap_time", 0).apply()
triggerScreenOff(context)
} else {
// First tap
prefs.edit().putLong("screen_off_last_tap_time", currentTime).apply()

val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.getSystemService(VibratorManager::class.java)?.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
}
if (vibrator != null) {
performHapticFeedback(vibrator, HapticFeedbackType.TICK)
}
context.startService(serviceIntent)
}
} else {
Toast.makeText(context, "Missing permissions, Check the app", Toast.LENGTH_SHORT)
.show()
// Double tap not required, trigger immediately
triggerScreenOff(context)
}
}
}

private fun triggerScreenOff(context: Context) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val selectedScreenOffMethod = try {
ScreenOffMethod.valueOf(prefs.getString("screen_off_method", ScreenOffMethod.ACCESSIBILITY.name) ?: ScreenOffMethod.ACCESSIBILITY.name)
} catch (e: IllegalArgumentException) {
ScreenOffMethod.ACCESSIBILITY
}

val hapticFeedbackType = try {
HapticFeedbackType.valueOf(prefs.getString("haptic_feedback_type", HapticFeedbackType.NONE.name) ?: HapticFeedbackType.NONE.name)
} catch (e: IllegalArgumentException) {
HapticFeedbackType.NONE
}

val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.getSystemService(VibratorManager::class.java)?.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
}

if (vibrator != null) {
performHapticFeedback(vibrator, hapticFeedbackType)
}

when (selectedScreenOffMethod) {
ScreenOffMethod.ACCESSIBILITY -> {
if (isAccessibilityEnabled(context)) {
val serviceIntent =
Intent(context, ScreenOffAccessibilityService::class.java).apply {
action = "LOCK_SCREEN"
}
context.startService(serviceIntent)
} else {
Toast.makeText(context, "Missing Accessibility permission, Check the app", Toast.LENGTH_SHORT)
.show()
}
}
ScreenOffMethod.INPUT -> {
if (ShellUtils.hasPermission(context)) {
// Simulate power button press using input keyevent
// Requires root or Shizuku, which ShellUtils handles
ShellUtils.runCommand(context, "input keyevent ${KeyEvent.KEYCODE_POWER}")
} else {
Toast.makeText(context, "Missing Shizuku/Root permission for Input method, Check the app", Toast.LENGTH_SHORT)
.show()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.sameerasw.essentials.ui.components.pickers

import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sameerasw.essentials.R
import com.sameerasw.essentials.domain.ScreenOffMethod
import com.sameerasw.essentials.utils.HapticUtil

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ScreenOffMethodPicker(
selectedMethod: ScreenOffMethod,
onMethodSelected: (ScreenOffMethod) -> Unit,
modifier: Modifier = Modifier,
options: List<Pair<Int, ScreenOffMethod>> = listOf(
R.string.screen_off_method_accessibility to ScreenOffMethod.ACCESSIBILITY,
R.string.screen_off_method_input to ScreenOffMethod.INPUT
)
) {
val labels = options.map { it.first }
val types = options.map { it.second }

val selectedIndex = types.indexOf(selectedMethod).coerceAtLeast(0)
val view = LocalView.current // Get the current View for haptic feedback

Row(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.surfaceBright,
shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd)
)
.padding(10.dp),
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
) {
val modifiers = List(labels.size) { Modifier.weight(1f) }

labels.forEachIndexed { index, label ->
ToggleButton(
checked = selectedIndex == index,
onCheckedChange = {
onMethodSelected(types[index])
HapticUtil.performLightHaptic(view) // Trigger haptic feedback
},
modifier = modifiers[index].semantics { role = Role.RadioButton },
shapes = when (index) {
0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
labels.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
},
) {
Text(
stringResource(label),
fontSize = dimensionResource(R.dimen.font_small).value.sp,
modifier = Modifier.basicMarquee(),
maxLines = 1
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -449,27 +449,26 @@ private fun parseMarkdown(text: String): AnnotatedString {
}

when {
matchValue.startsWith("**") && matchValue.endsWith("**") -> {
matchValue.startsWith("**") && matchValue.endsWith("**") && matchValue.length >= 4 -> {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(matchValue.substring(2, matchValue.length - 2))
}
}

(matchValue.startsWith("*") && matchValue.endsWith("*") && !matchValue.startsWith("**")) || (matchValue.startsWith(
"_"
) && matchValue.endsWith("_")) -> {
((matchValue.startsWith("*") && matchValue.endsWith("*") && !matchValue.startsWith("**")) ||
(matchValue.startsWith("_") && matchValue.endsWith("_"))) && matchValue.length >= 2 -> {
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
append(matchValue.substring(1, matchValue.length - 1))
}
}

matchValue.startsWith("<u>") && matchValue.endsWith("</u>") -> {
matchValue.startsWith("<u>") && matchValue.endsWith("</u>") && matchValue.length >= 7 -> {
withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
append(matchValue.substring(3, matchValue.length - 4))
}
}

matchValue.startsWith("`") && matchValue.endsWith("`") -> {
matchValue.startsWith("`") && matchValue.endsWith("`") && matchValue.length >= 2 -> {
withStyle(
style = SpanStyle(
fontFamily = FontFamily.Monospace,
Expand All @@ -481,7 +480,7 @@ private fun parseMarkdown(text: String): AnnotatedString {
}
}

matchValue.startsWith("[") && matchValue.contains("](") -> {
matchValue.startsWith("[") && matchValue.contains("](") && matchValue.endsWith(")") -> {
val title = matchValue.substringAfter("[").substringBefore("](")
val url = matchValue.substringAfter("](").substringBefore(")")
withLink(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.sameerasw.essentials.R
import com.sameerasw.essentials.domain.HapticFeedbackType
import com.sameerasw.essentials.domain.ScreenOffMethod
import com.sameerasw.essentials.ui.components.cards.FeatureCard
import com.sameerasw.essentials.ui.components.cards.IconToggleItem
import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer
import com.sameerasw.essentials.ui.components.pickers.HapticFeedbackPicker
import com.sameerasw.essentials.ui.components.pickers.ScreenOffMethodPicker
import com.sameerasw.essentials.ui.modifiers.highlight
import com.sameerasw.essentials.utils.performHapticFeedback
import com.sameerasw.essentials.viewmodels.MainViewModel
Expand All @@ -30,16 +38,86 @@ fun ScreenOffWidgetSettingsUI(
vibrator: Vibrator?,
prefs: SharedPreferences,
modifier: Modifier = Modifier,
highlightSetting: String? = null
highlightSetting: String? = null,
onShowPermissionSheet: (Boolean) -> Unit,
onSetChildFeatureForPermissions: (String?) -> Unit
) {
val context = LocalContext.current
val isShizukuPermissionGranted by viewModel.isShizukuPermissionGranted

var selectedScreenOffMethod by remember {
val name =
prefs.getString("screen_off_method", ScreenOffMethod.ACCESSIBILITY.name)
mutableStateOf(
try {
ScreenOffMethod.valueOf(name ?: ScreenOffMethod.ACCESSIBILITY.name)
} catch (@Suppress("UNUSED_PARAMETER") e: Exception) {
ScreenOffMethod.ACCESSIBILITY
}
)
}

var isDoubleTapRequired by remember {
mutableStateOf(prefs.getBoolean("screen_off_double_tap", false))
}

Column(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
// Screen Off Method Category
Text(
text = stringResource(R.string.screen_off_method_title),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)

RoundedCardContainer(
modifier = Modifier,
spacing = 8.dp,
cornerRadius = 24.dp
) {
ScreenOffMethodPicker(
selectedMethod = selectedScreenOffMethod,
onMethodSelected = { type ->
if (type == ScreenOffMethod.INPUT && !isShizukuPermissionGranted) {
onSetChildFeatureForPermissions(context.getString(R.string.screen_off_widget_input_permission_id))
onShowPermissionSheet(true)
} else {
prefs.edit {
putString("screen_off_method", type.name)
}
selectedScreenOffMethod = type
}
},
modifier = Modifier.highlight(highlightSetting == "screen_off_method_picker")
)
}

// Double Tap Toggle
RoundedCardContainer(
modifier = Modifier.padding(top = 16.dp),
spacing = 8.dp,
cornerRadius = 24.dp
) {
IconToggleItem(
title = stringResource(R.string.require_double_tap_title),
description = stringResource(R.string.require_double_tap_desc),
iconRes = R.drawable.rounded_touch_app_24,
isChecked = isDoubleTapRequired,
onCheckedChange = {
isDoubleTapRequired = it
prefs.edit {
putBoolean("screen_off_double_tap", it)
}
},
showToggle = true
)
}

// Haptic Feedback Category
Text(
text = stringResource(R.string.settings_section_haptic),
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values-es-rES/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1427,10 +1427,10 @@ Cuando llegue un mensaje o alerta de una aplicación seleccionada, la Pantalla s
<string name="lock_screen_clock_metro">Metro</string>
<string name="lock_screen_clock_numoverlap">Superposición de números</string>
<string name="lock_screen_clock_weather">El tiempo</string>
<string name="lock_screen_clock_select_label">Seleccionar estilo de reloj</string>
<string name="lock_screen_clock_select_label">Seleccionar estilo del reloj</string>
<string name="lock_screen_clock_current_label">Actual</string>
<string name="lock_screen_clock_apply_label">Aplicar</string>
<string name="lock_screen_clock_applied_toast">Estilo de reloj aplicado</string>
<string name="lock_screen_clock_applied_toast">Estilo del reloj aplicado</string>
<string name="lock_screen_clock_permission_required">Se requiere el permiso WRITE_SECURE_SETTINGS</string>
<string name="lock_screen_clock_default">Predeterminado</string>
<string name="label_weight">Grosor</string>
Expand Down
Loading