diff --git a/mac/PortalKit/Sources/PortalKit/Client/PortalClient.swift b/mac/PortalKit/Sources/PortalKit/Client/PortalClient.swift index 15ee727..1bcc3a6 100644 --- a/mac/PortalKit/Sources/PortalKit/Client/PortalClient.swift +++ b/mac/PortalKit/Sources/PortalKit/Client/PortalClient.swift @@ -118,6 +118,7 @@ public final class PortalClient: @unchecked Sendable { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Accept") + PortalClientIdentity.apply(to: &request) let data: Data let response: URLResponse @@ -190,6 +191,7 @@ public final class PortalClient: @unchecked Sendable { request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") + PortalClientIdentity.apply(to: &request) let payload: [String: String] = [ "pairingId": session.pairingId, @@ -447,6 +449,7 @@ public final class PortalClient: @unchecked Sendable { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") + PortalClientIdentity.apply(to: &request) let payload: [String: String] = [ "pairingId": session.pairingId, diff --git a/mac/PortalKit/Sources/PortalKit/Client/PortalClientIdentity.swift b/mac/PortalKit/Sources/PortalKit/Client/PortalClientIdentity.swift new file mode 100644 index 0000000..f3b5faa --- /dev/null +++ b/mac/PortalKit/Sources/PortalKit/Client/PortalClientIdentity.swift @@ -0,0 +1,58 @@ +// +// PortalClientIdentity.swift +// PortalKit +// +// Identity headers sent on pairing (and available for other Portal requests). +// + +import Foundation +import Darwin + +/// Values for `X-Client-Name`, `X-Client-Version`, `X-Device-Model`, and `User-Agent`. +public enum PortalClientIdentity { + public static var clientName: String { + if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String, + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return name + } + if let name = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String, + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return name + } + return "PortalCam" + } + + public static var clientVersion: String { + if let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + !v.isEmpty { + return v + } + return PortalKitVersion.version + } + + /// Hardware model identifier, e.g. `MacBookPro18,1`. + public static var deviceModel: String { + hwModel() ?? "Mac" + } + + public static var userAgent: String { + "PortalCam/\(clientVersion) (macOS; \(deviceModel))" + } + + /// Apply identity headers used by Portal TV pairing. + public static func apply(to request: inout URLRequest) { + request.setValue(clientName, forHTTPHeaderField: "X-Client-Name") + request.setValue(clientVersion, forHTTPHeaderField: "X-Client-Version") + request.setValue(deviceModel, forHTTPHeaderField: "X-Device-Model") + request.setValue(userAgent, forHTTPHeaderField: "User-Agent") + } + + private static func hwModel() -> String? { + var size = 0 + guard sysctlbyname("hw.model", nil, &size, nil, 0) == 0, size > 0 else { return nil } + var buf = [CChar](repeating: 0, count: size) + guard sysctlbyname("hw.model", &buf, &size, nil, 0) == 0 else { return nil } + let s = String(cString: buf).trimmingCharacters(in: .whitespacesAndNewlines) + return s.isEmpty ? nil : s + } +} diff --git a/portal-capability-test/AndroidManifest.xml b/portal-capability-test/AndroidManifest.xml index ff48f30..b7b03f4 100644 --- a/portal-capability-test/AndroidManifest.xml +++ b/portal-capability-test/AndroidManifest.xml @@ -14,7 +14,10 @@ - + + + diff --git a/portal-capability-test/app/build.gradle.kts b/portal-capability-test/app/build.gradle.kts new file mode 100644 index 0000000..89fe4df --- /dev/null +++ b/portal-capability-test/app/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.portaltv.capability" + compileSdk = 35 + + defaultConfig { + applicationId = "com.portaltv.capability" + minSdk = 28 + targetSdk = 28 + versionCode = 1 + versionName = "1.0" + } + + sourceSets { + getByName("main") { + manifest.srcFile("../AndroidManifest.xml") + java.setSrcDirs(listOf("../src", "../smartcamera/src")) + res.setSrcDirs(listOf("../res")) + assets.setSrcDirs(emptyList()) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + packaging { + resources.excludes += setOf("META-INF/INDEX.LIST", "META-INF/*.kotlin_module") + } + + lint { + abortOnError = false + } +} + +dependencies { + implementation("androidx.leanback:leanback:1.0.0") + implementation("androidx.fragment:fragment-ktx:1.8.5") + implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation("androidx.core:core-ktx:1.13.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") +} diff --git a/portal-capability-test/app/src/main/AndroidManifest.xml b/portal-capability-test/app/src/main/AndroidManifest.xml new file mode 120000 index 0000000..4db074a --- /dev/null +++ b/portal-capability-test/app/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +../../../AndroidManifest.xml \ No newline at end of file diff --git a/portal-capability-test/build.gradle.kts b/portal-capability-test/build.gradle.kts new file mode 100644 index 0000000..c7ad754 --- /dev/null +++ b/portal-capability-test/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.0.21" apply false +} diff --git a/portal-capability-test/deploy.sh b/portal-capability-test/deploy.sh index 71436ca..ad96961 100755 --- a/portal-capability-test/deploy.sh +++ b/portal-capability-test/deploy.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash set -euo pipefail - cd "$(dirname "$0")" -./build-apk.sh +echo "== Gradle assembleDebug ==" +gradle :app:assembleDebug + +APK="app/build/outputs/apk/debug/app-debug.apk" +cp -f "$APK" portal-capability-test.apk echo "== Installing APK via ADB ==" adb install -r portal-capability-test.apk -echo "== Starting MainActivity / PortalStreamingService ==" +echo "== Starting MainActivity ==" adb shell am start -n com.portaltv.capability/.MainActivity echo "== Done ==" diff --git a/portal-capability-test/gradle.properties b/portal-capability-test/gradle.properties new file mode 100644 index 0000000..18825f9 --- /dev/null +++ b/portal-capability-test/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true diff --git a/portal-capability-test/res/drawable/pc_badge.xml b/portal-capability-test/res/drawable/pc_badge.xml new file mode 100644 index 0000000..a6ceea4 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_badge.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_btn_danger.xml b/portal-capability-test/res/drawable/pc_btn_danger.xml new file mode 100644 index 0000000..0c25b3e --- /dev/null +++ b/portal-capability-test/res/drawable/pc_btn_danger.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/portal-capability-test/res/drawable/pc_btn_primary.xml b/portal-capability-test/res/drawable/pc_btn_primary.xml new file mode 100644 index 0000000..2a0e890 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_btn_primary.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/portal-capability-test/res/drawable/pc_btn_secondary.xml b/portal-capability-test/res/drawable/pc_btn_secondary.xml new file mode 100644 index 0000000..9084562 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_btn_secondary.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/portal-capability-test/res/drawable/pc_card.xml b/portal-capability-test/res/drawable/pc_card.xml new file mode 100644 index 0000000..9373022 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_card.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_card_inner.xml b/portal-capability-test/res/drawable/pc_card_inner.xml new file mode 100644 index 0000000..469dc33 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_card_inner.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/portal-capability-test/res/drawable/pc_dot_green.xml b/portal-capability-test/res/drawable/pc_dot_green.xml new file mode 100644 index 0000000..71faa67 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_dot_green.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_icon_circle_blue.xml b/portal-capability-test/res/drawable/pc_icon_circle_blue.xml new file mode 100644 index 0000000..563bf25 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_icon_circle_blue.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_icon_circle_green.xml b/portal-capability-test/res/drawable/pc_icon_circle_green.xml new file mode 100644 index 0000000..49672a6 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_icon_circle_green.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_icon_circle_yellow.xml b/portal-capability-test/res/drawable/pc_icon_circle_yellow.xml new file mode 100644 index 0000000..a7ad562 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_icon_circle_yellow.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_logo_bg.xml b/portal-capability-test/res/drawable/pc_logo_bg.xml new file mode 100644 index 0000000..88f637e --- /dev/null +++ b/portal-capability-test/res/drawable/pc_logo_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/drawable/pc_mode_item.xml b/portal-capability-test/res/drawable/pc_mode_item.xml new file mode 100644 index 0000000..6bffbb6 --- /dev/null +++ b/portal-capability-test/res/drawable/pc_mode_item.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/portal-capability-test/res/drawable/portal_banner.xml b/portal-capability-test/res/drawable/portal_banner.xml new file mode 100644 index 0000000..6d8f559 --- /dev/null +++ b/portal-capability-test/res/drawable/portal_banner.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/portal-capability-test/res/font/lucide.ttf b/portal-capability-test/res/font/lucide.ttf new file mode 100644 index 0000000..4adaca9 Binary files /dev/null and b/portal-capability-test/res/font/lucide.ttf differ diff --git a/portal-capability-test/res/layout/activity_main.xml b/portal-capability-test/res/layout/activity_main.xml new file mode 100644 index 0000000..c9926f5 --- /dev/null +++ b/portal-capability-test/res/layout/activity_main.xml @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +