Add popup via system alert window.

This commit is contained in:
darken
2022-01-25 09:59:57 +01:00
parent 56bedb64c4
commit d5d2ae784c
17 changed files with 554 additions and 247 deletions
+2
View File
@@ -28,6 +28,8 @@
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
@@ -62,6 +62,15 @@ enum class Permission(
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
},
),
SYSTEM_ALERT_WINDOW(
minApiLevel = Build.VERSION_CODES.BASE,
labelRes = R.string.permission_system_alert_window_label,
descriptionRes = R.string.permission_system_alert_window_description,
permissionId = "android.permission.SYSTEM_ALERT_WINDOW",
isGranted = {
android.provider.Settings.canDrawOverlays(it)
},
)
}
@@ -6,6 +6,7 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.permissions.Permission
import eu.darken.capod.common.permissions.isRequired
import eu.darken.capod.reaction.settings.ReactionSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -18,6 +19,7 @@ import javax.inject.Singleton
class PermissionTool @Inject constructor(
@ApplicationContext private val context: Context,
private val generalSettings: GeneralSettings,
private val reactionSettings: ReactionSettings,
) {
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
@@ -29,10 +31,12 @@ class PermissionTool @Inject constructor(
val missingPermissions: Flow<Set<Permission>> = combine(
permissionCheckTrigger,
generalSettings.monitorMode.flow,
) { _, monitorMode ->
reactionSettings.showPopUpOnCaseOpen.flow
) { _, monitorMode, showPopUp ->
Permission.values()
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
.filter { it != Permission.ACCESS_BACKGROUND_LOCATION || monitorMode == MonitorMode.ALWAYS }
.filter { it != Permission.SYSTEM_ALERT_WINDOW || showPopUp }
.filter { it.isRequired(context) }
.toSet()
}
@@ -33,11 +33,11 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
lateinit var adapter: OverviewAdapter
lateinit var permissionLauncher: ActivityResultLauncher<String>
var awaitingIgnoreBatteryOptimization = false
var awaitingPermission = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
awaitingIgnoreBatteryOptimization = savedInstanceState?.getBoolean("awaitingIgnoreBatteryOptimization") ?: false
awaitingPermission = savedInstanceState?.getBoolean("awaitingPermission") ?: false
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
log { "Request for $id was granted=$granted" }
@@ -77,16 +77,28 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
}
vm.requestPermissionEvent.observe2(ui) {
if (it == Permission.IGNORE_BATTERY_OPTIMIZATION) {
awaitingIgnoreBatteryOptimization = true
startActivity(
Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${requireContext().packageName}")
when (it) {
Permission.IGNORE_BATTERY_OPTIMIZATION -> {
awaitingPermission = true
startActivity(
Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${requireContext().packageName}")
)
)
)
} else {
permissionLauncher.launch(it.permissionId)
}
Permission.SYSTEM_ALERT_WINDOW -> {
awaitingPermission = true
startActivity(
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${requireContext().packageName}")
)
)
}
else -> {
permissionLauncher.launch(it.permissionId)
}
}
}
@@ -135,15 +147,15 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean("awaitingIgnoreBatteryOptimization", awaitingIgnoreBatteryOptimization)
outState.putBoolean("awaitingPermission", awaitingPermission)
super.onSaveInstanceState(outState)
}
override fun onResume() {
super.onResume()
if (awaitingIgnoreBatteryOptimization) {
awaitingIgnoreBatteryOptimization = false
log { "awaitingIgnoreBatteryOptimization=true" }
if (awaitingPermission) {
awaitingPermission = false
log { "awaitingPermission=true" }
vm.onPermissionResult(true)
}
}
@@ -93,7 +93,8 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
val caseLidState: LidState
get() {
return LidState.values().firstOrNull { it.rawRange.contains(rawCaseLidState.toInt()) } ?: LidState.UNKNOWN
val rawstate = rawCaseLidState
return LidState.values().firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
}
/**
@@ -1,5 +1,6 @@
package eu.darken.capod.reaction.popup
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
@@ -9,9 +10,10 @@ import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.reaction.popup.ui.PopUpNotification
import eu.darken.capod.reaction.popup.ui.PopUpWindow
import eu.darken.capod.reaction.settings.ReactionSettings
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@@ -19,7 +21,8 @@ import javax.inject.Singleton
class PopUpReaction @Inject constructor(
private val podMonitor: PodMonitor,
private val reactionSettings: ReactionSettings,
private val popUpNotification: PopUpNotification,
private val popupWindow: PopUpWindow,
private val dispatcherProvider: DispatcherProvider,
) {
fun monitor(): Flow<Unit> = reactionSettings.showPopUpOnCaseOpen.flow
@@ -51,10 +54,14 @@ class PopUpReaction @Inject constructor(
if (current.caseLidState == DualApplePods.LidState.OPEN && current.model != PodDevice.Model.UNKNOWN) {
log(TAG, INFO) { "Show popup" }
popUpNotification.showNotification(current)
withContext(dispatcherProvider.Main) {
popupWindow.show(current)
}
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
log(TAG, INFO) { "Hide popup" }
popUpNotification.hideNotification()
withContext(dispatcherProvider.Main) {
popupWindow.close()
}
}
}
}
@@ -1,82 +0,0 @@
package eu.darken.capod.reaction.popup.ui
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.common.BuildConfigWrap
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.notifications.PendingIntentCompat
import eu.darken.capod.main.ui.MainActivity
import eu.darken.capod.pods.core.PodDevice
import javax.inject.Inject
class PopUpNotification @Inject constructor(
@ApplicationContext private val context: Context,
private val notificationManager: NotificationManager,
private val notificationViewFactory: PopUpNotificationViewFactory
) {
private val builder: NotificationCompat.Builder
init {
NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_channel_reaction_popup_label),
NotificationManager.IMPORTANCE_HIGH
).run {
vibrationPattern = LongArray(1) { 0 }
enableVibration(true)
enableLights(false)
setSound(null, null)
notificationManager.createNotificationChannel(this)
}
val openIntent = Intent(context, MainActivity::class.java)
val openPi = PendingIntent.getActivity(
context,
0,
openIntent,
PendingIntentCompat.FLAG_IMMUTABLE
)
builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply {
setChannelId(NOTIFICATION_CHANNEL_ID)
setContentIntent(openPi)
priority = NotificationCompat.PRIORITY_LOW
setSmallIcon(R.drawable.ic_device_generic_earbuds)
setContentTitle(context.getString(R.string.app_name))
}
}
private fun getBuilder(device: PodDevice): NotificationCompat.Builder = builder.apply {
setStyle(NotificationCompat.DecoratedCustomViewStyle())
setCustomContentView(notificationViewFactory.createContentView(device))
setSmallIcon(device.iconRes)
setSubText(null)
}
fun showNotification(device: PodDevice) {
val notification = getBuilder(device).build()
log(TAG) { "showNotification(): $device" }
notificationManager.notify(NOTIFICATION_ID, notification)
}
fun hideNotification() {
log(TAG) { "hideNotification()" }
notificationManager.cancel(NOTIFICATION_ID)
}
companion object {
val TAG = logTag("Reaction", "PopUp", "Notifications")
private val NOTIFICATION_CHANNEL_ID =
"${BuildConfigWrap.APPLICATION_ID}.notification.channel.reaction.popup"
internal const val NOTIFICATION_ID = 9000
}
}
@@ -1,65 +0,0 @@
package eu.darken.capod.reaction.popup.ui
import android.content.Context
import android.widget.RemoteViews
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods
import javax.inject.Inject
class PopUpNotificationViewFactory @Inject constructor(
@ApplicationContext private val context: Context
) {
fun createContentView(device: PodDevice): RemoteViews = when (device) {
is DualApplePods -> createDualApplePods(device)
is SingleApplePods -> createSingleApplePods(device)
is BasicSingleApplePods -> createSingleBasicApplePods(device)
else -> throw IllegalArgumentException("Unexpected device: $device")
}
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
context.packageName,
R.layout.popup_notification_dual_pods
).apply {
setTextViewText(R.id.pod_label, device.getLabel(context))
device.getBatteryLevelLeftPod(context)
.let { if (device.isLeftPodCharging) "$it" else it }
.run { setTextViewText(R.id.pod_left, this) }
device.getBatteryLevelCase(context)
.let { if (device.isCaseCharging) "$it" else it }
.run { setTextViewText(R.id.pod_case, this) }
device.getBatteryLevelRightPod(context)
.let { if (device.isRightPodCharging) "$it" else it }
.run { setTextViewText(R.id.pod_right, this) }
}
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
context.packageName,
R.layout.popup_notification_single_pods
).apply {
setTextViewText(R.id.headphones_label, device.getLabel(context))
device.getBatteryLevelHeadset(context)
.let { if (!device.isHeadsetBeingCharged) "$it" else it }
.run { setTextViewText(R.id.headphones, this) }
}
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
context.packageName,
R.layout.popup_notification_single_pods_basic
).apply {
setTextViewText(R.id.headphones_label, device.getLabel(context))
device.getBatteryLevelHeadset(context)
.run { setTextViewText(R.id.headphones, this) }
}
}
@@ -0,0 +1,88 @@
package eu.darken.capod.reaction.popup.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isInvisible
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
import eu.darken.capod.databinding.PopupNotificationSinglePodsBasicBinding
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods
import javax.inject.Inject
class PopUpPodViewFactory @Inject constructor(
@ApplicationContext private val appContext: Context,
private val debugSettings: DebugSettings,
) {
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
is DualApplePods -> createDualApplePods(parent, device)
is SingleApplePods -> createSingleApplePods(parent, device) // Unused, has no case to trigger reaction?
is BasicSingleApplePods -> createSingleBasicApplePods(
parent,
device
) // Unused, has no case to trigger reaction?
else -> throw IllegalArgumentException("Unexpected device: $device")
}
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
device.apply {
podIcon.setImageResource(iconRes)
podLabel.text = getLabel(context)
signal.text = getSignalQuality(context)
signal.isInvisible = debugSettings.isDebugModeEnabled.value
// Left
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
// Case
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
podCaseBatteryLabel.text = getBatteryLevelCase(context)
// Right
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
}
}.root
private fun createSingleApplePods(parent: ViewGroup, device: SingleApplePods): View =
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
device.apply {
headphonesIcon.setImageResource(iconRes)
headphonesLabel.text = getLabel(context)
signal.text = getSignalQuality(context)
signal.isInvisible = debugSettings.isDebugModeEnabled.value
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
}
}.root
private fun createSingleBasicApplePods(parent: ViewGroup, device: BasicSingleApplePods): View =
PopupNotificationSinglePodsBasicBinding.inflate(layoutInflater, parent, false).apply {
device.apply {
headphonesIcon.setImageResource(iconRes)
headphonesLabel.text = getLabel(context)
signal.text = getSignalQuality(context)
signal.isInvisible = debugSettings.isDebugModeEnabled.value
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
}
}.root
}
@@ -0,0 +1,75 @@
package eu.darken.capod.reaction.popup.ui
import android.content.Context
import android.content.Context.WINDOW_SERVICE
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.appcompat.view.ContextThemeWrapper
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PopUpWindow @Inject constructor(
@ApplicationContext private val appContext: Context,
private val podViewFactory: PopUpPodViewFactory
) {
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val windowManager: WindowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
private val layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT, // Display it on top of other application windows
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // Don't let it grab the input focus
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // Make the underlying application window visible
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.BOTTOM
}
private val popUpView: View = layoutInflater.inflate(R.layout.popup_window_container_layout, null).apply {
findViewById<View>(R.id.close_action).setOnClickListener { close() }
}
private val deviceContainer = popUpView.findViewById<FrameLayout>(R.id.popup_content)
fun show(device: PodDevice) = try {
log(TAG) { "open()" }
if (popUpView.windowToken == null && popUpView.parent == null) {
val podView = podViewFactory.createContentView(deviceContainer, device)
deviceContainer.addView(podView)
windowManager.addView(popUpView, layoutParams)
} else {
log(TAG) { "View already added." }
}
} catch (e: Exception) {
log(TAG, ERROR) { "open() failed: ${e.asLog()}" }
}
fun close() = try {
log(TAG) { "close()" }
if (popUpView.parent != null) {
// (popUpView.parent as ViewGroup).removeAllViews()
// popUpView.invalidate()
windowManager.removeView(popUpView)
} else {
log(TAG) { "View was not added" }
}
} catch (e: Exception) {
log(TAG, ERROR) { "close() failed: ${e.asLog()}" }
}
companion object {
private val TAG = logTag("Reaction", "PopUp", "Window")
}
}
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>
@@ -1,72 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<TextView
android:id="@+id/pod_label"
style="@style/TextAppearance.Compat.Notification.Title"
<ImageView
android:id="@+id/pod_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_device_generic_earbuds"
app:layout_constraintBottom_toBottomOf="@id/pod_label"
app:layout_constraintEnd_toStartOf="@id/pod_label"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/pod_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:layout_constraintEnd_toStartOf="@id/signal"
app:layout_constraintStart_toEndOf="@id/pod_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="AirPods Pro" />
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:orientation="horizontal">
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
android:gravity="center"
android:minWidth="44dp"
app:layout_constraintBottom_toBottomOf="@+id/pod_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/pod_label"
tools:text="-90%" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@id/pod_left_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pod_label">
<TextView
android:id="@+id/pod_left"
style="@style/TextAppearance.Compat.Notification.Title"
<ImageView
android:id="@+id/pod_left_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableStart="@drawable/ic_airpod_left_24"
android:src="@drawable/ic_airpod_left_24"
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
<ImageView
android:id="@id/pod_left_battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_left_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="100%" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/pod_case"
style="@style/TextAppearance.Compat.Notification.Title"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pod_case_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/pod_left_container"
app:layout_constraintTop_toBottomOf="@id/pod_label">
<ImageView
android:id="@+id/pod_case_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
android:drawableStart="@drawable/ic_airpod_case_24"
android:src="@drawable/ic_airpod_case_24"
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
<ImageView
android:id="@id/pod_case_battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_case_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="100%" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/pod_right"
style="@style/TextAppearance.Compat.Notification.Title"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@id/pod_right_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/pod_case_container"
app:layout_constraintTop_toBottomOf="@id/pod_label">
<ImageView
android:id="@+id/pod_right_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableStart="@drawable/ic_airpod_right_24"
android:src="@drawable/ic_airpod_right_24"
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
<ImageView
android:id="@id/pod_right_battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_right_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="100%" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,29 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<TextView
android:id="@+id/headphones_label"
style="@style/TextAppearance.Compat.Notification.Title"
<ImageView
android:id="@+id/headphones_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_device_generic_earbuds"
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
app:layout_constraintEnd_toStartOf="@id/headphones_label"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/headphones_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="center"
app:layout_constraintEnd_toStartOf="@id/signal"
app:layout_constraintStart_toEndOf="@id/headphones_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="AirPods Max" />
<TextView
android:id="@+id/headphones"
style="@style/TextAppearance.Compat.Notification.Title"
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="16dp"
android:drawableStart="@drawable/ic_device_generic_headphones"
tools:text="100%" />
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
android:gravity="center"
android:minWidth="44dp"
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/headphones_label"
tools:text="-90%" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headphones_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headphones_label">
<ImageView
android:id="@id/headphones_battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="100%" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,38 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<TextView
<ImageView
android:id="@+id/headphones_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_device_generic_earbuds"
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
app:layout_constraintEnd_toStartOf="@id/headphones_label"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/headphones_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_label"
style="@style/TextAppearance.Compat.Notification.Title"
android:layout_width="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="center"
tools:text="AirPods Max" />
app:layout_constraintEnd_toStartOf="@id/signal"
app:layout_constraintStart_toEndOf="@id/headphones_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Beats Solo 3" />
<LinearLayout
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
android:gravity="center"
android:minWidth="44dp"
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/headphones_label"
tools:text="-90%" />
<TextView
android:id="@+id/headphones"
style="@style/TextAppearance.Compat.Notification.Title"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headphones_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headphones_label">
<ImageView
android:id="@id/headphones_battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="16dp"
android:drawableStart="@drawable/ic_device_generic_headphones"
android:gravity="center"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="100%" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="320dp"
android:minHeight="96dp"
android:layout_margin="8dp">
<FrameLayout
android:id="@+id/popup_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@id/close_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/close_action"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/general_close_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/popup_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
+3
View File
@@ -15,6 +15,7 @@
<string name="general_grant_permission_action">Grant permission</string>
<string name="general_upgrade_action">Upgrade</string>
<string name="general_check_action">Check</string>
<string name="general_close_action">Close</string>
<string name="debug_debuglog_size_label">Size</string>
<string name="debug_debuglog_size_compressed_label">Compressed size</string>
@@ -134,4 +135,6 @@
<string name="help_translate_description">Help translate this into your favorite language.</string>
<string name="settings_onepod_mode_label">One pod mode</string>
<string name="settings_onepod_mode_description">Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions.</string>
<string name="permission_system_alert_window_label">System Alert Window</string>
<string name="permission_system_alert_window_description">Allow CAPod to draw over other apps to make the feature \"Show PopUp\" possible.</string>
</resources>
+1
View File
@@ -14,5 +14,6 @@
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
<item name="android:singleLine">true</item>
<item name="android:ellipsize">end</item>
</style>
</resources>