mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-03 16:43:47 +00:00
widgets!
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
140
android/app/src/main/res/layout/noise_control_widget.xml
Normal file
140
android/app/src/main/res/layout/noise_control_widget.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
16
android/app/src/main/res/xml/noise_control_widget_info.xml
Normal file
16
android/app/src/main/res/xml/noise_control_widget_info.xml
Normal 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 |
BIN
android/imgs/transitions.mp4
Normal file
BIN
android/imgs/transitions.mp4
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 998 KiB After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user