Skip to content

Commit

Permalink
Feat: Add quick settings tile for LED control (#36)
Browse files Browse the repository at this point in the history
* Led control using quicksettings tile

* Update application description and features, bump version to 1.3.0

-   Updated full description: Enhanced details about LED control, safety mechanisms, "Eye Destroyer" mode with cooldown, Quick Settings tile, and the "Secret Action".
-   Updated README: Added "Eye Destroyer" mode's safety features, Quick Settings Tile functionality and "Secret Action".
-   Bumped app `versionCode` to 27 and `versionName` to 1.3.0.
-   Added a new screenshot `screenshot3.png`.
- Added Obtainium download badge to README.

* Update README.md

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
  • Loading branch information
Bartixxx32 and sourcery-ai[bot] authored Jan 20, 2025
1 parent 1b1f245 commit f5a8d72
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 181 deletions.
4 changes: 2 additions & 2 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,52 @@ OnePlus Flash Control is an Android application designed specifically for rooted

By leveraging root access, the app allows direct modification of system-level files to control the LED behavior, enabling a customized and enhanced flashlight experience on your OnePlus device.

## Support project

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/bartixxx32)
## Features

- **Master Brightness Control:** Adjust overall LED brightness with a single slider.
- **Individual LED Control:** Independently control the brightness of white and yellow LEDs.
- **"Eye Destroyer" Mode:** A feature to momentarily turn on the LEDs at maximum brightness for both white and yellow LEDs. Use with caution as it emits very intense light!
- **"Eye Destroyer" Mode:** A feature to momentarily turn on the LEDs at maximum brightness for both white and yellow LEDs.
- ⚠️ Use with caution as it emits very intense light and generates significant heat! ⚠️
- Safety features to prevent prolonged high brightness usage:
- If brightness exceeds 120 (on a scale of 0-255) for more than 20 seconds, it automatically reduces back to the default Android brightness value to prevent overheating or damage.
- "Eye Destroyer" Mode enforces a 5-second cooldown between uses to ensure safety and prevent excessive heat buildup.

- **Compatibility:** Designed for rooted OnePlus devices only.
- **Quad LED Control:** Control up to four LEDs on supported OnePlus devices (OnePlus 9 series and newer). Currently in development:
- Support for controlling additional LED modules (warm and cool tones)
- Brightness synchronization across all LEDs
- **Quick Settings Tile:** Conveniently control LED brightness and state using a Quick Settings tile:
- Single tap cycles through brightness levels.
- Double tap when LEDs are off to turn them on at 80% brightness.
- Double tap when LEDs are on to turn them off.
- **Secret Action:** Unlock hidden features by tapping the app's title 5 times within 5 seconds, enabling a fun experimental mode.


Testing needed: If you have a OnePlus 9/10/11 series device, please help by:
- Confirming the number and types of LED modules on your device
- Testing brightness control functionality and reporting any issues
- Sharing your device model and OxygenOS/Custom ROM version in GitHub Issues
**Testing needed:** If you have a OnePlus 9/10/11/12 series device, please help by:
- Confirming the number and types of LED modules on your device
- Testing brightness control functionality and reporting any issues
- Sharing your device model and OxygenOS/Custom ROM version in GitHub Issues

## Preview Screenshot

[![Preview Screenshot](https://ik.imagekit.io/bartixxx32/ghmirror/tr:w-0.2,r-20/Bartixxx32/Opflashcontrol-app/master/metadata/en-US/images/phoneScreenshots/screenshot1.png)](https://ik.imagekit.io/bartixxx32/ghmirror/Bartixxx32/Opflashcontrol-app/master/metadata/en-US/images/phoneScreenshots/screenshot1.png)
[![Preview Screenshot](https://ik.imagekit.io/bartixxx32/ghmirror/tr:w-0.2,r-20/Bartixxx32/Opflashcontrol-app/master/metadata/en-US/images/phoneScreenshots/screenshot2.png)](https://ik.imagekit.io/bartixxx32/ghmirror/Bartixxx32/Opflashcontrol-app/master/metadata/en-US/images/phoneScreenshots/screenshot1.png)
[![Preview Screenshot](https://ik.imagekit.io/bartixxx32/ghmirror/tr:w-0.2,r-20/Bartixxx32/Opflashcontrol-app/master/metadata/en-US/images/phoneScreenshots/screenshot3.png)](https://ik.imagekit.io/bartixxx32/ghmirror/Bartixxx32/Opflashcontrol-app/master/metadata/en-US/images/phoneScreenshots/screenshot3.png)

## Support project

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/bartixxx32)


## Download

[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.bartixxx.opflashcontrol/)

[<img src="https://www.openapk.net/images/badge_obtainium.png"
alt="Get it on Obtainium"
height="80">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.bartixxx.opflashcontrol%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2FBartixxx32%2FOpflashcontrol-app%22%2C%22author%22%3A%22Bartixxx32%22%2C%22name%22%3A%22OnePlus%20Flash%20Control%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)

[<img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png"
alt="Get it on Obtainium"
height="80">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.bartixxx.opflashcontrol%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2FBartixxx32%2FOpflashcontrol-app%22%2C%22author%22%3A%22Bartixxx32%22%2C%22name%22%3A%22OnePlus%20Flash%20Control%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)

[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.bartixxx.opflashcontrol/)

Or download the latest APK from the [Releases Section](https://github.com/Bartixxx32/Opflashcontrol-app/releases/latest).
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ android {
minSdk = 31
targetSdk = 35

versionCode = 26
versionName = "1.2.3"
versionCode = 27
versionName = "1.3.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
android:supportsRtl="true"
android:theme="@style/Theme.Opflashcontrol"
tools:targetApi="31" >
<service
android:name=".LEDControlTileService"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<activity
android:name=".SupportersActivity"
android:screenOrientation="portrait"
Expand Down
131 changes: 0 additions & 131 deletions app/src/main/java/com/bartixxx/opflashcontrol/BaseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import android.util.Log
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.slider.Slider
import java.io.DataOutputStream
import java.io.IOException

abstract class BaseActivity : AppCompatActivity() {

Expand All @@ -26,19 +24,12 @@ abstract class BaseActivity : AppCompatActivity() {
const val FLASH_YELLOW_LED_PATH = "/sys/class/leds/led:flash_1/brightness"
const val FLASH_WHITE2_LED_PATH = "/sys/class/leds/led:flash_2/brightness"
const val FLASH_YELLOW2_LED_PATH = "/sys/class/leds/led:flash_3/brightness"
val TOGGLE_PATHS = listOf(
"/sys/class/leds/led:switch_2/brightness"
)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}

// Helper method to sanitize brightness values (ensuring no 0 values are written)
private fun sanitizeBrightness(brightness: Int): Int {
return if (brightness == 0) 1 else brightness
}

protected fun setupSlider(
slider: Slider,
Expand Down Expand Up @@ -74,126 +65,4 @@ abstract class BaseActivity : AppCompatActivity() {
}
})
}

protected fun controlLeds(
action: String,
whiteLedPath: String,
yellowLedPath: String,
white2LedPath: String? = null,
yellow2LedPath: String? = null,
whiteBrightness: Int,
yellowBrightness: Int,
white2Brightness: Int = 0,
yellow2Brightness: Int = 0,
showToast: Boolean = true
) {
// Sanitize the brightness values to ensure no 0 values are written
val sanitizedWhiteBrightness = sanitizeBrightness(whiteBrightness)
val sanitizedYellowBrightness = sanitizeBrightness(yellowBrightness)
val sanitizedWhite2Brightness = sanitizeBrightness(white2Brightness)
val sanitizedYellow2Brightness = sanitizeBrightness(yellow2Brightness)

val commands = mutableListOf<String>()

if (action == "on") {
commands.addAll(commonOnCommands(whiteLedPath, yellowLedPath, white2LedPath, yellow2LedPath))
commands.addAll(listOf(
"echo $sanitizedWhiteBrightness > $whiteLedPath",
"echo $sanitizedYellowBrightness > $yellowLedPath",
white2LedPath?.let { "echo $sanitizedWhite2Brightness > $it" },
yellow2LedPath?.let { "echo $sanitizedYellow2Brightness > $it" }
).filterNotNull())
TOGGLE_PATHS.forEach { commands.add("echo 255 > $it") }
} else if (action == "off") {
commands.addAll(commonOffCommands(whiteLedPath, yellowLedPath, white2LedPath, yellow2LedPath))
}

executeRootCommands(commands, showToast)
}

private fun commonOnCommands(white: String, yellow: String, white2: String?, yellow2: String?): List<String> {
return listOf(
"echo 80 > $white",
"echo 80 > $yellow",
white2?.let { "echo 80 > $it" },
yellow2?.let { "echo 80 > $it" }
).filterNotNull() + TOGGLE_PATHS.map { "echo 0 > $it" }
}

private fun commonOffCommands(white: String, yellow: String, white2: String?, yellow2: String?): List<String> {
return listOf(
"echo 80 > $white",
"echo 80 > $yellow",
white2?.let { "echo 80 > $it" },
yellow2?.let { "echo 80 > $it" }
).filterNotNull() + TOGGLE_PATHS.map { "echo 0 > $it" }
}

protected fun executeRootCommands(commands: List<String>, showToast: Boolean = true) {
val maxRetries = 3 // Maximum number of retries
val initialDelay = 1000L // Initial delay in milliseconds
val maxDelay = 8000L // Maximum delay (8 seconds) for exponential backoff
var attempt = 0
var delay = initialDelay

while (attempt < maxRetries) {
try {
commands.forEach { Log.d("LEDControlApp", "Executing command: $it") }

val process = Runtime.getRuntime().exec("su")
process.outputStream.use { outputStream ->
DataOutputStream(outputStream).use { dataOutputStream ->
val batchCommands = commands.joinToString("\n") + "\nexit\n"
dataOutputStream.writeBytes(batchCommands)
dataOutputStream.flush()
}
}
process.waitFor()

// Show toast on the main thread if allowed
if (showToast) {
runOnUiThread {
Toast.makeText(this, getString(R.string.command_executed), Toast.LENGTH_SHORT).show()
}
}
return // Exit the method if the command was successfully executed
} catch (e: IOException) {
e.printStackTrace()
if (showToast) {
runOnUiThread {
Toast.makeText(this, getString(R.string.error_io), Toast.LENGTH_LONG).show()
}
}
} catch (e: SecurityException) {
e.printStackTrace()
if (showToast) {
runOnUiThread {
Toast.makeText(this, getString(R.string.error_permission), Toast.LENGTH_LONG).show()
}
}
} catch (e: InterruptedException) {
e.printStackTrace()
if (showToast) {
runOnUiThread {
Toast.makeText(this, getString(R.string.error_interrupted), Toast.LENGTH_LONG).show()
}
}
}

// Retry mechanism with exponential backoff
attempt++
if (attempt < maxRetries) {
Log.d("LEDControlApp", "Retrying... attempt #$attempt")
Thread.sleep(delay)
delay = (delay * 2).coerceAtMost(maxDelay) // Exponential backoff
}
}

// If we've exhausted all retries, show toast on the main thread if allowed
if (showToast) {
runOnUiThread {
Toast.makeText(this, getString(R.string.error_retry_failed), Toast.LENGTH_LONG).show()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ class ExperimentalActivity : BaseActivity() {

private var lightCycleDelay: Long = 500 // Default delay
private var lightCycleBrightness: Int = 255 // Default brightness
private lateinit var ledController: LedController

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ledController = LedController(this)
binding = ActivityExperimentalBinding.inflate(layoutInflater)
setContentView(binding.root)

Expand Down Expand Up @@ -119,10 +121,10 @@ class ExperimentalActivity : BaseActivity() {
try {
Log.d("ExperimentalActivity", "Light cycle: white LED on")
// Using the brightness from the slider
controlLeds("on", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = lightCycleBrightness, yellowBrightness = 0, showToast = false)
ledController.controlLeds("on", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = lightCycleBrightness, yellowBrightness = 0, showToast = false)
sleep(lightCycleDelay) // Use slider value for delay
Log.d("ExperimentalActivity", "Light cycle: yellow LED on")
controlLeds("on", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = 0, yellowBrightness = lightCycleBrightness, showToast = false)
ledController.controlLeds("on", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = 0, yellowBrightness = lightCycleBrightness, showToast = false)
sleep(lightCycleDelay)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
Expand All @@ -142,7 +144,7 @@ class ExperimentalActivity : BaseActivity() {
Log.d("ExperimentalActivity", "Light thread interrupted")
}
lightThread = null
controlLeds("off", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = 0, yellowBrightness = 0, showToast = false) // Turn LEDs off
ledController.controlLeds("off", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = 0, yellowBrightness = 0, showToast = false) // Turn LEDs off
}

private fun startBrightnessCycle() {
Expand All @@ -157,7 +159,7 @@ class ExperimentalActivity : BaseActivity() {
try {
Log.d("ExperimentalActivity", "Brightness cycle: setting brightness to $brightness")
// Use the brightness from the slider
controlLeds("on", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = brightness, yellowBrightness = brightness, showToast = false)
ledController.controlLeds("on", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = brightness, yellowBrightness = brightness, showToast = false)
sleep(50) // Small delay for smooth transition
brightness += increment
if (brightness > 255 || brightness < 1) {
Expand All @@ -182,7 +184,7 @@ class ExperimentalActivity : BaseActivity() {
Log.d("ExperimentalActivity", "Brightness thread interrupted")
}
brightnessThread = null
controlLeds("off", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = 0, yellowBrightness = 0, showToast = false) // Turn LEDs off
ledController.controlLeds("off", WHITE_LED_PATH, YELLOW_LED_PATH, whiteBrightness = 0, yellowBrightness = 0, showToast = false) // Turn LEDs off
}

private fun navigateBackToMain() {
Expand Down
Loading

0 comments on commit f5a8d72

Please sign in to comment.