Skip to content

Commit

Permalink
Merge branch 'master' of github.com:home-assistant/android
Browse files Browse the repository at this point in the history
# Conflicts:
#	.github/workflows/monthly.yaml
#	.github/workflows/onPush.yml
#	.github/workflows/pr.yml
#	.github/workflows/release.yml
#	.github/workflows/weekly.yaml
#	app/src/full/java/io/homeassistant/companion/android/sensors/LocationSensorManager.kt
#	build.gradle.kts
#	gradle/libs.versions.toml
  • Loading branch information
nestor committed Sep 20, 2023
2 parents 8d69b4a + a31cd2f commit cd10d5f
Show file tree
Hide file tree
Showing 117 changed files with 3,038 additions and 624 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/Bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ assignees: ''
<!--
- For Wear OS devices we will need the LogCat logs from the device.
- For Android Auto the logs can be retrieved from the connected device.
- Logs from the device can be taken from Settings > Companion App > Show and Share Logs
- Logs from the device can be taken from Settings > Companion App Troubleshooting > Show and Share Logs
-->
**Companion App Logs:**

Expand Down
33 changes: 16 additions & 17 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ GEM
specs:
CFPropertyList (3.0.6)
rexml
addressable (2.8.4)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.795.0)
aws-sdk-core (3.180.1)
aws-partitions (1.824.0)
aws-sdk-core (3.181.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.132.0)
aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-s3 (1.134.0)
aws-sdk-core (~> 3, >= 3.181.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
Expand All @@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.100.0)
excon (0.103.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
Expand Down Expand Up @@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.7)
fastlane (2.214.0)
fastlane (2.215.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
Expand All @@ -87,6 +87,7 @@ GEM
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
Expand All @@ -98,7 +99,7 @@ GEM
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
Expand All @@ -107,7 +108,7 @@ GEM
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-amazon_app_submission (0.4.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.46.0)
google-apis-androidpublisher_v3 (0.49.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.1)
addressable (~> 2.5, >= 2.5.1)
Expand Down Expand Up @@ -138,10 +139,9 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.7.0)
googleauth (1.8.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
Expand All @@ -152,9 +152,8 @@ GEM
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.1)
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.3.0)
nanaimo (0.3.0)
Expand All @@ -174,7 +173,7 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
signet (0.18.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
Expand All @@ -183,8 +182,8 @@ GEM
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
Expand All @@ -194,7 +193,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
unicode-display_width (2.4.2)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.22.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ class FirebaseCloudMessagingService : FirebaseMessagingService() {
launch {
try {
serverManager.integrationRepository(it.id).updateRegistration(
DeviceRegistration(
pushToken = token
)
deviceRegistration = DeviceRegistration(pushToken = token),
allowReregistration = false
)
} catch (e: Exception) {
Log.e(TAG, "Issue updating token", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.google.android.gms.location.SleepSegmentRequest
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
import io.homeassistant.companion.android.common.util.STATE_UNKNOWN
import java.util.concurrent.TimeUnit
import io.homeassistant.companion.android.common.R as commonR

Expand Down Expand Up @@ -168,8 +169,8 @@ class ActivitySensorManager : BroadcastReceiver(), SensorManager {
DetectedActivity.STILL -> "still"
DetectedActivity.TILTING -> "tilting"
DetectedActivity.WALKING -> "walking"
DetectedActivity.UNKNOWN -> "unknown"
else -> "unknown"
DetectedActivity.UNKNOWN -> STATE_UNKNOWN
else -> STATE_UNKNOWN
}
}

Expand All @@ -184,7 +185,7 @@ class ActivitySensorManager : BroadcastReceiver(), SensorManager {
SleepSegmentEvent.STATUS_SUCCESSFUL -> "successful"
SleepSegmentEvent.STATUS_MISSING_DATA -> "missing data"
SleepSegmentEvent.STATUS_NOT_DETECTED -> "not detected"
else -> "unknown"
else -> STATE_UNKNOWN
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.os.Build.VERSION.SDK_INT
import android.util.Log
import com.google.android.gms.location.LocationServices
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.util.STATE_UNKNOWN
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.SensorSetting
import io.homeassistant.companion.android.database.sensor.SensorSettingType
Expand Down Expand Up @@ -132,7 +133,7 @@ class GeocodeSensorManager : SensorManager {
onSensorUpdated(
context,
geocodedLocation,
if (!prettyAddress.isNullOrEmpty()) prettyAddress else "Unknown",
if (!prettyAddress.isNullOrEmpty()) prettyAddress else STATE_UNKNOWN,
geocodedLocation.statelessIcon,
attributes
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ fun SettingsWearTemplateTile(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
) {
val options = listOf(0, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 60 * 60, 5 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60)
val options = listOf(0, 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 60 * 60, 2 * 60 * 60, 5 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60)
for (option in options) {
DropdownMenuItem(onClick = {
onRefreshIntervalChanged(option)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ThreadManagerImpl @Inject constructor(
companion object {
private const val TAG = "ThreadManagerImpl"

// ID is a placeholder while we wait for Google to remove the requirement to provide one
// ID is a placeholder used in previous app versions / for older Home Assistant versions
private const val BORDER_AGENT_ID = "0000000000000001"
}

Expand All @@ -54,6 +54,8 @@ class ThreadManagerImpl @Inject constructor(
if (!appSupportsThread()) return ThreadManager.SyncResult.AppUnsupported
if (!coreSupportsThread(serverId)) return ThreadManager.SyncResult.ServerUnsupported

deleteOrphanedThreadCredentials(context, serverId)

val getDeviceDataset = scope.async { getPreferredDatasetFromDevice(context) }
val getCoreDatasets = scope.async { getDatasetsFromServer(serverId) }
val deviceThreadIntent = getDeviceDataset.await()
Expand All @@ -62,7 +64,10 @@ class ThreadManagerImpl @Inject constructor(

return if (deviceThreadIntent == null && coreThreadDataset != null) {
try {
importDatasetFromServer(context, coreThreadDataset.datasetId, serverId)
importDatasetFromServer(context, coreThreadDataset.datasetId, coreThreadDataset.preferredBorderAgentId, serverId)
coreThreadDataset.preferredBorderAgentId?.let {
serverManager.integrationRepository(serverId).setThreadBorderAgentIds(listOf(it))
} // else added using placeholder, will be removed when core is updated
Log.d(TAG, "Thread import to device completed")
ThreadManager.SyncResult.OnlyOnServer(imported = true)
} catch (e: Exception) {
Expand All @@ -83,16 +88,48 @@ class ThreadManagerImpl @Inject constructor(
var updated: Boolean? = null
if (!coreIsDevicePreferred) {
if (appIsDevicePreferred) {
// Update or remove the device preferred credential to match core state
// Update or remove the device preferred credential to match core state.
// The device credential store currently doesn't allow the user to choose
// which credential should be used. To prevent unexpected behavior, HA only
// contributes one credential at a time, which is for _this_ server.
try {
val localIds = serverManager.defaultServers.flatMap {
serverManager.integrationRepository(it.id).getThreadBorderAgentIds()
}
updated = if (coreThreadDataset.source != "Google") { // Credential from HA, update
importDatasetFromServer(context, coreThreadDataset.datasetId, serverId)
localIds.filter { it != coreThreadDataset.preferredBorderAgentId }.forEach { baId ->
try {
deleteThreadCredential(context, baId)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete credential for border agent ID $baId", e)
}
}
importDatasetFromServer(context, coreThreadDataset.datasetId, coreThreadDataset.preferredBorderAgentId, serverId)
serverManager.defaultServers.forEach {
serverManager.integrationRepository(it.id).setThreadBorderAgentIds(
if (it.id == serverId && coreThreadDataset.preferredBorderAgentId != null) {
listOf(coreThreadDataset.preferredBorderAgentId!!)
} else {
emptyList()
}
)
}
Log.d(TAG, "Thread update device completed: deleted ${localIds.size} datasets, updated 1")
true
} else { // Imported from another app, so this shouldn't be managed by HA
deleteThreadCredential(context)
} else { // Core prefers imported from other app, this shouldn't be managed by HA
localIds.forEach { baId ->
try {
deleteThreadCredential(context, baId)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete credential for border agent ID $baId", e)
}
}
serverManager.defaultServers.forEach {
serverManager.integrationRepository(it.id).setThreadBorderAgentIds(emptyList())
}
Log.d(TAG, "Thread update device completed: deleted ${localIds.size} datasets")
false
}
Log.d(TAG, "Thread update device completed")
} catch (e: Exception) {
Log.e(TAG, "Thread update device failed", e)
}
Expand Down Expand Up @@ -120,10 +157,21 @@ class ThreadManagerImpl @Inject constructor(
override suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse? =
getDatasetsFromServer(serverId)?.firstOrNull { it.preferred }

override suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int) {
@OptIn(ExperimentalStdlibApi::class)
override suspend fun importDatasetFromServer(
context: Context,
datasetId: String,
preferredBorderAgentId: String?,
serverId: Int
) {
val tlv = serverManager.webSocketRepository(serverId).getThreadDatasetTlv(datasetId)?.tlvAsByteArray
if (tlv != null) {
val threadBorderAgent = ThreadBorderAgent.newBuilder(BORDER_AGENT_ID.toByteArray()).build()
val borderAgentId = preferredBorderAgentId ?: run {
Log.w(TAG, "Adding dataset with placeholder border agent ID")
BORDER_AGENT_ID
}
val idAsBytes = borderAgentId.let { if (it.length == 16) it.toByteArray() else it.hexToByteArray() }
val threadBorderAgent = ThreadBorderAgent.newBuilder(idAsBytes).build()
val threadNetworkCredentials = ThreadNetworkCredentials.fromActiveOperationalDataset(tlv)
suspendCoroutine { cont ->
ThreadNetwork.getClient(context).addCredentials(threadBorderAgent, threadNetworkCredentials)
Expand Down Expand Up @@ -162,7 +210,13 @@ class ThreadManagerImpl @Inject constructor(
.addOnFailureListener { cont.resume(null) }
}
return try {
appCredentials?.any { isPreferredCredentials(context, it) } ?: false
appCredentials?.any {
val isPreferred = isPreferredCredentials(context, it)
if (isPreferred) {
Log.d(TAG, "Thread device prefers app added dataset: ${it.networkName} (PAN ${it.panId}, EXTPAN ${String(it.extendedPanId)})")
}
isPreferred
} ?: false
} catch (e: Exception) {
Log.e(TAG, "Thread app added credentials preferred check failed", e)
false
Expand All @@ -189,9 +243,32 @@ class ThreadManagerImpl @Inject constructor(
return null
}

private suspend fun deleteThreadCredential(context: Context) = suspendCoroutine { cont ->
// This only works because we currently always use the same border agent ID
val threadBorderAgent = ThreadBorderAgent.newBuilder(BORDER_AGENT_ID.toByteArray()).build()
private suspend fun deleteOrphanedThreadCredentials(context: Context, serverId: Int) {
if (serverManager.defaultServers.all { it.version?.isAtLeast(2023, 9) == true }) {
try {
deleteThreadCredential(context, BORDER_AGENT_ID)
} catch (e: Exception) {
// Expected, it may not exist
}
}

val orphanedCredentials = serverManager.integrationRepository(serverId).getOrphanedThreadBorderAgentIds()
if (orphanedCredentials.isEmpty()) return

orphanedCredentials.forEach {
try {
deleteThreadCredential(context, it)
} catch (e: Exception) {
Log.w(TAG, "Unable to delete credential for border agent ID $it", e)
}
}
serverManager.integrationRepository(serverId).clearOrphanedThreadBorderAgentIds()
}

@OptIn(ExperimentalStdlibApi::class)
private suspend fun deleteThreadCredential(context: Context, borderAgentId: String) = suspendCoroutine { cont ->
val idAsBytes = borderAgentId.let { if (it.length == 16) it.toByteArray() else it.hexToByteArray() }
val threadBorderAgent = ThreadBorderAgent.newBuilder(idAsBytes).build()
ThreadNetwork.getClient(context)
.removeCredentials(threadBorderAgent)
.addOnSuccessListener { cont.resume(true) }
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,21 @@
<service android:name=".controls.HaControlsProviderService"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true">
<meta-data
android:name="android.service.controls.META_DATA_PANEL_ACTIVITY"
android:value="${applicationId}/io.homeassistant.companion.android.controls.HaControlsPanelActivity" />
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService"/>
</intent-filter>
</service>

<activity android:name=".controls.HaControlsPanelActivity"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true"
android:resizeableActivity="true"
android:taskAffinity="io.homeassistant.companion.android.controls"
android:enabled="false" />

<receiver
android:name=".sensors.LocationSensorManager"
android:enabled="true"
Expand Down
Loading

0 comments on commit cd10d5f

Please sign in to comment.