try adding widget; add previews to each composable

This commit is contained in:
Kavish Devar
2024-12-16 16:13:52 +05:30
parent ff6c72ffa6
commit 34ace1fc6e
33 changed files with 302 additions and 571 deletions

View File

@@ -0,0 +1,254 @@
@file:Suppress("unused")
package me.kavishdevar.aln.utils
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
enum class Enums(val value: ByteArray) {
NOISE_CANCELLATION(Capabilities.NOISE_CANCELLATION),
CONVERSATION_AWARENESS(Capabilities.CONVERSATION_AWARENESS),
CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY(Capabilities.CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY),
PREFIX(byteArrayOf(0x04, 0x00, 0x04, 0x00)),
SETTINGS(byteArrayOf(0x09, 0x00)),
SUFFIX(byteArrayOf(0x00, 0x00, 0x00)),
NOTIFICATION_FILTER(byteArrayOf(0x0f)),
HANDSHAKE(byteArrayOf(0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)),
SPECIFIC_FEATURES(byteArrayOf(0x4d)),
SET_SPECIFIC_FEATURES(PREFIX.value + SPECIFIC_FEATURES.value + byteArrayOf(0x00,
0xff.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)),
REQUEST_NOTIFICATIONS(PREFIX.value + NOTIFICATION_FILTER.value + byteArrayOf(0x00, 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())),
NOISE_CANCELLATION_PREFIX(PREFIX.value + SETTINGS.value + NOISE_CANCELLATION.value),
NOISE_CANCELLATION_OFF(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.OFF.value + SUFFIX.value),
NOISE_CANCELLATION_ON(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.ON.value + SUFFIX.value),
NOISE_CANCELLATION_TRANSPARENCY(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.TRANSPARENCY.value + SUFFIX.value),
NOISE_CANCELLATION_ADAPTIVE(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.ADAPTIVE.value + SUFFIX.value),
SET_CONVERSATION_AWARENESS_OFF(PREFIX.value + SETTINGS.value + CONVERSATION_AWARENESS.value + Capabilities.ConversationAwareness.OFF.value + SUFFIX.value),
SET_CONVERSATION_AWARENESS_ON(PREFIX.value + SETTINGS.value + CONVERSATION_AWARENESS.value + Capabilities.ConversationAwareness.ON.value + SUFFIX.value),
CONVERSATION_AWARENESS_RECEIVE_PREFIX(PREFIX.value + byteArrayOf(0x4b, 0x00, 0x02, 0x00));
}
object BatteryComponent {
const val LEFT = 4
const val RIGHT = 2
const val CASE = 8
}
object BatteryStatus {
const val CHARGING = 1
const val NOT_CHARGING = 2
const val DISCONNECTED = 4
}
@Parcelize
data class Battery(val component: Int, val level: Int, val status: Int) : Parcelable {
fun getComponentName(): String? {
return when (component) {
BatteryComponent.LEFT -> "LEFT"
BatteryComponent.RIGHT -> "RIGHT"
BatteryComponent.CASE -> "CASE"
else -> null
}
}
fun getStatusName(): String? {
return when (status) {
BatteryStatus.CHARGING -> "CHARGING"
BatteryStatus.NOT_CHARGING -> "NOT_CHARGING"
BatteryStatus.DISCONNECTED -> "DISCONNECTED"
else -> null
}
}
}
enum class NoiseControlMode {
OFF, NOISE_CANCELLATION, TRANSPARENCY, ADAPTIVE
}
class AirPodsNotifications {
companion object {
const val AIRPODS_CONNECTED = "me.kavishdevar.aln.AIRPODS_CONNECTED"
const val AIRPODS_DATA = "me.kavishdevar.aln.AIRPODS_DATA"
const val EAR_DETECTION_DATA = "me.kavishdevar.aln.EAR_DETECTION_DATA"
const val ANC_DATA = "me.kavishdevar.aln.ANC_DATA"
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED"
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.aln.AIRPODS_CONNECTION_DETECTED"
const val DISCONNECT_RECEIVERS = "me.kavishdevar.aln.DISCONNECT_RECEIVERS"
}
class EarDetection {
private val notificationBit = Capabilities.EAR_DETECTION
private val notificationPrefix = Enums.PREFIX.value + notificationBit
var status: List<Byte> = listOf(0x01, 0x01)
fun setStatus(data: ByteArray) {
status = listOf(data[6], data[7])
}
fun isEarDetectionData(data: ByteArray): Boolean {
if (data.size != 8) {
return false
}
val prefixHex = notificationPrefix.joinToString("") { "%02x".format(it) }
val dataHex = data.joinToString("") { "%02x".format(it) }
return dataHex.startsWith(prefixHex)
}
}
class ANC {
private val notificationPrefix = Enums.NOISE_CANCELLATION_PREFIX.value
var status: Int = 1
private set
fun isANCData(data: ByteArray): Boolean {
if (data.size != 11) {
return false
}
val prefixHex = notificationPrefix.joinToString("") { "%02x".format(it) }
val dataHex = data.joinToString("") { "%02x".format(it) }
return dataHex.startsWith(prefixHex)
}
fun setStatus(data: ByteArray) {
status = data[7].toInt()
}
val name: String =
when (status) {
1 -> "OFF"
2 -> "ON"
3 -> "TRANSPARENCY"
4 -> "ADAPTIVE"
else -> "UNKNOWN"
}
}
class BatteryNotification {
private var first: Battery = Battery(BatteryComponent.LEFT, 0, BatteryStatus.DISCONNECTED)
private var second: Battery = Battery(BatteryComponent.RIGHT, 0, BatteryStatus.DISCONNECTED)
private var case: Battery = Battery(BatteryComponent.CASE, 0, BatteryStatus.DISCONNECTED)
fun isBatteryData(data: ByteArray): Boolean {
if (data.size != 22) {
return false
}
return data[0] == 0x04.toByte() && data[1] == 0x00.toByte() && data[2] == 0x04.toByte() &&
data[3] == 0x00.toByte() && data[4] == 0x04.toByte() && data[5] == 0x00.toByte()
}
fun setBattery(data: ByteArray) {
first = if (data[10].toInt() == BatteryStatus.DISCONNECTED) {
Battery(first.component, first.level, data[10].toInt())
} else {
Battery(data[7].toInt(), data[9].toInt(), data[10].toInt())
}
second = if (data[15].toInt() == BatteryStatus.DISCONNECTED) {
Battery(second.component, second.level, data[15].toInt())
} else {
Battery(data[12].toInt(), data[14].toInt(), data[15].toInt())
}
case = if (data[20].toInt() == BatteryStatus.DISCONNECTED && case.status != BatteryStatus.DISCONNECTED) {
Battery(case.component, case.level, data[20].toInt())
} else {
Battery(data[17].toInt(), data[19].toInt(), data[20].toInt())
}
}
fun getBattery(): List<Battery> {
val left = if (first.component == BatteryComponent.LEFT) first else second
val right = if (first.component == BatteryComponent.LEFT) second else first
return listOf(left, right, case)
}
}
class ConversationalAwarenessNotification {
private val NOTIFICATION_PREFIX = Enums.CONVERSATION_AWARENESS_RECEIVE_PREFIX.value
var status: Byte = 0
private set
fun isConversationalAwarenessData(data: ByteArray): Boolean {
if (data.size != 10) {
return false
}
val prefixHex = NOTIFICATION_PREFIX.joinToString("") { "%02x".format(it) }
val dataHex = data.joinToString("") { "%02x".format(it) }
return dataHex.startsWith(prefixHex)
}
fun setData(data: ByteArray) {
status = data[9]
}
}
}
class Capabilities {
companion object {
val NOISE_CANCELLATION = byteArrayOf(0x0d)
val CONVERSATION_AWARENESS = byteArrayOf(0x28)
val CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY = byteArrayOf(0x01, 0x02)
val EAR_DETECTION = byteArrayOf(0x06)
}
enum class NoiseCancellation(val value: ByteArray) {
OFF(byteArrayOf(0x01)),
ON(byteArrayOf(0x02)),
TRANSPARENCY(byteArrayOf(0x03)),
ADAPTIVE(byteArrayOf(0x04));
}
enum class ConversationAwareness(val value: ByteArray) {
OFF(byteArrayOf(0x02)),
ON(byteArrayOf(0x01));
}
}
enum class LongPressPackets(val value: ByteArray) {
ENABLE_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0F, 0x00, 0x00, 0x00)),
DISABLE_OFF_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
DISABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)),
DISABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
DISABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)),
ENABLE_OFF_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
ENABLE_OFF_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
ENABLE_OFF_FROM_TRANSPARENCY_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
DISABLE_TRANSPARENCY_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
DISABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)),
DISABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0a, 0x00, 0x00, 0x00)),
DISABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)),
ENABLE_TRANSPARENCY_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
ENABLE_TRANSPARENCY_FROM_ADAPTIVE_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
ENABLE_TRANSPARENCY_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0D, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x05, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0c, 0x00, 0x00, 0x00)),
DISABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x09, 0x00, 0x00, 0x00)),
ENABLE_ANC_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
ENABLE_ANC_FROM_ADAPTIVE_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
ENABLE_ANC_FROM_OFF_AND_ADAPTIVE(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_EVERYTHING(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x07, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x05, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x03, 0x00, 0x00, 0x00)),
ENABLE_ADAPTIVE_FROM_OFF_AND_TRANSPARENCY(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0d, 0x00, 0x00, 0x00)),
ENABLE_ADAPTIVE_FROM_TRANSPARENCY_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0e, 0x00, 0x00, 0x00)),
ENABLE_ADAPTIVE_FROM_OFF_AND_ANC(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0b, 0x00, 0x00, 0x00)),
ENABLE_EVERYTHING_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0E, 0x00, 0x00, 0x00)),
DISABLE_TRANSPARENCY_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0A, 0x00, 0x00, 0x00)),
DISABLE_ADAPTIVE_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x06, 0x00, 0x00, 0x00)),
DISABLE_ANC_OFF_DISABLED(byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1A, 0x0C, 0x00, 0x00, 0x00)),
}

View File

@@ -0,0 +1,170 @@
package me.kavishdevar.aln.utils
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PixelFormat
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.VideoView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.kavishdevar.aln.R
@SuppressLint("InflateParams", "ClickableViewAccessibility")
class Window (context: Context) {
private val mView: View
@Suppress("DEPRECATION")
@SuppressLint("NewApi")
private val mParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
height = WindowManager.LayoutParams.WRAP_CONTENT
width = WindowManager.LayoutParams.MATCH_PARENT
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
format = PixelFormat.TRANSLUCENT
gravity = Gravity.BOTTOM
dimAmount = 0.3f
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_FULLSCREEN or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_DIM_BEHIND or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
}
private val mWindowManager: WindowManager
init {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
mView = layoutInflater.inflate(R.layout.popup_window, null)
mParams.x = 0
mParams.y = 0
mParams.gravity = Gravity.BOTTOM
mView.setOnClickListener(View.OnClickListener {
close()
})
mView.findViewById<ImageButton>(R.id.close_button)
.setOnClickListener {
close()
}
val ll = mView.findViewById<LinearLayout>(R.id.linear_layout)
ll.setOnClickListener {
close()
}
@Suppress("DEPRECATION")
mView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
mView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
val touchY = event.rawY
val popupTop = mView.top
if (touchY < popupTop) {
close()
true
} else {
false
}
} else {
false
}
}
mWindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}
@SuppressLint("InlinedApi", "SetTextI18n")
fun open(name: String = "AirPods Pro", batteryNotification: AirPodsNotifications.BatteryNotification) {
try {
if (mView.windowToken == null) {
if (mView.parent == null) {
// Add the view initially off-screen
mWindowManager.addView(mView, mParams)
mView.findViewById<TextView>(R.id.name).text = name
val vid = mView.findViewById<VideoView>(R.id.video)
vid.setVideoPath("android.resource://me.kavishdevar.aln/" + R.raw.connected)
vid.resolveAdjustedSize(vid.width, vid.height)
vid.start()
vid.setOnCompletionListener {
vid.start()
}
val batteryStatus = batteryNotification.getBattery()
val batteryLeftText = mView.findViewById<TextView>(R.id.left_battery)
val batteryRightText = mView.findViewById<TextView>(R.id.right_battery)
val batteryCaseText = mView.findViewById<TextView>(R.id.case_battery)
batteryLeftText.text = batteryStatus.find { it.component == BatteryComponent.LEFT }?.let {
"\uDBC3\uDC8E ${it.level}%"
} ?: ""
batteryRightText.text = batteryStatus.find { it.component == BatteryComponent.RIGHT }?.let {
"\uDBC3\uDC8D ${it.level}%"
} ?: ""
batteryCaseText.text = batteryStatus.find { it.component == BatteryComponent.CASE }?.let {
"\uDBC3\uDE6C ${it.level}%"
} ?: ""
// Slide-up animation
val displayMetrics = mView.context.resources.displayMetrics
val screenHeight = displayMetrics.heightPixels
mView.translationY = screenHeight.toFloat() // Start below the screen
ObjectAnimator.ofFloat(mView, "translationY", 0f).apply {
duration = 500 // Animation duration in milliseconds
interpolator = DecelerateInterpolator() // Smooth deceleration
start()
}
CoroutineScope(MainScope().coroutineContext).launch {
delay(12000)
close()
}
}
}
} catch (e: Exception) {
Log.d("PopupService", e.toString())
}
}
fun close() {
try {
ObjectAnimator.ofFloat(mView, "translationY", mView.height.toFloat()).apply {
duration = 500 // Animation duration in milliseconds
interpolator = AccelerateInterpolator() // Smooth acceleration
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
try {
mWindowManager.removeView(mView)
} catch (e: Exception) {
Log.d("PopupService", e.toString())
}
}
})
start()
}
} catch (e: Exception) {
Log.d("PopupService", e.toString())
}
}
}