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
max_line_length = off
[*.{py,java,r,R}]
[*.{py,java,r,R,kt,xml,kts}]
indent_size = 4

View File

@@ -11,15 +11,19 @@
<uses-permission
android:name="android.permission.BLUETOOTH_PRIVILEGED"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"
<uses-permission
android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.UPDATE_DEVICE_STATS"
tools:ignore="ProtectedPermissions" />
<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"
tools:ignore="UnusedAttribute" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -32,6 +36,17 @@
android:theme="@style/Theme.ALN"
tools:ignore="UnusedAttribute"
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
android:name=".widgets.BatteryWidget"
android:exported="false">
@@ -93,4 +108,4 @@
</receiver>
</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!
*
*
* 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/>.
*/
@@ -28,7 +28,6 @@ import android.app.Service
import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothSocket
@@ -42,6 +41,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.res.Resources
import android.media.AudioManager
import android.os.BatteryManager
import android.os.Binder
@@ -51,6 +51,7 @@ import android.os.IBinder
import android.os.Looper
import android.os.ParcelUuid
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.widget.RemoteViews
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.Window
import me.kavishdevar.aln.widgets.BatteryWidget
import me.kavishdevar.aln.widgets.NoiseControlWidget
import org.lsposed.hiddenapibypass.HiddenApiBypass
object ServiceManager {
private var service: AirPodsService? = null
@Synchronized
fun getService(): AirPodsService? {
return service
}
@Synchronized
fun setService(service: AirPodsService?) {
this.service = service
}
@OptIn(ExperimentalMaterial3Api::class)
@Synchronized
fun restartService(context: Context) {
@@ -108,12 +113,14 @@ object ServiceManager {
}
// @Suppress("unused")
class AirPodsService: Service() {
class AirPodsService : Service() {
private var macAddress = ""
inner class LocalBinder : Binder() {
fun getService(): AirPodsService = this@AirPodsService
}
private lateinit var sharedPreferencesLogs: SharedPreferences
private lateinit var sharedPreferences: SharedPreferences
private val packetLogKey = "packet_log"
private val _packetLogsFlow = MutableStateFlow<Set<String>>(emptySet())
@@ -121,24 +128,26 @@ class AirPodsService: Service() {
override fun onCreate() {
super.onCreate()
sharedPreferences = getSharedPreferences("packet_logs", MODE_PRIVATE)
sharedPreferencesLogs = getSharedPreferences("packet_logs", MODE_PRIVATE)
}
private fun logPacket(packet: ByteArray, source: String) {
val packetHex = packet.joinToString(" ") { "%02X".format(it) }
val logEntry = "$source: $packetHex"
val logs = sharedPreferences.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
val logs =
sharedPreferencesLogs.getStringSet(packetLogKey, mutableSetOf())?.toMutableSet()
?: mutableSetOf()
logs.add(logEntry)
_packetLogsFlow.value = logs
sharedPreferences.edit { putStringSet(packetLogKey, logs) }
sharedPreferencesLogs.edit { putStringSet(packetLogKey, logs) }
}
fun getPacketLogs(): Set<String> {
return sharedPreferences.getStringSet(packetLogKey, emptySet()) ?: emptySet()
return sharedPreferencesLogs.getStringSet(packetLogKey, emptySet()) ?: emptySet()
}
private fun clearPacketLogs() {
sharedPreferences.edit { remove(packetLogKey).apply() }
sharedPreferencesLogs.edit { remove(packetLogKey).apply() }
}
@@ -163,18 +172,22 @@ class AirPodsService: Service() {
}
@Suppress("ClassName")
private object bluetoothReceiver: BroadcastReceiver() {
private object bluetoothReceiver : BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent) {
val bluetoothDevice =
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 {
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE") as BluetoothDevice?
}
val action = intent.action
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()) {
Log.d("AirPodsService", "Received bluetooth connection broadcast")
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
@@ -204,109 +217,27 @@ class AirPodsService: Service() {
private lateinit var earReceiver: BroadcastReceiver
var widgetMobileBatteryEnabled = false
val METADATA_UNTETHERED_LEFT_CHARGING = 13
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() {
object BatteryChangedIntentReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
ServiceManager.getService()?.updateBatteryWidget()
}
else if (intent.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
val level = intent.getIntExtra("level", 0)
val scale = intent.getIntExtra("scale", 100)
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 {
context?.unregisterReceiver(this)
} 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) {
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()
}
@SuppressLint("MissingPermission")
fun scanForAirPods(bluetoothAdapter: BluetoothAdapter): Flow<List<ScanResult>> = callbackFlow {
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
@@ -392,7 +300,10 @@ class AirPodsService: Service() {
val notificationIntent = Intent(this, MainActivity::class.java)
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")
@@ -434,15 +345,22 @@ class AirPodsService: Service() {
)
}
@OptIn(ExperimentalMaterial3Api::class)
fun updateBatteryWidget() {
val appWidgetManager = AppWidgetManager.getInstance(this)
val componentName = ComponentName(this, BatteryWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(packageName, R.layout.battery_widget).also {
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 }
val openActivityIntent = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
it.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent)
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(
R.id.left_battery_widget,
@@ -501,8 +419,10 @@ class AirPodsService: Service() {
)
if (widgetMobileBatteryEnabled) {
val batteryManager = getSystemService<BatteryManager>(BatteryManager::class.java)
val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val charging = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING
val batteryLevel =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val charging =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS) == BatteryManager.BATTERY_STATUS_CHARGING
it.setTextViewText(
R.id.phone_battery_widget,
"$batteryLevel%"
@@ -522,40 +442,105 @@ class AirPodsService: Service() {
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)
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)
var updatedNotification: Notification? = null
val notificationIntent = Intent(this, MainActivity::class.java)
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) {
updatedNotification = NotificationCompat.Builder(this, "background_service_status")
.setSmallIcon(R.drawable.airpods)
.setContentTitle(airpodsName)
.setContentText("""${batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
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.CASE }?.let {
if (it.status != BatteryStatus.DISCONNECTED) {
"Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
} else {
""
}
} ?: ""}""")
.setContentIntent(pendingIntent)
.setContentText(
"""${
batteryList?.find { it.component == BatteryComponent.LEFT }?.let {
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.CASE }?.let {
if (it.status != BatteryStatus.DISCONNECTED) {
"Case: ${if (it.status == BatteryStatus.CHARGING) "⚡" else ""} ${it.level}%"
} else {
""
}
} ?: ""
}""")
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
@@ -583,11 +568,13 @@ class AirPodsService: Service() {
Log.d("AirPodsService", "Service started")
ServiceManager.setService(this)
startForegroundNotification()
Log.d("AirPodsService", "Initializing CrossDevice")
CrossDevice.init(this)
Log.d("AirPodsService", "CrossDevice initialized")
sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
val serviceIntentFilter = IntentFilter().apply {
addAction("android.bluetooth.device.action.ACL_CONNECTED")
addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
@@ -601,7 +588,7 @@ class AirPodsService: Service() {
addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED")
}
connectionReceiver = object: BroadcastReceiver() {
connectionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == AirPodsNotifications.Companion.AIRPODS_CONNECTION_DETECTED) {
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)
.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 {
putString("name", name)}
putString("name", name)
}
}
Log.d("AirPodsQuickSwitchServices", CrossDevice.isAvailable.toString())
if (!CrossDevice.checkAirPodsConnectionStatus()) {
@@ -622,7 +612,11 @@ class AirPodsService: Service() {
connectToSocket(device!!)
isConnectedLocally = true
macAddress = device!!.address
updateNotificationContent(true, name.toString(), batteryNotification.getBattery())
updateNotificationContent(
true,
name.toString(),
batteryNotification.getBattery()
)
}
} else if (intent?.action == AirPodsNotifications.Companion.AIRPODS_DISCONNECTED) {
device = null
@@ -647,14 +641,11 @@ class AirPodsService: Service() {
registerReceiver(bluetoothReceiver, serviceIntentFilter)
}
widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean("show_phone_battery_in_widget", true)
if (widgetMobileBatteryEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter, RECEIVER_EXPORTED)
} else {
registerReceiver(PhoneBatteryReceiver, phoneBatteryIntentFilter)
}
}
widgetMobileBatteryEnabled = getSharedPreferences("settings", MODE_PRIVATE).getBoolean(
"show_phone_battery_in_widget",
true
)
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
if (bluetoothAdapter.isEnabled) {
CoroutineScope(Dispatchers.IO).launch {
@@ -663,10 +654,12 @@ class AirPodsService: Service() {
scanResults.forEach { scanResult ->
val device = scanResult.device
device.fetchUuidsWithSdp()
val manufacturerData = scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C)
val manufacturerData =
scanResult.scanRecord?.manufacturerSpecificData?.get(0x004C)
if (manufacturerData != null && manufacturerData != lastData) {
lastData = manufacturerData
val formattedHex = manufacturerData.joinToString(" ") { "%02X".format(it) }
val formattedHex =
manufacturerData.joinToString(" ") { "%02X".format(it) }
val rssi = scanResult.rssi
Log.d(
"AirPodsBLEService",
@@ -680,8 +673,7 @@ class AirPodsService: Service() {
bluetoothAdapter.bondedDevices.forEach { device ->
device.fetchUuidsWithSdp()
if (device.uuids != null)
{
if (device.uuids != null) {
if (device.uuids.contains(ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
bluetoothAdapter.getProfileProxy(
this,
@@ -720,7 +712,10 @@ class AirPodsService: Service() {
fun manuallyCheckForAudioSource() {
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)
}
}
@@ -806,7 +801,13 @@ class AirPodsService: Service() {
socket.let {
val 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 bytesRead = it.inputStream.read(buffer)
var data: ByteArray = byteArrayOf()
@@ -904,7 +905,10 @@ class AirPodsService: Service() {
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
}
if (newInEarData.contains(false) && inEarData == listOf(
@@ -912,7 +916,10 @@ class AirPodsService: Service() {
true
)
) {
Log.d("AirPods Parser", "User took one of two out.")
Log.d(
"AirPods Parser",
"User took one of two out."
)
MediaController.userPlayedTheMedia = false
}
@@ -924,7 +931,10 @@ class AirPodsService: Service() {
Log.d("AirPods Parser", "hi")
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
@@ -935,7 +945,7 @@ class AirPodsService: Service() {
MediaController.iPausedTheMedia = false
}
} else {
MediaController.sendPause()
MediaController.sendPause()
}
}
}
@@ -958,9 +968,8 @@ class AirPodsService: Service() {
CrossDevice.sendRemotePacket(data)
CrossDevice.ancBytes = data
ancNotification.setStatus(data)
sendBroadcast(Intent(AirPodsNotifications.Companion.ANC_DATA).apply {
putExtra("data", ancNotification.status)
})
sendANCBroadcast()
updateNoiseControlWidget()
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
} else if (batteryNotification.isBatteryData(data)) {
CrossDevice.sendRemotePacket(data)
@@ -992,15 +1001,6 @@ class AirPodsService: Service() {
} else {
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(
data
)
@@ -1090,12 +1090,15 @@ class AirPodsService: Service() {
1 -> {
sendPacket(Enums.NOISE_CANCELLATION_OFF.value)
}
2 -> {
sendPacket(Enums.NOISE_CANCELLATION_ON.value)
}
3 -> {
sendPacket(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
}
4 -> {
sendPacket(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
}
@@ -1107,52 +1110,118 @@ class AirPodsService: Service() {
}
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) {
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)
}
fun setPressSpeed(speed: Int) {
// 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)
}
fun setPressAndHoldDuration(speed: Int) {
// 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)
}
fun setVolumeSwipeSpeed(speed: Int) {
// 0 - default, 1 - longer, 2 - longest
val bytes = 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) }}")
val bytes =
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)
}
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)
}
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)
}
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)
}
val earDetectionNotification = AirPodsNotifications.EarDetection()
val ancNotification = AirPodsNotifications.ANC()
val batteryNotification = AirPodsNotifications.BatteryNotification()
val conversationAwarenessNotification = AirPodsNotifications.ConversationalAwarenessNotification()
val conversationAwarenessNotification =
AirPodsNotifications.ConversationalAwarenessNotification()
var earDetectionEnabled = true
@@ -1180,7 +1249,8 @@ class AirPodsService: Service() {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
try {
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
val method =
proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1190,14 +1260,15 @@ class AirPodsService: Service() {
}
}
override fun onServiceDisconnected(profile: Int) { }
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
try {
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
val method =
proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1207,7 +1278,7 @@ class AirPodsService: Service() {
}
}
override fun onServiceDisconnected(profile: Int) { }
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET)
}
@@ -1218,7 +1289,8 @@ class AirPodsService: Service() {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
try {
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
val method =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1228,14 +1300,15 @@ class AirPodsService: Service() {
}
}
override fun onServiceDisconnected(profile: Int) { }
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
try {
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
val method =
proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
method.invoke(proxy, device)
} catch (e: Exception) {
e.printStackTrace()
@@ -1245,14 +1318,16 @@ class AirPodsService: Service() {
}
}
override fun onServiceDisconnected(profile: Int) { }
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.HEADSET)
}
fun setName(name: String) {
val nameBytes = name.toByteArray()
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
nameBytes.size.toByte(), 0x00) + nameBytes
val bytes = byteArrayOf(
0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
nameBytes.size.toByte(), 0x00
) + nameBytes
sendPacket(bytes)
val hex = bytes.joinToString(" ") { "%02X".format(it) }
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 bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
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()
sendPacket(bytes)
}
@@ -1273,6 +1349,7 @@ class AirPodsService: Service() {
val bytes = hex.split(" ").map { it.toInt(16).toByte() }.toByteArray()
sendPacket(bytes)
}
fun findChangedIndex(oldArray: BooleanArray, newArray: BooleanArray): Int {
for (i in oldArray.indices) {
if (oldArray[i] != newArray[i]) {
@@ -1281,7 +1358,12 @@ class AirPodsService: Service() {
}
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)) {
return
}
@@ -1391,6 +1473,7 @@ class AirPodsService: Service() {
LongPressPackets.DISABLE_ANC_OFF_DISABLED.value
}
}
2 -> {
packet = if (newLongPressArray[2]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
@@ -1398,6 +1481,7 @@ class AirPodsService: Service() {
LongPressPackets.DISABLE_TRANSPARENCY_OFF_DISABLED.value
}
}
3 -> {
packet = if (newLongPressArray[3]) {
LongPressPackets.ENABLE_EVERYTHING_OFF_DISABLED.value
@@ -1439,4 +1523,9 @@ class AirPodsService: Service() {
}
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!
*
*
* 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/>.
*/
@@ -24,11 +24,8 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.util.Log
import android.widget.RemoteViews
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.core.graphics.createBitmap
import me.kavishdevar.aln.MainActivity
import me.kavishdevar.aln.R
import me.kavishdevar.aln.services.ServiceManager
@@ -39,29 +36,6 @@ class BatteryWidget : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
updateAppWidget(context, AppWidgetManager.getInstance(context), 0)
ServiceManager.getService()?.updateBatteryWidget()
}
}
@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_marginTop="12dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_shape"
android:background="@drawable/popup_button_shape"
android:contentDescription="Close Button"
android:src="@drawable/close"
app:layout_constraintEnd_toEndOf="parent"
@@ -106,4 +106,4 @@
android:textColor="@color/popup_text"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
<!--
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">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
<!--
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
-->
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>
<style name="Theme.ALN.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
</style>
</resources>

View File

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

View File

@@ -1,9 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="popup_background">#FFFFFF</color>
<color name="popup_text">@color/black</color>
<color name="widget_background">#87FFFFFF</color>
<color name="widget_text">@color/black</color>
</resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="popup_background">#FFFFFF</color>
<color name="popup_text">@color/black</color>
<color name="widget_background">#87FFFFFF</color>
<color name="widget_text">@color/black</color>
<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"?>
<resources>
<!--
<!--
Refer to App Widget Documentation for margin information
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>
<string name="app_name" translatable="false">ALN</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="accessibility">Accessibility</string>
<string name="tone_volume">Tone Volume</string>
<string name="audio">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="buds">Buds</string>
<string name="case_alt">Case</string>
<string name="test">Test</string>
<string name="name">Name</string>
<string name="noise_control">Noise Control</string>
<string name="off">Off</string>
<string name="transparency">Transparency</string>
<string name="adaptive">Adaptive</string>
<string name="noise_cancellation">Noise Cancellation</string>
<string name="press_and_hold_airpods">Press and Hold AirPods</string>
<string name="left">Left</string>
<string name="right">Right</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_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_description">Adjusts the volume of media in response to your environment.</string>
<string name="less_noise">Less 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_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_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_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="app_settings">App Settings</string>
<string name="conversational_awareness_customization">Conversational Awareness</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="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="app_name" translatable="false">ALN</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="accessibility">Accessibility</string>
<string name="tone_volume">Tone Volume</string>
<string name="audio">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="buds">Buds</string>
<string name="case_alt">Case</string>
<string name="test">Test</string>
<string name="name">Name</string>
<string name="noise_control">Noise Control</string>
<string name="off">Off</string>
<string name="transparency">Transparency</string>
<string name="adaptive">Adaptive</string>
<string name="noise_cancellation">Noise Cancellation</string>
<string name="press_and_hold_airpods">Press and Hold AirPods</string>
<string name="left">Left</string>
<string name="right">Right</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_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_description">Adjusts the volume of media in response to your environment.</string>
<string name="less_noise">Less 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_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_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_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="app_settings">App Settings</string>
<string name="conversational_awareness_customization">Conversational Awareness</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="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="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>

View File

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

View File

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

View File

@@ -11,6 +11,6 @@
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="1"
android:updatePeriodMillis="300000"
android:updatePeriodMillis="30000"
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