diff --git a/.gitignore b/.gitignore index 2cbec1e6..366ca372 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,4 @@ replay_pid* *.DS_Store *app/release/ -Build/ -build/reports/problems/problems-report.html +Build/ \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0191243f..d3d57b96 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -105,7 +105,10 @@ dependencies { // SymSpell for word suggestions implementation("com.darkrockstudios:symspellkt:3.4.0") - // Glance for Widgets implementation("androidx.glance:glance-appwidget:1.1.0") implementation("androidx.glance:glance-material3:1.1.0") + + // Watermark dependencies + implementation("androidx.exifinterface:exifinterface:1.3.7") + implementation("androidx.compose.material:material-icons-extended:1.7.0") // Compatible with Compose BOM } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eff3fc25..9c613a0a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -205,6 +205,18 @@ android:taskAffinity="" android:theme="@style/Theme.Essentials.Translucent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 24f161ab..27ecbf65 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -61,9 +61,8 @@ class SettingsRepository(private val context: Context) { const val KEY_DYNAMIC_NIGHT_LIGHT_ENABLED = "dynamic_night_light_enabled" const val KEY_DYNAMIC_NIGHT_LIGHT_SELECTED_APPS = "dynamic_night_light_selected_apps" - const val KEY_SNOOZE_DEBUGGING_ENABLED = "snooze_debugging_enabled" - const val KEY_SNOOZE_FILE_TRANSFER_ENABLED = "snooze_file_transfer_enabled" - const val KEY_SNOOZE_CHARGING_ENABLED = "snooze_charging_enabled" + const val KEY_SNOOZE_DISCOVERED_CHANNELS = "snooze_discovered_channels" + const val KEY_SNOOZE_BLOCKED_CHANNELS = "snooze_blocked_channels" const val KEY_FLASHLIGHT_ALWAYS_TURN_OFF_ENABLED = "flashlight_always_turn_off_enabled" const val KEY_FLASHLIGHT_FADE_ENABLED = "flashlight_fade_enabled" @@ -118,6 +117,8 @@ class SettingsRepository(private val context: Context) { const val KEY_SHOW_BLUETOOTH_DEVICES = "show_bluetooth_devices" const val KEY_BATTERY_WIDGET_MAX_DEVICES = "battery_widget_max_devices" const val KEY_BATTERY_WIDGET_BACKGROUND_ENABLED = "battery_widget_background_enabled" + + const val KEY_PINNED_FEATURES = "pinned_features" } // Observe changes @@ -129,6 +130,17 @@ class SettingsRepository(private val context: Context) { awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } } + val isPitchBlackThemeEnabled: Flow = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == KEY_PITCH_BLACK_THEME_ENABLED) { + trySend(getBoolean(KEY_PITCH_BLACK_THEME_ENABLED)) + } + } + trySend(getBoolean(KEY_PITCH_BLACK_THEME_ENABLED)) + prefs.registerOnSharedPreferenceChangeListener(listener) + awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } + } + fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { prefs.registerOnSharedPreferenceChangeListener(listener) } @@ -299,6 +311,45 @@ class SettingsRepository(private val context: Context) { saveAppSelection(key, current) } } + + // Snooze Notifications Helper + fun loadSnoozeDiscoveredChannels(): List { + val json = prefs.getString(KEY_SNOOZE_DISCOVERED_CHANNELS, null) + return if (json != null) { + val type = object : TypeToken>() {}.type + try { + gson.fromJson(json, type) ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } + + fun saveSnoozeDiscoveredChannels(channels: List) { + val json = gson.toJson(channels) + putString(KEY_SNOOZE_DISCOVERED_CHANNELS, json) + } + + fun loadSnoozeBlockedChannels(): Set { + val json = prefs.getString(KEY_SNOOZE_BLOCKED_CHANNELS, null) + return if (json != null) { + val type = object : TypeToken>() {}.type + try { + gson.fromJson(json, type) ?: emptySet() + } catch (e: Exception) { + emptySet() + } + } else { + emptySet() + } + } + + fun saveSnoozeBlockedChannels(blockedChannels: Set) { + val json = gson.toJson(blockedChannels) + putString(KEY_SNOOZE_BLOCKED_CHANNELS, json) + } // Config Export/Import fun getAllConfigsAsJsonString(): String { @@ -312,7 +363,9 @@ class SettingsRepository(private val context: Context) { p.all.forEach { (key, value) -> // Skip app lists as requested, and stale data - if (key.endsWith("_selected_apps") || key == "freeze_auto_excluded_apps" || key.startsWith("mac_battery_") || key == "airsync_mac_connected") { + if (key.endsWith("_selected_apps") || key == "freeze_auto_excluded_apps" || + key.startsWith("mac_battery_") || key == "airsync_mac_connected" || + key == KEY_SNOOZE_DISCOVERED_CHANNELS) { return@forEach } @@ -415,4 +468,18 @@ class SettingsRepository(private val context: Context) { fun isBatteryWidgetBackgroundEnabled(): Boolean = getBoolean(KEY_BATTERY_WIDGET_BACKGROUND_ENABLED, true) fun setBatteryWidgetBackgroundEnabled(enabled: Boolean) = putBoolean(KEY_BATTERY_WIDGET_BACKGROUND_ENABLED, enabled) + + fun getPinnedFeatures(): List { + val json = prefs.getString(KEY_PINNED_FEATURES, null) + return if (json != null) { + try { + gson.fromJson(json, object : TypeToken>() {}.type) ?: emptyList() + } catch (e: Exception) { emptyList() } + } else emptyList() + } + + fun savePinnedFeatures(features: List) { + val json = gson.toJson(features) + putString(KEY_PINNED_FEATURES, json) + } } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt index d40029fa..59177c3d 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Action.kt @@ -50,4 +50,19 @@ sealed interface Action { override val permissions: List = listOf("shizuku", "root") override val isConfigurable: Boolean = true } + + data class DeviceEffects( + val enabled: Boolean = true, + val grayscale: Boolean = false, + val suppressAmbient: Boolean = false, + val dimWallpaper: Boolean = false, + val nightMode: Boolean = false + ) : Action { + override val title: Int get() = R.string.diy_action_device_effects + override val icon: Int get() = R.drawable.rounded_bed_24 + override val permissions: List = listOf("notification_policy") + override val isConfigurable: Boolean = true + } + + } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt b/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt index 94e3c71f..b2ef49ee 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/diy/Automation.kt @@ -8,10 +8,12 @@ data class Automation( val actions: List = emptyList(), val entryAction: Action? = null, val exitAction: Action? = null, - val isEnabled: Boolean = true + val isEnabled: Boolean = true, + val selectedApps: List = emptyList() ) { enum class Type { TRIGGER, - STATE + STATE, + APP } } diff --git a/app/src/main/java/com/sameerasw/essentials/domain/model/SnoozeChannel.kt b/app/src/main/java/com/sameerasw/essentials/domain/model/SnoozeChannel.kt new file mode 100644 index 00000000..8ece27be --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/model/SnoozeChannel.kt @@ -0,0 +1,7 @@ +package com.sameerasw.essentials.domain.model + +data class SnoozeChannel( + val id: String, + val name: String, + val isBlocked: Boolean = false +) diff --git a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt index 10a235b2..0ab5a803 100644 --- a/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt +++ b/app/src/main/java/com/sameerasw/essentials/domain/registry/FeatureRegistry.kt @@ -181,26 +181,7 @@ object FeatureRegistry { description = R.string.feat_snooze_notifications_desc, permissionKeys = listOf("NOTIFICATION_LISTENER"), showToggle = false, - searchableSettings = listOf( - SearchSetting( - R.string.search_snooze_debug_title, - R.string.search_snooze_debug_desc, - "snooze_debugging", - R.array.keywords_adb_debug - ), - SearchSetting( - R.string.search_snooze_file_title, - R.string.search_snooze_file_desc, - "snooze_file_transfer", - R.array.keywords_mtp - ), - SearchSetting( - R.string.search_snooze_charge_title, - R.string.search_snooze_charge_desc, - "snooze_charging", - R.array.keywords_battery_charge - ) - ) + searchableSettings = emptyList() ) { override fun isEnabled(viewModel: MainViewModel) = false override fun isToggleEnabled(viewModel: MainViewModel, context: Context) = viewModel.isNotificationListenerEnabled.value @@ -319,6 +300,20 @@ object FeatureRegistry { "Stay awake", R.array.keywords_qs_stay_awake, R.string.feat_qs_tiles_title + ), + SearchSetting( + R.string.search_qs_private_dns_title, + R.string.search_qs_private_dns_desc, + "Private DNS", + R.array.keywords_network_visibility, + R.string.feat_qs_tiles_title + ), + SearchSetting( + R.string.search_qs_usb_debugging_title, + R.string.search_qs_usb_debugging_desc, + "USB Debugging", + R.array.keywords_adb_debug, + R.string.feat_qs_tiles_title ) ) ) { @@ -523,6 +518,21 @@ object FeatureRegistry { ) { override fun isEnabled(viewModel: MainViewModel) = true override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + }, + + object : Feature( + id = "Watermarks", + title = R.string.feat_watermark_title, + iconRes = R.drawable.rounded_draw_24, + category = R.string.cat_tools, + description = R.string.feat_watermark_desc, + showToggle = false + ) { + override fun isEnabled(viewModel: MainViewModel) = true + override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {} + override fun onClick(context: Context, viewModel: MainViewModel) { + context.startActivity(android.content.Intent(context, com.sameerasw.essentials.ui.composables.watermark.WatermarkActivity::class.java)) + } } ) } \ No newline at end of file diff --git a/app/src/main/java/com/sameerasw/essentials/domain/watermark/MetadataProvider.kt b/app/src/main/java/com/sameerasw/essentials/domain/watermark/MetadataProvider.kt new file mode 100644 index 00000000..dffce962 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/watermark/MetadataProvider.kt @@ -0,0 +1,45 @@ +package com.sameerasw.essentials.domain.watermark + +import android.content.Context +import android.net.Uri +import androidx.exifinterface.media.ExifInterface +import java.io.InputStream + +data class ExifData( + val make: String? = null, + val model: String? = null, + val aperture: String? = null, + val shutterSpeed: String? = null, + val iso: String? = null, + val date: String? = null, + val focalLength: String? = null +) + +class MetadataProvider( + private val context: Context +) { + fun extractExif(uri: Uri): ExifData { + var inputStream: InputStream? = null + return try { + inputStream = context.contentResolver.openInputStream(uri) + if (inputStream == null) return ExifData() + + val exif = ExifInterface(inputStream) + + ExifData( + make = exif.getAttribute(ExifInterface.TAG_MAKE), + model = exif.getAttribute(ExifInterface.TAG_MODEL), + aperture = exif.getAttribute(ExifInterface.TAG_F_NUMBER)?.let { "f/$it" }, + shutterSpeed = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.let { "${it}s" }, + iso = exif.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY)?.let { "ISO $it" }, + date = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL), + focalLength = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)?.let { "${it}mm" } + ) + } catch (e: Exception) { + e.printStackTrace() + ExifData() + } finally { + inputStream?.close() + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/domain/watermark/WatermarkEngine.kt b/app/src/main/java/com/sameerasw/essentials/domain/watermark/WatermarkEngine.kt new file mode 100644 index 00000000..6813670e --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/watermark/WatermarkEngine.kt @@ -0,0 +1,776 @@ +package com.sameerasw.essentials.domain.watermark + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Typeface +import android.net.Uri +import com.sameerasw.essentials.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import kotlin.math.max +import kotlin.math.roundToInt + +enum class WatermarkStyle { + OVERLAY, + FRAME +} + +enum class ColorMode { + LIGHT, + DARK, + ACCENT_LIGHT, + ACCENT_DARK +} + +data class WatermarkOptions( + val style: WatermarkStyle = WatermarkStyle.FRAME, + val showDeviceBrand: Boolean = true, + val showExif: Boolean = true, + // Granular EXIF options + val showFocalLength: Boolean = true, + val showAperture: Boolean = true, + val showIso: Boolean = true, + val showShutterSpeed: Boolean = true, + val showDate: Boolean = false, + val outputQuality: Int = 100, + val colorMode: ColorMode = ColorMode.LIGHT, + val accentColor: Int = android.graphics.Color.GRAY, + val moveToTop: Boolean = false, + val leftAlignOverlay: Boolean = false, + val brandTextSize: Int = 50, + val dataTextSize: Int = 50, + val showCustomText: Boolean = false, + val customText: String = "", + val customTextSize: Int = 50, + val padding: Int = 50, + val borderStroke: Int = 0, + val borderCorner: Int = 0, + val showLogo: Boolean = false, + val logoResId: Int? = null, + val logoSize: Int = 50 +) + +class WatermarkEngine( + private val context: Context, + private val metadataProvider: MetadataProvider +) { + suspend fun processImage(uri: Uri, options: WatermarkOptions): File = withContext(Dispatchers.IO) { + val inputStream = context.contentResolver.openInputStream(uri) + ?: throw IllegalStateException("Cannot open input stream") + + // Decode bitmap - mutable to allow drawing if Overlay + val originalBitmap = BitmapFactory.decodeStream(inputStream, null, BitmapFactory.Options().apply { + inMutable = true + inPreferredConfig = Bitmap.Config.ARGB_8888 + }) ?: throw IllegalStateException("Failed to decode bitmap") + + inputStream.close() // Close stream after decoding + + val resultBitmap = processBitmap(originalBitmap, uri, options) + + // Save to cache dir + val file = File(context.cacheDir, "watermarked_${System.currentTimeMillis()}.jpg") + val outStream = FileOutputStream(file) + resultBitmap.compress(Bitmap.CompressFormat.JPEG, options.outputQuality, outStream) + outStream.flush() + outStream.close() + + // Copy EXIF data + try { + val inputStreamExif = context.contentResolver.openInputStream(uri) + if (inputStreamExif != null) { + val oldExif = androidx.exifinterface.media.ExifInterface(inputStreamExif) + val newExif = androidx.exifinterface.media.ExifInterface(file) + + // Copy all tags + val attributes = arrayOf( + androidx.exifinterface.media.ExifInterface.TAG_DATETIME, + androidx.exifinterface.media.ExifInterface.TAG_DATETIME_DIGITIZED, + androidx.exifinterface.media.ExifInterface.TAG_DATETIME_ORIGINAL, + androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_TIME, + androidx.exifinterface.media.ExifInterface.TAG_F_NUMBER, + androidx.exifinterface.media.ExifInterface.TAG_FLASH, + androidx.exifinterface.media.ExifInterface.TAG_FOCAL_LENGTH, + androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE, + androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE_REF, + androidx.exifinterface.media.ExifInterface.TAG_GPS_DATESTAMP, + androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE, + androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE_REF, + androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE, + androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE_REF, + androidx.exifinterface.media.ExifInterface.TAG_GPS_PROCESSING_METHOD, + androidx.exifinterface.media.ExifInterface.TAG_GPS_TIMESTAMP, + androidx.exifinterface.media.ExifInterface.TAG_MAKE, + androidx.exifinterface.media.ExifInterface.TAG_MODEL, + androidx.exifinterface.media.ExifInterface.TAG_ISO_SPEED_RATINGS, + androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME, + androidx.exifinterface.media.ExifInterface.TAG_WHITE_BALANCE + ) + + for (attr in attributes) { + val value = oldExif.getAttribute(attr) + if (value != null) { + newExif.setAttribute(attr, value) + } + } + + // Add essentials tag + newExif.setAttribute(androidx.exifinterface.media.ExifInterface.TAG_IMAGE_DESCRIPTION, "Watermark by Essentials") + newExif.setAttribute(androidx.exifinterface.media.ExifInterface.TAG_USER_COMMENT, "Watermark by Essentials") + + newExif.saveAttributes() + inputStreamExif.close() + } + } catch (e: Exception) { + e.printStackTrace() + } + + // Recycle bitmaps + if (resultBitmap != originalBitmap) originalBitmap.recycle() + file + } + + suspend fun processBitmap(bitmap: Bitmap, uri: Uri, options: WatermarkOptions): Bitmap = withContext(Dispatchers.Default) { + val exifData = metadataProvider.extractExif(uri) + val result = when (options.style) { + WatermarkStyle.OVERLAY -> drawOverlay(bitmap, exifData, options) + WatermarkStyle.FRAME -> drawFrame(bitmap, exifData, options) + } + + applyBorder(result, options) + } + + private fun drawOverlay(bitmap: Bitmap, exifData: ExifData, options: WatermarkOptions): Bitmap { + val canvas = Canvas(bitmap) + + // Derive Colors + val colors = deriveColors(options) + val shadowColor = colors.shadowColor + val overlayTextColor = colors.overlayTextColor + + val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = overlayTextColor + textSize = bitmap.width * 0.03f // 3% of width + setShadowLayer(4f, 2f, 2f, shadowColor) + } + + val strokeSpacer = (bitmap.width * (options.borderStroke / 1000f)) + val margin = (bitmap.width * (options.padding / 1000f)) + strokeSpacer + var yPos = bitmap.height - margin + + // Apply scaling + val brandScale = 0.5f + (options.brandTextSize / 100f) + val dataScale = 0.5f + (options.dataTextSize / 100f) + val baseSize = bitmap.width * 0.03f + + // Logo Calculation + var logoBitmap: Bitmap? = null + var logoWidth = 0f + var logoHeight = 0f + + if (options.showLogo && options.logoResId != null) { + val logoScale = options.logoSize / 100f + val logoSizePx = baseSize * 4f * logoScale + logoBitmap = loadVectorBitmap(context, options.logoResId, overlayTextColor) + if (logoBitmap != null) { + val ratio = logoBitmap.width.toFloat() / logoBitmap.height.toFloat() + logoHeight = logoSizePx + logoWidth = logoHeight * ratio + } + } + + // Draw EXIF + if (options.showExif) { + val exifPaint = Paint(paint).apply { textSize = baseSize * dataScale } + val exifItems = buildExifList(exifData, options) + if (exifItems.isNotEmpty()) { + val maxWidth = bitmap.width - (margin * 2) + val rows = wrapExifItems(exifItems, exifPaint, maxWidth) + val reversedRows = rows.reversed() + + for (row in reversedRows) { + val rowWidth = measureRowWidth(row, exifPaint) + val rowHeight = measureRowHeight(row, exifPaint) + val xPos = if (options.leftAlignOverlay) margin else (bitmap.width - margin - rowWidth) + drawExifRow(canvas, row, xPos, yPos, exifPaint, shadowColor) + yPos -= rowHeight * 1.2f + } + } + } + + // Draw Custom Text + if (options.showCustomText && options.customText.isNotEmpty()) { + val customScale = 0.5f + (options.customTextSize / 100f) + val customPaint = Paint(paint).apply { + textSize = baseSize * customScale + typeface = Typeface.DEFAULT + } + val textBounds = Rect() + customPaint.getTextBounds(options.customText, 0, options.customText.length, textBounds) + if (options.showExif) yPos -= (customPaint.textSize * 0.5f) + val xPos = if (options.leftAlignOverlay) margin else (bitmap.width - margin - textBounds.width()) + canvas.drawText(options.customText, xPos, yPos, customPaint) + yPos -= customPaint.textSize * 1.2f + } + + // Draw Brand + if (options.showDeviceBrand) { + val brandString = buildBrandString(exifData) + val brandPaint = Paint(paint).apply { + typeface = Typeface.DEFAULT_BOLD + textSize = baseSize * brandScale + } + val textBounds = Rect() + brandPaint.getTextBounds(brandString, 0, brandString.length, textBounds) + val xPos = if (options.leftAlignOverlay) margin else (bitmap.width - margin - textBounds.width()) + canvas.drawText(brandString, xPos, yPos, brandPaint) + yPos -= brandPaint.textSize * 1.2f + } + + // Draw the Logo at the TOP of text content + if (logoBitmap != null) { + val logoX = if (options.leftAlignOverlay) margin else (bitmap.width - margin - logoWidth) + val logoY = yPos - logoHeight - (baseSize * 0.2f) // Increased spacing + + val destRect = RectF(logoX, logoY, logoX + logoWidth, logoY + logoHeight) + + if (shadowColor != null) { + val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + colorFilter = android.graphics.PorterDuffColorFilter(shadowColor, android.graphics.PorterDuff.Mode.SRC_IN) + alpha = 128 + } + val shadowRect = RectF(destRect).apply { offset(2f, 2f) } + canvas.drawBitmap(logoBitmap, null, shadowRect, shadowPaint) + } + canvas.drawBitmap(logoBitmap, null, destRect, null) + } + + return bitmap + } + + private fun drawFrame(bitmap: Bitmap, exifData: ExifData, options: WatermarkOptions): Bitmap { + var baseFrameHeight = (bitmap.height * 0.10f).roundToInt() + + // Derive Colors + val colors = deriveColors(options) + val bgColor = colors.bgColor + val textColor = colors.textColor + val secondaryTextColor = colors.secondaryTextColor + + // Setup paints early to measure + // Setup paints early to measure + val brandScale = 0.5f + (options.brandTextSize / 100f) + val dataScale = 0.5f + (options.dataTextSize / 100f) + + val brandPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = textColor + textSize = (baseFrameHeight * 0.3f) * brandScale + typeface = Typeface.DEFAULT_BOLD + } + + val exifPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = secondaryTextColor + textSize = (baseFrameHeight * 0.2f) * dataScale + } + + val margin = bitmap.width * (options.padding / 1000f) // 0 to 10% + + val maxAvailableWidth = if (options.showDeviceBrand) { + (bitmap.width - margin * 2) * 0.6f + } else { + (bitmap.width - margin * 2).toFloat() + } + + var exifRows: List> = emptyList() + var totalExifHeight = 0f + + if (options.showExif) { + val exifItems = buildExifList(exifData, options) + if (exifItems.isNotEmpty()) { + exifRows = wrapExifItems(exifItems, exifPaint, maxAvailableWidth) + totalExifHeight = exifRows.size * (exifPaint.textSize * 1.5f) + } + } + + // Dynamic Height Calculation + var leftSideHeight = 0f + if (options.showDeviceBrand) { + leftSideHeight += brandPaint.textSize + } + if (options.showCustomText && options.customText.isNotEmpty()) { + val customTextPaint = Paint(brandPaint).apply { + val customScale = 0.5f + (options.customTextSize / 100f) + textSize = (baseFrameHeight * 0.3f) * customScale + typeface = Typeface.DEFAULT + } + if (options.showDeviceBrand) { + leftSideHeight += (baseFrameHeight * 0.1f) + } + leftSideHeight += customTextPaint.textSize + } + + val contentHeightLeft = leftSideHeight + val contentHeightRight = totalExifHeight + + val minHeight = max(brandPaint.textSize, exifPaint.textSize) * 2f + + val strokeSpacer = (bitmap.width * (options.borderStroke / 1000f)).toInt() + val calculatedHeight = max(contentHeightLeft, contentHeightRight) + (margin * 2) + strokeSpacer + + val finalFrameHeight = max(minHeight.roundToInt(), calculatedHeight.roundToInt()) + + val newHeight = bitmap.height + finalFrameHeight + + val finalBitmap = Bitmap.createBitmap(bitmap.width, newHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(finalBitmap) + + // Draw background + canvas.drawColor(bgColor) + + // Create rounded version of source bitmap if needed + val sourceToDraw = if (options.borderCorner > 0) { + val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + val srcCanvas = Canvas(output) + val srcPaint = Paint(Paint.ANTI_ALIAS_FLAG) + val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) + + val minDim = kotlin.math.min(bitmap.width, bitmap.height) + val radius = minDim * (options.borderCorner / 1000f) + + srcCanvas.drawRoundRect(rect, radius, radius, srcPaint) + srcPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + srcCanvas.drawBitmap(bitmap, 0f, 0f, srcPaint) + output + } else { + bitmap + } + + // Draw Image and Text + if (options.moveToTop) { + // Draw Image shifted down by frameHeight + canvas.drawBitmap(sourceToDraw, 0f, finalFrameHeight.toFloat(), null) + + // Draw Text in "Forehead" + val centerY = (finalFrameHeight - strokeSpacer) / 2f + drawFrameContent( + canvas, exifData, options, margin, centerY, + brandPaint, exifPaint, exifRows, bitmap.width + ) + + } else { + // Draw Image at 0,0 + canvas.drawBitmap(sourceToDraw, 0f, 0f, null) + + // Draw Text in "Chin" + val centerY = bitmap.height + strokeSpacer + ((finalFrameHeight - strokeSpacer) / 2f) + drawFrameContent( + canvas, exifData, options, margin, centerY, + brandPaint, exifPaint, exifRows, bitmap.width + ) + } + + if (sourceToDraw != bitmap) { + sourceToDraw.recycle() + } + + return finalBitmap + } + + private fun drawFrameContent( + canvas: Canvas, exifData: ExifData, options: WatermarkOptions, + margin: Float, centerY: Float, + brandPaint: Paint, exifPaint: Paint, + exifRows: List>, canvasWidth: Int + + ) { + val colors = deriveColors(options) + val textColor = colors.textColor + + // Logo Calculation + var logoBitmap: Bitmap? = null + var logoWidth = 0f + var logoHeight = 0f + val logoSpacing = brandPaint.textSize * 0.5f + + if (options.showLogo && options.logoResId != null) { + val logoScale = options.logoSize / 100f + val logoSizePx = brandPaint.textSize * 2.5f * logoScale + logoBitmap = loadVectorBitmap(context, options.logoResId, textColor) + if (logoBitmap != null) { + val ratio = logoBitmap.width.toFloat() / logoBitmap.height.toFloat() + logoHeight = logoSizePx + logoWidth = logoHeight * ratio + } + } + + val logoAreaWidth = if (logoBitmap != null) logoWidth + logoSpacing else 0f + + // Brand & Custom Text on Left + var currentLeftY = centerY + var totalLeftHeight = 0f + val customScale = 0.5f + (options.customTextSize / 100f) + val customPaint = Paint(brandPaint).apply { + textSize = brandPaint.textSize * (customScale / (0.5f + (options.brandTextSize / 100f))) + typeface = Typeface.DEFAULT + } + + if (options.showDeviceBrand) totalLeftHeight += brandPaint.textSize + if (options.showCustomText && options.customText.isNotEmpty()) { + if (options.showDeviceBrand) totalLeftHeight += (brandPaint.textSize * 0.2f) + totalLeftHeight += customPaint.textSize + } + + currentLeftY = centerY - (totalLeftHeight / 2f) + (brandPaint.textSize / 1.5f) + + // Draw Logo + if (logoBitmap != null) { + val logoX = margin + val logoY = centerY - (logoHeight / 2f) + canvas.drawBitmap(logoBitmap, null, RectF(logoX, logoY, logoX + logoWidth, logoY + logoHeight), null) + } + + val textX = margin + logoAreaWidth + + if (options.showDeviceBrand) { + val brandString = buildBrandString(exifData) + canvas.drawText(brandString, textX, currentLeftY, brandPaint) + currentLeftY += (brandPaint.textSize * 0.2f) + customPaint.textSize + } else if (options.showCustomText && options.customText.isNotEmpty()) { + currentLeftY = centerY - (totalLeftHeight / 2f) + customPaint.textSize + } + + if (options.showCustomText && options.customText.isNotEmpty()) { + canvas.drawText(options.customText, textX, currentLeftY, customPaint) + } + + // Exif on Right + if (options.showExif && exifRows.isNotEmpty()) { + val lineHeight = exifPaint.textSize * 1.5f + val centeringOffset = (exifRows.size - 1) * lineHeight / 2f + var currentY = (centerY + exifPaint.textSize / 3f) - centeringOffset + + for (row in exifRows) { + val rowWidth = measureRowWidth(row, exifPaint) + val xPos = canvasWidth - margin - rowWidth + drawExifRow(canvas, row, xPos, currentY, exifPaint, null) + currentY += lineHeight + } + } + } + + private fun wrapExifItems(items: List, paint: Paint, maxWidth: Float): List> { + val rows = mutableListOf>() + if (items.isEmpty()) return rows + + var currentRow = mutableListOf() + var currentWidth = 0f + val separatorWidth = 0f + val itemSpacing = paint.textSize * 0.8f + + for (item in items) { + val itemWidth = measureItemWidth(item, paint) + + if (currentRow.isEmpty()) { + currentRow.add(item) + currentWidth += itemWidth + } else { + if (currentWidth + itemSpacing + itemWidth <= maxWidth) { + currentRow.add(item) + currentWidth += itemSpacing + itemWidth + } else { + rows.add(currentRow) + currentRow = mutableListOf(item) + currentWidth = itemWidth + } + } + } + if (currentRow.isNotEmpty()) { + rows.add(currentRow) + } + return rows + } + + private fun measureItemWidth(item: ExifItem, paint: Paint): Float { + // Icon + Padding + Text + val iconSize = paint.textSize * 1.2f + val padding = paint.textSize * 0.4f + val textWidth = paint.measureText(item.text) + return iconSize + padding + textWidth + } + + private fun measureRowWidth(row: List, paint: Paint): Float { + var width = 0f + val itemSpacing = paint.textSize * 0.8f + for (i in row.indices) { + width += measureItemWidth(row[i], paint) + if (i < row.size - 1) width += itemSpacing + } + return width + } + + private fun measureRowHeight(row: List, paint: Paint): Float { + return paint.textSize * 1.5f // Use standard height + } + + private fun drawExifRow( + canvas: Canvas, row: List, + xStart: Float, yPos: Float, + paint: Paint, shadowColor: Int? + ) { + var currentX = xStart + val iconSize = paint.textSize * 1.2f + val padding = paint.textSize * 0.4f + val itemSpacing = paint.textSize * 0.8f + + val iconY = yPos - (paint.textSize / 2f) - (iconSize / 2f) + + for (item in row) { + // Draw Icon + val iconBitmap = loadVectorBitmap(context, item.iconRes, paint.color) + if (iconBitmap != null) { + val destRect = Rect( + currentX.toInt(), + iconY.toInt(), + (currentX + iconSize).toInt(), + (iconY + iconSize).toInt() + ) + + if (shadowColor != null) { + val shadowPaint = Paint(paint).apply { + color = shadowColor + colorFilter = android.graphics.PorterDuffColorFilter(shadowColor, android.graphics.PorterDuff.Mode.SRC_IN) + } + val shadowRect = Rect(destRect) + shadowRect.offset(2, 2) + canvas.drawBitmap(iconBitmap, null, shadowRect, shadowPaint) + } + + canvas.drawBitmap(iconBitmap, null, destRect, null) // Already tinted if we created it tinted + } + + currentX += iconSize + padding + + // Draw Text + canvas.drawText(item.text, currentX, yPos, paint) + + currentX += paint.measureText(item.text) + itemSpacing + } + } + + // Cache for bitmaps + private val iconCache = mutableMapOf() + + private fun loadVectorBitmap(context: Context, resId: Int, color: Int): Bitmap? { + + try { + val drawable = androidx.core.content.ContextCompat.getDrawable(context, resId) ?: return null + val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.setTint(color) + drawable.draw(canvas) + return bitmap + } catch (e: Exception) { + return null + } + } + + private fun buildBrandString(exif: ExifData): String { + return if (!exif.make.isNullOrEmpty() && !exif.model.isNullOrEmpty()) { + if (exif.model.contains(exif.make, ignoreCase = true)) { + exif.model + } else { + "${exif.make} ${exif.model}" + } + } else { + exif.model ?: exif.make ?: "Shot on Device" + } + } + + private data class ExifItem(val text: String, val iconRes: Int) + + private fun buildExifList(exif: ExifData, options: WatermarkOptions): List { + val list = mutableListOf() + + if (options.showFocalLength) exif.focalLength?.let { + list.add(ExifItem(it, R.drawable.rounded_control_camera_24)) + } + if (options.showAperture) exif.aperture?.let { + list.add(ExifItem(it, R.drawable.rounded_camera_24)) + } + if (options.showShutterSpeed) exif.shutterSpeed?.let { + list.add(ExifItem(formatShutterSpeed(it), R.drawable.rounded_shutter_speed_24)) + } + if (options.showIso) exif.iso?.let { + list.add(ExifItem(it, R.drawable.rounded_grain_24)) + } + if (options.showDate) exif.date?.let { + list.add(ExifItem(formatDate(it), R.drawable.rounded_date_range_24)) + } + + return list + } + + private fun formatDate(dateString: String): String { + try { + // Input format: yyyy:MM:dd HH:mm:ss + val inputFormat = java.text.SimpleDateFormat("yyyy:MM:dd HH:mm:ss", java.util.Locale.US) + val date = inputFormat.parse(dateString) ?: return dateString + + // Output format components + val dayFormat = java.text.SimpleDateFormat("d", java.util.Locale.US) + val monthYearFormat = java.text.SimpleDateFormat("MMM yyyy", java.util.Locale.US) + + // Use system time format (12/24h) + val timeFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT) + + val day = dayFormat.format(date).toInt() + val suffix = getDaySuffix(day) + + return "$day$suffix ${monthYearFormat.format(date)}, ${timeFormat.format(date)}" + } catch (e: Exception) { + return dateString + } + } + + private fun getDaySuffix(n: Int): String { + if (n in 11..13) return "th" + return when (n % 10) { + 1 -> "st" + 2 -> "nd" + 3 -> "rd" + else -> "th" + } + } + + private fun formatShutterSpeed(raw: String): String { + // raw usually comes as "0.02s" or "1/100s" from MetadataProvider due to appended "s" in provider + // but if we are robust, we check. + val value = raw.removeSuffix("s") + // If it's a fraction, keep it (photographers prefer fractions) + if (value.contains("/")) return raw + + return try { + val doubleVal = value.toDouble() + // Round to max 2 decimals + // usage of %.2f might result in 0.00 for very fast speeds? + // User asked "maximum of 2 decimals", implying checking if it has more. + // But if it is 0.0005, 0.00 is bad. + // Maybe they mean for long exposures e.g. 2.534s -> 0.53s. + // Let's assume standard formatting. + if (doubleVal >= 1 || doubleVal == 0.0) { + java.lang.String.format(java.util.Locale.US, "%.2fs", doubleVal).removeSuffix(".00s").removeSuffix("0s") + "s" + } else { + // Formatting small decimals + // user request: "round to maximum of 2 decimals" + // If 0.016 -> 0.02s + java.lang.String.format(java.util.Locale.US, "%.2fs", doubleVal) + } + } catch (e: Exception) { + raw + } + } + + private fun applyBorder(bitmap: Bitmap, options: WatermarkOptions): Bitmap { + if (options.borderStroke == 0 && options.borderCorner == 0) return bitmap + + val roundedBitmap = if (options.borderCorner > 0 && options.style != WatermarkStyle.FRAME) { + val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat()) + + // Mapping: 0-100slider -> 0-10% of min dimension + val minDim = kotlin.math.min(bitmap.width, bitmap.height) + val radius = minDim * (options.borderCorner / 1000f) // Max 10% + + canvas.drawRoundRect(rect, radius, radius, paint) + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + canvas.drawBitmap(bitmap, 0f, 0f, paint) + + if (bitmap != output) bitmap.recycle() + output + } else { + bitmap + } + + // Border Stroke (Expand Canvas) + val finalBitmap = if (options.borderStroke > 0) { + val strokeWidth = (bitmap.width * (options.borderStroke / 1000f)).toInt() + + val newWidth = roundedBitmap.width + (strokeWidth * 2) + val newHeight = roundedBitmap.height + (strokeWidth * 2) + + val output = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + + val colors = deriveColors(options) + val bgColor = colors.bgColor + + canvas.drawColor(bgColor) + + canvas.drawBitmap(roundedBitmap, strokeWidth.toFloat(), strokeWidth.toFloat(), null) + + if (roundedBitmap != output) roundedBitmap.recycle() + output + } else { + roundedBitmap + } + + return finalBitmap + } + + private data class DerivedColors( + val bgColor: Int, + val textColor: Int, + val secondaryTextColor: Int, + val shadowColor: Int, + val overlayTextColor: Int + ) + + private fun deriveColors(options: WatermarkOptions): DerivedColors { + return when (options.colorMode) { + ColorMode.LIGHT -> DerivedColors(Color.WHITE, Color.BLACK, Color.GRAY, Color.BLACK, Color.WHITE) + ColorMode.DARK -> DerivedColors(Color.BLACK, Color.WHITE, Color.LTGRAY, Color.WHITE, Color.BLACK) + ColorMode.ACCENT_LIGHT -> getAccentColors(options.accentColor, false) + ColorMode.ACCENT_DARK -> getAccentColors(options.accentColor, true) + } + } + + private fun getAccentColors(baseColor: Int, dark: Boolean): DerivedColors { + val hsl = FloatArray(3) + androidx.core.graphics.ColorUtils.colorToHSL(baseColor, hsl) + + return if (dark) { + // Accent Dark: Dark BG, Light Text + hsl[2] = 0.15f // Dark BG + val bgColor = androidx.core.graphics.ColorUtils.HSLToColor(hsl) + hsl[2] = 0.9f // Light Text + val textColor = androidx.core.graphics.ColorUtils.HSLToColor(hsl) + hsl[2] = 0.7f // Secondary Text + val secondaryTextColor = androidx.core.graphics.ColorUtils.HSLToColor(hsl) + + DerivedColors(bgColor, textColor, secondaryTextColor, Color.WHITE, bgColor) + } else { + // Accent Light: Light BG, Dark Text + hsl[2] = 0.95f // Light BG + val bgColor = androidx.core.graphics.ColorUtils.HSLToColor(hsl) + hsl[2] = 0.15f // Dark Text + val textColor = androidx.core.graphics.ColorUtils.HSLToColor(hsl) + hsl[2] = 0.4f // Secondary Text + val secondaryTextColor = androidx.core.graphics.ColorUtils.HSLToColor(hsl) + + DerivedColors(bgColor, textColor, secondaryTextColor, Color.BLACK, bgColor) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/domain/watermark/WatermarkRepository.kt b/app/src/main/java/com/sameerasw/essentials/domain/watermark/WatermarkRepository.kt new file mode 100644 index 00000000..094d8d87 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/domain/watermark/WatermarkRepository.kt @@ -0,0 +1,171 @@ +package com.sameerasw.essentials.domain.watermark + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +// Setup DataStore extension +private val Context.dataStore by preferencesDataStore(name = "watermark_prefs") + +class WatermarkRepository( + private val context: Context +) { + private val PREF_STYLE = stringPreferencesKey("watermark_style") + private val PREF_SHOW_BRAND = booleanPreferencesKey("show_brand") + private val PREF_SHOW_EXIF = booleanPreferencesKey("show_exif") + private val PREF_SHOW_FOCAL_LENGTH = booleanPreferencesKey("show_focal_length") + private val PREF_SHOW_APERTURE = booleanPreferencesKey("show_aperture") + private val PREF_SHOW_ISO = booleanPreferencesKey("show_iso") + private val PREF_SHOW_SHUTTER = booleanPreferencesKey("show_shutter") + private val PREF_SHOW_DATE = booleanPreferencesKey("show_date") + private val PREF_COLOR_MODE = stringPreferencesKey("color_mode") + private val PREF_ACCENT_COLOR = androidx.datastore.preferences.core.intPreferencesKey("accent_color") + private val PREF_MOVE_TO_TOP = booleanPreferencesKey("move_to_top") + private val PREF_LEFT_ALIGN = booleanPreferencesKey("left_align") + private val PREF_BRAND_TEXT_SIZE = androidx.datastore.preferences.core.intPreferencesKey("brand_text_size") + private val PREF_DATA_TEXT_SIZE = androidx.datastore.preferences.core.intPreferencesKey("data_text_size") + private val PREF_SHOW_CUSTOM_TEXT = booleanPreferencesKey("show_custom_text") + private val PREF_CUSTOM_TEXT = stringPreferencesKey("custom_text") + private val PREF_CUSTOM_TEXT_SIZE = androidx.datastore.preferences.core.intPreferencesKey("custom_text_size") + private val PREF_PADDING = androidx.datastore.preferences.core.intPreferencesKey("padding") + private val PREF_BORDER_STROKE = androidx.datastore.preferences.core.intPreferencesKey("border_stroke") + private val PREF_BORDER_CORNER = androidx.datastore.preferences.core.intPreferencesKey("border_corner") + private val PREF_SHOW_LOGO = booleanPreferencesKey("show_logo") + private val PREF_LOGO_SIZE = androidx.datastore.preferences.core.intPreferencesKey("logo_size") + + val watermarkOptions: Flow = context.dataStore.data + .map { preferences -> + val styleStr = preferences[PREF_STYLE] ?: WatermarkStyle.FRAME.name + val style = try { + WatermarkStyle.valueOf(styleStr) + } catch (e: Exception) { + WatermarkStyle.FRAME + } + + WatermarkOptions( + style = style, + showDeviceBrand = preferences[PREF_SHOW_BRAND] ?: true, + showExif = preferences[PREF_SHOW_EXIF] ?: true, + showFocalLength = preferences[PREF_SHOW_FOCAL_LENGTH] ?: true, + showAperture = preferences[PREF_SHOW_APERTURE] ?: true, + showIso = preferences[PREF_SHOW_ISO] ?: true, + showShutterSpeed = preferences[PREF_SHOW_SHUTTER] ?: true, + showDate = preferences[PREF_SHOW_DATE] ?: false, + colorMode = try { + ColorMode.valueOf(preferences[PREF_COLOR_MODE] ?: ColorMode.LIGHT.name) + } catch (e: Exception) { + ColorMode.LIGHT + }, + accentColor = preferences[PREF_ACCENT_COLOR] ?: android.graphics.Color.GRAY, + moveToTop = preferences[PREF_MOVE_TO_TOP] ?: false, + leftAlignOverlay = preferences[PREF_LEFT_ALIGN] ?: false, + brandTextSize = preferences[PREF_BRAND_TEXT_SIZE] ?: 50, + dataTextSize = preferences[PREF_DATA_TEXT_SIZE] ?: 50, + showCustomText = preferences[PREF_SHOW_CUSTOM_TEXT] ?: false, + customText = preferences[PREF_CUSTOM_TEXT] ?: "", + customTextSize = preferences[PREF_CUSTOM_TEXT_SIZE] ?: 50, + padding = preferences[PREF_PADDING] ?: 50, + borderStroke = preferences[PREF_BORDER_STROKE] ?: 0, + borderCorner = preferences[PREF_BORDER_CORNER] ?: 0, + showLogo = preferences[PREF_SHOW_LOGO] ?: false, + logoResId = null, + logoSize = preferences[PREF_LOGO_SIZE] ?: 50 + ) + } + + suspend fun updateStyle(style: WatermarkStyle) { + context.dataStore.edit { it[PREF_STYLE] = style.name } + } + + suspend fun updateShowBrand(show: Boolean) { + context.dataStore.edit { it[PREF_SHOW_BRAND] = show } + } + + suspend fun updateShowExif(show: Boolean) { + context.dataStore.edit { it[PREF_SHOW_EXIF] = show } + } + + suspend fun updateExifSettings( + focalLength: Boolean, + aperture: Boolean, + iso: Boolean, + shutterSpeed: Boolean, + date: Boolean + ) { + context.dataStore.edit { + it[PREF_SHOW_FOCAL_LENGTH] = focalLength + it[PREF_SHOW_APERTURE] = aperture + it[PREF_SHOW_ISO] = iso + it[PREF_SHOW_SHUTTER] = shutterSpeed + it[PREF_SHOW_DATE] = date + } + } + + suspend fun updateColorMode(mode: ColorMode) { + context.dataStore.edit { it[PREF_COLOR_MODE] = mode.name } + } + + suspend fun updateAccentColor(color: Int) { + context.dataStore.edit { it[PREF_ACCENT_COLOR] = color } + } + + suspend fun updateMoveToTop(move: Boolean) { + context.dataStore.edit { it[PREF_MOVE_TO_TOP] = move } + } + + suspend fun updateLeftAlign(left: Boolean) { + context.dataStore.edit { it[PREF_LEFT_ALIGN] = left } + } + + suspend fun updateBrandTextSize(size: Int) { + context.dataStore.edit { it[PREF_BRAND_TEXT_SIZE] = size } + } + + suspend fun updateDataTextSize(size: Int) { + context.dataStore.edit { it[PREF_DATA_TEXT_SIZE] = size } + } + + suspend fun updateCustomTextSettings(show: Boolean, text: String, size: Int) { + context.dataStore.edit { + it[PREF_SHOW_CUSTOM_TEXT] = show + it[PREF_CUSTOM_TEXT] = text + it[PREF_CUSTOM_TEXT_SIZE] = size + } + } + + suspend fun updatePadding(padding: Int) { + context.dataStore.edit { it[PREF_PADDING] = padding } + } + + suspend fun updateCustomTextSize(size: Int) { + context.dataStore.edit { it[PREF_CUSTOM_TEXT_SIZE] = size } + } + + suspend fun updateBorderStroke(stroke: Int) { + context.dataStore.edit { it[PREF_BORDER_STROKE] = stroke } + } + + suspend fun updateBorderCorner(corner: Int) { + context.dataStore.edit { it[PREF_BORDER_CORNER] = corner } + } + + suspend fun updateLogoSettings(show: Boolean, size: Int) { + context.dataStore.edit { + it[PREF_SHOW_LOGO] = show + it[PREF_LOGO_SIZE] = size + } + } + + suspend fun updateLogoShow(show: Boolean) { + context.dataStore.edit { it[PREF_SHOW_LOGO] = show } + } + + + suspend fun updateLogoSize(size: Int) { + context.dataStore.edit { it[PREF_LOGO_SIZE] = size } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/EssentialsConditionProvider.kt b/app/src/main/java/com/sameerasw/essentials/services/EssentialsConditionProvider.kt new file mode 100644 index 00000000..83cd9a2f --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/EssentialsConditionProvider.kt @@ -0,0 +1,65 @@ +package com.sameerasw.essentials.services + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.net.Uri +import android.service.notification.Condition +import android.service.notification.ConditionProviderService +import android.util.Log + +class EssentialsConditionProvider : ConditionProviderService() { + + companion object { + private const val TAG = "EssentialsCPS" + val CONDITION_URI: Uri = Uri.parse("condition://com.sameerasw.essentials/focus") + + private var instance: EssentialsConditionProvider? = null + + fun setConditionState(context: Context, isActive: Boolean) { + Log.d(TAG, "Requesting condition state: $isActive") + val provider = instance + if (provider != null) { + provider.notifyChange(isActive) + } else { + try { + requestRebind(ComponentName(context, EssentialsConditionProvider::class.java)) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + override fun onConnected() { + + Log.d(TAG, "onConnected") + instance = this + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "onDestroy") + instance = null + } + + override fun onSubscribe(conditionId: Uri?) { + Log.d(TAG, "onSubscribe: $conditionId") + notifyChange(false) + } + + override fun onUnsubscribe(conditionId: Uri?) { + Log.d(TAG, "onUnsubscribe: $conditionId") + } + + private fun notifyChange(active: Boolean) { + Log.d(TAG, "notifyChange: active=$active") + val state = if (active) Condition.STATE_TRUE else Condition.STATE_FALSE + val condition = Condition(CONDITION_URI, "Essentials Focus", state) + try { + notifyCondition(condition) + } catch (e: Exception) { + Log.e(TAG, "Failed to notify condition", e) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt b/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt index a514a93b..619b2820 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/LocationReachedService.kt @@ -78,7 +78,7 @@ class LocationReachedService : Service() { } } - private fun stopTracking() { + private fun stopTracking() { val alarm = repository.getAlarm() repository.saveAlarm(alarm.copy(isEnabled = false)) stopSelf() @@ -170,7 +170,7 @@ class LocationReachedService : Service() { else getString(R.string.location_reached_dist_km, it) } ?: getString(R.string.location_reached_calculating) - val contentText = getString(R.string.location_reached_service_remaining, distanceText) + val contentText = getString(R.string.location_reached_service_remaining, distanceText, progress) if (Build.VERSION.SDK_INT >= 35) { val builder = Notification.Builder(this, CHANNEL_ID) diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt index d98d9392..a1994ad8 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt @@ -11,6 +11,7 @@ import android.content.IntentFilter import android.os.Build import android.os.IBinder import android.provider.Settings +import android.util.Log import android.view.View import android.view.WindowManager import androidx.core.app.NotificationCompat @@ -54,6 +55,14 @@ class NotificationLightingService : Service() { super.onCreate() createNotificationChannel() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + startForeground(NOTIF_ID, buildNotification()) + } catch (_: Exception) { + // ignore foreground start failures on certain OEMs + } + } + // Register screen on/off receiver to attempt to re-show overlay when screen state changes screenReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -87,10 +96,18 @@ class NotificationLightingService : Service() { override fun onBind(intent: Intent?): IBinder? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // All three permissions are required for Notification Lighting to function - if (!canDrawOverlays() || !isAccessibilityServiceEnabled()) { - stopSelf() - return START_NOT_STICKY + Log.d("NotificationLightingSvc", "onStartCommand: action=${intent?.action}") + // Accessibility service Android 12+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (!canDrawOverlays() || !isAccessibilityServiceEnabled()) { + stopSelf() + return START_NOT_STICKY + } + } else { + if (!canDrawOverlays()) { + stopSelf() + return START_NOT_STICKY + } } // Get corner radius from intent, default to OverlayHelper.CORNER_RADIUS_DP @@ -135,15 +152,6 @@ class NotificationLightingService : Service() { return START_NOT_STICKY } - // Ensure the process calls startForeground quickly when started via startForegroundService - // to avoid RemoteServiceException (ForegroundServiceDidNotStartInTimeException). - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - startForeground(NOTIF_ID, buildNotification()) - } catch (_: Exception) { - // ignore foreground start failures; we'll continue - } - } // If accessibility service is enabled, delegate showing to it for higher elevation if (isAccessibilityServiceEnabled()) { @@ -187,8 +195,7 @@ class NotificationLightingService : Service() { stopSelf() return START_NOT_STICKY } - - stopSelf() + showOverlay() return START_NOT_STICKY } @@ -230,7 +237,13 @@ class NotificationLightingService : Service() { val color = when { resolvedColor != null -> resolvedColor!! colorMode == NotificationLightingColorMode.CUSTOM -> customColor - else -> getColor(android.R.color.system_accent1_100) + else -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + getColor(android.R.color.system_accent1_100) + } else { + getColor(com.sameerasw.essentials.R.color.purple_500) + } + } } val overlay = OverlayHelper.createOverlayView( @@ -281,21 +294,23 @@ class NotificationLightingService : Service() { private fun getOverlayType(): Int { - // If the accessibility service is enabled, prefer the accessibility overlay type which - // can appear above more system surfaces on some devices (Tasker-style elevation). - return when { - isAccessibilityServiceEnabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { - // TYPE_ACCESSIBILITY_OVERLAY exists on recent APIs and gives AccessibilityServices - // more privilege to display above other UI in some cases. + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Android 12+ supports TYPE_ACCESSIBILITY_OVERLAY for AOD visibility + if (isAccessibilityServiceEnabled()) { try { WindowManager.LayoutParams::class.java.getField("TYPE_ACCESSIBILITY_OVERLAY").getInt(null) } catch (_: Exception) { - // Fallback if reflection fails WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } + } else { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - else -> WindowManager.LayoutParams.TYPE_PHONE + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Android 8.0-11: Always use TYPE_APPLICATION_OVERLAY for stability + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + @Suppress("DEPRECATION") + WindowManager.LayoutParams.TYPE_PHONE } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt index 0c1aab83..cb6920b8 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -17,9 +17,65 @@ import com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService import com.sameerasw.essentials.utils.AppUtil class NotificationListener : NotificationListenerService() { + + override fun onListenerConnected() { + super.onListenerConnected() + try { + // Initial discovery from active notifications + activeNotifications?.forEach { sbn -> + val isSystem = sbn.packageName == "android" || sbn.packageName == "com.android.systemui" + if (isSystem) { + discoverSystemChannel(sbn.packageName, sbn.notification.channelId, sbn.user) + } + } + } catch (_: Exception) {} + } + + private fun discoverSystemChannel(packageName: String, channelId: String?, userHandle: android.os.UserHandle) { + if (channelId.isNullOrBlank()) return + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + val prefs = applicationContext.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + val discoveredJson = prefs.getString("snooze_discovered_channels", null) + val gson = com.google.gson.Gson() + val type = object : com.google.gson.reflect.TypeToken>() {}.type + val discoveredChannels: MutableList = if (discoveredJson != null) { + try { gson.fromJson(discoveredJson, type) ?: mutableListOf() } catch (_: Exception) { mutableListOf() } + } else mutableListOf() + + if (discoveredChannels.none { it.id == channelId }) { + var foundName: String? = null + try { + val channels = getNotificationChannels(packageName, userHandle) + val channel = channels.find { it.id == channelId } + foundName = channel?.name?.toString() + } catch (_: Exception) {} + + val name = if (!foundName.isNullOrBlank()) foundName + else channelId.replace("_", " ").lowercase().replaceFirstChar { it.uppercase() } + + val finalName = if (packageName == "android") name else "[$packageName] $name" + + discoveredChannels.add(com.sameerasw.essentials.domain.model.SnoozeChannel(channelId, finalName)) + prefs.edit().putString("snooze_discovered_channels", gson.toJson(discoveredChannels)).apply() + } + } catch (_: Exception) {} + } + } @RequiresApi(Build.VERSION_CODES.O) override fun onNotificationPosted(sbn: StatusBarNotification) { + onNotificationPostedInternal(sbn) + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onNotificationPosted(sbn: StatusBarNotification, rankingMap: RankingMap) { + onNotificationPostedInternal(sbn) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun onNotificationPostedInternal(sbn: StatusBarNotification) { // Skip our own app's notifications early to avoid flooding logs and redundant processing if (sbn.packageName == packageName) { return @@ -34,38 +90,29 @@ class NotificationListener : NotificationListenerService() { // Handle Snooze System Notifications try { - val packageName = sbn.packageName - val isSystem = packageName == "android" || packageName == "com.android.systemui" - + val pkg = sbn.packageName + val isSystem = pkg == "android" || pkg.startsWith("com.android.") || pkg == "com.google.android.gms" + if (isSystem) { - val extras = sbn.notification.extras - val title = extras.getString(Notification.EXTRA_TITLE) ?: "" - val text = extras.getString(Notification.EXTRA_TEXT) ?: "" - val content = "$title $text" - - // 1. Debugging - if (prefs.getBoolean("snooze_debugging_enabled", false)) { - val debugRegex = Regex("(?i).*(usb|wireless)\\s*debugging\\s*connected.*") - if (debugRegex.containsMatchIn(content)) { + val channelId = sbn.notification.channelId + + // 1. Discovery + discoverSystemChannel(pkg, channelId, sbn.user) + + // 2. Snoozing + if (channelId != null) { + val blockedChannelsJson = prefs.getString("snooze_blocked_channels", null) + val blockedChannels: Set = if (blockedChannelsJson != null) { + try { + val type = object : com.google.gson.reflect.TypeToken>() {}.type + com.google.gson.Gson().fromJson(blockedChannelsJson, type) ?: emptySet() + } catch (_: Exception) { emptySet() } + } else emptySet() + + if (blockedChannels.contains(channelId)) { snoozeNotification(sbn.key, 24 * 60 * 60 * 1000L) // Snooze for 24 hours } } - - // 2. File Transfer - if (prefs.getBoolean("snooze_file_transfer_enabled", false)) { - val fileTransferRegex = Regex("(?i).*usb\\s*file\\s*transfer.*") - if (fileTransferRegex.containsMatchIn(content)) { - snoozeNotification(sbn.key, 24 * 60 * 60 * 1000L) - } - } - - // 3. Charging - if (prefs.getBoolean("snooze_charging_enabled", false)) { - val chargingRegex = Regex("(?i).*charging\\s*this\\s*device.*") - if (chargingRegex.containsMatchIn(content)) { - snoozeNotification(sbn.key, 24 * 60 * 60 * 1000L) - } - } } } catch (_: Exception) { // Safe to ignore @@ -82,7 +129,7 @@ class NotificationListener : NotificationListenerService() { extras.getString(Notification.EXTRA_TEMPLATE) == "android.app.Notification\$MediaStyle" if (isMedia) { - return + return } val prefs = applicationContext.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) @@ -92,12 +139,8 @@ class NotificationListener : NotificationListenerService() { if (skipSilent) { val ranking = Ranking() if (currentRanking.getRanking(sbn.key, ranking)) { - val importance = - ranking.importance - - val isSilent = - importance <= android.app.NotificationManager.IMPORTANCE_LOW - + val importance = ranking.importance + val isSilent = importance <= android.app.NotificationManager.IMPORTANCE_LOW if (isSilent) { return } @@ -113,7 +156,8 @@ class NotificationListener : NotificationListenerService() { val enabled = prefs.getBoolean("edge_lighting_enabled", false) if (enabled) { // Check all required permissions before triggering notification lighting - if (hasAllRequiredPermissions()) { + val hasPermissions = hasAllRequiredPermissions() + if (hasPermissions) { // Check if the app is selected for notification lighting val appSelected = isAppSelectedForNotificationLighting(sbn.packageName) if (appSelected) { @@ -225,9 +269,11 @@ class NotificationListener : NotificationListenerService() { return false } - // Check accessibility service is enabled - if (!isAccessibilityServiceEnabled()) { - return false + // Check accessibility service is enabled - only required for Android 12+ AOD support + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (!isAccessibilityServiceEnabled()) { + return false + } } return true @@ -274,14 +320,16 @@ class NotificationListener : NotificationListenerService() { val onlyShowWhenScreenOff = prefs.getBoolean("edge_lighting_only_screen_off", true) if (onlyShowWhenScreenOff) { val powerManager = getSystemService(Context.POWER_SERVICE) as android.os.PowerManager - val isScreenOn = - powerManager.isInteractive + val isScreenOn = powerManager.isInteractive if (isScreenOn) { return false } } - val json = prefs.getString("edge_lighting_selected_apps", null) ?: return true + val json = prefs.getString("edge_lighting_selected_apps", null) + if (json == null) { + return true + } // If no saved preferences, allow all apps by default @@ -291,7 +339,8 @@ class NotificationListener : NotificationListenerService() { // Find the app in the saved list val app = selectedApps.find { it.packageName == packageName } - return app?.isEnabled ?: true // Default to true if app not found + val result = app?.isEnabled ?: true + return result } catch (_: Exception) { // If there's an error, default to allowing all apps (backward compatibility) diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt index 6ad80e23..71ed32e1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt @@ -90,6 +90,9 @@ object AutomationManager { else -> {} } } + Automation.Type.APP -> { + // Handled by AppFlowHandler + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt index 3f3c496b..59a9c3b4 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/executors/CombinedActionExecutor.kt @@ -60,6 +60,67 @@ object CombinedActionExecutor { is Action.DimWallpaper -> { com.sameerasw.essentials.utils.ShellUtils.runCommand(context, "cmd wallpaper set-dim-amount ${action.dimAmount}") } + is Action.DeviceEffects -> { + if (Build.VERSION.SDK_INT >= 35) { // Android 15+ + val nm = context.getSystemService(android.app.NotificationManager::class.java) + if (nm.isNotificationPolicyAccessGranted) { + try { + if (action.enabled) { + // ENABLE/UPDATE EFFECTS + val effectsBuilder = android.service.notification.ZenDeviceEffects.Builder() + .setShouldDisplayGrayscale(action.grayscale) + .setShouldSuppressAmbientDisplay(action.suppressAmbient) + .setShouldDimWallpaper(action.dimWallpaper) + .setShouldUseNightMode(action.nightMode) + + val effects = effectsBuilder.build() + + val ruleId = "essentials_focus_mode" + val existingRule = nm.automaticZenRules.values.find { it.name == "Essentials Focus" } + val ruleKey = existingRule?.let { nm.automaticZenRules.entries.find { entry -> entry.value == it }?.key } + + val componentName = android.content.ComponentName(context, com.sameerasw.essentials.services.EssentialsConditionProvider::class.java) + val conditionUri = com.sameerasw.essentials.services.EssentialsConditionProvider.CONDITION_URI + + val ruleBuilder = android.app.AutomaticZenRule.Builder("Essentials Focus", conditionUri) + .setOwner(componentName) + .setDeviceEffects(effects) + .setInterruptionFilter(android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(android.service.notification.ZenPolicy.Builder().allowAlarms(true).build()) + .setConditionId(conditionUri) + .setConfigurationActivity(android.content.ComponentName(context, com.sameerasw.essentials.MainActivity::class.java)) + + if (ruleKey != null) { + nm.updateAutomaticZenRule(ruleKey, ruleBuilder.build()) + } else { + nm.addAutomaticZenRule(ruleBuilder.build()) + } + + // Trigger the condition to be TRUE + com.sameerasw.essentials.services.EssentialsConditionProvider.setConditionState(context, true) + + android.util.Log.d("DeviceEffects", "Updated ZenRule for Device Effects") + + } else { + // DISABLE EFFECTS + val existingRuleEntry = nm.automaticZenRules.entries.find { it.value.name == "Essentials Focus" } + existingRuleEntry?.let { entry -> + val rule = entry.value + rule.isEnabled = false + nm.updateAutomaticZenRule(entry.key, rule) + } + // Also notify condition false just in case + com.sameerasw.essentials.services.EssentialsConditionProvider.setConditionState(context, false) + + android.util.Log.d("DeviceEffects", "Disabled ZenRule for Device Effects") + } + + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt index 457fa726..94f903aa 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt @@ -12,10 +12,18 @@ import com.google.gson.Gson import com.sameerasw.essentials.domain.model.AppSelection import com.google.gson.reflect.TypeToken +import com.sameerasw.essentials.domain.diy.Automation +import com.sameerasw.essentials.domain.diy.DIYRepository +import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + class AppFlowHandler( private val service: AccessibilityService ) { private val handler = Handler(Looper.getMainLooper()) + private val scope = CoroutineScope(Dispatchers.Main) private val authenticatedPackages = mutableSetOf() @@ -23,6 +31,9 @@ class AppFlowHandler( private var lockingPackage: String? = null private var lastLockRequestTime: Long = 0 + // App Automation State + private val activeAppAutomationIds = mutableSetOf() + // Night Light State private var wasNightLightOnBeforeAutoToggle = false private var isNightLightAutoToggledOff = false @@ -31,6 +42,8 @@ class AppFlowHandler( private val ignoredSystemPackages = listOf( "android", + "com.android.systemui", + "com.google.android.inputmethod.latin" ) fun onPackageChanged(packageName: String) { @@ -40,6 +53,7 @@ class AppFlowHandler( checkAppLock(packageName) checkHighlightNightLight(packageName) + checkAppAutomations(packageName) } fun onAuthenticated(packageName: String) { @@ -163,4 +177,37 @@ class AppFlowHandler( Log.w("NightLight", "Failed to set night light: ${e.message}. Ensure WRITE_SECURE_SETTINGS is granted.") } } + + private fun checkAppAutomations(packageName: String) { + scope.launch { + val automations = DIYRepository.automations.value + val appAutomations = automations.filter { it.isEnabled && it.type == Automation.Type.APP } + + // Exiting Automations + // An automation is exiting if it was active, but the new package is NOT in its selected apps list + val exiting = appAutomations.filter { + activeAppAutomationIds.contains(it.id) && !it.selectedApps.contains(packageName) + } + + exiting.forEach { automation -> + activeAppAutomationIds.remove(automation.id) + automation.exitAction?.let { action -> + CombinedActionExecutor.execute(service, action) + } + } + + // Entering Automations + // An automation is entering if it was NOT active, and the new package IS in its selected apps list + val entering = appAutomations.filter { + !activeAppAutomationIds.contains(it.id) && it.selectedApps.contains(packageName) + } + + entering.forEach { automation -> + activeAppAutomationIds.add(automation.id) + automation.entryAction?.let { action -> + CombinedActionExecutor.execute(service, action) + } + } + } + } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt index 03b2cf65..3b8db0d0 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt @@ -12,6 +12,7 @@ import android.util.Log import android.view.KeyEvent import android.os.VibratorManager import android.os.Vibrator +import android.app.NotificationManager import com.sameerasw.essentials.domain.HapticFeedbackType import com.sameerasw.essentials.utils.performHapticFeedback import com.sameerasw.essentials.utils.ShizukuUtils @@ -21,6 +22,7 @@ class ButtonRemapHandler( private val service: AccessibilityService, private val flashlightHandler: FlashlightHandler ) { + private val soundModeHandler = SoundModeHandler(service) private val handler = Handler(Looper.getMainLooper()) private var isLongPressTriggered: Boolean = false private var lastPressedKeyCode: Int = -1 @@ -182,9 +184,16 @@ class ButtonRemapHandler( "Toggle mute" -> toggleRingerMode(AudioManager.RINGER_MODE_SILENT) "AI assistant" -> launchAssistant() "Take screenshot" -> takeScreenshot() + "Cycle sound modes" -> cycleSoundModes() + "Toggle media volume" -> toggleMediaVolume() } } + private fun cycleSoundModes() { + soundModeHandler.cycleNextMode() + triggerHapticFeedback() + } + private fun takeScreenshot() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT) @@ -201,6 +210,23 @@ class ButtonRemapHandler( triggerHapticFeedback() } + private fun toggleMediaVolume() { + val am = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val currentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC) + val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + + if (currentVolume > 0) { + // Mute and save current volume + prefs.edit().putInt("last_media_volume", currentVolume).apply() + am.setStreamVolume(AudioManager.STREAM_MUSIC, 0, AudioManager.FLAG_SHOW_UI) + } else { + // Restore last known volume or default to mid-range + val lastVolume = prefs.getInt("last_media_volume", am.getStreamMaxVolume(AudioManager.STREAM_MUSIC) / 2) + am.setStreamVolume(AudioManager.STREAM_MUSIC, lastVolume, AudioManager.FLAG_SHOW_UI) + } + triggerHapticFeedback() + } + private fun toggleRingerMode(targetMode: Int) { val am = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager val currentMode = am.ringerMode diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt index c529d4cb..ff2c7d3b 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt @@ -37,7 +37,7 @@ class FlashlightHandler( var isTorchOn = false private set - private var currentTorchId: String? = null + private var primaryCameraId: String? = null private var currentIntensityLevel: Int = 1 private var flashlightJob: Job? = null private var isInternalToggle = false @@ -47,16 +47,18 @@ class FlashlightHandler( private val torchCallback = object : CameraManager.TorchCallback() { override fun onTorchModeChanged(cameraId: String, enabled: Boolean) { + val primaryId = getCameraId() + if (cameraId != primaryId) return // Ignore updates from auxiliary camera IDs + super.onTorchModeChanged(cameraId, enabled) isTorchOn = enabled - currentTorchId = cameraId val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) val isGlobalEnabled = prefs.getBoolean("flashlight_global_enabled", false) val lastIntensity = prefs.getInt("flashlight_last_intensity", 1) if (enabled) { - currentTorchId = cameraId + primaryCameraId = cameraId if (isGlobalEnabled && !isInternalToggle) { // External trigger - smoothly fade in to last known intensity @@ -96,7 +98,7 @@ class FlashlightHandler( fun unregister() { torchCallback.let { cameraManager.unregisterTorchCallback(it) } - currentTorchId = null + primaryCameraId = null } fun handleIntent(intent: Intent) { @@ -111,7 +113,7 @@ class FlashlightHandler( if (!isTorchOn) { toggleFlashlight(overrideIntensity = level) } else { - currentTorchId?.let { id -> + getCameraId()?.let { id -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && FlashlightUtil.isIntensitySupported(service, id)) { try { @@ -151,7 +153,7 @@ class FlashlightHandler( notificationManager.createNotificationChannel(channel) } - val cameraId = currentTorchId ?: return + val cameraId = getCameraId() ?: return val maxLevel = FlashlightUtil.getMaxLevel(service, cameraId) val percentage = (intensity * 100) / maxOf(1, maxLevel) @@ -244,12 +246,29 @@ class FlashlightHandler( } private fun getCameraId(): String? { + primaryCameraId?.let { return it } try { + var targetCameraId: String? = null for (id in cameraManager.cameraIdList) { - val characteristics = cameraManager.getCameraCharacteristics(id) - val hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false - if (hasFlash) return id + val chars = cameraManager.getCameraCharacteristics(id) + val flashAvailable = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false + val lensFacing = chars.get(CameraCharacteristics.LENS_FACING) + if (flashAvailable && lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + targetCameraId = id + break + } } + if (targetCameraId == null) { + for (id in cameraManager.cameraIdList) { + val chars = cameraManager.getCameraCharacteristics(id) + if (chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true) { + targetCameraId = id + break + } + } + } + primaryCameraId = targetCameraId + return targetCameraId } catch (e: Exception) { Log.e("Flashlight", "Error getting camera ID", e) } @@ -285,7 +304,7 @@ class FlashlightHandler( val faceDownOnly = prefs.getBoolean("flashlight_pulse_facedown_only", true) if (faceDownOnly && !isProximityBlocked) return - val cameraId = currentTorchId ?: getCameraId() ?: return + val cameraId = getCameraId() ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && FlashlightUtil.isIntensitySupported(service, cameraId)) { @@ -319,6 +338,23 @@ class FlashlightHandler( isInternalToggle = false } + } else { + // Fallback for older versions or devices without intensity support + Log.d("Flashlight", "Pulse fallback with cameraId: $cameraId") + flashlightJob?.cancel() + flashlightJob = scope.launch { + isInternalToggle = true + try { + cameraManager.setTorchMode(cameraId, true) + kotlinx.coroutines.delay(700L) + cameraManager.setTorchMode(cameraId, false) + kotlinx.coroutines.delay(200L) + } catch (e: Exception) { + Log.e("Flashlight", "Fallback pulse failed for cameraId: $cameraId", e) + } finally { + isInternalToggle = false + } + } } } @@ -328,7 +364,7 @@ class FlashlightHandler( } fun adjustFlashlightIntensity(increase: Boolean) { - val cameraId = currentTorchId ?: return + val cameraId = getCameraId() ?: return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return try { @@ -406,7 +442,7 @@ class FlashlightHandler( if (targetCameraId != null) { val finalCameraId = targetCameraId - currentTorchId = finalCameraId + primaryCameraId = finalCameraId val maxLevel = FlashlightUtil.getMaxLevel(service, finalCameraId) val defaultLevel = FlashlightUtil.getDefaultLevel(service, finalCameraId) diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/NotificationLightingHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/NotificationLightingHandler.kt index e093f98e..dc33f7a0 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/NotificationLightingHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/NotificationLightingHandler.kt @@ -73,15 +73,8 @@ class NotificationLightingHandler( } try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { showNotificationLighting() - } else { - // Fallback or ignore for older versions if needed, but logic seems to require S - // Trying to run anyway if compatible, but original code annotated with RequiresApi(S) - // I will wrap calls or assume caller handles version check - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - showNotificationLighting() - } } } catch (e: Exception) { Log.e("NotificationLighting", "Failed to show notification lighting", e) @@ -118,17 +111,30 @@ class NotificationLightingHandler( windowManager = service.getSystemService(Context.WINDOW_SERVICE) as WindowManager val powerManager = service.getSystemService(Context.POWER_SERVICE) as PowerManager - val overlayType = try { - WindowManager.LayoutParams::class.java.getField("TYPE_ACCESSIBILITY_OVERLAY").getInt(null) - } catch (_: Exception) { + val overlayType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + WindowManager.LayoutParams::class.java.getField("TYPE_ACCESSIBILITY_OVERLAY").getInt(null) + } catch (_: Exception) { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + @Suppress("DEPRECATION") + WindowManager.LayoutParams.TYPE_PHONE } try { val color = when { resolvedColor != null -> resolvedColor!! colorMode == NotificationLightingColorMode.CUSTOM -> customColor - else -> service.getColor(android.R.color.system_accent1_100) + else -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + service.getColor(android.R.color.system_accent1_100) + } else { + service.getColor(com.sameerasw.essentials.R.color.purple_500) + } + } } val overlay = OverlayHelper.createOverlayView( diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt new file mode 100644 index 00000000..6196c2ec --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt @@ -0,0 +1,44 @@ +package com.sameerasw.essentials.services.handlers + +import android.app.NotificationManager +import android.content.Context +import android.media.AudioManager +import com.sameerasw.essentials.utils.HapticUtil + +class SoundModeHandler(private val context: Context) { + + fun cycleNextMode(): Int? { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (!notificationManager.isNotificationPolicyAccessGranted) { + return null + } + + val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + val defaultOrder = listOf("Sound", "Vibrate", "Silent") + val orderString = prefs.getString("sound_mode_order", defaultOrder.joinToString(",")) ?: defaultOrder.joinToString(",") + val order = orderString.split(",") + + val currentMode = when (audioManager.ringerMode) { + AudioManager.RINGER_MODE_NORMAL -> "Sound" + AudioManager.RINGER_MODE_VIBRATE -> "Vibrate" + AudioManager.RINGER_MODE_SILENT -> "Silent" + else -> "Sound" + } + + val currentIndex = order.indexOf(currentMode) + val nextIndex = (currentIndex + 1) % order.size + val nextMode = order[nextIndex] + + val nextRingerMode = when (nextMode) { + "Sound" -> AudioManager.RINGER_MODE_NORMAL + "Vibrate" -> AudioManager.RINGER_MODE_VIBRATE + "Silent" -> AudioManager.RINGER_MODE_SILENT + else -> AudioManager.RINGER_MODE_NORMAL + } + + audioManager.ringerMode = nextRingerMode + return nextRingerMode + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt new file mode 100644 index 00000000..221a8b22 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/PrivateDnsTileService.kt @@ -0,0 +1,84 @@ +package com.sameerasw.essentials.services.tiles + +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.Build +import android.provider.Settings +import android.service.quicksettings.Tile +import androidx.annotation.RequiresApi +import com.sameerasw.essentials.FeatureSettingsActivity +import com.sameerasw.essentials.R +import com.sameerasw.essentials.utils.PermissionUtils + +@RequiresApi(Build.VERSION_CODES.N) +class PrivateDnsTileService : BaseTileService() { + + companion object { + private const val PRIVATE_DNS_MODE = "private_dns_mode" + private const val PRIVATE_DNS_SPECIFIER = "private_dns_specifier" + + private const val MODE_OFF = "off" + private const val MODE_AUTO = "opportunistic" + private const val MODE_HOSTNAME = "hostname" + } + + override fun onClick() { + if (!hasFeaturePermission()) { + val intent = Intent(this, FeatureSettingsActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra("feature", "Quick settings tiles") + } + startActivityAndCollapse(intent) + return + } + super.onClick() + } + + override fun getTileLabel(): String = getString(R.string.tile_private_dns) + + override fun getTileSubtitle(): String { + return when (getPrivateDnsMode()) { + MODE_AUTO -> getString(R.string.tile_private_dns_auto) + MODE_HOSTNAME -> getPrivateDnsHostname() ?: getString(R.string.feat_qs_tiles_title) + else -> getString(R.string.tile_private_dns_off) + } + } + + override fun hasFeaturePermission(): Boolean { + return PermissionUtils.canWriteSecureSettings(this) + } + + override fun getTileIcon(): Icon? { + return Icon.createWithResource(this, R.drawable.rounded_dns_24) + } + + override fun getTileState(): Int { + return if (getPrivateDnsMode() != MODE_OFF) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + } + + override fun onTileClick() { + val currentMode = getPrivateDnsMode() + val nextMode = when (currentMode) { + MODE_OFF -> MODE_AUTO + MODE_AUTO -> { + if (getPrivateDnsHostname().isNullOrEmpty()) MODE_OFF else MODE_HOSTNAME + } + MODE_HOSTNAME -> MODE_OFF + else -> MODE_OFF + } + + try { + Settings.Global.putString(contentResolver, PRIVATE_DNS_MODE, nextMode) + } catch (e: Exception) { + // Handle error or permission missing + } + } + + private fun getPrivateDnsMode(): String { + return Settings.Global.getString(contentResolver, PRIVATE_DNS_MODE) ?: MODE_OFF + } + + private fun getPrivateDnsHostname(): String? { + return Settings.Global.getString(contentResolver, PRIVATE_DNS_SPECIFIER) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt index 31a61347..b22d8235 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/ScreenOffAccessibilityService.kt @@ -68,10 +68,12 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene Intent.ACTION_SCREEN_ON -> { notificationLightingHandler.onScreenOn() freezeHandler.removeCallbacks(freezeRunnable) + stopInputEventListener() } Intent.ACTION_SCREEN_OFF -> { appFlowHandler.clearAuthenticated() scheduleFreeze() + startInputEventListenerIfEnabled() } Intent.ACTION_USER_PRESENT -> { securityHandler.restoreAnimationScale() @@ -131,6 +133,7 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene sensorManager.unregisterListener(this) securityHandler.restoreAnimationScale() notificationLightingHandler.removeOverlay() + stopInputEventListener() serviceScope.cancel() super.onDestroy() } @@ -198,4 +201,31 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene } return super.onStartCommand(intent, flags, startId) } + + private fun startInputEventListenerIfEnabled() { + val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) + val isEnabled = prefs.getBoolean("button_remap_enabled", false) + val useShizuku = prefs.getBoolean("button_remap_use_shizuku", false) + + if (isEnabled && useShizuku) { + try { + val intent = Intent(this, InputEventListenerService::class.java) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } catch (e: Exception) { + // Ignore + } + } + } + + private fun stopInputEventListener() { + try { + stopService(Intent(this, InputEventListenerService::class.java)) + } catch (e: Exception) { + // Ignore + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/SoundModeTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/SoundModeTileService.kt index d710f864..2292aed2 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/tiles/SoundModeTileService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/SoundModeTileService.kt @@ -11,6 +11,7 @@ import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.sameerasw.essentials.R +import com.sameerasw.essentials.services.handlers.SoundModeHandler import com.sameerasw.essentials.utils.HapticUtil class SoundModeTileService : TileService() { @@ -78,42 +79,11 @@ class SoundModeTileService : TileService() { override fun onClick() { super.onClick() - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - if (!notificationManager.isNotificationPolicyAccessGranted) { - return - } - - val prefs = getSharedPreferences("essentials_prefs", MODE_PRIVATE) - val defaultOrder = listOf("Sound", "Vibrate", "Silent") - val orderString = prefs.getString("sound_mode_order", defaultOrder.joinToString(",")) ?: defaultOrder.joinToString(",") - val order = orderString.split(",") - - val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager - - val currentMode = when (audioManager.ringerMode) { - AudioManager.RINGER_MODE_NORMAL -> "Sound" - AudioManager.RINGER_MODE_VIBRATE -> "Vibrate" - AudioManager.RINGER_MODE_SILENT -> "Silent" - else -> "Sound" + val nextRingerMode = SoundModeHandler(this).cycleNextMode() + if (nextRingerMode != null) { + latestAudioStateUpdate = nextRingerMode + updateSoundTile() } - - val currentIndex = order.indexOf(currentMode) - val nextIndex = (currentIndex + 1) % order.size - val nextMode = order[nextIndex] - - val nextRingerMode = when (nextMode) { - "Sound" -> AudioManager.RINGER_MODE_NORMAL - "Vibrate" -> AudioManager.RINGER_MODE_VIBRATE - "Silent" -> AudioManager.RINGER_MODE_SILENT - else -> AudioManager.RINGER_MODE_NORMAL - } - - audioManager.ringerMode = nextRingerMode - HapticUtil.performHapticForService(this) - - latestAudioStateUpdate = nextRingerMode - - updateSoundTile() } override fun onStartListening() { diff --git a/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt b/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt new file mode 100644 index 00000000..01e9bfd0 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/services/tiles/UsbDebuggingTileService.kt @@ -0,0 +1,62 @@ +package com.sameerasw.essentials.services.tiles + +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.Build +import android.provider.Settings +import android.service.quicksettings.Tile +import androidx.annotation.RequiresApi +import com.sameerasw.essentials.FeatureSettingsActivity +import com.sameerasw.essentials.R +import com.sameerasw.essentials.utils.PermissionUtils + +@RequiresApi(Build.VERSION_CODES.N) +class UsbDebuggingTileService : BaseTileService() { + + override fun onClick() { + if (!hasFeaturePermission()) { + val intent = Intent(this, FeatureSettingsActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra("feature", "Quick settings tiles") + } + startActivityAndCollapse(intent) + return + } + super.onClick() + } + + override fun getTileLabel(): String = getString(R.string.tile_usb_debugging) + + override fun getTileSubtitle(): String { + return if (isAdbEnabled()) getString(R.string.on) else getString(R.string.off) + } + + override fun hasFeaturePermission(): Boolean { + return PermissionUtils.canWriteSecureSettings(this) + } + + override fun getTileIcon(): Icon? { + return Icon.createWithResource(this, R.drawable.rounded_adb_24) + } + + override fun getTileState(): Int { + return if (isAdbEnabled()) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + } + + override fun onTileClick() { + val newState = if (isAdbEnabled()) 0 else 1 + try { + Settings.Global.putInt(contentResolver, Settings.Global.ADB_ENABLED, newState) + } catch (e: Exception) { + // Permission check in BaseTileService should handle this + } + } + + private fun isAdbEnabled(): Boolean { + return try { + Settings.Global.getInt(contentResolver, Settings.Global.ADB_ENABLED) == 1 + } catch (e: Exception) { + false + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidget.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidget.kt index 1d281be3..df3d505f 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidget.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidget.kt @@ -25,8 +25,8 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.glance.Image import androidx.glance.ImageProvider -import com.google.android.material.color.MaterialColors import com.sameerasw.essentials.R +import androidx.core.graphics.toColorInt class BatteriesWidget : GlanceAppWidget() { override val sizeMode = androidx.glance.appwidget.SizeMode.Exact @@ -155,8 +155,7 @@ class BatteriesWidget : GlanceAppWidget() { val basePrimary = GlanceTheme.colors.primary.getColor(configContext).toArgb() val baseError = GlanceTheme.colors.error.getColor(configContext).toArgb() val onSurface = GlanceTheme.colors.onSurface.getColor(configContext).toArgb() - val surfaceColor = GlanceTheme.colors.surface.getColor(configContext).toArgb() - val warning = MaterialColors.harmonizeWithPrimary(configContext, android.graphics.Color.parseColor("#FFC107")) + val widgetBackgroundColor = GlanceTheme.colors.widgetBackground.getColor(configContext).toArgb() val isNightMode = (systemConfig.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) == @@ -166,9 +165,9 @@ class BatteriesWidget : GlanceAppWidget() { val colors = ThemeColors( primary = basePrimary, error = baseError, - warning = warning, + warning = "#FFC107".toColorInt(), track = ColorUtils.setAlphaComponent(onSurface, 30), - surface = surfaceColor, + surface = widgetBackgroundColor, iconTint = onSurface ) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt index bb0b67cb..23f431d5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/activities/AutomationEditorActivity.kt @@ -69,6 +69,18 @@ import com.sameerasw.essentials.ui.components.sheets.DimWallpaperSettingsSheet import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.domain.model.AppSelection +import com.sameerasw.essentials.domain.model.NotificationApp +import com.sameerasw.essentials.utils.AppUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.Switch +import androidx.compose.ui.draw.clip +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import com.sameerasw.essentials.ui.components.cards.AppToggleItem class AutomationEditorActivity : ComponentActivity() { @@ -104,7 +116,7 @@ class AutomationEditorActivity : ComponentActivity() { val isEditMode = existingAutomation != null val automationType = if (isEditMode) { - existingAutomation?.type ?: Automation.Type.TRIGGER + existingAutomation.type } else { try { Automation.Type.valueOf(automationTypeStr ?: Automation.Type.TRIGGER.name) @@ -113,7 +125,11 @@ class AutomationEditorActivity : ComponentActivity() { } } - val titleRes = if (isEditMode) R.string.diy_editor_edit_title else R.string.diy_editor_new_title + val titleRes = when(automationType) { + Automation.Type.TRIGGER -> if (isEditMode) R.string.diy_editor_edit_title else R.string.diy_editor_new_title + Automation.Type.STATE -> if (isEditMode) R.string.diy_editor_edit_title else R.string.diy_editor_new_title + Automation.Type.APP -> if (isEditMode) R.string.diy_editor_edit_title else R.string.diy_create_app_title + } setContent { val viewModel: com.sameerasw.essentials.viewmodels.MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel() @@ -143,6 +159,42 @@ class AutomationEditorActivity : ComponentActivity() { // Initialize with existing data or defaults var selectedTrigger by remember { mutableStateOf(existingAutomation?.trigger) } var selectedState by remember { mutableStateOf(existingAutomation?.state) } + var selectedApps by remember { mutableStateOf>(existingAutomation?.selectedApps ?: emptyList()) } + + // App Picker State + var searchQuery by remember { mutableStateOf("") } + var allApps by remember { mutableStateOf>(emptyList()) } + var isLoadingApps by remember { mutableStateOf(false) } + var showSystemApps by remember { mutableStateOf(false) } + + // Load apps if needed + LaunchedEffect(automationType) { + if (automationType == Automation.Type.APP) { + isLoadingApps = true + withContext(Dispatchers.IO) { + try { + val installed = AppUtil.getInstalledApps(context) + // Merge with selection if existing + val merged = AppUtil.mergeWithSavedApps(installed, selectedApps.map { AppSelection(it, true) }) + withContext(Dispatchers.Main) { + allApps = merged + isLoadingApps = false + } + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Main) { isLoadingApps = false } + } + } + } + } + + val filteredApps = remember(allApps, searchQuery, showSystemApps, selectedApps) { + allApps.filter { + val matchesSearch = searchQuery.isEmpty() || it.appName.contains(searchQuery, ignoreCase = true) + val isVisible = !it.isSystemApp || showSystemApps || selectedApps.contains(it.packageName) + matchesSearch && isVisible + }.sortedWith(compareByDescending { selectedApps.contains(it.packageName) }.thenBy { it.appName.lowercase() }) + } // Actions // For Trigger type @@ -160,12 +212,14 @@ class AutomationEditorActivity : ComponentActivity() { // Config Sheets var showDimSettings by remember { mutableStateOf(false) } - var configAction by remember { mutableStateOf(null) } + var showDeviceEffectsSettings by remember { mutableStateOf(false) } + var configAction by remember { mutableStateOf(null) } // Generic config action // Validation val isValid = when (automationType) { Automation.Type.TRIGGER -> selectedTrigger != null && selectedAction != null Automation.Type.STATE -> selectedState != null && (selectedInAction != null || selectedOutAction != null) + Automation.Type.APP -> selectedApps.isNotEmpty() && (selectedInAction != null || selectedOutAction != null) } Scaffold( @@ -194,7 +248,9 @@ class AutomationEditorActivity : ComponentActivity() { text = { Text(stringResource(R.string.action_delete)) }, onClick = { showMenu = false - DIYRepository.removeAutomation(existingAutomation!!.id) + if (existingAutomation != null) { + DIYRepository.removeAutomation(existingAutomation.id) + } finish() }, leadingIcon = { @@ -222,7 +278,7 @@ class AutomationEditorActivity : ComponentActivity() { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Only handle drag (user interaction) - if (source == NestedScrollSource.Drag) { + if (source == NestedScrollSource.UserInput) { accumulatedScroll += available.x if (kotlin.math.abs(accumulatedScroll) >= threshold) { @@ -252,59 +308,153 @@ class AutomationEditorActivity : ComponentActivity() { Box( modifier = Modifier .fillMaxSize() - .maskClip(MaterialTheme.shapes.extraLarge) + .clip(MaterialTheme.shapes.extraLarge) .background(MaterialTheme.colorScheme.surfaceContainerHigh) ) { if (index == 0) { // PAGE 0: Trigger or State Picker - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = stringResource(if (automationType == Automation.Type.TRIGGER) R.string.diy_select_trigger else R.string.diy_select_state), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(horizontal = 12.dp) - ) - - RoundedCardContainer(spacing = 2.dp) { - if (automationType == Automation.Type.TRIGGER) { - val triggers = listOf( - Trigger.ScreenOff, - Trigger.ScreenOn, - Trigger.DeviceUnlock, - Trigger.ChargerConnected, - Trigger.ChargerDisconnected - ) - triggers.forEach { trigger -> - EditorActionItem( - title = stringResource(trigger.title), - iconRes = trigger.icon, - isSelected = selectedTrigger == trigger, - isConfigurable = trigger.isConfigurable, - onClick = { selectedTrigger = trigger }, - onSettingsClick = { - // Handle trigger settings if needed later - } + if (automationType == Automation.Type.APP) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.diy_create_app_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 12.dp) + ) + + // Search Bar + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text(stringResource(R.string.label_search)) }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_search_24), + contentDescription = stringResource(R.string.action_search) ) + }, + singleLine = true, + shape = RoundedCornerShape(12.dp) + ) + + // System Apps Toggle + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { + HapticUtil.performVirtualKeyHaptic(view) + showSystemApps = !showSystemApps + } + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_settings_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.toggle_show_system_apps), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface + ) + Switch( + checked = showSystemApps, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + showSystemApps = it + } + ) + } + + if (isLoadingApps) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + LoadingIndicator() } } else { - val states = listOf( - DIYState.Charging, - DIYState.ScreenOn - ) - states.forEach { state -> - EditorActionItem( - title = stringResource(state.title), - iconRes = state.icon, - isSelected = selectedState == state, - onClick = { selectedState = state } - ) + LazyColumn( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(24.dp)), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + items(filteredApps, key = { it.packageName }) { app -> + val isSelected = selectedApps.contains(app.packageName) + AppToggleItem( + icon = app.icon, + title = app.appName, + isChecked = isSelected, + onCheckedChange = { isChecked -> + val current = selectedApps.toMutableList() + if (isChecked) current.add(app.packageName) else current.remove(app.packageName) + selectedApps = current + } + ) + } + } + } + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(if (automationType == Automation.Type.TRIGGER) R.string.diy_select_trigger else R.string.diy_select_state), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 12.dp) + ) + + RoundedCardContainer(spacing = 2.dp) { + if (automationType == Automation.Type.TRIGGER) { + val triggers = listOf( + Trigger.ScreenOff, + Trigger.ScreenOn, + Trigger.DeviceUnlock, + Trigger.ChargerConnected, + Trigger.ChargerDisconnected + ) + triggers.forEach { trigger -> + EditorActionItem( + title = stringResource(trigger.title), + iconRes = trigger.icon, + isSelected = selectedTrigger == trigger, + isConfigurable = trigger.isConfigurable, + onClick = { selectedTrigger = trigger }, + onSettingsClick = { + // Handle trigger settings if needed later + } + ) + } + } else { + val states = listOf( + DIYState.Charging, + DIYState.ScreenOn + ) + states.forEach { state -> + EditorActionItem( + title = stringResource(state.title), + iconRes = state.icon, + isSelected = selectedState == state, + onClick = { selectedState = state } + ) + } } } } @@ -326,7 +476,7 @@ class AutomationEditorActivity : ComponentActivity() { modifier = Modifier.padding(horizontal = 12.dp) ) - if (automationType == Automation.Type.STATE) { + if (automationType == Automation.Type.STATE || automationType == Automation.Type.APP) { // Tabs for In/Out val options = listOf( stringResource(R.string.diy_in_action_label), @@ -346,17 +496,21 @@ class AutomationEditorActivity : ComponentActivity() { } RoundedCardContainer(spacing = 2.dp) { - val actions = listOf( + val actions = mutableListOf( Action.TurnOnFlashlight, Action.TurnOffFlashlight, Action.ToggleFlashlight, Action.HapticVibration, Action.DimWallpaper() ) + // Only show Device Effects on Android 15+ + actions.add(Action.DeviceEffects()) + val currentSelection = when(automationType) { Automation.Type.TRIGGER -> selectedAction Automation.Type.STATE -> if (selectedActionTab == 0) selectedInAction else selectedOutAction + Automation.Type.APP -> if (selectedActionTab == 0) selectedInAction else selectedOutAction } // None option @@ -367,7 +521,7 @@ class AutomationEditorActivity : ComponentActivity() { onClick = { when(automationType) { Automation.Type.TRIGGER -> selectedAction = null - Automation.Type.STATE -> { + Automation.Type.STATE, Automation.Type.APP -> { if (selectedActionTab == 0) selectedInAction = null else selectedOutAction = null } @@ -377,7 +531,6 @@ class AutomationEditorActivity : ComponentActivity() { actions.forEach { action -> // Check if the current selection matches this action type and update 'action' with the selected values if so - // This ensures the loop variable 'action' doesn't overwrite the configured values in 'currentSelection' val resolvedAction = if (currentSelection != null && currentSelection::class == action::class) currentSelection else action EditorActionItem( @@ -388,16 +541,27 @@ class AutomationEditorActivity : ComponentActivity() { onClick = { when(automationType) { Automation.Type.TRIGGER -> selectedAction = resolvedAction - Automation.Type.STATE -> { + Automation.Type.STATE, Automation.Type.APP -> { if (selectedActionTab == 0) selectedInAction = resolvedAction else selectedOutAction = resolvedAction } } + // Check permissions immediately on selection + // For Device Effects, we need Notification Policy Access + if (resolvedAction is Action.DeviceEffects) { + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager + if (!nm.isNotificationPolicyAccessGranted) { + val intent = Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) + context.startActivity(intent) + } + } }, onSettingsClick = { + configAction = resolvedAction if (resolvedAction is Action.DimWallpaper) { - configAction = resolvedAction showDimSettings = true + } else if (resolvedAction is Action.DeviceEffects) { + showDeviceEffectsSettings = true } } ) @@ -408,16 +572,34 @@ class AutomationEditorActivity : ComponentActivity() { } } - if (showDimSettings && configAction != null) { + if (showDimSettings && configAction is Action.DimWallpaper) { DimWallpaperSettingsSheet( - initialAction = configAction!!, + initialAction = configAction as Action.DimWallpaper, onDismiss = { showDimSettings = false }, onSave = { newAction -> showDimSettings = false // Update the selection with configured action when(automationType) { Automation.Type.TRIGGER -> selectedAction = newAction - Automation.Type.STATE -> { + Automation.Type.STATE, Automation.Type.APP -> { + if (selectedActionTab == 0) selectedInAction = newAction + else selectedOutAction = newAction + } + } + configAction = null + } + ) + } + + if (showDeviceEffectsSettings && configAction is Action.DeviceEffects) { + com.sameerasw.essentials.ui.components.sheets.DeviceEffectsSettingsSheet( + initialAction = configAction as Action.DeviceEffects, + onDismiss = { showDeviceEffectsSettings = false }, + onSave = { newAction -> + showDeviceEffectsSettings = false + when(automationType) { + Automation.Type.TRIGGER -> selectedAction = newAction + Automation.Type.STATE, Automation.Type.APP -> { if (selectedActionTab == 0) selectedInAction = newAction else selectedOutAction = newAction } @@ -460,21 +642,30 @@ class AutomationEditorActivity : ComponentActivity() { // Save logic if (automationType == Automation.Type.TRIGGER) { val newAutomation = Automation( - id = if (isEditMode) existingAutomation!!.id else java.util.UUID.randomUUID().toString(), + id = if (isEditMode) existingAutomation.id else java.util.UUID.randomUUID().toString(), type = Automation.Type.TRIGGER, trigger = selectedTrigger, actions = listOfNotNull(selectedAction) ) if (isEditMode) DIYRepository.updateAutomation(newAutomation) else DIYRepository.addAutomation(newAutomation) - } else { + } else if (automationType == Automation.Type.STATE) { val newAutomation = Automation( - id = if (isEditMode) existingAutomation!!.id else java.util.UUID.randomUUID().toString(), + id = if (isEditMode) existingAutomation.id else java.util.UUID.randomUUID().toString(), type = Automation.Type.STATE, state = selectedState, entryAction = selectedInAction, exitAction = selectedOutAction ) if (isEditMode) DIYRepository.updateAutomation(newAutomation) else DIYRepository.addAutomation(newAutomation) + } else { + val newAutomation = Automation( + id = if (isEditMode) existingAutomation.id else java.util.UUID.randomUUID().toString(), + type = Automation.Type.APP, + selectedApps = selectedApps, + entryAction = selectedInAction, + exitAction = selectedOutAction + ) + if (isEditMode) DIYRepository.updateAutomation(newAutomation) else DIYRepository.addAutomation(newAutomation) } finish() }, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/FavoriteCarousel.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/FavoriteCarousel.kt new file mode 100644 index 00000000..2d7672cd --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/FavoriteCarousel.kt @@ -0,0 +1,164 @@ +package com.sameerasw.essentials.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import androidx.compose.material3.carousel.rememberCarouselState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.model.Feature +import com.sameerasw.essentials.domain.registry.FeatureRegistry +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem +import com.sameerasw.essentials.utils.ColorUtil +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FavoriteCarousel( + pinnedKeys: List, + onFeatureClick: (Feature) -> Unit, + onFeatureLongClick: (Feature) -> Unit, + modifier: Modifier = Modifier +) { + if (pinnedKeys.isEmpty()) return + + val pinnedFeatures = remember(pinnedKeys) { + val featuresMap = FeatureRegistry.ALL_FEATURES.associateBy { it.id } + pinnedKeys.mapNotNull { featuresMap[it] } + } + + if (pinnedFeatures.isEmpty()) return + + val carouselState = rememberCarouselState { pinnedFeatures.size } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + return if (available.x != 0f) { + Offset(x = available.x, y = 0f) + } else { + Offset.Zero + } + } + } + } + + Column( + modifier = modifier + .fillMaxWidth() + .nestedScroll(nestedScrollConnection) + ) { + + HorizontalMultiBrowseCarousel( + state = carouselState, + preferredItemWidth = 140.dp, + itemSpacing = 4.dp, + contentPadding = PaddingValues(horizontal = 18.dp), + modifier = Modifier + .fillMaxWidth() + .height(110.dp) + ) { index -> + val feature = pinnedFeatures[index] + val view = LocalView.current + val resolvedTitle = stringResource(id = feature.title) + var showMenu by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .maskClip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceBright) + .pointerInput(feature) { + detectTapGestures( + onLongPress = { + HapticUtil.performVirtualKeyHaptic(view) + showMenu = true + }, + onTap = { + HapticUtil.performVirtualKeyHaptic(view) + onFeatureClick(feature) + } + ) + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(48.dp) + .background( + color = ColorUtil.getPastelColorFor(resolvedTitle), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = feature.iconRes), + contentDescription = resolvedTitle, + modifier = Modifier.size(28.dp), + tint = ColorUtil.getVibrantColorFor(resolvedTitle) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = resolvedTitle, + style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + + SegmentedDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_unpin)) }, + onClick = { + showMenu = false + onFeatureLongClick(feature) + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.rounded_bookmark_remove_24), + contentDescription = null + ) + } + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt index 055fc628..405a1b42 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/ReusableTopAppBar.kt @@ -53,6 +53,7 @@ fun ReusableTopAppBar( isBeta: Boolean = false, backIconRes: Int = R.drawable.rounded_arrow_back_24, isSmall: Boolean = false, + containerColor: Color = MaterialTheme.colorScheme.surfaceContainer, actions: @Composable RowScope.() -> Unit = {} ) { val collapsedFraction = scrollBehavior?.state?.collapsedFraction ?: 0f @@ -241,7 +242,7 @@ fun ReusableTopAppBar( if (isSmall) { TopAppBar( colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer + containerColor = containerColor ), modifier = Modifier.padding(horizontal = 8.dp), title = titleContent, @@ -252,7 +253,7 @@ fun ReusableTopAppBar( } else { LargeFlexibleTopAppBar( colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer + containerColor = containerColor ), modifier = Modifier.padding(horizontal = 8.dp), expandedHeight = if (subtitle != null) 200.dp else 160.dp, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt index 0603b9a9..7ed8bd68 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/AppToggleItem.kt @@ -16,20 +16,65 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.graphics.Color +import com.sameerasw.essentials.R import com.sameerasw.essentials.utils.HapticUtil +private val GOOGLE_SYSTEM_USER_APPS = setOf( + "com.google.android.apps.scone", + "com.google.android.marvin.talkback", + "com.google.android.projection.gearhead", + "com.google.android.as", + "com.google.android.contactkeys", + "com.google.android.safetycore", + "com.google.android.webview", + "com.google.android.captiveportallogin", + "com.google.ambient.streaming", + "com.google.android.apps.pixel.dcservice", + "com.google.android.apps.turbo", + "com.google.android.apps.work.clouddpc", + "com.google.android.apps.diagnosticstool", + "com.google.android.apps.wellbeing", + "com.google.android.documentsui", + "com.google.android.odad", + "com.google.android.gms", + "com.google.ar.core", + "com.google.vending", + "com.google.android.apps.carrier.carrierwifi", + "com.google.android.modulemetadata", + "com.google.android.networkstack", + "com.google.android.apps.safetyhub", + "com.google.intelligence.sense", + "com.google.android.apps.camera.services", + "com.google.android.apps.nexuslauncher", + "com.google.android.apps.pixel.support", + "com.google.android.as.oss", + "com.android.settings", + "com.google.android.settings.intelligence", + "com.android.stk", + "com.google.android.soundpicker", + "com.google.mainline.telemetry", + "com.google.android.apps.accessibility.voiceaccess", + "com.google.android.cellbroadcastreceiver" +) + @Composable fun AppToggleItem( icon: ImageBitmap?, title: String, modifier: Modifier = Modifier, description: String? = null, + packageName: String? = null, + isSystemApp: Boolean = false, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true, @@ -37,6 +82,9 @@ fun AppToggleItem( showToggle: Boolean = true ) { val view = LocalView.current + val shouldShowSystemTag = remember(packageName, isSystemApp) { + isSystemApp || (packageName != null && GOOGLE_SYSTEM_USER_APPS.contains(packageName)) + } Row( modifier = modifier @@ -74,10 +122,33 @@ fun AppToggleItem( if (description != null) { Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium + ) + if (shouldShowSystemTag) { + Box( + modifier = Modifier + .size(18.dp) + .background( + color = MaterialTheme.colorScheme.primary, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + androidx.compose.material3.Icon( + painter = painterResource(id = R.drawable.round_android_24), + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.surfaceBright + ) + } + } + } Text( text = description, style = MaterialTheme.typography.labelMedium, @@ -85,11 +156,34 @@ fun AppToggleItem( ) } } else { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.weight(1f) - ) + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium + ) + if (shouldShowSystemTag) { + Box( + modifier = Modifier + .size(18.dp) + .background( + color = MaterialTheme.colorScheme.primary, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + androidx.compose.material3.Icon( + painter = painterResource(id = R.drawable.round_android_24), + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.surfaceBright + ) + } + } + } } if (showToggle) { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt index 4a72748b..99a9867c 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/cards/FeatureCard.kt @@ -3,11 +3,11 @@ package com.sameerasw.essentials.ui.components.cards import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -19,14 +19,19 @@ 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.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu +import com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem import com.sameerasw.essentials.utils.ColorUtil import com.sameerasw.essentials.utils.HapticUtil @@ -44,19 +49,28 @@ fun FeatureCard( onDisabledToggleClick: (() -> Unit)? = null, description: Any? = null, // Can be Int or String descriptionOverride: String? = null, // For cases where we search and prepend parent feature name - isBeta: Boolean = false + isBeta: Boolean = false, + isPinned: Boolean = false, + onPinToggle: (() -> Unit)? = null ) { val view = LocalView.current + var showMenu by remember { mutableStateOf(false) } Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceBright ), shape = MaterialTheme.shapes.extraSmall, - modifier = modifier.clickable { - HapticUtil.performVirtualKeyHaptic(view) - onClick() - }) { + modifier = modifier.combinedClickable( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onClick() + }, + onLongClick = { + HapticUtil.performVirtualKeyHaptic(view) + showMenu = true + } + )) { Box(modifier = Modifier .fillMaxWidth() .padding(16.dp)) { @@ -172,6 +186,29 @@ fun FeatureCard( } } } + + SegmentedDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + if (onPinToggle != null) { + SegmentedDropdownMenuItem( + text = { + Text(if (isPinned) stringResource(R.string.action_unpin) else stringResource(R.string.action_pin)) + }, + onClick = { + showMenu = false + onPinToggle() + }, + leadingIcon = { + Icon( + painter = painterResource(id = if (isPinned) R.drawable.rounded_bookmark_remove_24 else R.drawable.rounded_bookmark_24), + contentDescription = null + ) + } + ) + } + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt index c786f7d5..b0c3f8da 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/diy/AutomationItem.kt @@ -151,12 +151,19 @@ fun AutomationItem( .weight(1f) .fillMaxHeight(), ) { - val icon = - if (automation.type == Automation.Type.TRIGGER) automation.trigger?.icon else automation.state?.icon - val title = - if (automation.type == Automation.Type.TRIGGER) automation.trigger?.title else automation.state?.title + val icon = when (automation.type) { + Automation.Type.TRIGGER -> automation.trigger?.icon + Automation.Type.STATE -> automation.state?.icon + Automation.Type.APP -> R.drawable.rounded_apps_24 + } + + val titleString = when (automation.type) { + Automation.Type.TRIGGER -> automation.trigger?.title?.let { stringResource(it) } + Automation.Type.STATE -> automation.state?.title?.let { stringResource(it) } + Automation.Type.APP -> stringResource(R.string.diy_create_app_title) + " (${automation.selectedApps.size})" + } - if (icon != null && title != null) { + if (icon != null && titleString != null) { Surface( color = MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(16.dp), @@ -185,7 +192,7 @@ fun AutomationItem( ) Spacer(modifier = Modifier.width(12.dp)) Text( - text = stringResource(id = title), + text = titleString, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, fontWeight = FontWeight.SemiBold, diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt index 7e4c3a4a..8a945f9a 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/SegmentedPicker.kt @@ -26,6 +26,7 @@ fun SegmentedPicker( selectedItem: T, onItemSelected: (T) -> Unit, labelProvider: (T) -> String, + iconProvider: (@Composable (T) -> Unit)? = null, modifier: Modifier = Modifier, cornerShape: CornerSize = MaterialTheme.shapes.extraSmall.bottomEnd, ) { @@ -55,7 +56,16 @@ fun SegmentedPicker( else -> ButtonGroupDefaults.connectedMiddleButtonShapes() }, ) { - Text(labelProvider(item)) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + if (iconProvider != null) { + iconProvider(item) + androidx.compose.foundation.layout.Spacer(Modifier.padding(end = 8.dp)) + } + Text(labelProvider(item)) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt index a1bf4a11..01080058 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/AppSelectionSheets.kt @@ -221,6 +221,8 @@ fun AppSelectionSheet( AppToggleItem( icon = app.icon, title = app.appName, + packageName = app.packageName, + isSystemApp = app.isSystemApp, isChecked = app.isEnabled, onCheckedChange = { isChecked -> val updatedList = selectedApps.map { diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DeviceEffectsSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DeviceEffectsSettingsSheet.kt new file mode 100644 index 00000000..89c6e332 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DeviceEffectsSettingsSheet.kt @@ -0,0 +1,307 @@ +package com.sameerasw.essentials.ui.components.sheets + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.rememberModalBottomSheetState +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.diy.Action +import com.sameerasw.essentials.utils.ColorUtil +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeviceEffectsSettingsSheet( + initialAction: Action.DeviceEffects, + onDismiss: () -> Unit, + onSave: (Action.DeviceEffects) -> Unit +) { + val view = LocalView.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + var enabled by remember { mutableStateOf(initialAction.enabled) } + var grayscale by remember { mutableStateOf(initialAction.grayscale) } + var suppressAmbient by remember { mutableStateOf(initialAction.suppressAmbient) } + var dimWallpaper by remember { mutableStateOf(initialAction.dimWallpaper) } + var nightMode by remember { mutableStateOf(initialAction.nightMode) } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + dragHandle = null + ) { + Column( + modifier = Modifier + .padding(24.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + text = stringResource(R.string.diy_action_device_effects), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + // Info Card + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + val infoTitle = "Info" // Key for color generation + Box( + modifier = Modifier + .size(40.dp) + .background( + color = ColorUtil.getPastelColorFor(infoTitle), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.rounded_info_24), + contentDescription = null, + tint = ColorUtil.getVibrantColorFor(infoTitle), + modifier = Modifier.size(24.dp) + ) + } + Column { + Text( + text = stringResource(R.string.diy_device_effects_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (android.os.Build.VERSION.SDK_INT < 35) { + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(R.string.diy_device_effects_android_15_warning), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold + ) + } + } + } + } + + // Settings Container + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + // Master Toggle (Enable/Disable) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + HapticUtil.performVirtualKeyHaptic(view) + enabled = !enabled + } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + val toggleTitle = stringResource(R.string.diy_device_effects_enabled) + Box( + modifier = Modifier + .size(40.dp) + .background( + color = if (enabled) ColorUtil.getPastelColorFor(toggleTitle) else MaterialTheme.colorScheme.surfaceContainerHighest, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = if (enabled) R.drawable.rounded_check_circle_24 else R.drawable.rounded_cancel_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = if (enabled) ColorUtil.getVibrantColorFor(toggleTitle) else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Text( + text = stringResource(if (enabled) R.string.diy_device_effects_enabled else R.string.diy_device_effects_disabled), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface + ) + Switch( + checked = enabled, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + enabled = it + } + ) + } + } + + if (enabled) { + // Effect Toggles in a Card + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(0.dp) + ) { + EffectToggleItem( + title = stringResource(R.string.diy_effect_grayscale), + isChecked = grayscale, + onCheckedChange = { grayscale = it } + ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), modifier = Modifier.padding(horizontal = 16.dp)) + EffectToggleItem( + title = stringResource(R.string.diy_effect_suppress_ambient), + isChecked = suppressAmbient, + onCheckedChange = { suppressAmbient = it } + ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), modifier = Modifier.padding(horizontal = 16.dp)) + EffectToggleItem( + title = stringResource(R.string.diy_effect_dim_wallpaper), + isChecked = dimWallpaper, + onCheckedChange = { dimWallpaper = it } + ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), modifier = Modifier.padding(horizontal = 16.dp)) + EffectToggleItem( + title = stringResource(R.string.diy_effect_night_mode), + isChecked = nightMode, + onCheckedChange = { nightMode = it } + ) + } + } + } + } + + // Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onDismiss() + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_close_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_cancel)) + } + + Button( + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onSave(initialAction.copy( + enabled = enabled, + grayscale = grayscale, + suppressAmbient = suppressAmbient, + dimWallpaper = dimWallpaper, + nightMode = nightMode + )) + }, + enabled = android.os.Build.VERSION.SDK_INT >= 35, + modifier = Modifier.weight(1f) + ) { + Icon( + painter = painterResource(id = R.drawable.rounded_check_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.action_save)) + } + } + } + } +} + +@Composable +private fun EffectToggleItem( + title: String, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + val view = LocalView.current + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + HapticUtil.performVirtualKeyHaptic(view) + onCheckedChange(!isChecked) + } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Checkbox( + checked = isChecked, + onCheckedChange = { + HapticUtil.performVirtualKeyHaptic(view) + onCheckedChange(it) + } + ) + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt index 11bba8cd..11725832 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/DimWallpaperSettingsSheet.kt @@ -2,14 +2,19 @@ package com.sameerasw.essentials.ui.components.sheets import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -31,7 +36,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.domain.diy.Action -import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.utils.ColorUtil import com.sameerasw.essentials.utils.HapticUtil @OptIn(ExperimentalMaterial3Api::class) @@ -65,62 +70,110 @@ fun DimWallpaperSettingsSheet( color = MaterialTheme.colorScheme.onSurface ) - // Permissions Info - RoundedCardContainer { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - Icon( - painter = painterResource(R.drawable.rounded_info_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(R.string.diy_dim_wallpaper_desc), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Permission Icons - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Shizuku Icon - Icon( - painter = painterResource(R.drawable.rounded_adb_24), - contentDescription = "Shizuku", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - // Root Icon - Icon( - painter = painterResource(R.drawable.rounded_numbers_24), - contentDescription = "Root", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + // Permissions Info with Pastel Icon + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp) + ) { + Column { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + val infoTitle = "Dim Wallpaper" + Box( + modifier = Modifier + .size(40.dp) + .background( + color = ColorUtil.getPastelColorFor(infoTitle), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(R.drawable.rounded_info_24), + contentDescription = null, + tint = ColorUtil.getVibrantColorFor(infoTitle), + modifier = Modifier.size(24.dp) + ) + } + Text( + text = stringResource(R.string.diy_dim_wallpaper_desc), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Permission Icons + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally) + ) { + // Shizuku Icon + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon( + painter = painterResource(R.drawable.rounded_adb_24), + contentDescription = "Shizuku", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Text("Shizuku", style = MaterialTheme.typography.labelMedium) + } + + // Root Icon + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon( + painter = painterResource(R.drawable.rounded_numbers_24), + contentDescription = "Root", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + Text("Root", style = MaterialTheme.typography.labelMedium) + } + } } } - // Slider - Column { - Text( - text = "Dim Amount: ${(dimAmount * 100).toInt()}%", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Slider( - value = dimAmount, - onValueChange = { - dimAmount = it - HapticUtil.performSliderHaptic(view) - }, - valueRange = 0f..1f - ) + // Slider Card + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright + ), + shape = RoundedCornerShape(24.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Dim Amount", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "${(dimAmount * 100).toInt()}%", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + } + + Slider( + value = dimAmount, + onValueChange = { + dimAmount = it + HapticUtil.performSliderHaptic(view) + }, + valueRange = 0f..1f, + modifier = Modifier.padding(top = 8.dp) + ) + } } // Buttons diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt index 215961b3..4b0674d9 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/NewAutomationSheet.kt @@ -91,6 +91,14 @@ fun NewAutomationSheet( iconRes = R.drawable.rounded_toggle_on_24, onClick = { onOptionSelected(Automation.Type.STATE) } ) + + // App Option + AutomationTypeOption( + title = stringResource(R.string.diy_create_app_title), + description = stringResource(R.string.diy_create_app_desc), + iconRes = R.drawable.rounded_apps_24, + onClick = { onOptionSelected(Automation.Type.APP) } + ) } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt index 041e40db..a06c6a51 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/SetupFeatures.kt @@ -49,6 +49,7 @@ import com.sameerasw.essentials.domain.registry.PermissionRegistry import com.sameerasw.essentials.R import com.sameerasw.essentials.ui.components.cards.FeatureCard import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.FavoriteCarousel import com.sameerasw.essentials.ui.components.sheets.PermissionItem import com.sameerasw.essentials.ui.components.sheets.PermissionsBottomSheet import com.sameerasw.essentials.viewmodels.MainViewModel @@ -79,6 +80,7 @@ fun SetupFeatures( viewModel.isDynamicNightLightEnabled.value viewModel.isScreenLockedSecurityEnabled.value + val pinnedFeatureKeys by viewModel.pinnedFeatureKeys val context = LocalContext.current fun buildMapsPowerSavingPermissionItems(): List { @@ -653,6 +655,28 @@ fun SetupFeatures( ) ) + if (pinnedFeatureKeys.isNotEmpty() && viewModel.searchQuery.value.isEmpty()) { + FavoriteCarousel( + pinnedKeys = pinnedFeatureKeys, + onFeatureClick = { feature -> + if (feature.category == R.string.cat_security && context is FragmentActivity) { + BiometricHelper.showBiometricPrompt( + activity = context, + title = context.getString(R.string.biometric_title_settings_format, context.getString(feature.title)), + subtitle = context.getString(R.string.biometric_subtitle_access_settings), + onSuccess = { feature.onClick(context, viewModel) } + ) + } else { + feature.onClick(context, viewModel) + } + }, + onFeatureLongClick = { feature -> + viewModel.togglePinFeature(feature.id) + }, + modifier = Modifier.padding(top = 16.dp, bottom = 16.dp) + ) + } + val searchQuery = viewModel.searchQuery.value val searchResults = viewModel.searchResults.value val isSearchingViewModel = viewModel.isSearching.value @@ -735,7 +759,11 @@ fun SetupFeatures( showToggle = false, hasMoreSettings = true, isBeta = result.isBeta, // Added isBeta - descriptionOverride = if (result.parentFeature != null) "${result.parentFeature} > ${result.description}" else result.description + descriptionOverride = if (result.parentFeature != null) "${result.parentFeature} > ${result.description}" else result.description, + isPinned = pinnedFeatureKeys.contains(result.featureKey), + onPinToggle = { + viewModel.togglePinFeature(result.featureKey) + } ) } } @@ -797,7 +825,11 @@ fun SetupFeatures( showSheet = true }, description = feature.description, - isBeta = feature.isBeta + isBeta = feature.isBeta, + isPinned = pinnedFeatureKeys.contains(feature.id), + onPinToggle = { + viewModel.togglePinFeature(feature.id) + } ) } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt index ec5a104e..b4be3669 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/ButtonRemapSettingsUI.kt @@ -128,59 +128,25 @@ fun ButtonRemapSettingsUI( if (shellHasPermission) { viewModel.setButtonRemapUseShizuku(true, context) - ContextCompat.startForegroundService( - context, - Intent(context, InputEventListenerService::class.java) - ) } else if (shellIsAvailable && !isRootEnabled) { - // Shizuku logic (still needed for specific permission request if not granted) + // Shizuku logic shizukuHelper.requestPermission { _, grantResult -> if (grantResult == android.content.pm.PackageManager.PERMISSION_GRANTED) { viewModel.setButtonRemapUseShizuku(true, context) - ContextCompat.startForegroundService( - context, - Intent( - context, - InputEventListenerService::class.java - ) - ) } } } else if (isRootEnabled && !shellHasPermission) { - // Root logic - try to run a command to trigger su prompt + // Root logic viewModel.setButtonRemapUseShizuku(true, context) - com.sameerasw.essentials.utils.ShellUtils.runCommand( - context, - "id" - ) - if (com.sameerasw.essentials.utils.ShellUtils.hasPermission( - context - ) - ) { - ContextCompat.startForegroundService( - context, - Intent(context, InputEventListenerService::class.java) - ) - } + com.sameerasw.essentials.utils.ShellUtils.runCommand(context, "id") } else { // Provider not running viewModel.setButtonRemapUseShizuku(true, context) - val toastRes = - if (isRootEnabled) R.string.root_not_available_toast else R.string.shizuku_not_running_toast - android.widget.Toast.makeText( - context, - context.getString(toastRes), - android.widget.Toast.LENGTH_SHORT - ).show() + val toastRes = if (isRootEnabled) R.string.root_not_available_toast else R.string.shizuku_not_running_toast + android.widget.Toast.makeText(context, context.getString(toastRes), android.widget.Toast.LENGTH_SHORT).show() } } else { viewModel.setButtonRemapUseShizuku(false, context) - context.stopService( - Intent( - context, - InputEventListenerService::class.java - ) - ) } }, modifier = Modifier.highlight(highlightSetting == "shizuku_remap") @@ -425,6 +391,18 @@ fun ButtonRemapSettingsUI( onClick = { onActionSelected("AI assistant") }, iconRes = R.drawable.rounded_bubble_chart_24, ) + RemapActionItem( + title = stringResource(R.string.action_toggle_media_volume), + isSelected = currentAction == "Toggle media volume", + onClick = { onActionSelected("Toggle media volume") }, + iconRes = R.drawable.rounded_volume_off_24, + ) + RemapActionItem( + title = stringResource(R.string.action_cycle_sound_modes), + isSelected = currentAction == "Cycle sound modes", + onClick = { onActionSelected("Cycle sound modes") }, + iconRes = R.drawable.rounded_volume_up_24, + ) if (selectedScreenTab == 1) { RemapActionItem( title = stringResource(R.string.action_take_screenshot), diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt index 5aba27f3..bf3a6316 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/QuickSettingsTilesSettingsUI.kt @@ -45,6 +45,8 @@ import com.sameerasw.essentials.services.tiles.NfcTileService import com.sameerasw.essentials.services.tiles.StayAwakeTileService import com.sameerasw.essentials.services.tiles.AdaptiveBrightnessTileService import com.sameerasw.essentials.services.tiles.MapsPowerSavingTileService +import com.sameerasw.essentials.services.tiles.PrivateDnsTileService +import com.sameerasw.essentials.services.tiles.UsbDebuggingTileService import com.sameerasw.essentials.ui.modifiers.highlight data class QSTileInfo( @@ -80,7 +82,9 @@ fun QuickSettingsTilesSettingsUI( QSTileInfo(R.string.tile_stay_awake, R.drawable.rounded_av_timer_24, StayAwakeTileService::class.java), QSTileInfo(R.string.nfc_tile_label, R.drawable.rounded_nfc_24, NfcTileService::class.java), QSTileInfo(R.string.tile_adaptive_brightness, R.drawable.rounded_brightness_auto_24, AdaptiveBrightnessTileService::class.java), - QSTileInfo(R.string.feat_maps_power_saving_title, R.drawable.rounded_navigation_24, MapsPowerSavingTileService::class.java) + QSTileInfo(R.string.feat_maps_power_saving_title, R.drawable.rounded_navigation_24, MapsPowerSavingTileService::class.java), + QSTileInfo(R.string.tile_private_dns, R.drawable.rounded_dns_24, PrivateDnsTileService::class.java), + QSTileInfo(R.string.tile_usb_debugging, R.drawable.rounded_adb_24, UsbDebuggingTileService::class.java) ) Column( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/SnoozeNotificationsSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/SnoozeNotificationsSettingsUI.kt index f6c73582..b4c7db8c 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/SnoozeNotificationsSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/SnoozeNotificationsSettingsUI.kt @@ -38,41 +38,32 @@ fun SnoozeNotificationsSettingsUI( spacing = 2.dp, cornerRadius = 24.dp ) { - // Debugging - IconToggleItem( - iconRes = R.drawable.rounded_adb_24, - title = stringResource(R.string.search_snooze_debug_title), - isChecked = viewModel.isSnoozeDebuggingEnabled.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setSnoozeDebuggingEnabled(checked, context) - }, - modifier = Modifier.highlight(highlightSetting == "snooze_debugging") - ) + viewModel.snoozeChannels.value.forEach { channel -> + IconToggleItem( + iconRes = when (channel.id) { + "DEVELOPER_OPTIONS" -> R.drawable.rounded_adb_24 + "USB_CONNECTION" -> R.drawable.rounded_usb_24 + "BATTERY" -> R.drawable.rounded_charger_24 + else -> R.drawable.rounded_notification_settings_24 + }, + title = channel.name, + isChecked = channel.isBlocked, + onCheckedChange = { checked -> + HapticUtil.performVirtualKeyHaptic(view) + viewModel.setSnoozeChannelBlocked(channel.id, checked, context) + }, + modifier = Modifier.highlight(highlightSetting == channel.id) + ) + } - // File Transfer - IconToggleItem( - iconRes = R.drawable.rounded_usb_24, - title = stringResource(R.string.search_snooze_file_title), - isChecked = viewModel.isSnoozeFileTransferEnabled.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setSnoozeFileTransferEnabled(checked, context) - }, - modifier = Modifier.highlight(highlightSetting == "snooze_file_transfer") - ) - - // Charging - IconToggleItem( - iconRes = R.drawable.rounded_charger_24, - title = stringResource(R.string.search_snooze_charge_title), - isChecked = viewModel.isSnoozeChargingEnabled.value, - onCheckedChange = { checked -> - HapticUtil.performVirtualKeyHaptic(view) - viewModel.setSnoozeChargingEnabled(checked, context) - }, - modifier = Modifier.highlight(highlightSetting == "snooze_charging") - ) + if (viewModel.snoozeChannels.value.isEmpty()) { + androidx.compose.material3.Text( + text = stringResource(R.string.snooze_no_channels_discovered), + modifier = Modifier.padding(16.dp), + style = androidx.compose.material3.MaterialTheme.typography.bodyMedium, + color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkActivity.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkActivity.kt new file mode 100644 index 00000000..be863a09 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkActivity.kt @@ -0,0 +1,82 @@ +package com.sameerasw.essentials.ui.composables.watermark + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.WindowCompat +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel +import com.sameerasw.essentials.R +import com.sameerasw.essentials.data.repository.SettingsRepository +import com.sameerasw.essentials.ui.theme.EssentialsTheme +import com.sameerasw.essentials.viewmodels.WatermarkViewModel + +class WatermarkActivity : ComponentActivity() { + + private var initialUri by mutableStateOf(null) + + private val pickMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (uri != null) { + try { + val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION + contentResolver.takePersistableUriPermission(uri, flag) + } catch (e: Exception) { + // Ignore if not persistable + } + initialUri = uri + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + WindowCompat.setDecorFitsSystemWindows(window, false) + super.onCreate(savedInstanceState) + enableEdgeToEdge() + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + + // Handle Share Intent + if (intent?.action == Intent.ACTION_SEND && intent.type?.startsWith("image/") == true) { + (intent.getParcelableExtra(Intent.EXTRA_STREAM))?.let { + initialUri = it + } + } + + val settingsRepository = SettingsRepository(this) + + setContent { + val isPitchBlackThemeEnabled by settingsRepository.isPitchBlackThemeEnabled.collectAsState(initial = false) + + EssentialsTheme(pitchBlackTheme = isPitchBlackThemeEnabled) { + Surface(color = MaterialTheme.colorScheme.surfaceContainer) { + val context = LocalContext.current + val viewModel: WatermarkViewModel = viewModel( + factory = WatermarkViewModel.provideFactory(context) + ) + + WatermarkScreen( + initialUri = initialUri, + onPickImage = { + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + }, + onBack = { finish() }, + viewModel = viewModel + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkPreview.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkPreview.kt new file mode 100644 index 00000000..c3650a6a --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkPreview.kt @@ -0,0 +1,78 @@ +package com.sameerasw.essentials.ui.composables.watermark + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.sameerasw.essentials.R +import com.sameerasw.essentials.viewmodels.WatermarkUiState +import java.io.File + +@Composable +fun WatermarkPreview( + uiState: WatermarkUiState, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + when (uiState) { + is WatermarkUiState.Idle -> { + Text( + text = stringResource(R.string.watermark_pick_image), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + is WatermarkUiState.Processing -> { + CircularProgressIndicator() + } + is WatermarkUiState.Success -> { + val targetFile = uiState.file + var visibleFile by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(targetFile) } + var targetReady by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) } + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + AsyncImage( + model = visibleFile, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit + ) + + if (targetFile != visibleFile) { + AsyncImage( + model = targetFile, + contentDescription = "Preview", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + onSuccess = { + visibleFile = targetFile + } + ) + } + } + } + is WatermarkUiState.Error -> { + Text( + text = uiState.message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt new file mode 100644 index 00000000..78b41da1 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/watermark/WatermarkScreen.kt @@ -0,0 +1,1022 @@ +package com.sameerasw.essentials.ui.composables.watermark + +import android.graphics.drawable.Icon +import android.net.Uri +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButtonDefaults.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.watermark.ColorMode +import com.sameerasw.essentials.domain.watermark.WatermarkStyle +import com.sameerasw.essentials.ui.components.ReusableTopAppBar +import com.sameerasw.essentials.ui.components.cards.IconToggleItem +import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer +import com.sameerasw.essentials.ui.components.pickers.SegmentedPicker +import com.sameerasw.essentials.ui.components.sliders.ConfigSliderItem +import com.sameerasw.essentials.utils.HapticUtil.performSliderHaptic +import com.sameerasw.essentials.utils.HapticUtil.performUIHaptic +import com.sameerasw.essentials.viewmodels.WatermarkUiState +import com.sameerasw.essentials.viewmodels.WatermarkViewModel + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun WatermarkScreen( + initialUri: Uri?, + onPickImage: () -> Unit, + onBack: () -> Unit, + viewModel: WatermarkViewModel +) { + val context = LocalContext.current + val view = androidx.compose.ui.platform.LocalView.current // For haptics + var showExifSheet by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) } + var showCustomTextSheet by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) } + + val options by viewModel.options.collectAsState() + val previewState by viewModel.previewUiState.collectAsState() + val saveState by viewModel.uiState.collectAsState() + + LaunchedEffect(initialUri) { + if (initialUri != null) { + viewModel.loadPreview(initialUri) + } + } + + LaunchedEffect(saveState) { + when (saveState) { + is WatermarkUiState.Success -> { + Toast.makeText(context, R.string.watermark_save_success, Toast.LENGTH_SHORT).show() + viewModel.resetState() + } + is WatermarkUiState.Error -> { + Toast.makeText(context, (saveState as WatermarkUiState.Error).message, Toast.LENGTH_SHORT).show() + viewModel.resetState() + } + else -> {} + } + } + + Scaffold( + contentWindowInsets = androidx.compose.foundation.layout.WindowInsets(0, 0, 0, 0), + topBar = { + ReusableTopAppBar( + title = R.string.feat_watermark_title, + hasBack = true, + onBackClick = { + performUIHaptic(view) + onBack() + }, + isSmall = true, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + actions = { + val pickImageButton = @Composable { + // Pick Image Button + if (initialUri == null) { + // Primary when no image + androidx.compose.material3.Button(onClick = { + performUIHaptic(view) + onPickImage() + }) { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.rounded_add_photo_alternate_24), + contentDescription = stringResource(R.string.watermark_pick_image), + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.watermark_pick_image)) + } + } else { + // Secondary when image is there + androidx.compose.material3.OutlinedButton(onClick = { + performUIHaptic(view) + onPickImage() + }) { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.rounded_add_photo_alternate_24), + contentDescription = stringResource(R.string.watermark_pick_image), + modifier = Modifier.size(18.dp) + ) + } + } + } + + pickImageButton() + + // Save/Share Menu Button + if (initialUri != null) { + Spacer(Modifier.size(8.dp)) + var showMenu by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) } + + Box { + // Save Button (Primary) + androidx.compose.material3.Button(onClick = { + performUIHaptic(view) + showMenu = true + }) { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.rounded_save_24), + contentDescription = stringResource(R.string.action_save), + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.size(8.dp)) + Text(stringResource(R.string.action_save)) + } + + com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + // Share Option + com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_share)) }, + leadingIcon = { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.rounded_share_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + }, + onClick = { + showMenu = false + initialUri.let { uri -> + viewModel.shareImage(uri) { sharedUri -> + val shareIntent = android.content.Intent(android.content.Intent.ACTION_SEND).apply { + type = "image/jpeg" + putExtra(android.content.Intent.EXTRA_STREAM, sharedUri) + addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(android.content.Intent.createChooser(shareIntent, context.getString(R.string.action_share))) + } + } + }, + enabled = saveState !is WatermarkUiState.Processing + ) + + // Save Option + com.sameerasw.essentials.ui.components.menus.SegmentedDropdownMenuItem( + text = { Text(stringResource(R.string.action_save)) }, + leadingIcon = { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.rounded_save_24), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + }, + onClick = { + showMenu = false + viewModel.saveImage(initialUri) + }, + enabled = saveState !is WatermarkUiState.Processing + ) + } + } + } + } + ) + }, + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) { padding -> + val density = androidx.compose.ui.platform.LocalDensity.current + val configuration = androidx.compose.ui.platform.LocalConfiguration.current + val screenHeightDp = configuration.screenHeightDp.dp + + val maxPreviewHeightDp = screenHeightDp * 0.6f + val minPreviewHeightDp = screenHeightDp * 0.3f + + val maxPx = with(density) { maxPreviewHeightDp.toPx() } + val minPx = with(density) { minPreviewHeightDp.toPx() } + + var previewHeightPx by androidx.compose.runtime.remember { androidx.compose.runtime.mutableFloatStateOf(maxPx) } + + val nestedScrollConnection = androidx.compose.runtime.remember { + object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection { + override fun onPreScroll(available: androidx.compose.ui.geometry.Offset, source: androidx.compose.ui.input.nestedscroll.NestedScrollSource): androidx.compose.ui.geometry.Offset { + val delta = available.y + // Swiping Up (delta < 0): Collapse + if (delta < 0) { + val newHeight = (previewHeightPx + delta).coerceIn(minPx, maxPx) + val consumed = newHeight - previewHeightPx + if (kotlin.math.abs(consumed) > 0.5f) { + performSliderHaptic(view) + } + previewHeightPx = newHeight + return androidx.compose.ui.geometry.Offset(0f, consumed) + } + return androidx.compose.ui.geometry.Offset.Zero + } + + override fun onPostScroll(consumed: androidx.compose.ui.geometry.Offset, available: androidx.compose.ui.geometry.Offset, source: androidx.compose.ui.input.nestedscroll.NestedScrollSource): androidx.compose.ui.geometry.Offset { + val delta = available.y + // Swiping Down (delta > 0): Expand + if (delta > 0) { + val newHeight = (previewHeightPx + delta).coerceIn(minPx, maxPx) + val consumedY = newHeight - previewHeightPx + if (kotlin.math.abs(consumedY) > 0.5f) { + performSliderHaptic(view) + } + previewHeightPx = newHeight + return androidx.compose.ui.geometry.Offset(0f, consumedY) + } + return androidx.compose.ui.geometry.Offset.Zero + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .nestedScroll(nestedScrollConnection) + ) { + // Preview Area (Variable Height) + Box( + modifier = Modifier + .fillMaxWidth() + .height(with(density) { previewHeightPx.toDp() }) + .padding(16.dp) + .clip(if (initialUri == null) RoundedCornerShape(24.dp) else androidx.compose.ui.graphics.RectangleShape) + .background(if (initialUri == null) MaterialTheme.colorScheme.surfaceContainerHigh else androidx.compose.ui.graphics.Color.Transparent) + .clickable { + performUIHaptic(view) + if (initialUri == null) { + onPickImage() + } + } + .padding(if (initialUri == null) 32.dp else 0.dp), + contentAlignment = Alignment.Center + ) { + if (initialUri == null) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.rounded_add_photo_alternate_24), + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(Modifier.size(8.dp)) + Text( + stringResource(R.string.watermark_pick_image), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + + + // Implementing the "Last Success Persist" logic here locally + val current = previewState + var lastSuccess by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(null) } + + if (current is WatermarkUiState.Success) { + lastSuccess = current + } + + val showBlur = current is WatermarkUiState.Processing + + val blurRadius by androidx.compose.animation.core.animateDpAsState( + targetValue = if (showBlur) 16.dp else 0.dp, + label = "blur" + ) + + val alpha by androidx.compose.animation.core.animateFloatAsState( + targetValue = if (showBlur) 0.6f else 1f, + label = "alpha" + ) + + Box(contentAlignment = Alignment.Center) { + // Underlying Image + if (lastSuccess != null) { + Box( + modifier = Modifier + .blur(blurRadius) + .alpha(alpha) + ) { + WatermarkPreview(uiState = lastSuccess!!) + } + } else { + // First load? + if (current is WatermarkUiState.Processing) { + // First load, show nothing or placeholder maybe + } else { + WatermarkPreview(uiState = current) + } + } + + // Overlay + androidx.compose.animation.AnimatedVisibility( + visible = showBlur, + enter = androidx.compose.animation.fadeIn(), + exit = androidx.compose.animation.fadeOut() + ) { + LoadingIndicator() + } + } + } + } + + // Allow this part to take remaining space and scroll + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(androidx.compose.foundation.rememberScrollState()) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Controls Area + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + // Style Picker + SegmentedPicker( + items = WatermarkStyle.entries, + selectedItem = options.style, + onItemSelected = { + performUIHaptic(view) + viewModel.setStyle(it) + }, + labelProvider = { style -> + when (style) { + WatermarkStyle.OVERLAY -> context.getString(R.string.watermark_style_overlay) + WatermarkStyle.FRAME -> context.getString(R.string.watermark_style_frame) + } + }, + iconProvider = { style -> + val iconRes = when (style) { + WatermarkStyle.OVERLAY -> R.drawable.rounded_magnify_fullscreen_24 + WatermarkStyle.FRAME -> R.drawable.rounded_window_open_24 + } + + Icon( + painter = androidx.compose.ui.res.painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + }, + modifier = Modifier.fillMaxWidth() + ) + + // Style-specific options + if (options.style == WatermarkStyle.FRAME) { + com.sameerasw.essentials.ui.components.cards.IconToggleItem( + iconRes = R.drawable.rounded_top_panel_close_24, + title = stringResource(R.string.watermark_move_to_top), + isChecked = options.moveToTop, + onCheckedChange = { viewModel.setMoveToTop(it) } + ) + } else { + com.sameerasw.essentials.ui.components.cards.IconToggleItem( + iconRes = R.drawable.rounded_position_bottom_left_24, + title = stringResource(R.string.watermark_left_align), + isChecked = options.leftAlignOverlay, + onCheckedChange = { viewModel.setLeftAlign(it) } + ) + } + + // Show Brand Toggle + IconToggleItem( + iconRes = R.drawable.rounded_mobile_text_2_24, + title = stringResource(R.string.watermark_show_brand), + isChecked = options.showDeviceBrand, + onCheckedChange = { viewModel.setShowBrand(it) } + ) + + // Custom Text Entry + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .heightIn(min = 56.dp) + .clickable { + performUIHaptic(view) + showCustomTextSheet = true + } + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Spacer(modifier = Modifier.size(2.dp)) + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_edit_note_24), + contentDescription = stringResource(R.string.watermark_custom_text), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.size(2.dp)) + + Text( + text = stringResource(R.string.watermark_custom_text), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f) + ) + + if (options.showCustomText && options.customText.isNotEmpty()) { + Text( + text = options.customText, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 100.dp) + ) + } + + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_chevron_right_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Show EXIF Settings (Custom Row with Chevron) + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .heightIn(min = 56.dp) // Match standard item height + .clickable { + performUIHaptic(view) + showExifSheet = true + } + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Spacer(modifier = Modifier.size(2.dp)) + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_image_search_24), + contentDescription = stringResource(R.string.watermark_show_exif), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.size(2.dp)) + + Text( + text = stringResource(R.string.watermark_show_exif), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f) + ) + + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_chevron_right_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + + + // Spacing Slider + var paddingValue by androidx.compose.runtime.remember(options.padding) { androidx.compose.runtime.mutableFloatStateOf(options.padding.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.watermark_spacing), + value = paddingValue, + onValueChange = { + paddingValue = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setPadding(paddingValue.toInt()) }, + valueRange = 0f..100f, + increment = 5f, + valueFormatter = { "${it.toInt()}%" } + ) + } + + + // Font Size Section + val showFontSection = options.showDeviceBrand || options.showExif || options.showCustomText + + if (showFontSection) { + Text( + text = stringResource(R.string.watermark_font_options), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + if (options.showDeviceBrand) { + var brandSize by androidx.compose.runtime.remember(options.brandTextSize) { androidx.compose.runtime.mutableFloatStateOf(options.brandTextSize.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.watermark_text_size_brand), + value = brandSize, + onValueChange = { + brandSize = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setBrandTextSize(brandSize.toInt()) }, + valueRange = 0f..100f, + increment = 5f, + valueFormatter = { "${it.toInt()}%" } + ) + } + + if (options.showExif) { + var dataSize by androidx.compose.runtime.remember(options.dataTextSize) { androidx.compose.runtime.mutableFloatStateOf(options.dataTextSize.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.watermark_text_size_data), + value = dataSize, + onValueChange = { + dataSize = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setDataTextSize(dataSize.toInt()) }, + valueRange = 0f..100f, + increment = 5f, + valueFormatter = { "${it.toInt()}%" } + ) + } + + if (options.showCustomText) { + var customSize by androidx.compose.runtime.remember(options.customTextSize) { androidx.compose.runtime.mutableFloatStateOf(options.customTextSize.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.watermark_text_size_custom), + value = customSize, + onValueChange = { + customSize = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setCustomTextSize(customSize.toInt()) }, + valueRange = 0f..100f, + increment = 5f, + valueFormatter = { "${it.toInt()}%" } + ) + } + } + } + + + // Logo Section + Text( + text = stringResource(R.string.watermark_logo_section), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + val logoResId by viewModel.logoResId.collectAsState() + val showLogo by viewModel.showLogo.collectAsState() + + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + IconToggleItem( + iconRes = R.drawable.rounded_image_24, + title = stringResource(R.string.watermark_logo_show), + isChecked = showLogo, + onCheckedChange = { checked -> viewModel.setShowLogo(checked) } + ) + + if (showLogo) { + LogoCarouselPicker( + selectedResId = logoResId, + onLogoSelected = { resId -> viewModel.setLogoResId(resId) }, + modifier = Modifier + .fillMaxWidth() + ) + + var logoSizeValue by androidx.compose.runtime.remember(options.logoSize) { androidx.compose.runtime.mutableFloatStateOf(options.logoSize.toFloat()) } + ConfigSliderItem( + title = stringResource(R.string.watermark_logo_size), + value = logoSizeValue, + onValueChange = { + logoSizeValue = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setLogoSize(logoSizeValue.toInt()) }, + valueRange = 1f..100f, + increment = 1f, + valueFormatter = { "${it.toInt()}%" } + ) + } + } + + // Border Section + Text( + text = "Border", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + var strokeValue by androidx.compose.runtime.remember(options.borderStroke) { androidx.compose.runtime.mutableFloatStateOf(options.borderStroke.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.watermark_border_width), + value = strokeValue, + onValueChange = { + strokeValue = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setBorderStroke(strokeValue.toInt()) }, + valueRange = 0f..100f, + increment = 5f, + valueFormatter = { "${it.toInt()}%" } + ) + + var cornerValue by androidx.compose.runtime.remember(options.borderCorner) { androidx.compose.runtime.mutableFloatStateOf(options.borderCorner.toFloat()) } + + ConfigSliderItem( + title = stringResource(R.string.watermark_border_corners), + value = cornerValue, + onValueChange = { + cornerValue = it + performSliderHaptic(view) + }, + onValueChangeFinished = { viewModel.setBorderCorner(cornerValue.toInt()) }, + valueRange = 0f..100f, + increment = 5f, + valueFormatter = { "${it.toInt()}%" } + ) + } + + // Color Section + Text( + text = stringResource(R.string.watermark_color_section), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + RoundedCardContainer( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceBright) + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ColorModeOption( + mode = ColorMode.LIGHT, + isSelected = options.colorMode == ColorMode.LIGHT, + onClick = { viewModel.setColorMode(ColorMode.LIGHT) } + ) + ColorModeOption( + mode = ColorMode.DARK, + isSelected = options.colorMode == ColorMode.DARK, + onClick = { viewModel.setColorMode(ColorMode.DARK) } + ) + ColorModeOption( + mode = ColorMode.ACCENT_LIGHT, + accentColor = options.accentColor, + isSelected = options.colorMode == ColorMode.ACCENT_LIGHT, + onClick = { viewModel.setColorMode(ColorMode.ACCENT_LIGHT) } + ) + ColorModeOption( + mode = ColorMode.ACCENT_DARK, + accentColor = options.accentColor, + isSelected = options.colorMode == ColorMode.ACCENT_DARK, + onClick = { viewModel.setColorMode(ColorMode.ACCENT_DARK) } + ) + } + } + + // Bottom spacing for scrolling + Spacer(Modifier.height(24.dp)) + } + } + } + + if (showExifSheet) { + val view = androidx.compose.ui.platform.LocalView.current + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = { showExifSheet = false }, + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.watermark_exif_settings), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 8.dp) + ) + + RoundedCardContainer { + // Master Toggle + com.sameerasw.essentials.ui.components.cards.IconToggleItem( + iconRes = R.drawable.rounded_image_search_24, + title = stringResource(R.string.watermark_show_exif), + isChecked = options.showExif, + onCheckedChange = { viewModel.setShowExif(it) } + ) + } + + if (options.showExif) { + RoundedCardContainer { + // Granular toggles + // Helper for granular + val updateExif = { focal: Boolean, aperture: Boolean, iso: Boolean, shutter: Boolean, date: Boolean -> + viewModel.setExifSettings(focal, aperture, iso, shutter, date) + } + + IconToggleItem( + iconRes = R.drawable.rounded_control_camera_24, + title = stringResource(R.string.watermark_exif_focal_length), + isChecked = options.showFocalLength, + onCheckedChange = { updateExif(it, options.showAperture, options.showIso, options.showShutterSpeed, options.showDate) } + ) + + IconToggleItem( + iconRes = R.drawable.rounded_camera_24, + title = stringResource(R.string.watermark_exif_aperture), + isChecked = options.showAperture, + onCheckedChange = { updateExif(options.showFocalLength, it, options.showIso, options.showShutterSpeed, options.showDate) } + ) + + IconToggleItem( + iconRes = R.drawable.rounded_grain_24, + title = stringResource(R.string.watermark_exif_iso), + isChecked = options.showIso, + onCheckedChange = { updateExif(options.showFocalLength, options.showAperture, it, options.showShutterSpeed, options.showDate) } + ) + + IconToggleItem( + iconRes = R.drawable.rounded_shutter_speed_24, + title = stringResource(R.string.watermark_exif_shutter_speed), + isChecked = options.showShutterSpeed, + onCheckedChange = { updateExif(options.showFocalLength, options.showAperture, options.showIso, it, options.showDate) } + ) + + IconToggleItem( + iconRes = R.drawable.rounded_date_range_24, + title = stringResource(R.string.watermark_exif_date), + isChecked = options.showDate, + onCheckedChange = { updateExif(options.showFocalLength, options.showAperture, options.showIso, options.showShutterSpeed, it) } + ) + } + } + } + } + } + + if (showCustomTextSheet) { + val view = androidx.compose.ui.platform.LocalView.current + + // Local state for draft editing + var isEnabled by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(options.showCustomText) } + var draftText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(options.customText) } + + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = { showCustomTextSheet = false }, + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.watermark_custom_text_settings), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 8.dp) + ) + + RoundedCardContainer { + // Master Toggle + com.sameerasw.essentials.ui.components.cards.IconToggleItem( + iconRes = R.drawable.rounded_edit_note_24, + title = stringResource(R.string.watermark_custom_text), + isChecked = isEnabled, + onCheckedChange = { isEnabled = it } + ) + } + + if (isEnabled) { + RoundedCardContainer { + Column(modifier = Modifier.padding(16.dp)) { + // Text Input + androidx.compose.material3.OutlinedTextField( + value = draftText, + onValueChange = { draftText = it }, + label = { Text(stringResource(R.string.watermark_custom_text)) }, + placeholder = { Text(stringResource(R.string.watermark_custom_text_hint)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + + // Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = { + performUIHaptic(view) + showCustomTextSheet = false + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.action_cancel)) + } + + Button( + onClick = { + performUIHaptic(view) + viewModel.setCustomTextSettings(isEnabled, draftText, options.customTextSize) // preserve size + showCustomTextSheet = false + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.action_save_changes)) + } + } + } + } + } + } +} + +@Composable +private fun ColorModeOption( + mode: ColorMode, + isSelected: Boolean, + onClick: () -> Unit, + accentColor: Int? = null +) { + val view = androidx.compose.ui.platform.LocalView.current + val color = when (mode) { + ColorMode.LIGHT -> androidx.compose.ui.graphics.Color.White + ColorMode.DARK -> androidx.compose.ui.graphics.Color.Black + ColorMode.ACCENT_LIGHT, ColorMode.ACCENT_DARK -> { + // Derive a preview color for the circle + val base = accentColor ?: android.graphics.Color.GRAY + val hsl = FloatArray(3) + androidx.core.graphics.ColorUtils.colorToHSL(base, hsl) + if (mode == ColorMode.ACCENT_LIGHT) { + hsl[2] = 0.8f // Light shade + } else { + hsl[2] = 0.2f // Dark shade + } + androidx.compose.ui.graphics.Color(androidx.core.graphics.ColorUtils.HSLToColor(hsl)) + } + } + + val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant + val borderWidth = if (isSelected) 3.dp else 1.dp + + Box( + modifier = Modifier + .size(48.dp) + .clip(androidx.compose.foundation.shape.CircleShape) + .background(color) + .border( + width = borderWidth, + color = borderColor, + shape = androidx.compose.foundation.shape.CircleShape + ) + .clickable { + performUIHaptic(view) + onClick() + }, + contentAlignment = Alignment.Center + ) { + if (mode == ColorMode.ACCENT_LIGHT || mode == ColorMode.ACCENT_DARK) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = R.drawable.rounded_image_24), + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = if (mode == ColorMode.ACCENT_LIGHT) androidx.compose.ui.graphics.Color.Black else androidx.compose.ui.graphics.Color.White + ) + } + } +} + +@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) +@Composable +private fun LogoCarouselPicker( + selectedResId: Int?, + onLogoSelected: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val logos = listOf( + R.drawable.apple, + R.drawable.cmf, + R.drawable.google, + R.drawable.moto, + R.drawable.nothing, + R.drawable.oppo, + R.drawable.samsung, + R.drawable.sony, + R.drawable.vivo, + R.drawable.xiaomi + ) + + val carouselState = androidx.compose.material3.carousel.rememberCarouselState { logos.size } + val view = androidx.compose.ui.platform.LocalView.current + + HorizontalMultiBrowseCarousel( + state = carouselState, + preferredItemWidth = 80.dp, + minSmallItemWidth = 5.dp, + maxSmallItemWidth = 200.dp, + itemSpacing = 2.dp, + modifier = modifier + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceBright) + .height(84.dp), + contentPadding = PaddingValues(4.dp) + ) { index -> + val resId = logos[index] + val isSelected = selectedResId == resId + val containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHigh + val contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 1.dp) + .clip(RoundedCornerShape(8.dp)) + .background(containerColor) + .clickable { + performUIHaptic(view) + onLogoSelected(resId) + }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = androidx.compose.ui.res.painterResource(id = resId), + contentDescription = null, + modifier = Modifier.size(36.dp), + tint = contentColor + ) + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt index 83edafc4..e989a93a 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt @@ -26,20 +26,8 @@ object PermissionUtils { } fun canWriteSecureSettings(context: Context): Boolean { - return try { - val currentValue = Settings.Secure.getString( - context.contentResolver, - "icon_blacklist" - ) - Settings.Secure.putString( - context.contentResolver, - "icon_blacklist", - currentValue ?: "" - ) - true - } catch (e: Exception) { - false - } + return context.checkSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) == + android.content.pm.PackageManager.PERMISSION_GRANTED } fun hasNotificationListenerPermission(context: Context): Boolean { diff --git a/app/src/main/java/com/sameerasw/essentials/utils/RootUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/RootUtils.kt index 5ea5bd79..94c13c3d 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/RootUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/RootUtils.kt @@ -7,10 +7,7 @@ object RootUtils { fun isRootAvailable(): Boolean { return try { - val process = Runtime.getRuntime().exec("su") - val os = DataOutputStream(process.outputStream) - os.writeBytes("exit\n") - os.flush() + val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", "which su")) val exitCode = process.waitFor() exitCode == 0 } catch (e: Exception) { diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index b6a05e12..8c8972ac 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -52,6 +52,7 @@ class MainViewModel : ViewModel() { val isCaffeinateActive = mutableStateOf(false) val isShizukuPermissionGranted = mutableStateOf(false) val isShizukuAvailable = mutableStateOf(false) + val pinnedFeatureKeys = mutableStateOf>(emptyList()) val isNotificationListenerEnabled = mutableStateOf(false) val isMapsPowerSavingEnabled = mutableStateOf(false) val isNotificationLightingEnabled = mutableStateOf(false) @@ -72,9 +73,7 @@ class MainViewModel : ViewModel() { val volumeDownActionOn = mutableStateOf("None") val remapHapticType = mutableStateOf(HapticFeedbackType.DOUBLE) val isDynamicNightLightEnabled = mutableStateOf(false) - val isSnoozeDebuggingEnabled = mutableStateOf(false) - val isSnoozeFileTransferEnabled = mutableStateOf(false) - val isSnoozeChargingEnabled = mutableStateOf(false) + val snoozeChannels = mutableStateOf>(emptyList()) val isFlashlightAlwaysTurnOffEnabled = mutableStateOf(false) val isFlashlightFadeEnabled = mutableStateOf(false) val isFlashlightAdjustEnabled = mutableStateOf(false) @@ -159,6 +158,7 @@ class MainViewModel : ViewModel() { private var lastUpdateCheckTime: Long = 0 private lateinit var settingsRepository: SettingsRepository private lateinit var updateRepository: UpdateRepository + private var appContext: Context? = null private val preferenceChangeListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> // We still use this listener for now, attached via Repository @@ -203,10 +203,17 @@ class MainViewModel : ViewModel() { SettingsRepository.KEY_MAC_BATTERY_LAST_UPDATED -> macBatteryLastUpdated.value = settingsRepository.getLong(key, 0L) SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED -> isMacConnected.value = settingsRepository.getBoolean(key, false) SettingsRepository.KEY_BATTERY_WIDGET_MAX_DEVICES -> batteryWidgetMaxDevices.intValue = settingsRepository.getInt(key, 8) + SettingsRepository.KEY_SNOOZE_DISCOVERED_CHANNELS, SettingsRepository.KEY_SNOOZE_BLOCKED_CHANNELS -> { + appContext?.let { loadSnoozeChannels(it) } + } + SettingsRepository.KEY_PINNED_FEATURES -> { + pinnedFeatureKeys.value = settingsRepository.getPinnedFeatures() + } } } fun check(context: Context) { + appContext = context.applicationContext settingsRepository = SettingsRepository(context) updateRepository = UpdateRepository() @@ -235,10 +242,6 @@ class MainViewModel : ViewModel() { isBluetoothPermissionGranted.value = PermissionUtils.hasBluetoothPermission(context) - isRootAvailable.value = com.sameerasw.essentials.utils.RootUtils.isRootAvailable() - isRootPermissionGranted.value = com.sameerasw.essentials.utils.RootUtils.isRootPermissionGranted() - - settingsRepository.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) settingsRepository.registerOnSharedPreferenceChangeListener(preferenceChangeListener) isWidgetEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_WIDGET_ENABLED) @@ -259,6 +262,15 @@ class MainViewModel : ViewModel() { notificationLightingIndicatorX.value = settingsRepository.getFloat(SettingsRepository.KEY_EDGE_LIGHTING_INDICATOR_X, 50f) notificationLightingIndicatorY.value = settingsRepository.getFloat(SettingsRepository.KEY_EDGE_LIGHTING_INDICATOR_Y, 2f) isRootEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_USE_ROOT) + + if (isRootEnabled.value) { + isRootAvailable.value = com.sameerasw.essentials.utils.RootUtils.isRootAvailable() + isRootPermissionGranted.value = com.sameerasw.essentials.utils.RootUtils.isRootPermissionGranted() + } else { + isRootAvailable.value = false + isRootPermissionGranted.value = false + } + notificationLightingIndicatorScale.value = settingsRepository.getFloat(SettingsRepository.KEY_EDGE_LIGHTING_INDICATOR_SCALE, 1.0f) notificationLightingGlowSides.value = settingsRepository.getNotificationLightingGlowSides() @@ -299,9 +311,7 @@ class MainViewModel : ViewModel() { } isDynamicNightLightEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_DYNAMIC_NIGHT_LIGHT_ENABLED) - isSnoozeDebuggingEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_SNOOZE_DEBUGGING_ENABLED) - isSnoozeFileTransferEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_SNOOZE_FILE_TRANSFER_ENABLED) - isSnoozeChargingEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_SNOOZE_CHARGING_ENABLED) + loadSnoozeChannels(context) isFlashlightAlwaysTurnOffEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_FLASHLIGHT_ALWAYS_TURN_OFF_ENABLED) isFlashlightFadeEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_FLASHLIGHT_FADE_ENABLED) isFlashlightAdjustEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_FLASHLIGHT_ADJUST_INTENSITY_ENABLED) @@ -347,6 +357,7 @@ class MainViewModel : ViewModel() { freezeAutoExcludedApps.value = settingsRepository.getFreezeAutoExcludedApps() isDeveloperModeEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_DEVELOPER_MODE_ENABLED) isPreReleaseCheckEnabled.value = settingsRepository.getBoolean(SettingsRepository.KEY_CHECK_PRE_RELEASES_ENABLED) + pinnedFeatureKeys.value = settingsRepository.getPinnedFeatures() } fun onSearchQueryChanged(query: String, context: Context) { @@ -362,6 +373,17 @@ class MainViewModel : ViewModel() { isSearching.value = false } + fun togglePinFeature(featureId: String) { + val current = pinnedFeatureKeys.value.toMutableList() + if (current.contains(featureId)) { + current.remove(featureId) + } else { + current.add(featureId) // Append at the end to keep order + } + pinnedFeatureKeys.value = current + settingsRepository.savePinnedFeatures(current) + } + fun setAutoUpdateEnabled(enabled: Boolean, context: Context) { isAutoUpdateEnabled.value = enabled settingsRepository.putBoolean(SettingsRepository.KEY_AUTO_UPDATE_ENABLED, enabled) @@ -1138,19 +1160,26 @@ class MainViewModel : ViewModel() { } } - fun setSnoozeDebuggingEnabled(enabled: Boolean, context: Context) { - isSnoozeDebuggingEnabled.value = enabled - settingsRepository.putBoolean(SettingsRepository.KEY_SNOOZE_DEBUGGING_ENABLED, enabled) - } - - fun setSnoozeFileTransferEnabled(enabled: Boolean, context: Context) { - isSnoozeFileTransferEnabled.value = enabled - settingsRepository.putBoolean(SettingsRepository.KEY_SNOOZE_FILE_TRANSFER_ENABLED, enabled) + fun loadSnoozeChannels(context: Context) { + val discovered = settingsRepository.loadSnoozeDiscoveredChannels() + val blocked = settingsRepository.loadSnoozeBlockedChannels() + + val channels = discovered.map { channel -> + channel.copy(isBlocked = blocked.contains(channel.id)) + } + + snoozeChannels.value = channels.distinctBy { it.id }.sortedBy { it.name } } - fun setSnoozeChargingEnabled(enabled: Boolean, context: Context) { - isSnoozeChargingEnabled.value = enabled - settingsRepository.putBoolean(SettingsRepository.KEY_SNOOZE_CHARGING_ENABLED, enabled) + fun setSnoozeChannelBlocked(channelId: String, blocked: Boolean, context: Context) { + val currentBlocked = settingsRepository.loadSnoozeBlockedChannels().toMutableSet() + if (blocked) { + currentBlocked.add(channelId) + } else { + currentBlocked.remove(channelId) + } + settingsRepository.saveSnoozeBlockedChannels(currentBlocked) + loadSnoozeChannels(context) } fun setFlashlightAlwaysTurnOffEnabled(enabled: Boolean, context: Context) { diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt index 5d1efe99..c2e6a01a 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt @@ -360,13 +360,7 @@ class StatusBarIconViewModel : ViewModel() { } private fun canWriteSecureSettings(context: Context): Boolean { - return try { - val currentValue = Settings.Secure.getString(context.contentResolver, ICON_BLACKLIST_SETTING) - Settings.Secure.putString(context.contentResolver, ICON_BLACKLIST_SETTING, currentValue ?: "") - true - } catch (@Suppress("UNUSED_PARAMETER") e: Exception) { - false - } + return com.sameerasw.essentials.utils.PermissionUtils.canWriteSecureSettings(context) } fun getAdbCommand(): String { diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/WatermarkViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatermarkViewModel.kt new file mode 100644 index 00000000..866b03d7 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/WatermarkViewModel.kt @@ -0,0 +1,466 @@ +package com.sameerasw.essentials.viewmodels + +import android.content.Context +import android.net.Uri +import android.widget.Toast +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.sameerasw.essentials.R +import com.sameerasw.essentials.domain.watermark.ColorMode +import com.sameerasw.essentials.domain.watermark.ExifData +import com.sameerasw.essentials.domain.watermark.MetadataProvider +import com.sameerasw.essentials.domain.watermark.WatermarkEngine +import com.sameerasw.essentials.domain.watermark.WatermarkOptions +import com.sameerasw.essentials.domain.watermark.WatermarkRepository +import com.sameerasw.essentials.domain.watermark.WatermarkStyle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.io.File + +sealed class WatermarkUiState { + data object Idle : WatermarkUiState() + data object Processing : WatermarkUiState() + data class Success(val file: File) : WatermarkUiState() + data class Error(val message: String) : WatermarkUiState() +} + +class WatermarkViewModel( + private val watermarkEngine: WatermarkEngine, + private val watermarkRepository: WatermarkRepository, + private val metadataProvider: MetadataProvider, + private val context: Context +) : ViewModel() { + + companion object { + fun provideFactory(context: Context): ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + val appContext = context.applicationContext + val metadataProvider = MetadataProvider(appContext) + val engine = WatermarkEngine(appContext, metadataProvider) + val repository = WatermarkRepository(appContext) + return WatermarkViewModel(engine, repository, metadataProvider, appContext) as T + } + } + } + + private val _uiState = MutableStateFlow(WatermarkUiState.Idle) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _previewUiState = MutableStateFlow(WatermarkUiState.Idle) + val previewUiState: StateFlow = _previewUiState.asStateFlow() + + private val _options = MutableStateFlow(WatermarkOptions()) + val options: StateFlow = _options.asStateFlow() + + // Transient logo state (not persisted, depends on image EXIF) + private val _logoResId = MutableStateFlow(null) + val logoResId: StateFlow = _logoResId.asStateFlow() + + private val _showLogo = MutableStateFlow(false) + val showLogo: StateFlow = _showLogo.asStateFlow() + + private var previewSourceBitmap: android.graphics.Bitmap? = null + private var currentUri: Uri? = null + + init { + viewModelScope.launch { + watermarkRepository.watermarkOptions.collectLatest { savedOptions -> + _options.value = savedOptions + updatePreview() + } + } + } + + private fun detectOemLogo(exif: ExifData): Int? { + val make = exif.make?.lowercase() ?: "" + val model = exif.model?.lowercase() ?: "" + + return when { + make.contains("apple") || model.contains("iphone") -> R.drawable.apple + make.contains("google") || model.contains("pixel") -> R.drawable.google + make.contains("samsung") -> R.drawable.samsung + make.contains("xiaomi") || make.contains("redmi") || make.contains("poco") -> R.drawable.xiaomi + make.contains("oppo") -> R.drawable.oppo + make.contains("vivo") -> R.drawable.vivo + make.contains("sony") -> R.drawable.sony + make.contains("nothing") -> R.drawable.nothing + make.contains("cmf") -> R.drawable.cmf + make.contains("motorola") || make.contains("moto") -> R.drawable.moto + else -> null + } + } + + fun loadPreview(uri: Uri) { + currentUri = uri + viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) { + try { + // Decode scaled version + val inputStream = context.contentResolver.openInputStream(uri) + val options = android.graphics.BitmapFactory.Options() + options.inJustDecodeBounds = true + android.graphics.BitmapFactory.decodeStream(inputStream, null, options) + inputStream?.close() + + // Calculate sample size to fit around 1080p + val reqWidth = 1080 + val reqHeight = 1080 + var inSampleSize = 1 + if (options.outHeight > reqHeight || options.outWidth > reqWidth) { + val halfHeight: Int = options.outHeight / 2 + val halfWidth: Int = options.outWidth / 2 + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2 + } + } + + val decodeOptions = android.graphics.BitmapFactory.Options().apply { + this.inSampleSize = inSampleSize + this.inMutable = true // Ensure mutable + } + + val is2 = context.contentResolver.openInputStream(uri) + val bitmap = android.graphics.BitmapFactory.decodeStream(is2, null, decodeOptions) + is2?.close() + + if (bitmap != null) { + previewSourceBitmap = bitmap + + // Always derive logo from EXIF on load + val exif = metadataProvider.extractExif(uri) + val detected = detectOemLogo(exif) + _logoResId.value = detected + _showLogo.value = detected != null + + extractColorFromUri(uri) + updatePreview() + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun extractColorFromUri(uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + val inputStream = context.contentResolver.openInputStream(uri) + val options = android.graphics.BitmapFactory.Options().apply { + inSampleSize = 2 + } + val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream, null, options) + inputStream?.close() + + if (bitmap != null) { + androidx.palette.graphics.Palette.from(bitmap) + .maximumColorCount(32) + .clearFilters() + .generate { palette -> + val color = palette?.vibrantSwatch?.rgb + ?: palette?.mutedSwatch?.rgb + ?: palette?.lightVibrantSwatch?.rgb + ?: palette?.darkVibrantSwatch?.rgb + ?: palette?.dominantSwatch?.rgb + ?: android.graphics.Color.GRAY + + viewModelScope.launch { + watermarkRepository.updateAccentColor(color) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun updatePreview() { + val bitmap = previewSourceBitmap ?: return + val uri = currentUri ?: return + viewModelScope.launch { + _previewUiState.value = WatermarkUiState.Processing + try { + kotlinx.coroutines.delay(600) + val workingBitmap = bitmap.copy(bitmap.config ?: android.graphics.Bitmap.Config.ARGB_8888, true) + + // Merge transient logo settings with base options + val currentOptions = _options.value.copy( + logoResId = _logoResId.value, + showLogo = _showLogo.value + ) + + val result = watermarkEngine.processBitmap(workingBitmap, uri, currentOptions) + + val timestamp = System.currentTimeMillis() + val file = File(context.cacheDir, "preview_watermark_$timestamp.jpg") + val out = java.io.FileOutputStream(file) + result.compress(android.graphics.Bitmap.CompressFormat.JPEG, 80, out) + out.close() + + (_previewUiState.value as? WatermarkUiState.Success)?.file?.let { oldFile -> + if (oldFile.exists() && oldFile.name.startsWith("preview_watermark_")) { + oldFile.delete() + } + } + + _previewUiState.value = WatermarkUiState.Success(file) + } catch (e: Exception) { + e.printStackTrace() + _previewUiState.value = WatermarkUiState.Error(e.message ?: "Unknown error") + } + } + } + + fun setStyle(style: WatermarkStyle) { + viewModelScope.launch { + watermarkRepository.updateStyle(style) + } + } + + fun setShowBrand(show: Boolean) { + viewModelScope.launch { + watermarkRepository.updateShowBrand(show) + } + } + + fun setShowExif(show: Boolean) { + viewModelScope.launch { + watermarkRepository.updateShowExif(show) + } + } + + fun setExifSettings( + focalLength: Boolean, + aperture: Boolean, + iso: Boolean, + shutterSpeed: Boolean, + date: Boolean + ) { + viewModelScope.launch { + watermarkRepository.updateExifSettings(focalLength, aperture, iso, shutterSpeed, date) + // Trigger preview update + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setColorMode(mode: ColorMode) { + viewModelScope.launch { + watermarkRepository.updateColorMode(mode) + } + } + + fun setMoveToTop(move: Boolean) { + viewModelScope.launch { + watermarkRepository.updateMoveToTop(move) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setLeftAlign(left: Boolean) { + viewModelScope.launch { + watermarkRepository.updateLeftAlign(left) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setBrandTextSize(size: Int) { + viewModelScope.launch { + watermarkRepository.updateBrandTextSize(size) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setDataTextSize(size: Int) { + viewModelScope.launch { + watermarkRepository.updateDataTextSize(size) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setCustomTextSettings(show: Boolean, text: String, size: Int) { + viewModelScope.launch { + watermarkRepository.updateCustomTextSettings(show, text, size) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setCustomTextSize(size: Int) { + viewModelScope.launch { + watermarkRepository.updateCustomTextSize(size) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setPadding(padding: Int) { + viewModelScope.launch { + watermarkRepository.updatePadding(padding) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setBorderStroke(stroke: Int) { + viewModelScope.launch { + watermarkRepository.updateBorderStroke(stroke) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setBorderCorner(corner: Int) { + viewModelScope.launch { + watermarkRepository.updateBorderCorner(corner) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun setLogoSettings(show: Boolean, resId: Int?, size: Int) { + _showLogo.value = show + _logoResId.value = resId + viewModelScope.launch { + watermarkRepository.updateLogoSize(size) + watermarkRepository.updateLogoShow(show) + updatePreview() + } + } + + fun setShowLogo(show: Boolean) { + _showLogo.value = show + viewModelScope.launch { + watermarkRepository.updateLogoShow(show) + updatePreview() + } + } + + fun setLogoResId(resId: Int?) { + _logoResId.value = resId + updatePreview() + } + + fun setLogoSize(size: Int) { + viewModelScope.launch { + watermarkRepository.updateLogoSize(size) + previewSourceBitmap?.let { updatePreview() } + } + } + + fun saveImage(uri: Uri) { + viewModelScope.launch { + _uiState.value = WatermarkUiState.Processing + try { + // Merge transient logo options + val finalOptions = _options.value.copy( + logoResId = _logoResId.value, + showLogo = _showLogo.value + ) + // Process image to a temporary file first + val tempFile = watermarkEngine.processImage(uri, finalOptions) + + // Save to MediaStore (Gallery) + val values = android.content.ContentValues().apply { + put( + android.provider.MediaStore.Images.Media.DISPLAY_NAME, + "WM_${System.currentTimeMillis()}.jpg" + ) + put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + // RELATIVE_PATH is available on Android 10+ (API 29) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + put( + android.provider.MediaStore.Images.Media.RELATIVE_PATH, + "Pictures/Essentials" + ) + } + } + + val resolver = context.contentResolver + val collection = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + android.provider.MediaStore.Images.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY) + } else { + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + val resultUri = resolver.insert(collection, values) + + if (resultUri != null) { + resolver.openOutputStream(resultUri)?.use { outStream -> + tempFile.inputStream().use { inStream -> + inStream.copyTo(outStream) + } + } + _uiState.value = + WatermarkUiState.Success(tempFile) // Or success with URI? State expects File, but it's just for success message. + } else { + throw Exception("Failed to create MediaStore entry") + } + } catch (e: Exception) { + e.printStackTrace() + _uiState.value = WatermarkUiState.Error(e.message ?: "Unknown error") + } + } + } + + fun shareImage(uri: Uri, onShareReady: (Uri) -> Unit) { + viewModelScope.launch { + _uiState.value = WatermarkUiState.Processing + try { + // Merge transient logo options + val finalOptions = _options.value.copy( + logoResId = _logoResId.value, + showLogo = _showLogo.value + ) + // Process image to a temporary file + val tempFile = watermarkEngine.processImage(uri, finalOptions) + val savedUri = saveToMediaStore(tempFile) + if (savedUri != null) { + _uiState.value = WatermarkUiState.Idle + onShareReady(savedUri) + } else { + _uiState.value = WatermarkUiState.Error("Failed to prepare image for sharing") + } + } catch (e: Exception) { + e.printStackTrace() + _uiState.value = WatermarkUiState.Error(e.message ?: "Unknown error") + } + } + } + + private fun saveToMediaStore(sourceFile: File): Uri? { + try { + val values = android.content.ContentValues().apply { + put(android.provider.MediaStore.Images.Media.DISPLAY_NAME, "WM_SHARE_${System.currentTimeMillis()}.jpg") + put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + put(android.provider.MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Essentials/Watermarks") + } + } + val resolver = context.contentResolver + val collection = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + android.provider.MediaStore.Images.Media.getContentUri(android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY) + } else { + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + val resultUri = resolver.insert(collection, values) ?: return null + + resolver.openOutputStream(resultUri)?.use { outStream -> + sourceFile.inputStream().use { inStream -> + inStream.copyTo(outStream) + } + } + return resultUri + } catch (e: Exception) { + e.printStackTrace() + return null + } + } + + fun resetState() { + _uiState.value = WatermarkUiState.Idle + } +} diff --git a/app/src/main/res/drawable/apple.xml b/app/src/main/res/drawable/apple.xml new file mode 100644 index 00000000..1c20b800 --- /dev/null +++ b/app/src/main/res/drawable/apple.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/cmf.xml b/app/src/main/res/drawable/cmf.xml new file mode 100644 index 00000000..464772b2 --- /dev/null +++ b/app/src/main/res/drawable/cmf.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/google.xml b/app/src/main/res/drawable/google.xml new file mode 100644 index 00000000..c0496e45 --- /dev/null +++ b/app/src/main/res/drawable/google.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/moto.xml b/app/src/main/res/drawable/moto.xml new file mode 100644 index 00000000..e5683641 --- /dev/null +++ b/app/src/main/res/drawable/moto.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/nothing.xml b/app/src/main/res/drawable/nothing.xml new file mode 100644 index 00000000..b4153c1b --- /dev/null +++ b/app/src/main/res/drawable/nothing.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/oppo.xml b/app/src/main/res/drawable/oppo.xml new file mode 100644 index 00000000..1875ee4f --- /dev/null +++ b/app/src/main/res/drawable/oppo.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/round_android_24.xml b/app/src/main/res/drawable/round_android_24.xml new file mode 100644 index 00000000..4db0bf42 --- /dev/null +++ b/app/src/main/res/drawable/round_android_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_king_bed_24.xml b/app/src/main/res/drawable/round_king_bed_24.xml new file mode 100644 index 00000000..601a6ddc --- /dev/null +++ b/app/src/main/res/drawable/round_king_bed_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_add_photo_alternate_24.xml b/app/src/main/res/drawable/rounded_add_photo_alternate_24.xml new file mode 100644 index 00000000..0b44125a --- /dev/null +++ b/app/src/main/res/drawable/rounded_add_photo_alternate_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_bed_24.xml b/app/src/main/res/drawable/rounded_bed_24.xml new file mode 100644 index 00000000..8260d43e --- /dev/null +++ b/app/src/main/res/drawable/rounded_bed_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_bookmark_remove_24.xml b/app/src/main/res/drawable/rounded_bookmark_remove_24.xml new file mode 100644 index 00000000..605e32ab --- /dev/null +++ b/app/src/main/res/drawable/rounded_bookmark_remove_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_camera_24.xml b/app/src/main/res/drawable/rounded_camera_24.xml new file mode 100644 index 00000000..3a01dc0c --- /dev/null +++ b/app/src/main/res/drawable/rounded_camera_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_cancel_24.xml b/app/src/main/res/drawable/rounded_cancel_24.xml new file mode 100644 index 00000000..8d26e4fe --- /dev/null +++ b/app/src/main/res/drawable/rounded_cancel_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_check_circle_24.xml b/app/src/main/res/drawable/rounded_check_circle_24.xml new file mode 100644 index 00000000..a426909f --- /dev/null +++ b/app/src/main/res/drawable/rounded_check_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_control_camera_24.xml b/app/src/main/res/drawable/rounded_control_camera_24.xml new file mode 100644 index 00000000..a02edfd2 --- /dev/null +++ b/app/src/main/res/drawable/rounded_control_camera_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_date_range_24.xml b/app/src/main/res/drawable/rounded_date_range_24.xml new file mode 100644 index 00000000..52f34f06 --- /dev/null +++ b/app/src/main/res/drawable/rounded_date_range_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_dns_24.xml b/app/src/main/res/drawable/rounded_dns_24.xml new file mode 100644 index 00000000..1cce0c46 --- /dev/null +++ b/app/src/main/res/drawable/rounded_dns_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_edit_note_24.xml b/app/src/main/res/drawable/rounded_edit_note_24.xml new file mode 100644 index 00000000..0870396a --- /dev/null +++ b/app/src/main/res/drawable/rounded_edit_note_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_grain_24.xml b/app/src/main/res/drawable/rounded_grain_24.xml new file mode 100644 index 00000000..1cb3e4b3 --- /dev/null +++ b/app/src/main/res/drawable/rounded_grain_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_image_24.xml b/app/src/main/res/drawable/rounded_image_24.xml new file mode 100644 index 00000000..5544c381 --- /dev/null +++ b/app/src/main/res/drawable/rounded_image_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_image_search_24.xml b/app/src/main/res/drawable/rounded_image_search_24.xml new file mode 100644 index 00000000..1fc7cba9 --- /dev/null +++ b/app/src/main/res/drawable/rounded_image_search_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_position_bottom_left_24.xml b/app/src/main/res/drawable/rounded_position_bottom_left_24.xml new file mode 100644 index 00000000..a78a00cc --- /dev/null +++ b/app/src/main/res/drawable/rounded_position_bottom_left_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_shutter_speed_24.xml b/app/src/main/res/drawable/rounded_shutter_speed_24.xml new file mode 100644 index 00000000..6ab96847 --- /dev/null +++ b/app/src/main/res/drawable/rounded_shutter_speed_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_top_panel_close_24.xml b/app/src/main/res/drawable/rounded_top_panel_close_24.xml new file mode 100644 index 00000000..3116aa89 --- /dev/null +++ b/app/src/main/res/drawable/rounded_top_panel_close_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_window_open_24.xml b/app/src/main/res/drawable/rounded_window_open_24.xml new file mode 100644 index 00000000..08c6cc65 --- /dev/null +++ b/app/src/main/res/drawable/rounded_window_open_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/samsung.xml b/app/src/main/res/drawable/samsung.xml new file mode 100644 index 00000000..7fc874f3 --- /dev/null +++ b/app/src/main/res/drawable/samsung.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/sony.xml b/app/src/main/res/drawable/sony.xml new file mode 100644 index 00000000..cded39ab --- /dev/null +++ b/app/src/main/res/drawable/sony.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/vivo.xml b/app/src/main/res/drawable/vivo.xml new file mode 100644 index 00000000..8d6174fe --- /dev/null +++ b/app/src/main/res/drawable/vivo.xml @@ -0,0 +1,22 @@ + + + + diff --git a/app/src/main/res/drawable/xiaomi.xml b/app/src/main/res/drawable/xiaomi.xml new file mode 100644 index 00000000..95cd9fc4 --- /dev/null +++ b/app/src/main/res/drawable/xiaomi.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ach/strings.xml b/app/src/main/res/values-ach/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-ach/strings.xml +++ b/app/src/main/res/values-ach/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2828ec5d..e24f6e26 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,21 +1,21 @@ - Essentials - BETA + أساسيات + تجريبي Essentials Accessibility Service\n\nThis service is required for the following advanced features:\n\n• Physical Button Remapping:\nDetects volume button presses even when the screen is off to trigger actions like the Flashlight.\n\n• Per-App Settings:\nMonitors the currently active app to apply specific profiles for Dynamic Night Light, Notification Lighting Colors, and App Lock.\n\n• Screen Control:\nAllows the app to lock the screen (e.g. via Double Tap or Widgets) and detect screen state changes.\n\n• Security:\nPrevents unauthorized changes by detecting window content when the device is locked.\n\nNo input text or sensitive user data is collected or transmitted. - App icon - App Freezing - Disable apps that are rarely used - App Freezing - Open App Freezing - Frozen App + أيقونة التطبيق + تجميد التطبيق + تعطيل التطبيقات التي نادراً إستخدامها + تجميد التطبيق + تجميد التطبيق المفتوح + تطبيق مجمد Empty screen off widget - App Freezing - Flashlight Pulse + تجميد التطبيق + نبض المصباح Check for pre-releases Might be unstable - Security + الأمن Enable app lock App Lock Security Authenticate to enable app lock @@ -27,14 +27,14 @@ Another note, the biometric authentication prompt only lets you use STRONG secure class methods. Face unlock security methods in WEAK class in devices such as Pixel 7 will only be able to utilize the available other STRONG auth methods such as fingerprint or pin. Enable Button Remap - Use Shizuku + Works with screen off (Recommended) Shizuku is not running Detected %1$s Status: %1$s - Open Shizuku - Flashlight - Flashlight options + فتح Shizuku + ضوء الكشاف + إعدادات ضوء الكشاف Adjust fading and other settings Pitch black theme Use pure black background in dark mode @@ -44,14 +44,16 @@ Screen On Volume Up Volume Down - Toggle flashlight + تشغيل ضوء الكشاف Media play/pause Media next Media previous - Toggle vibrate + تشغيل الإهتزاز Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -66,25 +68,25 @@ Other Always turn off flashlight Even while display is on - Settings + الإعدادات - Notification - Show Notification - Post Notifications - Allows the app to show notifications - Grant Permission - Caffeinate Active - Active - Screen is being kept awake + الإشعار + إظهار الإشعار + إشعارات المنشورات + يسمح للتطبيق بعرض الإشعارات + منح الإذن + الكافيين فعال + فعال + يتم إبقاء الشاشة مضاءة Ignore battery optimization Recommended for reliable service on some devices Abort with screen off Timeout Presets Select available durations for QS tile - 5m - 10m - 30m - 1h + 5 دقائق + 10 دقائق + 30 دقيقة + ساعة Starting in %1$ds… %1$s remaining @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -367,9 +413,9 @@ Master toggle for app locking Select locked apps Choose which apps require authentication - Pick apps to freeze - Choose which apps can be frozen - Freeze all apps + اختر التطبيقات للتجميد + اختر التطبيقات التي يمكن تجميدها + تجميد جميع التطبيقات Immediately freeze all picked apps Freeze when locked Freeze selected apps when device locks @@ -378,8 +424,8 @@ Shizuku Required for advanced commands. Install Shizuku from the Play Store. - Install Shizuku - Shizuku permission + تثبيت Shizuku + منح الإذن Required to run power-saving commands while maps is navigating. Root Access Permissions required for system actions using Root privileges. @@ -401,8 +447,8 @@ Required to display the notification lighting overlay on the screen Device Administrator Required to hard-lock the device (disabling biometrics) on unauthorized access attempts - Grant Permission - Copy ADB + منح الإذن + نسخ الـADB Check Enable in Settings How to grant @@ -411,7 +457,7 @@ Essentials Freeze - Frozen + مُجمد DIY Disabled apps Do It Yourself @@ -420,19 +466,19 @@ Enable in settings Switch to Essentials Keyboard is active and ready! - Enabled + مفعل Disabled Adaptive Brightness Toggle adaptive brightness Maps Power Saving Toggle maps power saving mode Search for Tools, Mods and Tweaks - Search - Stop - Search + البحث + إيقاف + البحث Back - Done + تم Preview Help Guide Update Available @@ -454,8 +500,8 @@ Charger Disconnected Charging Screen On - Vibrate - Show Notification + إهتزاز + عرض الإشعارات Remove Notification Turn On Flashlight Turn Off Flashlight @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids @@ -750,7 +808,7 @@ Notification lighting does not work It depends on the OEM. Some like OneUI does not seem to allow overlays above the AOD preventing the lighting effects being shown. In this case, try the ambient display as a workaround. Button remap does not work while display is off - Some OEMs limit the accessibility service reporting once the display is actually off but they may still work while the AOD is on. \nIn this case, you may able to use button remaps with AOD on but not with off. \n\nAs a workaround, you will need to use Shizuku permissions and turn on the \'Use Shizuku\' toggle in button remap settings which identifies and listen to hardware input events.\nThis is not guaranteed to work on all devices and needs testing.\n\nAnd even if it\'s on, Shizuku method only will be used when it\'s needed. Otherwise it will always fallback to Accessibility which also handles the blocking of the actual input during long press. + Flashlight brightness does not work Only a limited number of devices got hardware and software support adjusting the flashlight intensity. \n\n\'The minimum version of Android is 13 (SDK33).\nFlashlight brightness control only supports HAL version 3.8 and higher, so among the supported devices, the latest ones (For example, Pixel 6/7, Samsung S23, etc.)\'\npolodarb/Flashlight-Tiramisu What the hell is this app? @@ -769,13 +827,13 @@ Essentials Bug Report Send via - Location reached - Get notified when you arrive at a specific destination. + هل وصلنا بعد؟ + تنبيهات الوجهات القريبة Destination Set Destination Tracking: %1$.4f, %2$.4f No destination set - Open your map app, pick a location, and share it to Essentials. + افتح خرائط Google، واختر موقعًا، وشاركه مع Essentials. Radius: %d m Distance to trigger the alarm Enable notification @@ -801,7 +859,7 @@ Required to wake your device upon arrival. Tap to grant. %1$d m %1$.1f km - Travel Alarm active + منبه السفر مفعل %1$s remaining (%2$d%%) Travel Progress Shows real-time distance to destination diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 50ec600e..8cb27ea7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -27,7 +27,7 @@ Noch ein Hinweis: Die biometrische Authentifizierungsaufforderung unterstützt ausschließlich sichere Methoden der Klasse STARK. Gesichtserkennungsmethoden der Klasse SCHWACH auf Geräten wie dem Pixel 7 können nur die sonstig verfügbaren STARK-Authentifizierungsmethoden wie Fingerabdruck oder PIN nutzen. Aktiviere Tastenneubelegung - Nutze Shizuku + Use Shizuku Funktioniert mit ausgeschaltetem Display (Empfohlen) Shizuku läuft nicht Erkannte %1$s @@ -52,6 +52,8 @@ Stummschaltung umschalten KI-Assistent Bildschirmfoto machen + Cycle sound modes + Toggle media volume Wenn der Bildschirm ausgeschaltet ist, kann durch langes Drücken der ausgewählten Taste die zugehörige Aktion ausgelöst werden. Auf Pixel-Geräten wird diese Aktion aufgrund von Systembeschränkungen nur ausgelöst, wenn das Always-On-Display aktiviert ist. Wenn der Bildschirm angeschaltet ist, kann durch langes Drücken der ausgewählten Taste die zugehörige Aktion ausgelöst werden. Taschenlampenintensität @@ -73,22 +75,22 @@ Benachrichtigungen senden Erlaubt es der App Benachrichtigungen zu zeigen Berechtigung gewähren - Caffeinate Active - Active - Screen is being kept awake - Ignore battery optimization - Recommended for reliable service on some devices - Abort with screen off - Timeout Presets - Select available durations for QS tile + Koffein aktiv + Aktiv + Bildschirm wird wach gehalten + Ignoriere Batterie-Optimierungen + Empfohlen für zuverlässigen Betrieb auf einigen Geräten + Abbruch bei ausgeschaltetem Bildschirm + Zeitvorlagen + Wähle eine Dauer für die Schnelleinstellungskachel 5min 10min 30min 1std - Starting in %1$ds… - %1$s remaining - Persistent notification for Caffeinate + Startet in %1$ds… + %1$s verbleibend + Dauerhafte Benachrichtigung für Koffein Aktiviere dynamisches Nachtlicht Apps, mit deaktiviertem Nachtlicht @@ -166,11 +168,17 @@ Apps einfrieren Taschenlampenimpuls Wach bleiben - Essentials Keyboard - English (US) + Essentials-Tastatur + Englisch (US) Aktiv Inaktiv NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging An Aus @@ -270,12 +278,12 @@ Kachel in den Schnelleinstellungen zum Umschalten des Tonmodus \'Wach bleiben\'-Kachel Kachel in den Schnelleinstellungen zum Umschalten des Wachbleibens - Show Bluetooth devices - Display battery level of connected Bluetooth devices - Limit max devices - Adjust max devices visible in widget - Widget background - Show widget background + Bluetooth-Geräte anzeigen + Zeige den Akkustand von verbundenen Bluetooth-Geräten + Max. Geräteanzahl begrenzen + Anzahl der im Widget angezeigten Geräte anpassen + Widget-Hintergrund + Widget-Hintergrund anzeigen Auslöser-Automatisierung Eine Aktion planen, die bei einer Beobachtung ausgelöst wird @@ -299,6 +307,45 @@ Sperre Apps mit Biometrie Einfrieren Selten verwendete Apps deaktivieren + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Haptisches Feedback bei Widgets Wähle haptisches Feedback für Widget-Tipps @@ -308,8 +355,8 @@ Verstecke mobile Daten in bestimmten Modi Alle Icons zurücksetzen Sichtbarkeit der Statusleisten-Icons auf Standard zurücksetzen - Abort Caffeinate with screen off - Automatically turn off Caffeinate when manually locking the device + Koffein bei ausgeschaltetem Bildschirm abbrechen + Koffein automatisch deaktivieren, wenn das Gerät manuell gesperrt wird Lichtstil Wählen Sie zwischen Streifeneffekt, Leuchteffekt, Dreheffekt und mehr Eckradius @@ -320,12 +367,7 @@ Langsames Blinken der Taschenlampe bei neuen Benachrichtigungen Nur, wenn das Handy umgedreht ist Die Taschenlampe blinkt nur, wenn das Gerät mit der Vorderseite nach unten liegt - Debugging-Benachrichtigungen deaktivieren - Persistente ADB/USB-Debugging-Benachrichtigungen ausblenden - Benachrichtigung zur Dateiübertragung deaktivieren - Persistente Benachrichtigungen zur USB-Dateiübertragung ausblenden - Lade-Benachrichtigung deaktivieren - Persistente Benachrichtigungen zum Aufladen ausblenden + No system channels discovered yet. They will appear here once detected. UI-Unschärfe Systemweite UI-Unschärfe umschalten Bubbles @@ -355,6 +397,10 @@ Taschenlampenimpuls Taschenlampenimpuls bei Benachrichtigungen ein-/ausschalten Entwickleroption „Wach bleiben“ umschalten + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Aktiviere Tastenneubelegung Hauptschalter für die Neuzuweisung der Lautstärketasten Haptisches Feedback neu zuordnen @@ -379,7 +425,7 @@ Shizuku Benötigt für erweiterte Befehle. Installiere Shizuku aus dem Play Store. Installiere Shizuku - Shizuku-Berechtigung + Shizuku permission Benötigt, um Energiesparbefehle auszuführen, während Maps navigiert. Root-Zugang Benötigt Berechtigungen für Systemaktionen mittels Root-Rechte. @@ -395,8 +441,8 @@ Geschützte Einstellungen umschreiben Benötigt für Statusleisten-Symbole und Sicherheit bei Bildschirmsperre Benötigt, um das Nachtlicht zu aktivieren. Über ADB oder Root gewähren. - Modify System Settings - Required to toggle Adaptive Brightness and other system settings + Systemeinstellungen ändern + Benötigt, zum Umschalten der adaptiven Helligkeit und anderer Systemeinstellungen Overlay-Berechtigung Benötigt, um das Benachrichtigungslicht auf dem Bildschirm anzuzeigen Geräteadministrator @@ -406,8 +452,8 @@ Prüfen In den Einstellungen aktivieren Wie man gewährt - Battery Optimization - Ensure the service is not killed by the system to save power. + Akkuoptimierung + Stelle sicher, dass der Service nicht von dem System gestoppt wird, um Strom zu sparen. Essentials Einfrieren @@ -416,16 +462,16 @@ Deaktivierte Apps Do-It-Yourself - Keyboard Setup - Enable in settings - Switch to Essentials - Keyboard is active and ready! - Enabled - Disabled - Adaptive Brightness - Toggle adaptive brightness - Maps Power Saving - Toggle maps power saving mode + Tastatur-Einrichtung + In den Einstellungen aktivieren + Wechsel zu Essentials + Tastatur ist aktiv und bereit! + Aktiviert + Deaktiviert + Adaptive Helligkeit + Adaptive Helligkeit umschalten + Maps-Energiesparmodus + Maps-Energiesparmodus umschalten Suche nach Werkzeugen, Modifikationen und Anpassungen Suche Stop @@ -463,6 +509,8 @@ Hintergrund dimmen Diese Aktion erfordert Shizuku oder einen Root-Zugang um den Hintergrund zu dimmen. Auslöser auswählen + App + Automatisierung basierend auf geöffneter App Zustand auswählen Aktion auswählen Eintrittsaktion @@ -476,6 +524,16 @@ Automatisierungsdienst Automatisierungen aktiv Überwachung von Systemereignissen für deine Automatisierungen + Geräteeffekte + Deaktiviere Geräteeffekte + Steuer Effekte auf Systemebene wie Graustufen, AOD-Unterdrückung, Hintergrundbild-Dimmung und Nachtmodus. + Graustufen + Ambient Display unterdrücken + Hintergrund dimmen + Nachtmodus + Für diese Funktion ist Android 15 oder höher erforderlich. + Aktiviert + Deaktiviert Sameera Wijerathna Die alles umfassende Werkzeugkiste für Ihr Pixel und Android-Geräte @@ -528,11 +586,11 @@ icon - keyboard - height - padding - haptic - input + tastatur + höhe + abstand + haptik + eingabe sichtbar @@ -602,9 +660,9 @@ warten zeitüberschreitung - Always dark theme - Pitch black theme - Clipboard History + Immer dunkles Design + Tiefschwarzes Design + Zwischenablage-Verlauf liste wähler @@ -750,7 +808,7 @@ Das Benachrichtigungslicht funktioniert nicht Das hängt vom OEM ab. Einige wie OneUI scheinen keine Überlagerungen über dem Always-On-Display zuzulassen, wodurch die Lichteffekte nicht angezeigt werden können. Versuche in diesem Fall, das Ambient-Display als Workaround zu verwenden. Die Neuzuweisung der Tasten funktioniert nicht, wenn das Display ausgeschaltet ist - Einige OEMs schränken die Barrierefreiheitsdienste ein, sobald das Display ausgeschaltet ist, aber sie funktionieren möglicherweise weiterhin, wenn das Always-on-Display eingeschaltet ist. \nIn diesem Fall kannst du möglicherweise die Tastenbelegung mit eingeschaltetem Always-on-Display verwenden, jedoch nicht mit ausgeschaltetem Always-on-Display. \n\nAls Workaround musst du die Shizuku-Berechtigungen verwenden und in den Einstellungen für die Tastenbelegung die Option „Shizuku verwenden” aktivieren, die Hardware-Eingabeereignisse identifiziert und abhört. \nEs kann nicht garantiert werden, dass dies auf allen Geräten funktioniert, daher muss es getestet werden. \n\nSelbst wenn diese Option aktiviert ist, wird die Shizuku-Methode nur bei Bedarf verwendet. Andernfalls wird immer auf den Barrierefreiheitsdienst zurückgegriffen, welcher auch das Blockieren der tatsächlichen Eingabe bei langem Drücken übernimmt. + Some OEMs limit the accessibility service reporting once the display is actually off but they may still work while the AOD is on. \nIn this case, you may able to use button remaps with AOD on but not with off. \n\nAs a workaround, you will need to use Shizuku permissions and turn on the \'Use Shizuku\' toggle in button remap settings which identifies and listen to hardware input events.\nThis is not guaranteed to work on all devices and needs testing.\n\nAnd even if it\'s on, Shizuku method only will be used when it\'s needed. Otherwise it will always fallback to Accessibility which also handles the blocking of the actual input during long press. Die Helligkeit der Taschenlampe funktioniert nicht Nur eine begrenzte Anzahl von Geräten verfügt über Hardware- und Software-Unterstützung zur Einstellung der Taschenlampenintensität. \n\nDie Mindestversion von Android ist 13 (SDK33).\nDie Helligkeitseinstellung der Taschenlampe unterstützt nur HAL Version 3.8 und höher, daher gehören zu den unterstützten Geräten die neuesten Modelle (z. B. Pixel 6/7, Samsung S23 usw.).\'\npolodarb/Flashlight-Tiramisu Was zum Teufel ist das für eine App? @@ -813,35 +871,35 @@ Anstelle von Shizuku Root-Zugang nicht verfügbar. Bitte überprüfe deinen Root-Manager. - Keyboard - Keys - Customize layout and behavior - Keyboard Height - Adjust the total vertical size of the keyboard - Bottom Padding - Add space below the keyboard - Haptic Feedback - Vibrate on key press - Test the keyboard - Test the keyboard - Keyboard Height - Bottom Padding - Haptic Feedback - Key Roundness - Key Roundness - Adjust the corner radius of keys - Move functions to bottom - Functions side padding - Haptic feedback strength - Keyboard shape - Round - Flat - Inverse - Batteries - Monitor your device battery levels - Battery Status - Connect to AirSync - Display battery from your connected mac device in AirSync - Download AirSync App - Required for Mac battery sync + Tastatur + Tasten + Layout und Funktionalität anpassen + Tastaturhöhe + Passe die vertikale Gesamtgröße der Tastatur an + Unterer Abstand + Platz unterhalb der Tastatur hinzufügen + Haptisches Feedback + Bei Tastendruck vibrieren + Teste die Tastatur + Teste die Tastatur + Tastaturhöhe + Unterer Abstand + Haptisches Feedback + Tastenrundheit + Tastenrundheit + Eckradius der Tasten anpassen + Funktionen nach unten verschieben + Seitenabstand Funktionen + Stärke des haptischen Feedbacks + Tastaturform + Rund + Flach + Invers + Akkus + Überwache den Akkustand deines Geräts + Batteriestatus + Mit AirSync verbinden + Zeige den Akku deines verbundenen Mac-Geräts in AirSync an + AirSync-App herunterladen + Benötigt für die Mac-Akkusynchronisierung diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 21fea74e..fcc11ab5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -11,9 +11,9 @@ Application gelée Widget de verrouillage invisible Gel d\'applis - Flashlight Pulse - Vérifier pour des pré-versions - Peut être instable + Impulsion de lampe torche + Obtenir les préversions (bêta) + Peut-être instable Sécurité Activer le verrouillage d\'applis @@ -27,7 +27,7 @@ Mais également, la fenêtre d\'authentification biométrique ne vous laisse utiliser que des méthodes d\'authentification FORTES. Le déverrouillage par reconnaissance faciale est considéré comme FAIBLE pour les appareils comme les Pixel 7 qui ne pourront utiliser que les autres méthodes FORTES disponibles comme l\'authentification par empreinte digitale ou code PIN. Activer la réattribution de boutons - Utiliser Shizuku + Utiliser Shizuku ou l\'accès racine (root) Fonctionne avec l\'écran éteint (Recommandé) Shizuku n\'est pas actif %1$s Détecté @@ -35,9 +35,9 @@ Ouvrir Shizuku Lampe-torche Options de la lampe-torche - Adjust fading and other settings - Pitch black theme - Use pure black background in dark mode + Ajuster la disparition et les autres paramètres + Thème Noir Absolu + Utiliser un fond noir pure en thème sombre Retour haptique Réattribuer l\'appui long Écran éteint @@ -48,17 +48,19 @@ Jouer/mettre en pause le média Prochain média Média précédent - Basculer le mode vibreur - Basculer le mode silencieux + Activer le mode vibreur + Activer le mode silencieux Assistant IA Prendre une capture d\'écran + Cycle sound modes + Toggle media volume Quand l\'écran est éteint, appuyez longuement sur le bouton sélectionné pour déclencher son action assignée. Sur les appareils Pixel, cette action ne se déclenche que si le Always-On Display est activé à cause de limitations systèmes. Quand l\'écran est allumé, appuyez longuement sur le bouton sélectionné pour déclencher son action assignée. Intensité de la lampe-torche - Fade in and out - Smoothly toggle flashlight - Global controls - Fade-in flashlight globally + Fondu d\'entrée et de sortie + Allumer/éteindre de manière fluide la lampe-torche + Contrôles globlaux + Fondu global de lampe-torche Ajuster l\'intensité Les boutons Volume + et - ajustent l\'intensité de lampe-torche Mise à jour en direct @@ -73,22 +75,22 @@ Envoyer des notifications Autorise l\'application à afficher des notifications Accorder la permission - Caffeinate Active - Active - Screen is being kept awake - Ignore battery optimization - Recommended for reliable service on some devices - Abort with screen off - Timeout Presets - Select available durations for QS tile + Caffeinate activé + Actif + L\'écran est maintenu allumé + Ignorer l\'optimisation de la batterie + Recommandé pour un service fiable sur certains appareils + Arrêter au verrouillage de l\'écran + Préréglages de délais + Sélectionner les durées valables pour le bloc de \"Réglages rapides\" 5m 10m 30m 1h - Starting in %1$ds… - %1$s remaining - Persistent notification for Caffeinate + Démarrage dans %1$ds… + %1$s restant + Notification persistante pour Caffeinate Activer le mode d\'Éclairage nocturne dynamique Applis qui désactivent le mode d\'Éclairage nocturne @@ -121,19 +123,19 @@ Afficher seulement quand l\'écran est éteint Ignorer les notifications silencieuses Ignorer les notifications persistantes - Flashlight Pulse - Flashlight pulse + Impulsion de lampe torche + Impulsion de lampe torche Seulement quand le téléphone est vers le bas Style - Stroke adjustment + Ajustement du trait Rayon des coins - Stroke thickness - Glow adjustment - Glow spread + Épaisseur du trait + Ajustement de la brillance + Propagation de la brillance Placement Position horizontale Position verticale - Indicator adjustment + Ajustement de l\'indicateur Taille Durée Animation @@ -145,7 +147,7 @@ Convient si vous n\'utilisez pas le Always-On Display. Réveille l\'écran et affiche de la lumière Affiche l\'écran de verrouillage - No black overlay + Pas de superposition noire Ajouter Déjà ajouté @@ -164,24 +166,30 @@ Audio mono Lampe-torche Gel d\'applis - Flashlight Pulse + Impulsion de lampe torche Rester allumé - Essentials Keyboard - English (US) + Clavier Essentials + Anglais (US) Actif Inactif NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging Activer Désactiver Sécurité d\'écran verrouillé - Protection de l\'écran de verrouillage + Écran de verrouillage Authentifiez-vous pour activer la protection de l\'écran de verrouillage Authentifiez-vous pour désactiver la protection de l\'écran de verrouillage ⚠️ ATTENTION Cette fonctionnalité n\'est pas parfaite. Dans certaines situations il est possible que quelqu\'un puisse tout de même interagir avec les blocs de paramètres rapides. \nGardez également à l\'esprit qu\'Android va toujours autoriser les redémarrages forcés et les Pixels vont toujours permettre d\'éteindre l\'appareil depuis l\'écran de verrouillage. Veuillez retirer le bloc des \"Réglages rapides\" du Mode Avion car il ne peut être bloqués (il n\'ouvre aucune fenêtre que l\'appli peut bloquer). - When enabled, the Quick Settings panel will be immediately closed and the device will be locked down if someone attempt to interact with Internet tiles while the device is locked. \n\nThis will also disable biometric unlock to prevent further unauthorized access. Animation scale will be reduced to 0.1x while locked to make it even harder to interact with. + Une fois activé, le menu \"Réglages rapides\" sera immédiatement fermé et l\'appareil sera verrouillé si quelqu\'un essaye d\'interagir avec les blocs de réglages réseaux quand l\'appareil est verrouillé. \n\nCela va également désactiver l\'authentification biométrique pour prévenir tout accès non autorisé. L\'échelle des animations va également être réduite à x0,1 quand l\'appareil est verrouillé pour rendre encore plus difficile l\'intéraction avec le menu. Réorganiser les modes Appuyez longuement pour basculer @@ -191,8 +199,8 @@ Silencieux Connectivité - Phone & Network - Audio & Media + Téléphone & Réseau + Audio & Média Status du système Spécifique au constructeur @@ -224,7 +232,7 @@ Synchronisation Profil géré Ne pas déranger - Privacy & Secure Folder + Confidentialité & Dossier sécurisé Status de sécurité (SU) Clavier / Souris OTG Fonctionnalités intelligentes Samsung @@ -238,7 +246,7 @@ Cacher les données mobiles quand un réseau Wi-Fi est connecté Cache les données mobiles dans certains modes Réinitialiser les icônes - Please note that the implementation of these options may depend on the OEM and some may not be functional at all. + Veuillez noter que l\'implémentation de ces options peut dépendre du constructeur et certaines peuvent ne pas fonctionner du tout. Autre @@ -248,7 +256,7 @@ Outils Visuels Système - Sécurité & Confidentialité + Sécurité et confidentialité Recherchez des outils, des mods, ou des améliorations Aucun résultat pour \"%1$s\" @@ -262,25 +270,25 @@ Contrôler la visibilité des icônes de la barre d\'état Caffeinate Maintenir l\'écran allumé - Mode économie d\'énergie Maps + Économie d\'énergie Maps Pour tout appareil Android - Notification lighting - Light up for notifications + Éclairage de notifications + Éclairer pour les notifications Raccourci de mode de sonnerie Bloc \"Réglages rapides\" pour changer le mode de sonnerie Bloc \"rester allumé\" Bloc \"Réglages rapides\" pour activer/désactiver le maintien de l\'écran allumé - Show Bluetooth devices - Display battery level of connected Bluetooth devices - Limit max devices - Adjust max devices visible in widget - Widget background - Show widget background + Afficher les appareils Bluetooth + Afficher le niveau de batterie des appareils Bluetooth connectés + Nombre d\'appareils max + Ajuster le nombre d\'appareils maximum visibles sur le widget + Fond du widget + Afficher le fond du widget Déclencher une automatisation Prévoir une action à déclencher lors d\'une observation - State Automation - Schedule an action to execute based on the state of a condition in and out + Automatisation de l\'état + Prévoir une action à exécuter basé sur l\'état d\'une condition Nouvelle automatisation Modifier l\'automatisation Actions de lien @@ -291,41 +299,75 @@ Voir tous Réattribution de boutons Réattribuer les boutons physiques de l\'appareil - Mode Éclairage nocturne dynamique - Bascule le mode Éclairage nocturne en fonction de l\'appli utilisée - Protection de l\'écran de verrouillage + Éclairage nocturne dynamique + Activer/désactiver le mode Éclairage nocturne en fonction de l\'appli utilisée + Écran de verrouillage Empêcher l\'utilisation des contrôles réseaux Verrouillage d\'applis - Protéger les applications avec la biométrie + Protéger les applis avec la biométrie Gel d\'applis Désactive les applications rarement utilisées + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Retour haptique du Widget - Pick haptic feedback for widget taps + Choisir un retour haptique pour les appuis de widget Wi-Fi intelligent Cacher les données mobiles quand un réseau Wi-Fi est connecté Données intelligentes Cache les données mobiles dans certains modes Réinitialiser les icônes - Reset status bar icon visibility to default - Abort Caffeinate with screen off - Automatically turn off Caffeinate when manually locking the device + Réinitialiser la visibilité des icônes de la barre d\'état + Arrêter Caffeinate au verrouillage + Automatiquement désactiver Caffeinate quand vous verrouillez manuellement l\'appareil Style d\'éclairage - Choose between Stroke, Glow, Spinner, and more + Choisissez entre trait, brillance, rotation et plus encore Rayon des coins - Adjust the corner radius of the notification lighting + Ajuster le rayon des coins des notifications lumineuses Ignorer les notifications silencieuses Ne pas afficher de lumière pour les notifications silencieuses - Flashlight pulse - Slowly pulse flashlight for new notifications - Only while facing down - Pulse flashlight only when device is face down - Désactiver les notifications de débogage - Cacher les notifications de débogage ADB/USB persistantes. - Désactiver la notification de transfert de fichiers - Cache les notifications de transfert de fichiers USB persistantes - Désactiver la notification de recharge - Cacher les notifications persistantes de recharge + Impulsion de lampe torche + Faire clignoter lentement la lampe-torche pour les nouvelles notifications + Seulement quand le téléphone est vers le bas + Impulsion de lampe torche seulement quand l\'appareil est vers le bas + No system channels discovered yet. They will appear here once detected. Flou d\'interface Basculer le flou d\'interface à l\'échelle du système Bulles @@ -339,32 +381,36 @@ Caffeinate Basculer le maintient de l\'écran allumé Mode de sonnerie - Cycle sound modes (Ring/Vibrate/Silent) - Notification Lighting - Toggle notification lighting service + Changer le mode de sonnerie (Sonnerie/Vibreur/Silencieux) + Éclairage de notifications + Activer/désactiver le service de notifications lumineuses Éclairage nocturne dynamique - Night light automation toggle + Basculer l\'automatisation du mode Éclairage nocturne Sécurité de verrouillage - Network security on lockscreen toggle + Activer/désactiver la protection de l\'écran de verrouillage Audio mono Basculer l\'audio mono forcé Lampe-torche - Dedicated flashlight toggle + Bouton lampe-torche dédié Gel d\'applis Lancer la grille de gel d\'applis - Flashlight Pulse - Toggle notification flashlight pulse - Toggle stay awake developer option + Impulsion de lampe torche + Activer/désactiver l\'impulsion de lampe-torche + Basculer l\'option développeur \"Rester allumé\" + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Activer la réattribution de boutons - Master toggle for volume button remapping + Basculer globalement la réattribution du bouton volume Reconfigurer le retour haptique Vibration quand le bouton réattribué est appuyé - Basculer la lampe-torche + Allumer/éteindre la lampe-torche Basculer la lampe-torche avec les boutons volume Éclairage nocturne dynamique - Master switch for dynamic night light + Basculer globalement l\'éclairage nocturne dynamique Activer le verrouillage d\'applis - Master toggle for app locking + Basculer globalement le verrouillage d\'applis Sélectionner les applis verrouillées Choisir quelles applis nécessitent l\'authentification Choisir les applis à geler @@ -372,42 +418,42 @@ Geler toutes les applis Geler immédiatement toutes les applis choisies Geler au verrouillage - Freeze selected apps when device locks + Geler les applications sélectionnées quand l\'appareil est verrouillé Délai avant de geler Délai avant le gel après le verrouilage de l\'appareil Shizuku Requis pour les commandes avancées. Installez Shizuku depuis le Play Store. Installer Shizuku - Permission Shizuku - Required to run power-saving commands while maps is navigating. + Accorder la permission + Requise pour exécuter des commandes d\'économie d\'énergie pendant la navigation de Maps. Accès racine (root) - Permissions required for system actions using Root privileges. - Notification Listener - Required to detect when Maps is navigating. - Required to detect new notifications - Required to detect and snooze notifications + Permissions requises pour les actions systèmes en utilisant les privilèges racine (root). + Analyseur de notification + Requise pour détecter quand Maps est en navigation. + Requise pour détecter les nouvelles notifications + Requise pour détecter et mettre en attente les notifications Service d\'accessibilité - Requis pour le verrouillage d\'applis, le widget de verrouillage et d\'autres fonctionnalités pour détecter les interactions. + Requis pour le verrouillage d\'applis, le widget de verrouillage et d\'autres fonctionnalités pour détecter les interactions Requis pour déclencher l\'éclairage lors de la réception d\'une nouvelle notification Requis pour intercepter les appuis sur les boutons physiques - Requis pour surveiller les applications en avant-plan + Requis pour surveiller les applications en avant-plan. Modifier les paramètres de sécurité - Required for Statusbar icons and Screen Locked Security - Needed to toggle Night Light. Grant via ADB or root. - Modify System Settings - Required to toggle Adaptive Brightness and other system settings + Requise pour les icônes de la barre d\'état et la protection de l\'écran de verrouillage + Requise pour activer/désactiver l\'Éclairage nocturne. Autoriser via ADB ou root. + Modifier les paramètres système + Requise pour activer/désactiver la luminosité adaptative et d\'autres paramètres système Permission de superposition - Required to display the notification lighting overlay on the screen + Requise pour afficher la superposition de l\'éclairage de notification sur l\'écran Administrateur de l\'appareil - Required to hard-lock the device (disabling biometrics) on unauthorized access attempts + Requise pour faire un verrouillage forcé de l\'appareil (désactiver la biométrie) lors d\'accès non autorisés Accorder la permission Copier la commande ADB Vérifier Activer dans les paramètres Comment accorder - Battery Optimization - Ensure the service is not killed by the system to save power. + Optimisation de la batterie + Assure que le service ne soit pas fermé par le système pour économise de l\'énergie. Essentials Gel d\'applis @@ -416,19 +462,19 @@ Applications désactivées Do It Yourself - Keyboard Setup - Enable in settings - Switch to Essentials - Keyboard is active and ready! - Enabled - Disabled - Adaptive Brightness - Toggle adaptive brightness - Maps Power Saving - Toggle maps power saving mode - Search for Tools, Mods and Tweaks + Configuration du clavier + Activer dans les paramètres + Changer vers Essentials + Le clavier est actif et prêt ! + Activé + Désactivé + Luminosité adaptative + Activer/désactiver la luminosité adaptative + Économie d\'énergie Maps + Activer/désactiver le mode économie d\'énergie Maps + Recherchez des outils, des mods, ou des améliorations Rechercher - Stop + Arrêter Rechercher Retour @@ -444,29 +490,31 @@ Déclencheur État Action - In - Out + Entrée + Sortie Automatisation Écran éteint Écran allumé Déverrouillage de l\'appareil - Chargeur connecté - Chargeur déconnecté - + Câble branché + Câble débranché + En charge Écran allumé Vibreur Afficher une notification Supprimer la notification Allumer la lampe-torche Éteindre la lampe-torche - Activer/désactiver la lampe-torche - Dim Wallpaper - This action requires Shizuku or Root to adjust system wallpaper dimming. - Sélectionner un déclencheur - État de sélection - Action de sélection - In Action - Out Action + Allumer/éteindre la lampe-torche + Fond d\'écran assombri + Cette action nécessite Shizuku ou un accès racine (root) pour ajuster l\'assombrissement du fond d\'écran. + Déclencheur + Appli + Automatisation basée sur l\'ouverture d\'appli + État + Action + Entrée + Sortie Annuler Enregistrer Modifier @@ -475,7 +523,17 @@ Désactiver Service d\'automatisation Automatisations actives - Monitoring system events for your automations + Analyse des événements système pour vos automatisations + Effets d\'appareil + Désactiver les effets d\'appareil + Contrôler les effets systèmes comme l\'échelle des gris, la suppression de l\'Always-On Display, l\'assombrissement du fond d\'écran et le mode nuit. + Échelle des gris + Enlever l\'éclairage ambient + Fond d\'écran assombri + Mode nuit + Cette fonctionnalité nécessite Android 15 ou supérieur. + Activé + Désactivé Sameera Wijerathna L\'outil tout-en-un pour votre Pixel ou vos appareils Android @@ -504,7 +562,7 @@ Authentifiez-vous pour accéder aux paramètres Authentifiez-vous pour activer cette fonctionnalité Authentifiez-vous pour désactiver cette fonctionnalité - %1$s Paramètres + Paramètres de \"%1$s\" fonctionnalité paramètres cacher @@ -528,11 +586,11 @@ icône - keyboard - height + clavier + hauteur padding - haptic - input + haptique + entrée visible @@ -545,7 +603,7 @@ lumière lampe-torche - pulse + impulsion notification @@ -554,20 +612,20 @@ allumé - awake + allumé développeur alimentation charge - glow + brillance notification led - round - shape - edge + rond + forme + coin secure @@ -582,13 +640,13 @@ hear - stay + rester on timeout - touch - wake + touché + réveiller affichage @@ -602,9 +660,9 @@ attendre timeout - Always dark theme - Pitch black theme - Clipboard History + Toujours le mode sombre + Thème Noir Absolu + Historique du presse-papiers liste sélecteur @@ -612,7 +670,7 @@ animation - visual + visuel look @@ -636,18 +694,18 @@ énergie - blur + flou glass vignette float - window - overlay + fenêtre + superposition toujours - display + affichage horloge @@ -702,7 +760,7 @@ Éteindre Luminosité de la lampe-torche - Paramètres de %1$s + Paramètres de \"%1$s\" Confirmez votre identité pour ouvrir les paramètres Authentification requise Confirmez votre identité @@ -721,40 +779,40 @@ Zero Avatar du développeur - Aidr & Guides - Besoin de plus d\'aide ? Contactez-nous, + Aide & Guides + Besoin de plus d\'aide ? Contactez-nous : Réduire Étendre Groupe de support E-mail Envoyer un e-mail Aucune application d\'e-mail disponible - Step %1$d Image + Image de l\'étape %1$d Permissions d\'accessibilité, de notification et de superposition - You may get this access denied message if you try to grant sensitive permissions such as accessibility, notification listener or overlay permissions. To grant it, check the steps below. - 1. Allez dans la page d\'informations de l\'application Essentials + Vous pouvez obtenir ce message d\'accès refusé si vous essayer d\'accorder des permissions sensibles telles que l\'accessibilité, l\'analyse des notifications ou les permissions de superposition. Pour l\'accorder, regardez les étapes ci-dessous. + 1. Allez dans la page d\'informations de l\'application Essentials. 2. Ouvrez le menu à 3 points en haut à droite et sélectionnez \"Autoriser les paramètres restreints\". Vous devrez peut-être vous authentifier avec la biométrie. Une fois fait, réessayez d\'accorder la permission. Shizuku Shziuku est un outil puissant qui permet aux applications d\'utiliser directement les APIs système directement avec ADB ou l\'accès racine (root). Il est requis pour des fonctionnalités comme le mode économie d\'énergie maps ou le gel d\'applis. Il aide également à donner certaines permissions comme WRITE_SECURE_SETTINGS. \n\nMais la version de Shizuku disponible sur le Play Store peut-être obsolète et sera probablement inutilisable sur les versions récentes d\'Android. Dans ce cas, merci d\'installer la dernière version depuis le GitHub officiel ou un fork mis à jour. - Mode économie d\'énergie Maps - This feature automatically triggers Google Maps power saving mode which is currently exclusive to the Pixel 10 series. A community member discovered that it is still usable on any Android device by launching the maps minMode activity with root privileges. \n\nAnd then, I had it automated with Tasker to automatically trigger when the screen turns off during a navigation session and then was able to achieve the same with just runtime Shizuku permissions. \n\nIt is intended to be shown over the AOD of Pixel 10 series so because of that, you may see an occasional message popping up on the display that it does not support landscape mode. That is not avoidable by the app and you can ignore. + Économie d\'énergie Maps + Cette fonctionnalité déclenche automatiquement le mode économie d\'énergie de Google Maps qui est actuellement réservé à la série des Pixel 10. Un membre de la communauté à découvert qu\'il est tout de même utilisable sur n\'importe quel appareil Android en lançant l\'activité minMode avec les privilèges racine (root). \n\nDonc, je l\'ai automatisé avec Tasker pour le déclencher automatiquement quand l\'écran est éteint pendant une session de navigation et j\'ai pu faire la même chose en n\'utilisant que les permissions Shizuku. \n\nCette fonctionnalité est prévue pour s\'afficher par dessus le Always-On Display de la série Pixel 10 donc vous devriez occasionnellement voir apparaître sur l\'écran un message concernant l\'incompatibilité avec le mode paysage. Cette application ne peut empêcher ça mais il suffit de les ignorer. Mode silencieux - You may have noticed that the silent mode also triggers DND. \n\nThis is due to how the Android implemented it as even if we use the same API to switch to vibrate mode, it for some reason turns on DND along with the silent mode and this is not avoidable at this moment. :( + Vous avez peut-être remarqué que le mode silencieux déclenche également le mode \"Ne pas déranger\". \n\nCeci est dû à comment Android l\'a l\'implémenté et même si on utilise la même API pour passer en mode vibreur, pour quelconque raison cela active le mode NPD en parallèle du mode silencieux et ce n\'est possible de faire autrement pour l\'instant :( Qu\'est-ce que le gel ? Faites une pause et séparez-vous des applis qui vous distraient tout en économisant un peu d\'énergie en empêchant les applis de tourner en arrière-plan. Convient pour les applis rarement utilisées. \n\nNon recommandé pour les services de communication car ils ne vous enverront aucune notification à moins de les dégeler. \n\nIl est fortement recommandé de ne pas geler des applis systèmes car cela peut causer une instabilité du système. Procédez avec prudence, vous avez été avertis. \n\nInspiré par Hail <3 Est-ce que le verrouillage d\'applis et la protection de l\'écran de verrouillage sont réellement sécurisés ? - Absolument pas. \n\nToute appli tierce ne peut interférer à 100% avec les interactions normales d\'un appareil et le verrouillage d\'applis n\'est qu\'une superposition pour empêcher d\'interagir avec elles. Il y a toujours des moyens de le contourner et il n\'est pas parfait. \n\nDe même pour la fonctionnalité de protection de l\'écran de verrouillage qui détecte simplement une tentative d\'action avec les blocs de réglages réseau dans les réglages rapides et se superpose pour empêcher leur utilisation, ils sont toujours disponibles pour quiconque sur les appareils Pixels et si quelqu\'un souhaite réellement désactiver le réseau il le pourra, surtout avec un bloc de réglage du Mode Avion que cette appli ne peut bloquer. \n\nCes fonctionnalités sont expérimentales et ne devraient pas être considérées comme des sécurités fortes. \n\nAlternatives sécurisées :\n - Verrouillage d\'applis : Le \"Dossier Sécurisé\" et \"Espace privé\" sur les Pixels et Samsung.\n - Empêcher l\'accès aux réseaux mobiles : Vérifiez que les paramètres de protection contre le vol ainsi que ceux de localisation hors-ligne/éteint soient activés sur l\'appareil. Vous devriez également envisager d\'installer Graphene OS pour une meilleure sécurité. + Absolument pas. \n\nAucune appli tierce ne peut interférer à 100% avec le comportement normale d\'un appareil et le verrouillage d\'applis n\'est qu\'une superposition pour empêcher d\'interagir avec les applications. Il y a toujours des moyens de le contourner et ce n\'est pas un système parfait. \n\nDe même pour la fonctionnalité de protection de l\'écran de verrouillage qui détecte simplement une tentative d\'action avec les blocs de réglages réseau dans les réglages rapides et se superpose pour empêcher leur utilisation, ils sont toujours disponibles pour quiconque sur les appareils Pixels et si quelqu\'un souhaite réellement désactiver le réseau il le pourra, surtout avec un bloc de réglage du Mode Avion que cette appli ne peut bloquer. \n\nCes fonctionnalités sont expérimentales et ne devraient pas être considérées comme sûres. \n\nAlternatives sécurisées :\n - Verrouillage d\'applis : Le \"Dossier Sécurisé\" et \"Espace privé\" sur les Pixels et Samsung.\n - Empêcher l\'accès aux réseaux mobiles : Vérifiez que les paramètres de protection contre le vol ainsi que ceux de localisation hors-ligne/éteint soient activés sur l\'appareil. Vous devriez également envisager d\'installer Graphene OS pour une meilleure sécurité. Icônes de la barre d\'état - You may notice that even after resetting the statusbar icons, Some icons such as device rotation, wired headphone icons may stay visible. This is due to how the statubar blacklist is implemented in Android and how your OEM may have customized them. \nYou may need further adjustments. \n\nAlso not all icon visibility options may work as they depend on the OEM implementations and availability. + Vous avez peut-être remarqué que même après avoir réinitialisé les icônes de la barre d\'état, certaines icônes comme celles de la rotation de l\'appareil ou les écouteurs filaires peuvent rester visibles. Ceci est dû à comment la liste noire de la barre d\'état est implémenté dans Android et de comment votre constructeur les a personnalisé. \nVous pourriez avoir à effectuer plus d\'ajustements. \n\nDe plus, toutes les options de visibilité des icônes peuvent ne pas marcher car elle dépendent de l\'implémentation faite par votre constructeur et la disponibilité. L\'éclairage de notification ne marche pas Cela dépend du constructeur. Certains comme OneUI n\'ont pas l\'air d\'autoriser les superpositions au-dessus de l\'Always-On Display ce qui empêche les effets de lumière d\'être affichés. Dans ce cas, essayez l\'éclairage ambiant comme solution. La réatttribution de boutons ne marche pas quand l\'écran est éteint Certains constructeurs limitent le service d\'accessibilité quand l\'écran est éteint mais peuvent toujours marcher si le Always-On Display est actif. \nDans ce cas, vous pouvez peut-être utiliser les boutons réattribués avec l\'Always-On Display allumé, mais pas lorsqu\'il est éteint. \n\nComme solution, vous devrez utiliser la permission Shizuku et activer \"Utiliser Shizuku ou l\'accès racine (root)\" dans les paramètres de réattributions de boutons de l\'application ce qui permet d\'identifier des saisies physiques.\nCe n\'est pas garanti que cela fonctionne sur tous les appareils et a besoin d\'être testé.\n\nEt même si elle st activée, la méthode Shizuku ne sera utilisée qu\'en cas de besoin. Autrement, le service d\'accessibilité sera utilisé. Le contrôle de la luminosité de la lampe-torche ne marche pas - Only a limited number of devices got hardware and software support adjusting the flashlight intensity. \n\n\'The minimum version of Android is 13 (SDK33).\nFlashlight brightness control only supports HAL version 3.8 and higher, so among the supported devices, the latest ones (For example, Pixel 6/7, Samsung S23, etc.)\'\npolodarb/Flashlight-Tiramisu + Seul un nombre limité d\'appareil a le support matériel et logiciel pour ajuster la luminosité de lampe-torche. \n\nLa version minimum est Android 13 (SDK 33). \nLe contrôle de la luminosité de la lampe-torche supporte uniquement HAL 3.8 ou ultérieur, donc parmis les appareils supportés, les plus récents (Par exemple, Pixel 6/7, Galaxy S23, etc...)\'\npolodarb/Flashlight-Tiramisu À quoi sert cette application ? - Good question,\n\nI always wanted to extract the most out of my devices as I\'ve been a rooted user for ever since I got my first Project Treble device. And I\'ve been loving the Tasker app which is like the god when comes automation and utilizing every possible API and internal features of Android.\n\nSo I am not unrooted and back on stock Android beta experience and wanted to get the most out from what is possible with given privileges. Might as well share them. So with my beginner knowledge in Kotlin Jetpack and with the support of many research and assist tools and also the great community, I built an all-in-one app containing everything I wanted to be in my Android with given permissions. And here it is.\n\nFeature requests are welcome, I will consider and see if they are achievable with available permissions and my skills. Nowadays what is not possible. :)\n\nWhy not on Play Store?\nI don\'t wanna risk getting my Developer account banned due to the highly sensitive and internal permissions and APIs being used in the app. But with the way Android sideloading is headed, let\'s see what we have to do. I do understand the concerns of sideloaded apps being malicious.\nWhile we are at the topic, Checkout my other app AirSync if you are a mac + Android user. *shameless plug*\n\nEnjoy, Keep building! (っ◕‿◕)っ + Bonne question \n\nJ\'ai toujours voulu obtenir le plus possible de mes appareils et j\'ai toujours été un utilisateur root depuis que j\'ai obtenu mon premier appareil Project Treble. J\'ai adoré l\'application Takser qui est divine pour les automatisations et utiliser toutes les APIs et fonctionnalités internes disponibles dans Android. \n\nDonc j\'ai réinstallé Android d\'origine en bêta pour essayer de faire le plus de choses possibles avec les permissions qu\'on nous donne. Et même les partager. Donc avec mes connaissances de débutant en Kotlin Jetpack et avec de nombreuses recherches et d\'outils d\'assistance ainsi que la superbe communauté, j\'ai construit une application tout-en-un qui contiendrait tout ce que je veux dans mon Android avec les permissions disponibles. Et voilà.\n\nLes demandes de nouvelles fonctionnalités sont disponibles, je les étudierai et je les implémenterai dans la mesure du possible et de mes compétences. De nos jours, qu\'est-ce qui est impossible ? :)\n\nPourquoi ne pas être sur le Play Store ?\nJe ne veux pas risquer le bannissement de mon compte développeur car l\'application demande l\'accès à des permissions très sensibles . Je comprends que certains peuvent se méfier d\'application externes qui pourraient être malicieuses.\nD\'ailleurs en parlant de ça, jetez un coup d\'oeil à mon autre appli AirSync si vous êtes un utilisateur Mac + Android.\n\nProfitez, bon bricolage! (っ◕‿◕)っ Signaler un bogue Signalement copié dans le presse-papiers @@ -773,7 +831,7 @@ Soyez notifiés lorsque vous arrivez à une destination Destination Définir destination - Traçage : %1$.4f%2$.4f + Suivi : %1$.4f%2$.4f Aucune destination définie Ouvrez Google Maps, choisissez un endroit et partagez-le dans Essentials. Rayon : %d m @@ -787,7 +845,7 @@ Vous êtes arrivé à destination. Actuellement à %1$.1fkm Supprimer la destination - Processing location… + Traitement de la position… DISTANCE RESTANTE Calcul… Arrêter le traçage @@ -797,51 +855,51 @@ Supprimer Aucune destination Ouvrir Maps - Full-Screen Alarm Permission - Required to wake your device upon arrival. Tap to grant. + Permission d\'alarme en plein écran + Requise pour réveiller votre appareil à l\'arrivée. Appuyez pour autoriser. %1$d m %1$.1f km - Travel Alarm active - %1$s remaining (%2$d%%) - Travel Progress - Shows real-time distance to destination + Alarme de trajet active + %1$s restant + Progrès du trajet + Affiche la distance de la destination en temps réel Destination à proximité - Prepare to get off - Dismiss - Destination set: %1$.4f, %2$.4f + Préparer à partir + Ignorer + Destination définie : %1$.4f, %2$.4f Utiliser l\'accès root (racine) À la place de Shizuku L\'accès racine (root) n\'est pas disponible. Merci de vérifier votre manager de root. - Keyboard - Keys - Customize layout and behavior - Keyboard Height - Adjust the total vertical size of the keyboard - Bottom Padding - Add space below the keyboard - Haptic Feedback - Vibrate on key press - Test the keyboard - Test the keyboard - Keyboard Height - Bottom Padding - Haptic Feedback - Key Roundness - Key Roundness - Adjust the corner radius of keys - Move functions to bottom - Functions side padding - Haptic feedback strength - Keyboard shape - Round - Flat + Clavier + Touches + Personnaliser l\'apparence et le comportement + Hauteur du clavier + Ajuster la taille verticale totale du clavier + Écart du bas + Ajouter de l\'espace en dessous du clavier + Retour haptique + Vibrer à l\'appui sur une touche + Tester le clavier + Tester le clavier + Hauteur du clavier + Écart du bas + Retour haptique + Arrondi des touches + Arrondi des touches + Ajuster le rayon des coins des touches + Déplacer les fonctions en bas + Écart latéral des fonctions + Force du retour haptique + Forme du clavier + Rond + Plat Inverse Batteries - Monitor your device battery levels - Battery Status - Connect to AirSync - Display battery from your connected mac device in AirSync - Download AirSync App - Required for Mac battery sync + Surveillez le niveau de batterie de vos appareils + Status de la batterie + Connecter à AirSync + Afficher la batterie de votre appareil Mac connecté dans AirSync + Télécharger l\'appli AirSync + Requis pour synchroniser la batterie d\'un Mac diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7b5279c5..78d51805 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -28,7 +28,7 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Another note, the biometric authentication prompt only lets you use STRONG secure class methods. Face unlock security methods in WEAK class in devices such as Pixel 7 will only be able to utilize the available other STRONG auth methods such as fingerprint or pin. Abilita la rimappatura dei tasti - Usa Shizuku + Use Shizuku Works with screen off (Recommended) Shizuku non è in esecuzione Rilevato %1$s @@ -53,6 +53,8 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Attiva/Disattiva il silenzioso Assistente AI Fai uno screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. Quando lo schermo è acceso, tieni premuto il tasto selezionato per avviare l\'azione selezionata. Intensità Torcia @@ -172,6 +174,12 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -300,6 +308,45 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -321,12 +368,7 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -356,6 +398,10 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -464,6 +510,8 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -477,6 +525,16 @@ Le app bloccate richiederanno l\'autenticazione all\'apertura. L\'app rimarrà s Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 3fc26021..7070ac65 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -52,6 +52,8 @@ ミュート アシスタント スクリーンショット + Cycle sound modes + Toggle media volume 画面がオフのときに、選択したボタンを長押しすると、割り当てられたアクションが実行されます。Pixelデバイスでは、システムの制限により、このアクションはAODがオンの場合にのみ実行されます。 画面がオンのときに、選択したボタンを長押しすると、割り当てられたアクションがトリガーされます。 フラッシュライトの明るさ @@ -171,6 +173,12 @@ 動作中 停止中 NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging オン オフ @@ -270,12 +278,12 @@ クイック設定タイルでサイレントモードを切り替える スリープしないタイル クイック設定タイルで充電中にスリープするかを切り替える - Show Bluetooth devices - Display battery level of connected Bluetooth devices - Limit max devices - Adjust max devices visible in widget - Widget background - Show widget background + Bluetoothデバイスを表示 + 接続されているBluetoothデバイスのバッテリー残量を表示する + デバイスの上限 + ウィジェットに表示するデバイスの最大数を調整する + バックグラウンドウィジェット + バックグラウンドでウィジェットを表示する トリガーオートメーション トリガーされるアクションをスケジュールする @@ -299,6 +307,45 @@ 生体認証でアプリを保護 フリーズ あまり使わないアプリを無効にする + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes ウィジェットの触覚フィードバック ウィジェットをタップしたときの触覚フィードバックを選択する @@ -320,12 +367,7 @@ 新しい通知が届いたらフラッシュライトをゆっくり点滅させる 下を向いているときのみ デバイスが下を向いているときのみフラッシュライトを点滅 - デバッグ通知を無効にする - 永続的なADB/USBデバッグの通知を無効にする - ファイル転送通知を無効にする - 永続的なUSBファイル転送通知を無効にする - 充電通知を無効にする - システムからの充電通知を無効にする + No system channels discovered yet. They will appear here once detected. UIブラー システム全体のUIぼかしを切り替える バブル @@ -355,6 +397,10 @@ ライト点滅 通知が来たときにライトを点滅させるか切り替える 開発者向けオプションのスリープモードにしないを切り替える + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option ボタンリマップを有効にする 音量ボタンの機能を再設定する機能を使うか切り替える リマップ触覚フィードバック @@ -463,6 +509,8 @@ 暗い壁紙 このアクションでは、壁紙を暗くするために、ShizukuまたはRootが必要です。 トリガーを選択 + アプリ + Automate based on open app ステータスを選択 アクションを選択 条件と合うとき @@ -474,69 +522,79 @@ 有効 無効 オートメーションサービス - Automations Active - Monitoring system events for your automations + オートメーション動作中 + 自動化システムイベントを監視中です + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna - The all-in-one toolbox for your Pixel and Androids - Hello from Essentials + あなたのPixelとAndroidのオールインワンツールボックス + Essentialsからこんにちは - System - Custom - App specific + システム + カスタム + アプリ固有 - Authentication failed - This is a beta version. Please report any bugs. - Long press an app in the grid to add a shortcut - App not found or uninstalled + 認証失敗 + これはベータ版です。バグがありましたらご報告ください。 + グリッド内のアプリを長押ししてショートカットを作成します + アプリが見つからないかアンインストールされています - App Updates - Notifications for new app updates - Update available - Unknown + アプリアップデート + 新しいアップデートの通知 + アップデートがあります + 不明 5G 4G 3G Shizuku (Rikka) Shizuku (TuoZi) - Search - Required to hard-lock the device when unauthorized network changes are attempted on lock screen. - Authenticate to access settings - Authenticate to enable this feature - Authenticate to disable this feature - %1$s Settings - feature - settings - hide - show - visibility - Error loading apps: %1$s + 検索 + ロック画面で不正なネットワーク変更が試みられた場合、デバイスをハードロックする必要があります。 + 設定にアクセスするための認証 + この機能を有効化するための認証 + この機能を無効化するための認証 + %1$s設定 + 機能 + 設定 + 非表示 + 表示 + 可視性 + アプリ読み込みエラー:%1$s - vibration - touch - feel + バイブレーション + タッチ + 感じる - network - visibility - auto - hide + ネットワーク + 可視性 + 自動 + 非表示 - restore - default - icon + 復元 + デフォルト + アイコン - keyboard - height - padding - haptic - input + キーボード + 高さ + 間隔 + 触覚 + 入力 - visible - alert + 表示 + アラート ライト @@ -617,57 +675,57 @@ 静かな - ignore - filter + 無視 + フィルター - automation - auto - lock + 自動化 + 自動 + ロック - adb - usb - debug + ADB + USB + デバッグ - battery - charge - power + バッテリー + 充電 + 電源 - blur - glass - vignette + ブラー + ガラス + ビネット - float - window - overlay + フロート + ウインドウ + オーバーレイ - always - display - clock + 常に + 画面 + 時計 - audio - mute - volume + オーディオ + ミュート + 音量 - blue - filter - auto + + フィルター + 自動 - freeze - shizuku + フリーズ + Shizuku - manual - now - shizuku + マニュアル + + Shizuku proximity @@ -690,13 +748,13 @@ You are up to date This is a pre-release version and might be unstable. Release Notes v%1$s - View on GitHub - Download APK + GitHubを見る + APKをダウンロード - None - Subtle - Double - Click + なし + 微妙 + ダブル + クリック ティック オフにする @@ -729,7 +787,7 @@ メール メールを送る メールアプリがありません - Step %1$d Image + ステップ%1$dの画像 アクセシビリティ、通知、他のアプリの上に重ねて表示権限 アクセシビリティ、通知の読み取り、オーバーレイなどの機密性の高い権限を付与しようとすると、このアクセス拒否メッセージが表示されることがあります。付与するには以下の手順を確認してください。 @@ -744,47 +802,47 @@ フリーズとは? 一時停止してアプリを邪魔することなく、バックグラウンドでアプリが動作しないようにすることで、電力を少し節約できます。あまり使用しないアプリに適しています。\n\nSNSなどの通信サービスは、フリーズを解除しない限り、緊急時に通知されないため、推奨されません。\n\nシステムアプリをフリーズするとシステムが不安定になる可能性があるため、フリーズしないことを強くおすすめします。注意して操作してください。警告しましたからね!\n\nInspired by Hail <3 アプリロックとロック画面セキュリティは本当に安全ですか? - Absolutely not. \n\nAny 3rd party application can not 100% interfere with regular device interactions and even the app lock is only an overlay above selected apps to prevent interacting with them. There are workarounds and it is not foolproof. \n\nSame goes with the screen locked security feature which detects someone trying to interact with the network tiles which for some reason are still accessible for anyone on Pixels. So if they try hard enough they might still be able to change them and especially if you have a flight mode QS tile added, this app can not prevent interactions with it. \n\nThese features are made just as experiments for light usage and would never recommend as strong security and privacy solutions. \n\nSecure alternatives:\n - App lock: Private Space and Secure folder on Pixels and Samsung\n - Preventing mobile networks access: Make sure your theft protection and offline/ power off find my device settings are on. You may look into Graphene OS as well. - Statusbar icons - You may notice that even after resetting the statusbar icons, Some icons such as device rotation, wired headphone icons may stay visible. This is due to how the statubar blacklist is implemented in Android and how your OEM may have customized them. \nYou may need further adjustments. \n\nAlso not all icon visibility options may work as they depend on the OEM implementations and availability. - Notification lighting does not work - It depends on the OEM. Some like OneUI does not seem to allow overlays above the AOD preventing the lighting effects being shown. In this case, try the ambient display as a workaround. - Button remap does not work while display is off + 絶対に安全とは限りません。\n\nサードパーティ製アプリは、通常のデバイス操作を100%妨害することはできません。アプリロックでさえ、特定のアプリとのやり取りを阻止するためのオーバーレイにすぎません。回避策としては有効でありますが、万全ではありません。\n\nロック画面セキュリティ機能も同様です。この機能は、ネットワークタイルへの操作を試みる人物を検出しますが、何らかの理由でPixelでは誰でもアクセスできます。そのため、十分に試行錯誤すれば、タイルを変更できる可能性があります。特に、機内モードのクイック設定タイルを追加している場合は、このアプリでタイルとのやり取りを阻止することはできません。\n\nこれらの機能は、軽い使用のための実験として作成されており、強力なセキュリティおよびプライバシーソリューションとして推奨されていません。\n\n安全な代替手段:\n・アプリロック:PixelやSamsungにあるプライベートスペースやセキュアフォルダ\n・ロック画面セキュリティ:盗難防止機能とオフラインや電源オフ時のデバイスを探す設定がオンになっていることを確認してください。Graphene OSも検討してみてください。 + ステータスバーアイコン + ステータスバーアイコンをリセットした後でも、デバイスの自動回転やヘッドセットアイコンなど、一部のアイコンが表示されたままになる場合があります。これは、Androidでのステータスバーのブラックリストの実装方法と、OEMによるカスタマイズが原因です。\n更に調整が必要になる場合があります。\n\nまた、OEMの実装と可用性に依存するため、全てのアイコン表示オプションが機能するとは限りません。 + 通知ライトが機能しません + OEMによって動作が異なります。OneUIなど、一部のOEMでは常時表示ディスプレイの上にオーバーレイを許可していないため、通知ライトが表示されません。この場合、回避策として、アンビエントディスプレイを試してください。 + 画面がオフのときのボタンリマップが機能しません Some OEMs limit the accessibility service reporting once the display is actually off but they may still work while the AOD is on. \nIn this case, you may able to use button remaps with AOD on but not with off. \n\nAs a workaround, you will need to use Shizuku permissions and turn on the \'Use Shizuku\' toggle in button remap settings which identifies and listen to hardware input events.\nThis is not guaranteed to work on all devices and needs testing.\n\nAnd even if it\'s on, Shizuku method only will be used when it\'s needed. Otherwise it will always fallback to Accessibility which also handles the blocking of the actual input during long press. - Flashlight brightness does not work - Only a limited number of devices got hardware and software support adjusting the flashlight intensity. \n\n\'The minimum version of Android is 13 (SDK33).\nFlashlight brightness control only supports HAL version 3.8 and higher, so among the supported devices, the latest ones (For example, Pixel 6/7, Samsung S23, etc.)\'\npolodarb/Flashlight-Tiramisu - What the hell is this app? - Good question,\n\nI always wanted to extract the most out of my devices as I\'ve been a rooted user for ever since I got my first Project Treble device. And I\'ve been loving the Tasker app which is like the god when comes automation and utilizing every possible API and internal features of Android.\n\nSo I am not unrooted and back on stock Android beta experience and wanted to get the most out from what is possible with given privileges. Might as well share them. So with my beginner knowledge in Kotlin Jetpack and with the support of many research and assist tools and also the great community, I built an all-in-one app containing everything I wanted to be in my Android with given permissions. And here it is.\n\nFeature requests are welcome, I will consider and see if they are achievable with available permissions and my skills. Nowadays what is not possible. :)\n\nWhy not on Play Store?\nI don\'t wanna risk getting my Developer account banned due to the highly sensitive and internal permissions and APIs being used in the app. But with the way Android sideloading is headed, let\'s see what we have to do. I do understand the concerns of sideloaded apps being malicious.\nWhile we are at the topic, Checkout my other app AirSync if you are a mac + Android user. *shameless plug*\n\nEnjoy, Keep building! (っ◕‿◕)っ + フラッシュライトの明るさ調整が機能しません + フラッシュライトの明るさ調整をハードウェアとソフトウェアでサポートしているデバイスは限られています。\n\n「Androidの最小バージョンは13 (SDK33) です。\nフラッシュライトの明るさ調整はHALバージョン3.8以降のみをサポートしているため、サポートされている中で最新のもの(例:Pixel 6/7、Samsung S23など)のみが対象です。」\npolodarb/Flashlight-Tiramisu + このアプリは一体何ですか? + いい質問ですね、\n\n最初のProject Trebleデバイスを入手して以来、ずっとRoot化ユーザーだったので、デバイスを最大限に活用したいと思っていました。そして、自動化やAndroidのあらゆるAPIと内部機能の活用に関しては神のようなTaskerアプリが大好きです。\n\nそのため、Root化を解除し、ストックAndroidベータ版に戻り、与えられた権限でできることを最大限に活用したいと考え、せっかくなので共有したいと思いました。そこで、Kotlin Jetpackの初心者レベルの知識と、多くの調査・支援ツール、そして素晴らしいコミュニティのサポートを受けて、Androidに欲しいものをすべて含んだ、与えられた権限でオールインワンのアプリを構築しました。そしてこれが完成形です。\n\n機能リクエストは歓迎します。利用可能な権限と私のスキルで実現可能かどうか検討します。最近は不可能なことばかりです。:)\n\nなぜPlayストアではだめなのですか?\nアプリで使用されている非常に機密性の高い内部権限とAPIが原因で、開発者アカウントが停止されるリスクがあるからです。しかし、Androidのサイドローディングの方向性を考えると、どうすればよいか見てみましょう。サイドローディングされたアプリが悪意のあるものであるという懸念は理解しています。\nついでに、MacとAndroidの両方をお使いの方は、私の別のアプリ「AirSync」もチェックしてみてください。(宣伝です)\n\nEnjoy, Keep building! (っ◕‿◕)っ - Report Bug - Bug report copied to clipboard - Bug report - Share logs - Include logs and details - Device Info - Raw Report - Open GitHub Issue - Email Report - Copy to Clipboard - Essentials Bug Report - Send via + バグを報告 + バグレポートをクリップボードにコピーしました + バグレポート + ログを共有 + ログと詳細を含める + デバイス情報 + Rawレポート + GitHub Issueを開く + メールで報告 + クリップボードにコピー + Essentials Bug Report (バグレポート) + 送信方法 Location reached Get notified when you arrive at a specific destination. - Destination - Set Destination - Tracking: %1$.4f, %2$.4f - No destination set + 目的地 + 目的地をセット + トラッキング中:%1$.4f, %2$.4f + 目的地がセットされていません Open your map app, pick a location, and share it to Essentials. Radius: %d m - Distance to trigger the alarm - Enable notification - Location - Used to detect arrival at your destination. - Background Location - Required to monitor your arrival while the app is closed or the screen is off. - Destination Reached! - You have arrived at your destination. + 知らせる距離 + 通知オン + 位置情報 + 目的地への到着を検出するために使用します。 + バックグラウンド位置情報 + アプリが閉じているときや画面がオフのときに到着を監視するために必要です。 + 目的地に到着しました! + あなたは目的地に到着しました。 Currently %1$.1f km away Clear Destination Processing location… @@ -804,16 +862,16 @@ Travel Alarm active %1$s remaining (%2$d%%) Travel Progress - Shows real-time distance to destination + 目的地までの距離をリアルタイムで表示 Destination Nearby Prepare to get off Dismiss - Destination set: %1$.4f, %2$.4f - Use Root - Instead of Shizuku - Root access not available. Please check your root manager. + 目的地をセット:%1$.4f, %2$.4f + Rootを使う + Shizukuの代わりに + Rootアクセスができません。Rootマネージャーを確認してください。 - Keyboard + キーボード Keys Customize layout and behavior Keyboard Height diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2828ec5d..72aa7386 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids @@ -802,7 +860,7 @@ %1$d m %1$.1f km Travel Alarm active - %1$s remaining (%2$d%%) + %1$s remaining Travel Progress Shows real-time distance to destination Destination Nearby diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 8d28e05a..fd16b8d5 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -10,12 +10,12 @@ Deschide Înghețarea aplicațiilor Aplicații înghețate Empty screen off widget - App Freezing + Înghețarea aplicațiilor Flashlight Pulse Check for pre-releases Might be unstable - Security + Securitate Enable app lock App Lock Security Authenticate to enable app lock @@ -29,11 +29,11 @@ Enable Button Remap Use Shizuku Works with screen off (Recommended) - Shizuku is not running - Detected %1$s - Status: %1$s - Open Shizuku - Flashlight + Shizuku nu rulează + Detectat %1$s + Stare: %1$s + Deschide Shizuku + Lanternă Flashlight options Adjust fading and other settings Pitch black theme @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -66,9 +68,9 @@ Other Always turn off flashlight Even while display is on - Settings + Setări - Notification + Notificări Show Notification Post Notifications Allows the app to show notifications @@ -124,7 +126,7 @@ Flashlight Pulse Flashlight pulse Only while facing down - Style + Stil Stroke adjustment Corner radius Stroke thickness @@ -171,8 +173,14 @@ Active Inactive NFC - On - Off + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging + Pornit + Oprit Screen locked security Screen Locked Security @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2828ec5d..ba4aba23 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,105 +1,107 @@ Essentials - BETA - Essentials Accessibility Service\n\nThis service is required for the following advanced features:\n\n• Physical Button Remapping:\nDetects volume button presses even when the screen is off to trigger actions like the Flashlight.\n\n• Per-App Settings:\nMonitors the currently active app to apply specific profiles for Dynamic Night Light, Notification Lighting Colors, and App Lock.\n\n• Screen Control:\nAllows the app to lock the screen (e.g. via Double Tap or Widgets) and detect screen state changes.\n\n• Security:\nPrevents unauthorized changes by detecting window content when the device is locked.\n\nNo input text or sensitive user data is collected or transmitted. + БЕТА + Служба специальных возможностей Essentials\n\nЭта служба необходима для следующих функций:\n\n• Переназначение физических кнопок:\nОбнаруживает нажатие кнопок громкости, даже если экран выключен, чтобы вызывать такие действия, как фонарик.\n\n• Индивидуальные настройки приложений:\nОтслеживает текущее активное приложение для применения конкретных профилей для Динамической ночной подсветки, Цвета подсветки уведомлений и блокировку приложений.\n\n• Контроль экрана:\nПозволяет приложению блокировать экран (например, через двойное нажатие или виджет) и обнаруживать изменения состояния экрана.\n\n• Безопасность:\nПредотвращает несанкционированные изменения, обнаруживая содержимое окна, когда устройство заблокировано.\n\nНикакой вводимый текст или конфиденциальные данные не собираются и не передаются. App icon App Freezing - Disable apps that are rarely used - App Freezing - Open App Freezing + Отключить редко используемые приложения + Заморозка приложений + Открыть заморозку приложений Frozen App - Empty screen off widget - App Freezing + Пустой виджет для отключения экрана + Заморозка приложений Flashlight Pulse - Check for pre-releases - Might be unstable + Проверять наличие предварительных релизов + Могут быть нестабильны - Security - Enable app lock - App Lock Security - Authenticate to enable app lock - Authenticate to disable app lock - Select locked apps - Choose which apps require authentication - Secure your apps with biometric authentication. Locked apps will require authentication when launching, Stays unlocked until the screen turns off. - Beware that this is not a robust solution as this is only a 3rd party application. If you need strong security, consider using Private Space or other such features. - Another note, the biometric authentication prompt only lets you use STRONG secure class methods. Face unlock security methods in WEAK class in devices such as Pixel 7 will only be able to utilize the available other STRONG auth methods such as fingerprint or pin. + Безопасность + Включить блокировку приложений + Защита блокировки экрана + Подтвердите свою личность, чтобы включить блокировку приложений + Подтвердите свою личность, чтобы отключить блокировку приложения + Выбрать заблокированные приложения + Выберите, какие приложения будут требовать аутентификации + Защитите свои приложения с помощью биометрической аутентификации. Заблокированные приложения требуют аутентификации при запуске и остаются разблокированными до тех пор, пока экран не погаснет. + Имейте в виду, что это не надежное решение, поскольку это всего лишь стороннее приложение. Если вам нужна надежная защита, рассмотрите возможность использования личного пространства или других подобных функций. + Обратите внимание, что запрос на биометрическую аутентификацию позволяет использовать только методы класса STRONG. Методы разблокировки по лицу относятся к классу WEAK. Поэтому устройства, например Pixel 7, смогут использовать только другие доступные методы надежной аутентификации, такие как отпечаток пальца или pin-код. - Enable Button Remap + Включить переназначение кнопок Use Shizuku - Works with screen off (Recommended) - Shizuku is not running - Detected %1$s - Status: %1$s - Open Shizuku - Flashlight - Flashlight options - Adjust fading and other settings - Pitch black theme - Use pure black background in dark mode - Haptic Feedback - Remap Long Press - Screen Off - Screen On - Volume Up - Volume Down - Toggle flashlight - Media play/pause - Media next - Media previous - Toggle vibrate - Toggle mute - AI assistant - Take screenshot - When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. - When the screen is on, long-press the selected button to trigger its assigned action. - Flashlight Intensity - Fade in and out - Smoothly toggle flashlight - Global controls + Работает если экран выключен (рекомендуется) + Shizuki не запущен + Обнаружен %1$s + Статус: %1$s + Открыть Shizuku + Фонарик + Настройки фонарика + Отрегулируйте затухание и другие настройки + Чёрная тема + Использовать чистый чёрный фон в тёмной теме + Тактильная обратная связь + Переназначить долгое нажатие + Отключить экран + Включить экран + Увеличение громкости + Уменьшение громкости + Фонарик + Воспроизведение/пауза + Следующее медиа + Предыдущее медиа + Вибрация + Без звука + AI ассистент + Сделать снимок экрана + Cycle sound modes + Toggle media volume + Когда экран выключен, нажмите и удерживайте выбранную кнопку, чтобы активировать назначенное действие. На устройствах Pixel это действие запускается только при включенном AOD из-за системных ограничений. + Когда экран включен, нажмите и удерживайте выбранную кнопку, чтобы активировать назначенное ей действие. + Яркость фонарика + Плавное включение и выключение + Плавное переключение фонарика + Глобальное управление Fade-in flashlight globally - Adjust intensity - Volume + - adjusts flashlight intensity - Live update - Show brightness in status bar - Other - Always turn off flashlight - Even while display is on - Settings + Регулировка яркости + Громкость + - регулирует яркость фонарика + Индикатор яркости + Показывает яркость в строке состояния + Другое + Всегда выключать фонарик + Даже при включенном экране + Настройки - Notification - Show Notification - Post Notifications - Allows the app to show notifications - Grant Permission - Caffeinate Active - Active - Screen is being kept awake - Ignore battery optimization - Recommended for reliable service on some devices - Abort with screen off - Timeout Presets - Select available durations for QS tile - 5m - 10m - 30m - 1h + Уведомления + Показывать уведомления + Отправка уведомлений + Разрешить приложению отправку уведомлений + Выдать разрешение + Caffeinate активен + Активен + Экран остаётся включенным + Игнорировать оптимизацию батареи + Рекомендуется для надежной работы на некоторых устройствах + Прервать если экран выключен + Предустановки времени + Выберите доступные интервалы для плитки быстрого доступа + + 10м + 30м + - Starting in %1$ds… - %1$s remaining - Persistent notification for Caffeinate + Запуск через %1$ds… + %1$s осталось + Постоянное уведомление для Caffeinate - Enable Dynamic Night Light - Apps that toggle off night light - Select apps + Включает динамическую ночную подсветку + Приложения, отключающие ночную подсветку + Выбрать приложения App Control - Freeze - Unfreeze + Заморозить + Разморозить More options - Freeze all apps - Unfreeze all apps + Заморозить все приложения + Разморозить все приложения Pick apps to freeze Choose which apps can be frozen Automation @@ -114,43 +116,43 @@ Freeze selected apps when the device locks. Choose a delay to avoid freezing apps if you unlock the screen shortly after turning it off. Freezing system apps might be dangerous and may cause unexpected behavior. Freeze when locked - Accessibility Service + Служба специальных возможностей Required to detect screen state for automatic freezing. - Enable in Settings + Включить в настройках - Only show when screen off - Skip silent notifications - Skip persistent notifications + Показывать только если экран выключен + Пропускать уведомления без звука + Пропускать постоянные уведомления Flashlight Pulse Flashlight pulse - Only while facing down + Только экраном вниз Style Stroke adjustment - Corner radius + Радиус угла Stroke thickness - Glow adjustment + Регулировка свечения Glow spread - Placement - Horizontal position - Vertical position - Indicator adjustment - Scale - Duration + Размещение + Горизонтальная позиция + Вертикальная позиция + Регулировка индикатора + Размер + Продолжительность Animation Pulse count Pulse duration - Color Mode + Цвет Ambient display Ambient display - Suitable if you are not using AOD. - Wake screen and show lighting - Show lock screen - No black overlay + Подходит, если вы не используете AOD. + Будит экран и включает подсветку + Показать экран блокировки + Без чёрного фона - Add - Already added - Requires Android 13+ - UI Blur + Добавить + Уже добавлено + Требуется Android 13+ + Размытие интерфейса Bubbles Sensitive Content Tap to Wake @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -178,17 +186,17 @@ Screen Locked Security Authenticate to enable screen locked security Authenticate to disable screen locked security - ⚠️ WARNING - This feature is not foolproof. There may be edge cases where someone still being able to interact with the tile. \nAlso keep in mind that Android will always allow to do a forced reboot and Pixels will always allow the device to be turned off from the lock screen as well. - Make sure to remove the airplane mode tile from quick settings as that is not preventable because it does not open a dialog window. - When enabled, the Quick Settings panel will be immediately closed and the device will be locked down if someone attempt to interact with Internet tiles while the device is locked. \n\nThis will also disable biometric unlock to prevent further unauthorized access. Animation scale will be reduced to 0.1x while locked to make it even harder to interact with. + ⚠️ ВНИМАНИЕ + Эта функция не является надежной. В некоторых случаях пользователь все еще может взаимодействовать с плиткой. \nТакже имейте в виду, что Android всегда разрешает принудительную перезагрузку, а Пиксели всегда позволяют отключить устройство с экрана блокировки. + Обязательно удалите плитку \"Режим полета\" из \"быстрых настроек\", так как это невозможно предотвратить, поскольку она не открывает диалоговое окно. + Если эта функция включена, панель быстрых настроек будет немедленно закрыта, а устройство заблокировано, если кто-либо попытается взаимодействовать с интернет-плитками, пока устройство заблокировано. \n\nЭто также отключит биометрическую разблокировку, чтобы предотвратить дальнейший несанкционированный доступ. Во время блокировки масштаб анимации будет уменьшен до 0.1x, что еще больше усложнит взаимодействие с устройством. - Re-order modes - Long press to toggle - Drag to reorder - Sound - Vibrate - Silent + Изменить порядок режимов + Долгое нажатие для переключения + Перетащите, чтобы изменить порядок + Звук + Вибрация + Без звука Connectivity Phone & Network @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 2828ec5d..9f19fc6b 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2828ec5d..72aa7386 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -52,6 +52,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -171,6 +173,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -299,6 +307,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback Pick haptic feedback for widget taps @@ -320,12 +367,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -355,6 +397,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -463,6 +509,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -476,6 +524,16 @@ Automation Service Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids @@ -802,7 +860,7 @@ %1$d m %1$.1f km Travel Alarm active - %1$s remaining (%2$d%%) + %1$s remaining Travel Progress Shows real-time distance to destination Destination Nearby diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a23d06cd..53c40856 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,6 +53,8 @@ Toggle mute AI assistant Take screenshot + Cycle sound modes + Toggle media volume When the screen is off, long-press the selected button to trigger its assigned action. On Pixel devices, this action only gets triggered if the AOD is on due to system limitations. When the screen is on, long-press the selected button to trigger its assigned action. Flashlight Intensity @@ -177,6 +179,12 @@ Active Inactive NFC + Private DNS + Toggle Private DNS mode + Auto + Off + USB Debugging + Toggle USB Debugging On Off @@ -256,6 +264,9 @@ Toggle visibility for %1$s + Favorites + Pin to Favorites + Unpin from Favorites @@ -316,6 +327,45 @@ Secure apps with biometrics Freeze Disable rarely used apps + Watermark + Add EXIF data and logos to photos + Style + Overlay + Frame + Device Brand + EXIF Data + Pick Image + Image saved to gallery + Failed to save image + Processing... + Share + EXIF Settings + Focal Length + Aperture + ISO + Shutter Speed + Date & Time + Move to Top + Align Left + Brand Size + Data Size + Text Size + Font Size + Custom Text + Enter your text... + Custom Text Settings + Spacing + Border Width + Round Corners + Color + Light + Dark + Accent Light + Accent Dark + Logo + Show Logo + Logo Size + Save Changes Widget Haptic feedback @@ -338,12 +388,7 @@ Slowly pulse flashlight for new notifications Only while facing down Pulse flashlight only when device is face down - Disable debugging notifications - Hide persistent ADB/USB debugging notifications - Disable file transfer notification - Hide persistent USB file transfer notifications - Disable charging notification - Hide system charging notifications + No system channels discovered yet. They will appear here once detected. UI Blur Toggle system-wide UI blur Bubbles @@ -373,6 +418,10 @@ Flashlight Pulse Toggle notification flashlight pulse Toggle stay awake developer option + Private DNS + Cycle Private DNS modes (Off/Auto/Hostname) + USB Debugging + Toggle USB Debugging developer option Enable Button Remap Master toggle for volume button remapping Remap Haptic Feedback @@ -492,6 +541,8 @@ Dim Wallpaper This action requires Shizuku or Root to adjust system wallpaper dimming. Select Trigger + App + Automate based on open app Select State Select Action In Action @@ -507,6 +558,17 @@ Automations Active Monitoring system events for your automations + Device Effects + Disable Device Effects + Control system-level effects like grayscale, AOD suppression, wallpaper dimming, and night mode. + Grayscale + Suppress Ambient Display + Dim Wallpaper + Night Mode + This feature requires Android 15 or higher. + Enabled + Disabled + Sameera Wijerathna The all-in-one toolbox for your Pixel and Androids @@ -863,7 +925,7 @@ %1$d m %1$.1f km Travel alarm active - %1$s remaining + %1$s remaining (%2$d%%) Travel Progress Shows real-time distance to destination Destination Nearby