Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

val whatsNewCounter = 1
buildConfigField("int", "WHATS_NEW_COUNTER", whatsNewCounter.toString())
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,15 @@
android:exported="false"
android:foregroundServiceType="specialUse" />

<service
android:name=".services.EssentialsWearableListenerService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/" />
</intent-filter>
</service>

<receiver
android:name=".services.ScreenOffWidgetProvider"
android:exported="false"
Expand Down Expand Up @@ -680,6 +689,8 @@
<action android:name="com.sameerasw.essentials.ACTION_FLASHLIGHT_INCREASE" />
<action android:name="com.sameerasw.essentials.ACTION_FLASHLIGHT_DECREASE" />
<action android:name="com.sameerasw.essentials.ACTION_FLASHLIGHT_OFF" />
<action android:name="com.sameerasw.essentials.ACTION_FLASHLIGHT_TOGGLE" />
<action android:name="com.sameerasw.essentials.ACTION_SET_INTENSITY" />
</intent-filter>
</receiver>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class EssentialsApp : Application() {
com.sameerasw.essentials.domain.diy.DIYRepository.init(this)
com.sameerasw.essentials.services.automation.AutomationManager.init(this)
com.sameerasw.essentials.services.CalendarSyncManager.init(this)
com.sameerasw.essentials.services.DeviceInfoSyncManager.init(this)
com.sameerasw.essentials.utils.ServiceUtils.startRequiredServices(this)

initSentry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ class FeatureSettingsActivity : AppCompatActivity() {
}

if (featureId == "Watch") {
val context = androidx.compose.ui.platform.LocalContext.current
LaunchedEffect(Unit) {
watchViewModel.check(context)
}
WatchSettingsUI(
viewModel = watchViewModel,
modifier = Modifier.padding(top = 16.dp)
Expand Down Expand Up @@ -610,6 +614,15 @@ class FeatureSettingsActivity : AppCompatActivity() {
)
}

"Lock from Watch" -> {
com.sameerasw.essentials.ui.composables.configs.RemoteLockSettingsUI(
mainViewModel = viewModel,
watchViewModel = watchViewModel,
modifier = Modifier.padding(top = 16.dp),
highlightSetting = highlightSetting
)
}

"Maps power saving mode" -> {
MapsPowerSavingSettingsUI(
viewModel = viewModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class SettingsRepository(private val context: Context) {
const val KEY_CALENDAR_SYNC_ENABLED = "calendar_sync_enabled"
const val KEY_CALENDAR_SYNC_SELECTED_CALENDARS = "calendar_sync_selected_calendars"
const val KEY_CALENDAR_SYNC_PERIODIC_ENABLED = "calendar_sync_periodic_enabled"
const val KEY_REMOTE_LOCK_MODE = "remote_lock_mode" // 0: Screen off, 1: Lock

const val KEY_GITHUB_ACCESS_TOKEN = "github_access_token"
const val KEY_GITHUB_USER_PROFILE = "github_user_profile"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,21 @@ object FeatureRegistry {
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) =
viewModel.setCalendarSyncEnabled(enabled, context)
},

object : Feature(
id = "Lock from Watch",
title = R.string.feat_lock_from_watch_title,
iconRes = R.drawable.rounded_lock_24,
category = R.string.cat_tools,
description = R.string.feat_lock_from_watch_desc,
aboutDescription = R.string.feat_lock_from_watch_desc,
parentFeatureId = "Watch",
hasMoreSettings = true,
showToggle = false
) {
override fun isEnabled(viewModel: MainViewModel) = true
override fun onToggle(viewModel: MainViewModel, context: Context, enabled: Boolean) {}
},


object : Feature(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.sameerasw.essentials.services

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.hardware.camera2.CameraManager
import com.sameerasw.essentials.utils.FlashlightUtil
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable

object DeviceInfoSyncManager {
private const val TAG = "DeviceInfoSyncManager"
private const val SYNC_PATH = "/device_info"
private val handler = Handler(Looper.getMainLooper())
private var isInitialized = false

private var isTorchOn = false
private var torchLevel = 1
private var maxTorchLevel = 1
private var isIntensitySupported = false

private val torchCallback = object : CameraManager.TorchCallback() {
override fun onTorchModeChanged(cameraId: String, enabled: Boolean) {
val context = currentContext ?: return
val primaryId = FlashlightUtil.getCameraId(context)
if (cameraId == primaryId) {
isTorchOn = enabled

var level = FlashlightUtil.getCurrentLevel(context, cameraId)
// Fallback to last known intensity if system returns default level 1
if (enabled && level <= 1) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
level = prefs.getInt("flashlight_last_intensity", level)
}

torchLevel = level
maxTorchLevel = FlashlightUtil.getMaxLevel(context, cameraId)
isIntensitySupported = FlashlightUtil.isIntensitySupported(context, cameraId)
syncDeviceInfo(context)
}
}

override fun onTorchStrengthLevelChanged(cameraId: String, newStrengthLevel: Int) {
val context = currentContext ?: return
val primaryId = FlashlightUtil.getCameraId(context)
if (cameraId == primaryId) {
torchLevel = newStrengthLevel
syncDeviceInfo(context)
}
}
}

private val syncRunnable = object : Runnable {
override fun run() {
syncDeviceInfo(currentContext ?: return)
handler.postDelayed(this, 5 * 60 * 1000) // Sync every 5 minutes
}
}

private var currentContext: Context? = null

fun init(context: Context) {
if (isInitialized) return
currentContext = context.applicationContext
isInitialized = true

// Initial sync
syncDeviceInfo(context)

// Start periodic sync
handler.postDelayed(syncRunnable, 5 * 60 * 1000)

// Sync on battery change
context.registerReceiver(object : android.content.BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
syncDeviceInfo(context)
}
}, IntentFilter(Intent.ACTION_BATTERY_CHANGED))

// Sync on ringer mode change
context.registerReceiver(object : android.content.BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
syncDeviceInfo(context)
}
}, IntentFilter(android.media.AudioManager.RINGER_MODE_CHANGED_ACTION))

// Sync on flashlight change
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
cameraManager.registerTorchCallback(torchCallback, handler)

// Get initial flashlight state
val id = FlashlightUtil.getCameraId(context)
if (id != null) {
isIntensitySupported = FlashlightUtil.isIntensitySupported(context, id)
maxTorchLevel = FlashlightUtil.getMaxLevel(context, id)
torchLevel = FlashlightUtil.getCurrentLevel(context, id)
}
}

private val syncDebouncer = Runnable {
currentContext?.let { performSync(it) }
}

private fun syncDeviceInfo(context: Context) {
handler.removeCallbacks(syncDebouncer)
handler.postDelayed(syncDebouncer, 250L)
}

fun forceSync(context: Context) {
handler.removeCallbacks(syncDebouncer)
performSync(context)
}

private fun performSync(context: Context) {
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
context.registerReceiver(null, ifilter)
}

val level: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
val batteryPct = if (level != -1 && scale != -1) (level / scale.toFloat() * 100).toInt() else -1

val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
val plugged: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL ||
plugged == BatteryManager.BATTERY_PLUGGED_AC ||
plugged == BatteryManager.BATTERY_PLUGGED_USB ||
plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS

val putDataMapReq = PutDataMapRequest.create(SYNC_PATH)
val ringerMode = (context.getSystemService(Context.AUDIO_SERVICE) as? android.media.AudioManager)?.ringerMode ?: 2
val deviceName = android.provider.Settings.Global.getString(context.contentResolver, android.provider.Settings.Global.DEVICE_NAME)
?: android.os.Build.MODEL

val dataMap = putDataMapReq.dataMap
dataMap.putInt("battery_level", batteryPct)
dataMap.putBoolean("is_charging", isCharging)
dataMap.putBoolean("flashlight_on", isTorchOn)
dataMap.putInt("flashlight_level", torchLevel)
dataMap.putInt("flashlight_max_level", maxTorchLevel)
dataMap.putBoolean("flashlight_intensity_supported", isIntensitySupported)
dataMap.putInt("ringer_mode", ringerMode)
dataMap.putString("device_name", deviceName)
dataMap.putLong("timestamp", System.currentTimeMillis())

val putDataReq = putDataMapReq.asPutDataRequest()
putDataReq.setUrgent()

Wearable.getDataClient(context).putDataItem(putDataReq)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.sameerasw.essentials.services

import android.util.Log
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.WearableListenerService

class EssentialsWearableListenerService : WearableListenerService() {
companion object {
private const val TAG = "EssentialsWearableListener"
private const val PATH_REQUEST_SYNC = "/request_device_info_sync"
}

override fun onMessageReceived(messageEvent: MessageEvent) {
super.onMessageReceived(messageEvent)

when (messageEvent.path) {
PATH_REQUEST_SYNC -> {
DeviceInfoSyncManager.forceSync(this)
}
"/toggle_flashlight" -> {
val intent = android.content.Intent(this, com.sameerasw.essentials.services.receivers.FlashlightActionReceiver::class.java).apply {
action = com.sameerasw.essentials.services.receivers.FlashlightActionReceiver.ACTION_TOGGLE
}
sendBroadcast(intent)
}
"/set_flashlight_intensity" -> {
val intensity = try {
String(messageEvent.data).toInt()
} catch (e: Exception) {
1
}
val intent = android.content.Intent(this, com.sameerasw.essentials.services.receivers.FlashlightActionReceiver::class.java).apply {
action = com.sameerasw.essentials.services.receivers.FlashlightActionReceiver.ACTION_SET_INTENSITY
putExtra(com.sameerasw.essentials.services.receivers.FlashlightActionReceiver.EXTRA_INTENSITY, intensity)
}
sendBroadcast(intent)
}
"/toggle_sound_mode" -> {
com.sameerasw.essentials.services.handlers.SoundModeHandler(this).cycleNextMode()
}
"/lock_device" -> {
val repository = com.sameerasw.essentials.data.repository.SettingsRepository(this)
val mode = repository.getInt(com.sameerasw.essentials.data.repository.SettingsRepository.KEY_REMOTE_LOCK_MODE, 0)

if (mode == 1) {
// Device Admin Lock
val dpm = getSystemService(android.content.Context.DEVICE_POLICY_SERVICE) as android.app.admin.DevicePolicyManager
val adminComponent = android.content.ComponentName(this, com.sameerasw.essentials.services.receivers.SecurityDeviceAdminReceiver::class.java)
if (dpm.isAdminActive(adminComponent)) {
dpm.lockNow()
}
} else {
// Accessibility Lock
val intent = android.content.Intent(this, com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService::class.java).apply {
action = "LOCK_SCREEN"
}
startService(intent)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
"FORCE_TURN_OFF_AOD" -> {
aodForceTurnOffHandler.forceTurnOff()
}

FlashlightActionReceiver.ACTION_TOGGLE,
FlashlightActionReceiver.ACTION_OFF,
FlashlightActionReceiver.ACTION_SET_INTENSITY,
FlashlightActionReceiver.ACTION_INCREASE,
FlashlightActionReceiver.ACTION_DECREASE -> {
flashlightHandler.handleIntent(intent)
}
}
}
}
Expand All @@ -120,6 +128,11 @@ class ScreenOffAccessibilityService : AccessibilityService(), SensorEventListene
addAction(InputEventListenerService.ACTION_VOLUME_LONG_PRESSED)
addAction("SHOW_AMBIENT_GLANCE")
addAction("FORCE_TURN_OFF_AOD")
addAction(FlashlightActionReceiver.ACTION_TOGGLE)
addAction(FlashlightActionReceiver.ACTION_OFF)
addAction(FlashlightActionReceiver.ACTION_SET_INTENSITY)
addAction(FlashlightActionReceiver.ACTION_INCREASE)
addAction(FlashlightActionReceiver.ACTION_DECREASE)
}
registerReceiver(screenReceiver, filter, RECEIVER_EXPORTED)

Expand Down
Loading