This commit is contained in:
Kavish Devar
2025-01-28 11:19:55 +05:30
parent ae188a72dc
commit 35da57f0a5
31 changed files with 776 additions and 402 deletions

View File

@@ -16,6 +16,5 @@ indent_size = 4
trim_trailing_whitespace = false trim_trailing_whitespace = false
max_line_length = off max_line_length = off
[*.{py,java,r,R}] [*.{py,java,r,R,kt,xml,kts}]
indent_size = 4 indent_size = 4

View File

@@ -11,15 +11,19 @@
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH_PRIVILEGED" android:name="android.permission.BLUETOOTH_PRIVILEGED"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BATTERY_STATS" <uses-permission
tools:ignore="ProtectedPermissions"/> android:name="android.permission.BATTERY_STATS"
<uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.UPDATE_DEVICE_STATS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" <uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" android:usesPermissionFlags="neverForLocation"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -32,6 +36,17 @@
android:theme="@style/Theme.ALN" android:theme="@style/Theme.ALN"
tools:ignore="UnusedAttribute" tools:ignore="UnusedAttribute"
tools:targetApi="31"> tools:targetApi="31">
<receiver
android:name=".widgets.NoiseControlWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/noise_control_widget_info" />
</receiver>
<receiver <receiver
android:name=".widgets.BatteryWidget" android:name=".widgets.BatteryWidget"
android:exported="false"> android:exported="false">
@@ -93,4 +108,4 @@
</receiver> </receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -1,17 +1,17 @@
/* /*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
* *
* Copyright (C) 2024 Kavish Devar * Copyright (C) 2024 Kavish Devar
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published * it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License. * by the Free Software Foundation, either version 3 of the License.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -28,7 +28,6 @@ import android.app.Service
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
@@ -42,6 +41,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources
import android.media.AudioManager import android.media.AudioManager
import android.os.BatteryManager import android.os.BatteryManager
import android.os.Binder import android.os.Binder
@@ -51,6 +51,7 @@ import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.os.ParcelUuid import android.os.ParcelUuid
import android.util.Log import android.util.Log
import android.util.TypedValue
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
@@ -79,18 +80,22 @@ import me.kavishdevar.aln.utils.LongPressPackets
import me.kavishdevar.aln.utils.MediaController import me.kavishdevar.aln.utils.MediaController
import me.kavishdevar.aln.utils.Window import me.kavishdevar.aln.utils.Window
import me.kavishdevar.aln.widgets.BatteryWidget import me.kavishdevar.aln.widgets.BatteryWidget
import me.kavishdevar.aln.widgets.NoiseControlWidget
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
object ServiceManager { object ServiceManager {
private var service: AirPodsService? = null private var service: AirPodsService? = null
@Synchronized @Synchronized
fun getService(): AirPodsService? { fun getService(): AirPodsService? {
return service return service
} }
@Synchronized @Synchronized
fun setService(service: AirPodsService?) { fun setService(service: AirPodsService?) {
this.service = service this.service = service
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Synchronized @Synchronized
fun restartService(context: Context) { fun restartService(context: Context) {
@@ -108,12 +113,14 @@ object ServiceManager {
} }
// @Suppress("unused") // @Suppress("unused")
class AirPodsService: Service() { class AirPodsService : Service() {
private var macAddress = "" private var macAddress = ""
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getService(): AirPodsService = this@AirPodsService fun getService(): AirPodsService = this@AirPodsService
} }
private lateinit var sharedPreferencesLogs: SharedPreferences
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private val packetLogKey = "packet_log" private val packetLogKey = "packet_log"
private val _packetLogsFlow = MutableStateFlow<Set<String>>(emptySet()) private val _packetLogsFlow = MutableStateFlow<Set<String>>(emptySet())
@@ -121,24 +128,26 @@ class AirPodsService: Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
sharedPreferences = getSharedPreferences("packet_logs", MODE_PRIVATE) sharedPreferencesLogs = getSharedPreferences("packet_logs", MODE_PRIVATE)
} }
private fun logPacket(packet: ByteArray, source: String) { private fun logPacket(packet: ByteArray, source: String) {
val packetHex = packet.joinToString(" ") { "%02X".format(it) } val packetHex = packet.joinToString(" ") { "%02X".format(it) }
val logEntry = "$source: $packetHex" val logEntry = "$source: $packetHex"
val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf() val logs =
sharedPreferencesLogs.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet()
?: mutableSetOf()
logs.add(logEntry) logs.add(logEntry)
_packetLogsFlow.value = logs _packetLogsFlow.value = logs
sharedPreferences.edit { putStringSet(packetLogKey, logs) } sharedPreferencesLogs.edit { putStringSet(packetLogKey, logs) }
} }
fun getPacketLogs(): Set<String> { fun getPacketLogs(): Set<String> {
return sharedPreferences.getStringSet(packetLogKey, emptySet()) ?: emptySet() return sharedPreferencesLogs.getStringSet(packetLogKey, emptySet()) ?: emptySet()
} }
private fun clearPacketLogs() { private fun clearPacketLogs() {
sharedPreferences.edit { remove(packetLogKey).apply() } sharedPreferencesLogs.edit { remove(packetLogKey).apply() }
} }
@@ -163,18 +172,22 @@ class AirPodsService: Service() {
} }
@Suppress("ClassName") @Suppress("ClassName")
private object bluetoothReceiver: BroadcastReceiver() { private object bluetoothReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent) { override fun onReceive(context: Context?, intent: Intent) {
val bluetoothDevice = val bluetoothDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java) intent.getParcelableExtra(
"android.bluetooth.device.extra.DEVICE",
BluetoothDevice::class.java
)
} else { } else {
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE") as BluetoothDevice? intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE") as BluetoothDevice?
} }
val action = intent.action val action = intent.action
val context = context?.applicationContext val context = context?.applicationContext
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)?.getString("name", bluetoothDevice?.name) val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
?.getString("name", bluetoothDevice?.name)
if (bluetoothDevice != null && action != null && !action.isEmpty()) { if (bluetoothDevice != null && action != null && !action.isEmpty()) {
Log.d("AirPodsService", "Received bluetooth connection broadcast") Log.d("AirPodsService", "Received bluetooth connection broadcast")
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) { if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
@@ -204,109 +217,27 @@ class AirPodsService: Service() {
private lateinit var earReceiver: BroadcastReceiver private lateinit var earReceiver: BroadcastReceiver
var widgetMobileBatteryEnabled = false var widgetMobileBatteryEnabled = false
val METADATA_UNTETHERED_LEFT_CHARGING = 13 object BatteryChangedIntentReceiver : BroadcastReceiver() {
val METADATA_UNTETHERED_LEFT_BATTERY = 10
val METADATA_UNTETHERED_RIGHT_CHARGING = 14
val METADATA_UNTETHERED_RIGHT_BATTERY = 11
val METADATA_UNTETHERED_CASE_CHARGING = 15
val METADATA_UNTETHERED_CASE_BATTERY = 12
@SuppressLint("MissingPermission")
fun setBatteryLevels(
leftStatus: Boolean, leftLevel: Int,
rightStatus: Boolean, rightLevel: Int,
caseStatus: Boolean, caseLevel: Int,
device: BluetoothDevice
) {
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothDevice;")
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"setMetadata",
METADATA_UNTETHERED_LEFT_CHARGING,
leftStatus.toString().toByteArray()
)
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"setMetadata",
METADATA_UNTETHERED_LEFT_BATTERY,
leftLevel.toString().toByteArray()
)
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"setMetadata",
METADATA_UNTETHERED_RIGHT_CHARGING,
rightStatus.toString().toByteArray()
)
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"setMetadata",
METADATA_UNTETHERED_RIGHT_BATTERY,
rightLevel.toString().toByteArray()
)
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"setMetadata",
METADATA_UNTETHERED_CASE_CHARGING,
caseStatus.toString().toByteArray()
)
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"setMetadata",
METADATA_UNTETHERED_CASE_BATTERY,
caseLevel.toString().toByteArray()
)
HiddenApiBypass.invoke(
BluetoothDevice::class.java,
device,
"sendVendorSpecificHeadsetEvent",
"+IPHONEACCEV",
BluetoothHeadset.AT_CMD_TYPE_SET,
1,
leftLevel,
2,
rightLevel,
3,
caseLevel
)
// Prepare the intent to broadcast vendor-specific headset event
val intent = Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, "+IPHONEACCEV")
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, BluetoothHeadset.AT_CMD_TYPE_SET)
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arrayOf(
1, leftLevel,
2, rightLevel,
3, caseLevel
))
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
putExtra(BluetoothDevice.EXTRA_NAME, device.name)
addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + 76)
}
// Send the broadcast to update the battery levels
sendBroadcast(intent)
// Broadcast battery level changes
val batteryIntent = Intent("android.bluet9ooth.device.action.BATTERY_LEVEL_CHANGED").apply {
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
putExtra("android.bluetooth.device.extra.BATTERY_LEVEL", leftLevel) // Update with appropriate levels
}
sendBroadcast(batteryIntent)
}
object PhoneBatteryReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) { override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_CHANGED) { if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
ServiceManager.getService()?.updateBatteryWidget() val level = intent.getIntExtra("level", 0)
} val scale = intent.getIntExtra("scale", 100)
else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { val batteryPct = level * 100 / scale
val charging = intent.getIntExtra(
BatteryManager.EXTRA_STATUS,
-1
) == BatteryManager.BATTERY_STATUS_CHARGING
if (ServiceManager.getService()?.widgetMobileBatteryEnabled == true) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context!!, BatteryWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(context.packageName, R.layout.battery_widget)
remoteViews.setTextViewText(R.id.phone_battery_widget, "$batteryPct%")
remoteViews.setProgressBar(R.id.phone_battery_progress, 100, batteryPct, false)
appWidgetManager.updateAppWidget(widgetIds, remoteViews)
}
} else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
try { try {
context?.unregisterReceiver(this) context?.unregisterReceiver(this)
} catch (e: Exception) { } catch (e: Exception) {
@@ -315,35 +246,12 @@ class AirPodsService: Service() {
} }
} }
} }
val phoneBatteryIntentFilter = IntentFilter().apply {
addAction(Intent.ACTION_BATTERY_CHANGED)
addAction(AirPodsNotifications.DISCONNECT_RECEIVERS)
}
fun setPhoneBatteryInWidget(enabled: Boolean) { fun setPhoneBatteryInWidget(enabled: Boolean) {
widgetMobileBatteryEnabled = enabled widgetMobileBatteryEnabled = enabled
if (enabled) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(
PhoneBatteryReceiver,
phoneBatteryIntentFilter,
RECEIVER_EXPORTED
)
} else {
registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter)
}
} catch (e: Exception) {
e.printStackTrace()
}
} else {
try {
unregisterReceiver(PhoneBatteryReceiver)
} catch (e: Exception) {
e.printStackTrace()
}
}
updateBatteryWidget() updateBatteryWidget()
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun scanForAirPods(bluetoothAdapter: BluetoothAdapter): Flow<List<ScanResult>> = callbackFlow { fun scanForAirPods(bluetoothAdapter: BluetoothAdapter): Flow<List<ScanResult>> = callbackFlow {
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
@@ -392,7 +300,10 @@ class AirPodsService: Service() {
val notificationIntent = Intent(this, MainActivity::class.java) val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val notification = NotificationCompat.Builder(this, "background_service_status") val notification = NotificationCompat.Builder(this, "background_service_status")
@@ -434,15 +345,22 @@ class AirPodsService: Service() {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
fun updateBatteryWidget() { fun updateBatteryWidget() {
val appWidgetManager = AppWidgetManager.getInstance(this) val appWidgetManager = AppWidgetManager.getInstance(this)
val componentName = ComponentName(this, BatteryWidget::class.java) val componentName = ComponentName(this, BatteryWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName) val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(packageName, R.layout.battery_widget).also { val remoteViews = RemoteViews(packageName, R.layout.battery_widget).also {
val leftBattery = batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT } val openActivityIntent = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val rightBattery = batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT } it.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent)
val caseBattery = batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }
val leftBattery =
batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT }
val rightBattery =
batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT }
val caseBattery =
batteryNotification.getBattery().find { it.component == BatteryComponent.CASE }
it.setTextViewText( it.setTextViewText(
R.id.left_battery_widget, R.id.left_battery_widget,
@@ -501,8 +419,10 @@ class AirPodsService: Service() {
) )
if (widgetMobileBatteryEnabled) { if (widgetMobileBatteryEnabled) {
val batteryManager = getSystemService<BatteryManager>(BatteryManager::class.java) val batteryManager = getSystemService<BatteryManager>(BatteryManager::class.java)
val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) val batteryLevel =
val charging = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val charging =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING
it.setTextViewText( it.setTextViewText(
R.id.phone_battery_widget, R.id.phone_battery_widget,
"$batteryLevel%" "$batteryLevel%"
@@ -522,40 +442,105 @@ class AirPodsService: Service() {
appWidgetManager.updateAppWidget(widgetIds, remoteViews) appWidgetManager.updateAppWidget(widgetIds, remoteViews)
} }
fun updateNoiseControlWidget() {
val appWidgetManager = AppWidgetManager.getInstance(this)
val componentName = ComponentName(this, NoiseControlWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(packageName, R.layout.noise_control_widget).also {
val ancStatus = ancNotification.status
it.setInt(
R.id.widget_off_button,
"setBackgroundResource",
if (ancStatus == 1) R.drawable.widget_button_checked_shape_start else R.drawable.widget_button_shape_start
)
it.setInt(
R.id.widget_transparency_button,
"setBackgroundResource",
if (ancStatus == 3) (if (sharedPreferences.getBoolean("off_listening_mode", true)) R.drawable.widget_button_checked_shape_middle else R.drawable.widget_button_checked_shape_start) else (if (sharedPreferences.getBoolean("off_listening_mode", true)) R.drawable.widget_button_shape_middle else R.drawable.widget_button_shape_start)
)
it.setInt(
R.id.widget_adaptive_button,
"setBackgroundResource",
if (ancStatus == 4) R.drawable.widget_button_checked_shape_middle else R.drawable.widget_button_shape_middle
)
it.setInt(
R.id.widget_anc_button,
"setBackgroundResource",
if (ancStatus == 2) R.drawable.widget_button_checked_shape_end else R.drawable.widget_button_shape_end
)
it.setViewVisibility(
R.id.widget_off_button,
if (sharedPreferences.getBoolean("off_listening_mode", true)) View.VISIBLE else View.GONE
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
it.setViewLayoutMargin(
R.id.widget_transparency_button,
RemoteViews.MARGIN_START,
if (sharedPreferences.getBoolean("off_listening_mode", true)) 2f else 12f,
TypedValue.COMPLEX_UNIT_DIP
)
} else {
it.setViewPadding(
R.id.widget_transparency_button,
if (sharedPreferences.getBoolean("off_listening_mode", true)) 2.dpToPx() else 12.dpToPx(),
12.dpToPx(),
2.dpToPx(),
12.dpToPx()
)
}
}
appWidgetManager.updateAppWidget(widgetIds, remoteViews)
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun updateNotificationContent(connected: Boolean, airpodsName: String? = null, batteryList: List<Battery>? = null) { fun updateNotificationContent(
connected: Boolean,
airpodsName: String? = null,
batteryList: List<Battery>? = null
) {
val notificationManager = getSystemService(NotificationManager::class.java) val notificationManager = getSystemService(NotificationManager::class.java)
var updatedNotification: Notification? = null var updatedNotification: Notification? = null
val notificationIntent = Intent(this, MainActivity::class.java) val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
if (connected) { if (connected) {
updatedNotification = NotificationCompat.Builder(this, "background_service_status") updatedNotification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods) .setSmallIcon(R.drawable.airpods)
.setContentTitle(airpodsName) .setContentTitle(airpodsName)
.setContentText("""${batteryList?.find { it.component == BatteryComponent.LEFT }?.let { .setContentText(
if (it.status != BatteryStatus.DISCONNECTED) { """${
"L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
} else { if (it.status != BatteryStatus.DISCONNECTED) {
"" "L: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
} } else {
} ?: ""} ${batteryList?.find { it.component == BatteryComponent.RIGHT }?.let { ""
if (it.status != BatteryStatus.DISCONNECTED) { }
"R: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" } ?: ""
} else { } ${
"" batteryList?.find { it.component == BatteryComponent.RIGHT }?.let {
} if (it.status != BatteryStatus.DISCONNECTED) {
} ?: ""} ${batteryList?.find { it.component == BatteryComponent.CASE }?.let { "R: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
if (it.status != BatteryStatus.DISCONNECTED) { } else {
"Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%" ""
} else { }
"" } ?: ""
} } ${
} ?: ""}""") batteryList?.find { it.component == BatteryComponent.CASE }?.let {
.setContentIntent(pendingIntent) if (it.status != BatteryStatus.DISCONNECTED) {
"Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
} else {
""
}
} ?: ""
}""")
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE) .setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true) .setOngoing(true)
@@ -583,11 +568,13 @@ class AirPodsService: Service() {
Log.d("AirPodsService", "Service started") Log.d("AirPodsService", "Service started")
ServiceManager.setService(this) ServiceManager.setService(this)
startForegroundNotification() startForegroundNotification()
Log.d("AirPodsService", "Initializing CrossDevice") Log.d("AirPodsService", "Initializing CrossDevice")
CrossDevice.init(this) CrossDevice.init(this)
Log.d("AirPodsService", "CrossDevice initialized") Log.d("AirPodsService", "CrossDevice initialized")
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
val serviceIntentFilter = IntentFilter().apply { val serviceIntentFilter = IntentFilter().apply {
addAction("android.bluetooth.device.action.ACL_CONNECTED") addAction("android.bluetooth.device.action.ACL_CONNECTED")
addAction("android.bluetooth.device.action.ACL_DISCONNECTED") addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
@@ -601,7 +588,7 @@ class AirPodsService: Service() {
addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED") addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED")
} }
connectionReceiver = object: BroadcastReceiver() { connectionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) { if (intent?.action == AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) {
device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -611,9 +598,12 @@ class AirPodsService: Service() {
} }
val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE) val name = this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)
.getString("name", device?.name) .getString("name", device?.name)
if (this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).getString("name", null) == null) { if (this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)
.getString("name", null) == null
) {
this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).edit { this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE).edit {
putString("name", name)} putString("name", name)
}
} }
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString()) Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
if (!CrossDevice.checkAirPodsConnectionStatus()) { if (!CrossDevice.checkAirPodsConnectionStatus()) {
@@ -622,7 +612,11 @@ class AirPodsService: Service() {
connectToSocket(device!!) connectToSocket(device!!)
isConnectedLocally = true isConnectedLocally = true
macAddress = device!!.address macAddress = device!!.address
updateNotificationContent(true, name.toString(), batteryNotification.getBattery()) updateNotificationContent(
true,
name.toString(),
batteryNotification.getBattery()
)
} }
} else if (intent?.action == AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) { } else if (intent?.action == AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) {
device = null device = null
@@ -647,14 +641,11 @@ class AirPodsService: Service() {
registerReceiver(bluetoothReceiver, serviceIntentFilter) registerReceiver(bluetoothReceiver, serviceIntentFilter)
} }
widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean("show_phone_battery_in_widget", true) widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean(
if (widgetMobileBatteryEnabled) { "show_phone_battery_in_widget",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { true
registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter, RECEIVER_EXPORTED) )
} else {
registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter)
}
}
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
if (bluetoothAdapter.isEnabled) { if (bluetoothAdapter.isEnabled) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -663,10 +654,12 @@ class AirPodsService: Service() {
scanResults.forEach { scanResult -> scanResults.forEach { scanResult ->
val device = scanResult.device val device = scanResult.device
device.fetchUuidsWithSdp() device.fetchUuidsWithSdp()
val manufacturerData = scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C) val manufacturerData =
scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C)
if (manufacturerData != null && manufacturerData != lastData) { if (manufacturerData != null && manufacturerData != lastData) {
lastData = manufacturerData lastData = manufacturerData
val formattedHex = manufacturerData.joinToString(" ") { "%02X".format(it) } val formattedHex =
manufacturerData.joinToString(" ") { "%02X".format(it) }
val rssi = scanResult.rssi val rssi = scanResult.rssi
Log.d( Log.d(
"AirPodsBLEService", "AirPodsBLEService",
@@ -680,8 +673,7 @@ class AirPodsService: Service() {
bluetoothAdapter.bondedDevices.forEach { device -> bluetoothAdapter.bondedDevices.forEach { device ->
device.fetchUuidsWithSdp() device.fetchUuidsWithSdp()
if (device.uuids != null) if (device.uuids != null) {
{
if (device.uuids.contains(ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"))) { if (device.uuids.contains(ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
bluetoothAdapter.getProfileProxy( bluetoothAdapter.getProfileProxy(
this, this,
@@ -720,7 +712,10 @@ class AirPodsService: Service() {
fun manuallyCheckForAudioSource() { fun manuallyCheckForAudioSource() {
if (earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) { if (earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) {
Log.d("AirPodsService", "For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!") Log.d(
"AirPodsService",
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
)
disconnectAudio(this, device) disconnectAudio(this, device)
} }
} }
@@ -806,7 +801,13 @@ class AirPodsService: Service() {
socket.let { socket.let {
val audioManager = val audioManager =
this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
MediaController.initialize(audioManager, this@AirPodsService.getSharedPreferences("settings", MODE_PRIVATE)) MediaController.initialize(
audioManager,
this@AirPodsService.getSharedPreferences(
"settings",
MODE_PRIVATE
)
)
val buffer = ByteArray(1024) val buffer = ByteArray(1024)
val bytesRead = it.inputStream.read(buffer) val bytesRead = it.inputStream.read(buffer)
var data: ByteArray = byteArrayOf() var data: ByteArray = byteArrayOf()
@@ -904,7 +905,10 @@ class AirPodsService: Service() {
true true
) )
) { ) {
Log.d("AirPods Parser", "User put in both AirPods from just one.") Log.d(
"AirPods Parser",
"User put in both AirPods from just one."
)
MediaController.userPlayedTheMedia = false MediaController.userPlayedTheMedia = false
} }
if (newInEarData.contains(false) && inEarData == listOf( if (newInEarData.contains(false) && inEarData == listOf(
@@ -912,7 +916,10 @@ class AirPodsService: Service() {
true true
) )
) { ) {
Log.d("AirPods Parser", "User took one of two out.") Log.d(
"AirPods Parser",
"User took one of two out."
)
MediaController.userPlayedTheMedia = false MediaController.userPlayedTheMedia = false
} }
@@ -924,7 +931,10 @@ class AirPodsService: Service() {
Log.d("AirPods Parser", "hi") Log.d("AirPods Parser", "hi")
return return
} }
Log.d("AirPods Parser", "this shouldn't be run if the last log was 'hi'.") Log.d(
"AirPods Parser",
"this shouldn't be run if the last log was 'hi'."
)
inEarData = newInEarData inEarData = newInEarData
@@ -935,7 +945,7 @@ class AirPodsService: Service() {
MediaController.iPausedTheMedia = false MediaController.iPausedTheMedia = false
} }
} else { } else {
MediaController.sendPause() MediaController.sendPause()
} }
} }
} }
@@ -958,9 +968,8 @@ class AirPodsService: Service() {
CrossDevice.sendRemotePacket(data) CrossDevice.sendRemotePacket(data)
CrossDevice.ancBytes = data CrossDevice.ancBytes = data
ancNotification.setStatus(data) ancNotification.setStatus(data)
sendBroadcast(Intent(AirPodsNotifications.Companion.ANC_DATA).apply { sendANCBroadcast()
putExtra("data", ancNotification.status) updateNoiseControlWidget()
})
Log.d("AirPods Parser", "ANC: ${ancNotification.status}") Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
} else if (batteryNotification.isBatteryData(data)) { } else if (batteryNotification.isBatteryData(data)) {
CrossDevice.sendRemotePacket(data) CrossDevice.sendRemotePacket(data)
@@ -992,15 +1001,6 @@ class AirPodsService: Service() {
} else { } else {
connectAudio(this@AirPodsService, device) connectAudio(this@AirPodsService, device)
} }
// setBatteryLevels(
// batteryNotification.getBattery()[0].status == 1,
// batteryNotification.getBattery()[0].level,
// batteryNotification.getBattery()[1].status == 1,
// batteryNotification.getBattery()[1].level,
// batteryNotification.getBattery()[2].status == 1,
// batteryNotification.getBattery()[2].level,
// device
// )
} else if (conversationAwarenessNotification.isConversationalAwarenessData( } else if (conversationAwarenessNotification.isConversationalAwarenessData(
data data
) )
@@ -1090,12 +1090,15 @@ class AirPodsService: Service() {
1 -> { 1 -> {
sendPacket(Enums.NOISE_CANCELLATION_OFF.value) sendPacket(Enums.NOISE_CANCELLATION_OFF.value)
} }
2 -> { 2 -> {
sendPacket(Enums.NOISE_CANCELLATION_ON.value) sendPacket(Enums.NOISE_CANCELLATION_ON.value)
} }
3 -> { 3 -> {
sendPacket(Enums.NOISE_CANCELLATION_TRANSPARENCY.value) sendPacket(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
} }
4 -> { 4 -> {
sendPacket(Enums.NOISE_CANCELLATION_ADAPTIVE.value) sendPacket(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
} }
@@ -1107,52 +1110,118 @@ class AirPodsService: Service() {
} }
fun setOffListeningMode(enabled: Boolean) { fun setOffListeningMode(enabled: Boolean) {
sendPacket(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00)) sendPacket(
byteArrayOf(
0x04,
0x00,
0x04,
0x00,
0x09,
0x00,
0x34,
if (enabled) 0x01 else 0x02,
0x00,
0x00,
0x00
)
)
updateNoiseControlWidget()
} }
fun setAdaptiveStrength(strength: Int) { fun setAdaptiveStrength(strength: Int) {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00) val bytes =
byteArrayOf(
0x04,
0x00,
0x04,
0x00,
0x09,
0x00,
0x2E,
strength.toByte(),
0x00,
0x00,
0x00
)
sendPacket(bytes) sendPacket(bytes)
} }
fun setPressSpeed(speed: Int) { fun setPressSpeed(speed: Int) {
// 0x00 = default, 0x01 = slower, 0x02 = slowest // 0x00 = default, 0x01 = slower, 0x02 = slowest
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00) val bytes =
byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x17, speed.toByte(), 0x00, 0x00, 0x00)
sendPacket(bytes) sendPacket(bytes)
} }
fun setPressAndHoldDuration(speed: Int) { fun setPressAndHoldDuration(speed: Int) {
// 0 - default, 1 - slower, 2 - slowest // 0 - default, 1 - slower, 2 - slowest
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00) val bytes =
byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x18, speed.toByte(), 0x00, 0x00, 0x00)
sendPacket(bytes) sendPacket(bytes)
} }
fun setVolumeSwipeSpeed(speed: Int) { fun setVolumeSwipeSpeed(speed: Int) {
// 0 - default, 1 - longer, 2 - longest // 0 - default, 1 - longer, 2 - longest
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00) val bytes =
Log.d("AirPodsService", "Setting volume swipe speed to $speed by packet ${bytes.joinToString(" ") { "%02X".format(it) }}") byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x23, speed.toByte(), 0x00, 0x00, 0x00)
Log.d(
"AirPodsService",
"Setting volume swipe speed to $speed by packet ${
bytes.joinToString(" ") {
"%02X".format(
it
)
}
}"
)
sendPacket(bytes) sendPacket(bytes)
} }
fun setNoiseCancellationWithOnePod(enabled: Boolean) { fun setNoiseCancellationWithOnePod(enabled: Boolean) {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1B, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00) val bytes = byteArrayOf(
0x04,
0x00,
0x04,
0x00,
0x09,
0x00,
0x1B,
if (enabled) 0x01 else 0x02,
0x00,
0x00,
0x00
)
sendPacket(bytes) sendPacket(bytes)
} }
fun setVolumeControl(enabled: Boolean) { fun setVolumeControl(enabled: Boolean) {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x25, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00) val bytes = byteArrayOf(
0x04,
0x00,
0x04,
0x00,
0x09,
0x00,
0x25,
if (enabled) 0x01 else 0x02,
0x00,
0x00,
0x00
)
sendPacket(bytes) sendPacket(bytes)
} }
fun setToneVolume(volume: Int) { fun setToneVolume(volume: Int) {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00) val bytes =
byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x1F, volume.toByte(), 0x50, 0x00, 0x00)
sendPacket(bytes) sendPacket(bytes)
} }
val earDetectionNotification = AirPodsNotifications.EarDetection() val earDetectionNotification = AirPodsNotifications.EarDetection()
val ancNotification = AirPodsNotifications.ANC() val ancNotification = AirPodsNotifications.ANC()
val batteryNotification = AirPodsNotifications.BatteryNotification() val batteryNotification = AirPodsNotifications.BatteryNotification()
val conversationAwarenessNotification = AirPodsNotifications.ConversationalAwarenessNotification() val conversationAwarenessNotification =
AirPodsNotifications.ConversationalAwarenessNotification()
var earDetectionEnabled = true var earDetectionEnabled = true
@@ -1180,7 +1249,8 @@ class AirPodsService: Service() {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) { if (profile == BluetoothProfile.A2DP) {
try { try {
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java) val method =
proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
method.invoke(proxy, device) method.invoke(proxy, device)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -1190,14 +1260,15 @@ class AirPodsService: Service() {
} }
} }
override fun onServiceDisconnected(profile: Int) { } override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP) }, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener { bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) { if (profile == BluetoothProfile.HEADSET) {
try { try {
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java) val method =
proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
method.invoke(proxy, device) method.invoke(proxy, device)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -1207,7 +1278,7 @@ class AirPodsService: Service() {
} }
} }
override fun onServiceDisconnected(profile: Int) { } override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET) }, BluetoothProfile.HEADSET)
} }
@@ -1218,7 +1289,8 @@ class AirPodsService: Service() {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) { if (profile == BluetoothProfile.A2DP) {
try { try {
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java) val method =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
method.invoke(proxy, device) method.invoke(proxy, device)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -1228,14 +1300,15 @@ class AirPodsService: Service() {
} }
} }
override fun onServiceDisconnected(profile: Int) { } override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP) }, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener { bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) { if (profile == BluetoothProfile.HEADSET) {
try { try {
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java) val method =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
method.invoke(proxy, device) method.invoke(proxy, device)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -1245,14 +1318,16 @@ class AirPodsService: Service() {
} }
} }
override fun onServiceDisconnected(profile: Int) { } override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET) }, BluetoothProfile.HEADSET)
} }
fun setName(name: String) { fun setName(name: String) {
val nameBytes = name.toByteArray() val nameBytes = name.toByteArray()
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01, val bytes = byteArrayOf(
nameBytes.size.toByte(), 0x00) + nameBytes 0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
nameBytes.size.toByte(), 0x00
) + nameBytes
sendPacket(bytes) sendPacket(bytes)
val hex = bytes.joinToString(" ") { "%02X".format(it) } val hex = bytes.joinToString(" ") { "%02X".format(it) }
updateNotificationContent(true, name, batteryNotification.getBattery()) updateNotificationContent(true, name, batteryNotification.getBattery())
@@ -1263,7 +1338,8 @@ class AirPodsService: Service() {
var hex = "04 00 04 00 09 00 26 ${if (enabled) "01" else "02"} 00 00 00" var hex = "04 00 04 00 09 00 26 ${if (enabled) "01" else "02"} 00 00 00"
var bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray() var bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes) sendPacket(bytes)
hex = "04 00 04 00 17 00 00 00 10 00 12 00 08 E${if (enabled) "6" else "5"} 05 10 02 42 0B 08 50 10 02 1A 05 02 ${if (enabled) "32" else "00"} 00 00 00" hex =
"04 00 04 00 17 00 00 00 10 00 12 00 08 E${if (enabled) "6" else "5"} 05 10 02 42 0B 08 50 10 02 1A 05 02 ${if (enabled) "32" else "00"} 00 00 00"
bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray() bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes) sendPacket(bytes)
} }
@@ -1273,6 +1349,7 @@ class AirPodsService: Service() {
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray() val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes) sendPacket(bytes)
} }
fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int { fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int {
for (i in oldArray.indices) { for (i in oldArray.indices) {
if (oldArray[i] != newArray[i]) { if (oldArray[i] != newArray[i]) {
@@ -1281,7 +1358,12 @@ class AirPodsService: Service() {
} }
throw IllegalArgumentException("No element has changed") throw IllegalArgumentException("No element has changed")
} }
fun updateLongPress(oldLongPressArray: BooleanArray, newLongPressArray: BooleanArray, offListeningMode: Boolean) {
fun updateLongPress(
oldLongPressArray: BooleanArray,
newLongPressArray: BooleanArray,
offListeningMode: Boolean
) {
if (oldLongPressArray.contentEquals(newLongPressArray)) { if (oldLongPressArray.contentEquals(newLongPressArray)) {
return return
} }
@@ -1391,6 +1473,7 @@ class AirPodsService: Service() {
LongPressPackets.DISABLE_ANC_OFF_DISABLED.value LongPressPackets.DISABLE_ANC_OFF_DISABLED.value
} }
} }
2 -> { 2 -> {
packet = if (newLongPressArray[2]) { packet = if (newLongPressArray[2]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
@@ -1398,6 +1481,7 @@ class AirPodsService: Service() {
LongPressPackets.DISABLE_TRANSPARENCY_OFF_DISABLED.value LongPressPackets.DISABLE_TRANSPARENCY_OFF_DISABLED.value
} }
} }
3 -> { 3 -> {
packet = if (newLongPressArray[3]) { packet = if (newLongPressArray[3]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
@@ -1439,4 +1523,9 @@ class AirPodsService: Service() {
} }
super.onDestroy() super.onDestroy()
} }
} }
private fun Int.dpToPx(): Int {
val density = Resources.getSystem().displayMetrics.density
return (this * density).toInt()
}

View File

@@ -1,17 +1,17 @@
/* /*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality! * AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
* *
* Copyright (C) 2024 Kavish Devar * Copyright (C) 2024 Kavish Devar
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published * it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License. * by the Free Software Foundation, either version 3 of the License.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -24,11 +24,8 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Canvas
import android.util.Log
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.core.graphics.createBitmap
import me.kavishdevar.aln.MainActivity import me.kavishdevar.aln.MainActivity
import me.kavishdevar.aln.R import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.ServiceManager import me.kavishdevar.aln.services.ServiceManager
@@ -39,29 +36,6 @@ class BatteryWidget : AppWidgetProvider() {
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray appWidgetIds: IntArray
) { ) {
for (appWidgetId in appWidgetIds) { ServiceManager.getService()?.updateBatteryWidget()
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
updateAppWidget(context, AppWidgetManager.getInstance(context), 0)
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val service = ServiceManager.getService()
val views = RemoteViews(context.packageName, R.layout.battery_widget)
service?.updateBatteryWidget()
val openActivityIntent = PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}

View File

@@ -0,0 +1,83 @@
/*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
*
* Copyright (C) 2024 Kavish Devar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package me.kavishdevar.aln.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.ServiceManager
class NoiseControlWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
val views = RemoteViews(context.packageName, R.layout.noise_control_widget)
val offIntent = Intent(context, NoiseControlWidget::class.java).apply {
action = "ACTION_SET_ANC_MODE"
putExtra("ANC_MODE", 1)
}
val transparencyIntent = Intent(context, NoiseControlWidget::class.java).apply {
action = "ACTION_SET_ANC_MODE"
putExtra("ANC_MODE", 3)
}
val adaptiveIntent = Intent(context, NoiseControlWidget::class.java).apply {
action = "ACTION_SET_ANC_MODE"
putExtra("ANC_MODE", 4)
}
val ancIntent = Intent(context, NoiseControlWidget::class.java).apply {
action = "ACTION_SET_ANC_MODE"
putExtra("ANC_MODE", 2)
}
views.setOnClickPendingIntent(
R.id.widget_off_button,
PendingIntent.getBroadcast(context, 0, offIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
views.setOnClickPendingIntent(
R.id.widget_transparency_button,
PendingIntent.getBroadcast(context, 1, transparencyIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
views.setOnClickPendingIntent(
R.id.widget_adaptive_button,
PendingIntent.getBroadcast(context, 2, adaptiveIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
views.setOnClickPendingIntent(
R.id.widget_anc_button,
PendingIntent.getBroadcast(context, 3, ancIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
)
ServiceManager.getService()?.updateNoiseControlWidget()
appWidgetManager.updateAppWidget(appWidgetIds, views)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (intent.action == "ACTION_SET_ANC_MODE") {
val mode = intent.getIntExtra("ANC_MODE", 1)
ServiceManager.getService()?.setANCMode(mode)
}
}
}

View File

@@ -1,14 +0,0 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#121212" />
<corners android:radius="16dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#404040" />
<corners android:radius="12dp" />
</shape>
</item>
</selector>

View File

@@ -1,10 +0,0 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke
android:width="6dp"
android:color="#00ff00" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,9 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#2C2A2F" />
<corners android:topLeftRadius="4dp" android:topRightRadius="24dp" android:bottomLeftRadius="4dp" android:bottomRightRadius="24dp" />
<padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,9 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#2C2A2F" />
<corners android:radius="4dp" />
<padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,9 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#2C2A2F" />
<corners android:topLeftRadius="24dp" android:topRightRadius="4dp" android:bottomLeftRadius="24dp" android:bottomRightRadius="4dp" />
<padding android:bottom="8dp" android:left="8dp" android:right="8dp" android:top="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,16 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#3D3B40" />
<corners android:topLeftRadius="4dp" android:topRightRadius="24dp" android:bottomLeftRadius="4dp" android:bottomRightRadius="24dp" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#49474E" />
<corners android:topLeftRadius="4dp" android:topRightRadius="24dp" android:bottomLeftRadius="4dp" android:bottomRightRadius="24dp" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,16 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#3D3B40" />
<corners android:radius="4dp" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#49474E" />
<corners android:radius="4dp" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,16 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#3D3B40" />
<corners android:topLeftRadius="24dp" android:topRightRadius="4dp" android:bottomLeftRadius="24dp" android:bottomRightRadius="4dp" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#49474E" />
<corners android:topLeftRadius="24dp" android:topRightRadius="4dp" android:bottomLeftRadius="24dp" android:bottomRightRadius="4dp" />
<padding android:left="8dp" android:top="8dp" android:right="8dp" android:bottom="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,140 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.ALN.AppWidget.Container"
android:id="@+id/noise_control_widget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/Theme.ALN.AppWidgetContainer">
<LinearLayout
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/widget_off_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:background="@drawable/widget_button_shape_start"
android:clickable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="52dp"
android:layout_height="52dp"
android:src="@drawable/noise_cancellation"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:shadowColor="@color/black"
android:shadowRadius="12"
android:text="@string/off"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/widget_transparency_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:background="@drawable/widget_button_shape_middle"
android:clickable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="52dp"
android:layout_height="52dp"
android:src="@drawable/transparency"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:shadowColor="@color/black"
android:shadowRadius="12"
android:text="@string/transparency"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/widget_adaptive_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:background="@drawable/widget_button_shape_middle"
android:clickable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="52dp"
android:layout_height="52dp"
android:src="@drawable/adaptive"
android:textSize="12sp"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:shadowColor="@color/black"
android:shadowRadius="12"
android:text="@string/adaptive"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/widget_anc_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginVertical="12dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:background="@drawable/widget_button_shape_end"
android:clickable="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="52dp"
android:layout_height="52dp"
android:src="@drawable/noise_cancellation"
android:tint="@color/white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:shadowColor="@color/black"
android:shadowRadius="12"
android:text="@string/noise_cancellation"
android:textColor="@color/white"
android:textSize="12sp"
tools:ignore="NestedWeights" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@@ -37,7 +37,7 @@
android:layout_height="28dp" android:layout_height="28dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:background="@drawable/button_shape" android:background="@drawable/popup_button_shape"
android:contentDescription="Close Button" android:contentDescription="Close Button"
android:src="@drawable/close" android:src="@drawable/close"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -106,4 +106,4 @@
android:textColor="@color/popup_text" android:textColor="@color/popup_text"
android:textSize="20sp" /> android:textSize="20sp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- <!--
Having themes.xml for night-v31 because of the priority order of the resource qualifiers. Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
--> -->
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight"> <style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item> <item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item> <item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style> </style>
</resources> </resources>

View File

@@ -1,14 +1,14 @@
<resources> <resources>
<style name="Widget.ALN.AppWidget.Container" parent="android:Widget"> <style name="Widget.ALN.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item> <item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item> <item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item> <item name="android:background">@drawable/app_widget_background</item>
</style> </style>
<style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget"> <style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item> <item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item> <item name="android:background">@drawable/app_widget_inner_view_background</item>
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style> </style>
</resources> </resources>

View File

@@ -1,16 +1,16 @@
<resources> <resources>
<style name="Widget.ALN.AppWidget.Container" parent="android:Widget"> <style name="Widget.ALN.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item> <item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item> <item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item> <item name="android:background">@drawable/app_widget_background</item>
<item name="android:clipToOutline">true</item> <item name="android:clipToOutline">true</item>
</style> </style>
<style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget"> <style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item> <item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item> <item name="android:background">@drawable/app_widget_inner_view_background</item>
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:clipToOutline">true</item> <item name="android:clipToOutline">true</item>
</style> </style>
</resources> </resources>

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- <!--
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
and @android:dimen/system_app_widget_internal_padding requires API level 31 and @android:dimen/system_app_widget_internal_padding requires API level 31
--> -->
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight"> <style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item> <item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item> <item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style> </style>
</resources> </resources>

View File

@@ -1,7 +1,7 @@
<resources> <resources>
<declare-styleable name="AppWidgetAttrs"> <declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" /> <attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" /> <attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" /> <attr name="appWidgetRadius" format="dimension" />
</declare-styleable> </declare-styleable>
</resources> </resources>

View File

@@ -1,9 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="popup_background">#FFFFFF</color> <color name="popup_background">#FFFFFF</color>
<color name="popup_text">@color/black</color> <color name="popup_text">@color/black</color>
<color name="widget_background">#87FFFFFF</color> <color name="widget_background">#87FFFFFF</color>
<color name="widget_text">@color/black</color> <color name="widget_text">@color/black</color>
</resources> <color name="light_blue_50">#FFE1F5FE</color>
<color name="light_blue_200">#FF81D4FA</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- <!--
Refer to App Widget Documentation for margin information Refer to App Widget Documentation for margin information
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
--> -->
<dimen name="widget_margin">0dp</dimen> <dimen name="widget_margin">0dp</dimen>
</resources> </resources>

View File

@@ -1,42 +1,45 @@
<resources> <resources>
<string name="app_name" translatable="false">ALN</string> <string name="app_name" translatable="false">ALN</string>
<string name="title_activity_custom_device" translatable="false">GATT Testing</string> <string name="title_activity_custom_device" translatable="false">GATT Testing</string>
<string name="app_widget_description">See your AirPods battery status right from your home screen!</string> <string name="app_widget_description">See your AirPods battery status right from your home screen!</string>
<string name="accessibility">Accessibility</string> <string name="accessibility">Accessibility</string>
<string name="tone_volume">Tone Volume</string> <string name="tone_volume">Tone Volume</string>
<string name="audio">Audio</string> <string name="audio">Audio</string>
<string name="adaptive_audio">Adaptive Audio</string> <string name="adaptive_audio">Adaptive Audio</string>
<string name="adaptive_audio_description">Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.</string> <string name="adaptive_audio_description">Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.</string>
<string name="buds">Buds</string> <string name="buds">Buds</string>
<string name="case_alt">Case</string> <string name="case_alt">Case</string>
<string name="test">Test</string> <string name="test">Test</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="noise_control">Noise Control</string> <string name="noise_control">Noise Control</string>
<string name="off">Off</string> <string name="off">Off</string>
<string name="transparency">Transparency</string> <string name="transparency">Transparency</string>
<string name="adaptive">Adaptive</string> <string name="adaptive">Adaptive</string>
<string name="noise_cancellation">Noise Cancellation</string> <string name="noise_cancellation">Noise Cancellation</string>
<string name="press_and_hold_airpods">Press and Hold AirPods</string> <string name="press_and_hold_airpods">Press and Hold AirPods</string>
<string name="left">Left</string> <string name="left">Left</string>
<string name="right">Right</string> <string name="right">Right</string>
<string name="adjusts_volume">Adjusts the volume of media in response to your environment</string> <string name="adjusts_volume">Adjusts the volume of media in response to your environment</string>
<string name="conversational_awareness">Conversational Awareness</string> <string name="conversational_awareness">Conversational Awareness</string>
<string name="conversational_awareness_description">Lowers media volume and reduces background noise when you start speaking to other people.</string> <string name="conversational_awareness_description">Lowers media volume and reduces background noise when you start speaking to other people.</string>
<string name="personalized_volume">Personalized Volume</string> <string name="personalized_volume">Personalized Volume</string>
<string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string> <string name="personalized_volume_description">Adjusts the volume of media in response to your environment.</string>
<string name="less_noise">Less Noise</string> <string name="less_noise">Less Noise</string>
<string name="more_noise">More Noise</string> <string name="more_noise">More Noise</string>
<string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string> <string name="noise_cancellation_single_airpod">Noise Cancellation with Single AirPod</string>
<string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.</string> <string name="noise_cancellation_single_airpod_description">Allow AirPods to be put in noise cancellation mode when only one AirPods is in your ear.</string>
<string name="volume_control">Volume Control</string> <string name="volume_control">Volume Control</string>
<string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string> <string name="volume_control_description">Adjust the volume by swiping up or down on the sensor located on the AirPods Pro stem.</string>
<string name="airpods_not_connected">AirPods not connected</string> <string name="airpods_not_connected">AirPods not connected</string>
<string name="airpods_not_connected_description">Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)</string> <string name="airpods_not_connected_description">Please connect your AirPods to access settings. If you\'re stuck here, then try reopening the app again after closing it from the recents.\n(DO NOT KILL THE APP!)</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="app_settings">App Settings</string> <string name="app_settings">App Settings</string>
<string name="conversational_awareness_customization">Conversational Awareness</string> <string name="conversational_awareness_customization">Conversational Awareness</string>
<string name="relative_conversational_awareness_volume">Relative volume</string> <string name="relative_conversational_awareness_volume">Relative volume</string>
<string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string> <string name="relative_conversational_awareness_volume_description">Reduces to a percentage of the current volume instead of the maximum volume.</string>
<string name="conversational_awareness_pause_music">Pause Music</string> <string name="conversational_awareness_pause_music">Pause Music</string>
<string name="conversational_awareness_pause_music_description">When you start speaking, music will be paused.</string> <string name="conversational_awareness_pause_music_description">When you start speaking, music will be paused.</string>
<string name="appwidget_text">EXAMPLE</string>
<string name="add_widget">Add widget</string>
<string name="noise_control_widget_description">Control Noise Control Mode directly from your Home Screen.</string>
</resources> </resources>

View File

@@ -1,12 +1,12 @@
<resources> <resources>
<style name="Widget.ALN.AppWidget.Container" parent="android:Widget"> <style name="Widget.ALN.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item> <item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item> <item name="android:background">?android:attr/colorBackground</item>
</style> </style>
<style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget"> <style name="Widget.ALN.AppWidget.InnerView" parent="android:Widget">
<item name="android:background">?android:attr/colorBackground</item> <item name="android:background">?android:attr/colorBackground</item>
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style> </style>
</resources> </resources>

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.ALN" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.ALN" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault"> <style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<item name="appWidgetRadius">32dp</item> <item name="appWidgetRadius">32dp</item>
<item name="appWidgetPadding">0dp</item> <item name="appWidgetPadding">0dp</item>
</style> </style>
<style name="Theme.ALN.AppWidgetContainer" parent="Theme.ALN.AppWidgetContainerParent"> <style name="Theme.ALN.AppWidgetContainer" parent="Theme.ALN.AppWidgetContainerParent">
<item name="appWidgetPadding">0dp</item> <item name="appWidgetPadding">0dp</item>
</style> </style>
</resources> </resources>

View File

@@ -11,6 +11,6 @@
android:resizeMode="horizontal|vertical" android:resizeMode="horizontal|vertical"
android:targetCellWidth="3" android:targetCellWidth="3"
android:targetCellHeight="1" android:targetCellHeight="1"
android:updatePeriodMillis="300000" android:updatePeriodMillis="30000"
android:widgetCategory="home_screen|keyguard" android:widgetCategory="home_screen|keyguard"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute" />

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:description="@string/noise_control_widget_description"
android:initialKeyguardLayout="@layout/noise_control_widget"
android:initialLayout="@layout/noise_control_widget"
android:minWidth="180dp"
android:minHeight="40dp"
android:previewImage="@drawable/example_appwidget_preview"
android:previewLayout="@layout/noise_control_widget"
android:resizeMode="horizontal"
android:targetCellWidth="3"
android:targetCellHeight="1"
android:updatePeriodMillis="30000"
android:widgetCategory="home_screen|keyguard"
tools:ignore="UnusedAttribute" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB