diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index 005ce6ce36..72622538db 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -10,6 +10,7 @@ on: - 'scripts/build-ios-app.sh' - 'scripts/run-ios-ui-tests.sh' - 'scripts/run-watch-ui-tests.sh' + - 'scripts/run-tv-ui-tests.sh' - 'scripts/run-ios-native-tests.sh' - 'scripts/ios/notification-tests/native-tests/**' - 'scripts/ios/notification-tests/install-native-notification-tests.sh' @@ -19,6 +20,7 @@ on: - 'scripts/ios/screenshots/**' - 'scripts/ios/screenshots-metal/**' - 'scripts/ios/screenshots-watch/**' + - 'scripts/ios/screenshots-tv/**' - 'scripts/templates/**' - '!scripts/templates/**/*.md' - 'CodenameOne/src/**' @@ -44,6 +46,7 @@ on: - 'scripts/build-ios-app.sh' - 'scripts/run-ios-ui-tests.sh' - 'scripts/run-watch-ui-tests.sh' + - 'scripts/run-tv-ui-tests.sh' - 'scripts/run-ios-native-tests.sh' - 'scripts/ios/notification-tests/native-tests/**' - 'scripts/ios/notification-tests/install-native-notification-tests.sh' @@ -53,6 +56,7 @@ on: - 'scripts/ios/screenshots/**' - 'scripts/ios/screenshots-metal/**' - 'scripts/ios/screenshots-watch/**' + - 'scripts/ios/screenshots-tv/**' - 'scripts/templates/**' - '!scripts/templates/**/*.md' - 'CodenameOne/src/**' @@ -653,3 +657,149 @@ jobs: path: artifacts if-no-files-found: warn retention-days: 14 + + build-ios-tv: + # Native Apple TV (tvOS) screenshot pipeline. The tvOS slice auto-enables + # from codename1.tvMain in the sample, so build-ios-app.sh generates the + #
TV target alongside the iOS app. tvOS reuses the iOS UIApplicationMain + # entry and the Metal renderer (no OpenGL ES on tvOS); this job builds the TV + # target for the appletvsimulator, renders the cn1ss suite and streams frames + # to the same Cn1ssScreenshotServer the iOS/watch jobs use, comparing against + # scripts/ios/screenshots-tv. + # + # BLOCKING (a hard golden gate, like build-ios-watch): the tvOS slice + # compiles end-to-end and the golden set is seeded from a CI capture (see + # Ports/iOSPort/nativeSources/TVOS_PORT.md). A mismatch fails the job. + needs: build-port + permissions: + contents: read + pull-requests: write + issues: write + runs-on: macos-15 + timeout-minutes: 60 + concurrency: + group: mac-ci-${{ github.workflow }}-tv-${{ github.ref_name }} + cancel-in-progress: true + + env: + GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + + steps: + - uses: actions/checkout@v6 + + - name: Cache CocoaPods and user gems + uses: actions/cache@v5 + with: + path: | + ~/.gem + ~/Library/Caches/CocoaPods + ~/.cocoapods/repos + key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }} + restore-keys: | + ${{ runner.os }}-pods-v1- + + - name: Ensure CocoaPods tooling + run: | + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + set -euo pipefail + if ! command -v ruby >/dev/null; then + echo "ruby not found"; exit 1 + fi + GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')" + export PATH="$GEM_USER_DIR/bin:$PATH" + if ! command -v pod >/dev/null 2>&1; then + gem install cocoapods xcodeproj --no-document --user-install + fi + pod --version + + - name: Compute setup-workspace hash + id: setup_hash + run: | + set -euo pipefail + echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Set TMPDIR + run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + + - name: Cache codenameone-tools + uses: actions/cache@v5 + with: + path: ${{ runner.temp }}/codenameone-tools + key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + ${{ runner.os }}-cn1-tools- + + - name: Cache Maven repository + uses: actions/cache@v5 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2- + + - name: Restore cn1-binaries cache + uses: actions/cache@v5 + with: + path: ../cn1-binaries + key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + cn1-binaries-${{ runner.os }}- + + - name: Restore built CN1 + iOS port artifacts + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/com/codenameone + Themes + Ports/iOSPort/nativeSources + key: ${{ needs.build-port.outputs.cn1_built_cache_key }} + fail-on-cache-miss: true + + - name: Build sample iOS app (generates the tvOS target) + id: build-ios-app + run: ./scripts/build-ios-app.sh -q -DskipTests + timeout-minutes: 30 + + - name: Ensure tvOS simulator runtime + run: | + set -euo pipefail + if ! xcrun simctl list runtimes available 2>/dev/null | grep -qi tvOS; then + echo "No tvOS runtime found; attempting download" + xcodebuild -downloadPlatform tvOS || true + fi + xcrun simctl list runtimes available 2>/dev/null | grep -i tvOS || true + xcrun simctl list devices available 2>/dev/null | grep -i "Apple TV" || true + + - name: Run tvOS UI screenshot tests + env: + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/tv-ui-tests + SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-tv + CN1SS_COMMENT_MARKER: '' + CN1SS_PREVIEW_SUBDIR: ios-tv + CN1SS_REPORT_TITLE: 'Apple TV (tvOS) screenshot updates' + CN1SS_SUCCESS_MESSAGE: '✅ Native Apple TV (tvOS, Metal) screenshot tests passed.' + CN1SS_COMMENT_LOG_PREFIX: '[run-tv-ui-tests]' + CN1SS_FAIL_ON_MISMATCH: '1' + # Tolerate a missing golden set while the tvOS native slice is being + # brought up; tighten to match build-ios-watch once goldens are seeded. + CN1SS_ALLOWED_MISSING: '4' + run: | + set -euo pipefail + mkdir -p "${ARTIFACTS_DIR}" + echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'" + echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'" + ./scripts/run-tv-ui-tests.sh \ + "${{ steps.build-ios-app.outputs.workspace }}" \ + "${{ steps.build-ios-app.outputs.scheme }}" + timeout-minutes: 45 + + - name: Upload tv artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: tv-ui-tests + path: artifacts + if-no-files-found: warn + retention-days: 14 diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 326e735cdf..b3498a57fe 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -5612,6 +5612,17 @@ public boolean isWatch() { return false; } + /// Indicates whether the application is running on a television form factor + /// (Apple TV / Android TV / Google TV). Notice that this is often a guess + /// derived from the device/skin metadata. + /// + /// #### Returns + /// + /// true if the device is assumed to be a TV + public boolean isTV() { + return false; + } + /// Returns true if the device has dialing capabilities /// /// #### Returns diff --git a/CodenameOne/src/com/codename1/ui/CN.java b/CodenameOne/src/com/codename1/ui/CN.java index 9ed3c6d9ab..05edf5e4e5 100644 --- a/CodenameOne/src/com/codename1/ui/CN.java +++ b/CodenameOne/src/com/codename1/ui/CN.java @@ -902,6 +902,17 @@ public static boolean isWatch() { return Display.impl.isWatch(); } + /// Indicates whether the application is running on a television form factor + /// (Apple TV / Android TV / Google TV). Notice that this is often a guess + /// derived from the device metadata. + /// + /// #### Returns + /// + /// true if the device is assumed to be a TV + public static boolean isTV() { + return Display.impl.isTV(); + } + /// Returns the size of the desktop hosting the application window when running on a desktop platform. /// /// #### Returns diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 1a217240d1..84772ce884 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -4391,6 +4391,17 @@ public boolean isWatch() { return impl.isWatch(); } + /// Indicates whether the application is running on a television form factor + /// (Apple TV / Android TV / Google TV). Notice that this is often a guess + /// derived from the device metadata. + /// + /// #### Returns + /// + /// true if the device is assumed to be a TV + public boolean isTV() { + return impl.isTV(); + } + /// Returns true if the device has dialing capabilities /// /// #### Returns diff --git a/CodenameOne/src/com/codename1/ui/util/Resources.java b/CodenameOne/src/com/codename1/ui/util/Resources.java index f3715ad2a4..e75b2438da 100644 --- a/CodenameOne/src/com/codename1/ui/util/Resources.java +++ b/CodenameOne/src/com/codename1/ui/util/Resources.java @@ -1523,7 +1523,8 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException { if ("HTML5".equals(platformName)) { platformName = Display.getInstance().getProperty("HTML5.platformName", "mac"); } - String deviceType = Display.getInstance().isDesktop() ? "desktop" : Display.getInstance().isTablet() ? "tablet" : "phone"; + Display d = Display.getInstance(); + String deviceType = d.isDesktop() ? "desktop" : d.isTablet() ? "tablet" : d.isTV() ? "tv" : d.isWatch() ? "watch" : "phone"; String platformPrefix = "platform-" + platformName + "-"; String densityPrefix = "density-" + densityStr + "-"; String devicePrefix = "device-" + deviceType + "-"; diff --git a/CodenameOneDesigner/build.xml b/CodenameOneDesigner/build.xml index cdfb14154f..e196f722bc 100644 --- a/CodenameOneDesigner/build.xml +++ b/CodenameOneDesigner/build.xml @@ -165,5 +165,10 @@ + + + + + diff --git a/CodenameOneDesigner/test/com/codename1/designer/css/CSSDeviceFormFactorMediaQueryTest.java b/CodenameOneDesigner/test/com/codename1/designer/css/CSSDeviceFormFactorMediaQueryTest.java new file mode 100644 index 0000000000..431740f7f1 --- /dev/null +++ b/CodenameOneDesigner/test/com/codename1/designer/css/CSSDeviceFormFactorMediaQueryTest.java @@ -0,0 +1,104 @@ +package com.codename1.designer.css; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Hashtable; + +/** + * Regression tests for device form-factor media queries compiling into + * {@code device--} prefixed UIIDs. The {@code tv} and {@code watch} + * form factors reuse the same generic {@code device-*} mechanism that already + * backs {@code device-desktop}/{@code device-tablet}/{@code device-phone}; + * at runtime Resources.loadTheme selects the matching prefix via + * Display.isTV()/isWatch(). + */ +public class CSSDeviceFormFactorMediaQueryTest { + + public static void main(String[] args) throws Exception { + installHeadlessImplementation(); + testTvMediaCompilesToDeviceTvUiids(); + testWatchMediaCompilesToDeviceWatchUiids(); + } + + /** + * CSSTheme.load touches Display / Util, which need a CodenameOneImplementation. + * Install the same minimal headless stub the no-cef CLI uses (see + * NoCefCSSCLI#installHeadlessImplementation) so this test runs standalone. + */ + private static void installHeadlessImplementation() throws Exception { + HeadlessCssCompilerImplementation stub = new HeadlessCssCompilerImplementation(); + Class displayCls = Class.forName("com.codename1.ui.Display"); + java.lang.reflect.Field implField = displayCls.getDeclaredField("impl"); + implField.setAccessible(true); + if (implField.get(null) == null) { + implField.set(null, stub); + } + com.codename1.io.Util.setImplementation(stub); + } + + private static void testTvMediaCompilesToDeviceTvUiids() throws Exception { + Path cssFile = Files.createTempFile("cn1-tv-media", ".css"); + Path resFile = Files.createTempFile("cn1-tv-media", ".res"); + try { + String css = "Button { color: #111111; }" + + "@media device-tv {" + + " Button { color: #00ff00; background-color: #001100; }" + + "}"; + Files.write(cssFile, css.getBytes(StandardCharsets.UTF_8)); + + CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL()); + theme.resourceFile = resFile.toFile(); + theme.res = new com.codename1.ui.util.EditableResourcesForCSS(resFile.toFile()); + theme.res.setTheme("Theme", new Hashtable()); + theme.updateResources(); + + Hashtable themeProps = theme.res.getTheme("Theme"); + assertEquals("111111", themeProps.get("Button.fgColor"), "Base Button fgColor"); + assertEquals("00FF00", themeProps.get("device-tv-Button.fgColor"), "TV Button fgColor"); + assertEquals("001100", themeProps.get("device-tv-Button.bgColor"), "TV Button bgColor"); + } finally { + deleteIfExists(cssFile); + deleteIfExists(resFile); + } + } + + private static void testWatchMediaCompilesToDeviceWatchUiids() throws Exception { + Path cssFile = Files.createTempFile("cn1-watch-media", ".css"); + Path resFile = Files.createTempFile("cn1-watch-media", ".res"); + try { + String css = "Button { color: #111111; }" + + "@media device-watch {" + + " Button { color: #2222ff; }" + + "}"; + Files.write(cssFile, css.getBytes(StandardCharsets.UTF_8)); + + CSSTheme theme = CSSTheme.load(cssFile.toUri().toURL()); + theme.resourceFile = resFile.toFile(); + theme.res = new com.codename1.ui.util.EditableResourcesForCSS(resFile.toFile()); + theme.res.setTheme("Theme", new Hashtable()); + theme.updateResources(); + + Hashtable themeProps = theme.res.getTheme("Theme"); + assertEquals("111111", themeProps.get("Button.fgColor"), "Base Button fgColor"); + assertEquals("2222FF", themeProps.get("device-watch-Button.fgColor"), "Watch Button fgColor"); + } finally { + deleteIfExists(cssFile); + deleteIfExists(resFile); + } + } + + private static void deleteIfExists(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + } + + private static void assertEquals(Object expected, Object actual, String message) { + if (expected == null ? actual != null : !expected.equals(actual)) { + throw new AssertionError(message + " expected=" + expected + " actual=" + actual); + } + } +} diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index c87e295fd8..3e3cb7e388 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -5683,6 +5683,31 @@ public boolean isWatch() { return watchCache; } + private Boolean tvCache; + + @Override + public boolean isTV() { + if(tvCache == null) { + // PackageManager.FEATURE_TELEVISION ("android.hardware.type.television") + // and FEATURE_LEANBACK ("android.software.leanback") are the canonical + // Android TV / Google TV markers; use the string literals so this + // compiles regardless of the configured minimum SDK level. + android.content.pm.PackageManager pm = getContext().getPackageManager(); + boolean tv = pm.hasSystemFeature("android.hardware.type.television") + || pm.hasSystemFeature("android.software.leanback"); + if(!tv) { + // Fall back to the runtime UI mode (covers emulators/devices that + // expose the TV ui-mode without declaring the hardware feature). + android.app.UiModeManager um = (android.app.UiModeManager) + getContext().getSystemService(Context.UI_MODE_SERVICE); + tv = um != null && um.getCurrentModeType() + == Configuration.UI_MODE_TYPE_TELEVISION; + } + tvCache = tv; + } + return tvCache; + } + /** * Executes r on the UI thread and blocks the EDT to completion * @param r runnable to execute @@ -8283,6 +8308,9 @@ public String[] getPlatformOverrides() { if (isWatch()) { return new String[]{"watch", "android", "android-watch"}; } + if (isTV()) { + return new String[]{"tv", "android", "android-tv"}; + } if (isTablet()) { return new String[]{"tablet", "android", "android-tab"}; } else { diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java b/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java index 5272823672..6b35cb182a 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java @@ -165,6 +165,55 @@ static void register() { + "default so the iOS build is unaffected; enable it for a packaged " + "companion submission."); + // Apple TV native build (tvOS). tvOS has UIKit + Metal but no OpenGL ES, + // so it is handled like the Mac Catalyst slice: Metal renderer + GL stub + // headers + GL-only sources excluded, as a separate appletvos target. + set("{{@tvNative}}.label", "Apple TV (tvOS)"); + set("{{@tvNative}}.description", + "Builds an Apple TV app from the same project. tvOS reuses the " + + "iOS UIKit entry and the Metal renderer (it lacks OpenGL ES), so " + + "the tvOS app is a separate appletvos target compiled from the " + + "same sources. CN.isTV() returns true at runtime."); + + set("{{#tvNative#tvNative.enabled}}.label", "Enable tvOS target"); + set("{{#tvNative#tvNative.enabled}}.type", "Select"); + set("{{#tvNative#tvNative.enabled}}.values", "false,true"); + set("{{#tvNative#tvNative.enabled}}.description", + "When true, adds an Apple TV app target to the generated Xcode " + + "project. Also auto-enabled whenever codename1.tvMain is declared " + + "in codenameone_settings.properties. Requires the Ruby xcodeproj " + + "gem (bundled with CocoaPods)."); + + set("{{#tvNative#tvNative.mainClass}}.label", "tvOS lifecycle class"); + set("{{#tvNative#tvNative.mainClass}}.type", "String"); + set("{{#tvNative#tvNative.mainClass}}.description", + "Fully-qualified tvOS entry/lifecycle class. Normally set via " + + "codename1.tvMain; this hint is an override. May equal the phone " + + "main class (the tvOS app reuses the shared UIApplicationMain " + + "entry). Defaults to the phone main class when tvNative.enabled=true."); + + set("{{#tvNative#tvNative.bundleId}}.label", "tvOS bundle identifier"); + set("{{#tvNative#tvNative.bundleId}}.type", "String"); + set("{{#tvNative#tvNative.bundleId}}.description", + "Bundle id of the Apple TV app. Defaults to .tvos."); + + set("{{#tvNative#tvNative.minDeploymentTarget}}.label", "Minimum tvOS version"); + set("{{#tvNative#tvNative.minDeploymentTarget}}.type", "String"); + set("{{#tvNative#tvNative.minDeploymentTarget}}.description", + "TVOS_DEPLOYMENT_TARGET for the tvOS target. Defaults to 13.0."); + + set("{{#tvNative#tvNative.teamId}}.label", "Apple team id"); + set("{{#tvNative#tvNative.teamId}}.type", "String"); + set("{{#tvNative#tvNative.teamId}}.description", + "Development team for signing the tvOS target. Defaults to the " + + "iOS team id (ios.teamId / ios.release.teamId)."); + + set("{{#tvNative#tvNative.displayName}}.label", "tvOS app name"); + set("{{#tvNative#tvNative.displayName}}.type", "String"); + set("{{#tvNative#tvNative.displayName}}.description", + "Name shown under the tvOS app icon. Defaults to the app display " + + "name (codename1.displayName), then the main class name."); + // Wear OS native build (Android). A Wear OS app is a regular Android app // that declares the watch hardware feature; the CN1 UI renders through // the normal Android pipeline (no separate backend, unlike watchOS). @@ -194,6 +243,27 @@ static void register() { + "standalone), so it installs and runs directly on the watch " + "without a companion phone app. Defaults to true. Only applies " + "when android.wear=true."); + + // Android TV / Google TV: the same APK plus manifest metadata (Leanback + // launcher category + leanback feature + optional touchscreen) and a + // generated 320x180 banner. CN.isTV() returns true at runtime. + set("{{@androidTv}}.label", "Android TV / Google TV"); + set("{{@androidTv}}.description", + "Builds the Android app for Android TV / Google TV: adds the " + + "Leanback launcher category so the app appears on the TV home " + + "screen, declares the android.software.leanback feature, makes " + + "the touchscreen optional and generates a 320x180 launcher " + + "banner from the app icon. The same APK still runs on phones " + + "and tablets; CN.isTV() returns true at runtime."); + + set("{{#androidTv#android.tv}}.label", "Enable Android TV build"); + set("{{#androidTv#android.tv}}.type", "Select"); + set("{{#androidTv#android.tv}}.values", "false,true"); + set("{{#androidTv#android.tv}}.description", + "When true, adds Android TV manifest metadata (LEANBACK_LAUNCHER " + + "category, android.software.leanback uses-feature, touchscreen " + + "required=false) and a generated tv_banner drawable. With the " + + "hint off the manifest is unchanged."); } /** Idempotent setter: does not overwrite user / project-level hint metadata. */ diff --git a/Ports/iOSPort/nativeSources/CN1Camera.m b/Ports/iOSPort/nativeSources/CN1Camera.m index a7f91dc44c..1b92d2a49a 100644 --- a/Ports/iOSPort/nativeSources/CN1Camera.m +++ b/Ports/iOSPort/nativeSources/CN1Camera.m @@ -24,7 +24,7 @@ #import "CN1Camera.h" #import "xmlvm.h" -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #import "java_lang_String.h" #import "com_codename1_impl_ios_IOSCameraImpl.h" @@ -415,7 +415,7 @@ - (void)fireVideoStopped:(int)cbId path:(NSString *)path { JAVA_OBJECT com_codename1_impl_ios_IOSNative_cn1CameraEnumerate___R_java_lang_String( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV NSString *s = [CN1Camera enumerateCameras]; return fromNSString(CN1_THREAD_GET_STATE_PASS_ARG s); #else @@ -427,7 +427,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_cn1CameraOpen___java_lang_String_int_ CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT cameraId, JAVA_INT previewW, JAVA_INT previewH, JAVA_BOOLEAN captureAudio) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV NSString *idStr = toNSString(CN1_THREAD_GET_STATE_PASS_ARG cameraId); CN1Camera *cam = [[CN1Camera alloc] init]; BOOL ok = [cam openWithCameraId:idStr previewW:previewW previewH:previewH @@ -445,7 +445,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_cn1CameraOpen___java_lang_String_int_ JAVA_LONG com_codename1_impl_ios_IOSNative_cn1CameraCreatePreviewView___long_R_long( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -466,7 +466,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraTakePhoto___long_int_int_int_java CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_INT width, JAVA_INT height, JAVA_INT quality, JAVA_OBJECT filePath, JAVA_INT callbackId) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -483,7 +483,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraTakePhoto___long_int_int_int_java JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_cn1CameraStartVideo___long_java_lang_String_boolean_R_boolean( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_OBJECT filePath, JAVA_BOOLEAN captureAudio) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -503,7 +503,7 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_cn1CameraStartVideo___long_java_la void com_codename1_impl_ios_IOSNative_cn1CameraStopVideo___long_int( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_INT callbackId) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -518,7 +518,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraStopVideo___long_int( void com_codename1_impl_ios_IOSNative_cn1CameraSetFrameDelivery___long_boolean_int( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_BOOLEAN enabled, JAVA_INT maxFps) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -531,7 +531,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraSetFrameDelivery___long_boolean_i void com_codename1_impl_ios_IOSNative_cn1CameraSetFlash___long_int( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_INT mode) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -544,7 +544,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraSetFlash___long_int( void com_codename1_impl_ios_IOSNative_cn1CameraSetZoom___long_float( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_FLOAT ratio) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -557,7 +557,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraSetZoom___long_float( void com_codename1_impl_ios_IOSNative_cn1CameraFocus___long_float_float( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer, JAVA_FLOAT xNorm, JAVA_FLOAT yNorm) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -569,7 +569,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraFocus___long_float_float( void com_codename1_impl_ios_IOSNative_cn1CameraPause___long( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -581,7 +581,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraPause___long( void com_codename1_impl_ios_IOSNative_cn1CameraResume___long( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer) { -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; #else @@ -594,7 +594,7 @@ void com_codename1_impl_ios_IOSNative_cn1CameraResume___long( void com_codename1_impl_ios_IOSNative_cn1CameraClose___long( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG sessionPeer) { if (sessionPeer == 0) return; -#ifdef INCLUDE_CN1_CAMERA +#if defined(INCLUDE_CN1_CAMERA) && !TARGET_OS_TV #ifndef CN1_USE_ARC CN1Camera *cam = (CN1Camera *)sessionPeer; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index e437aa5580..92232c7acc 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -131,7 +131,12 @@ - (CodenameOne_GLViewController *)cn1EnsureViewController // the NIB name on Mac so UIViewController synthesises a plain // UIView; the Metal layer is attached programmatically further // down the init chain, so the XIB's IBOutlet wiring isn't needed. -#if TARGET_OS_MACCATALYST + // tvOS excludes the iOS XIBs from its bundle too (TvNativeBuilder's + // EXCLUDED_SOURCE_FILE_NAMES), so loading 'CodenameOne_GLViewController' + // as a NIB crashes at launch ("Could not load NIB in bundle"). Pass nil + // there as well -- the Metal layer is attached programmatically, so the + // XIB's IBOutlet wiring isn't needed. +#if TARGET_OS_MACCATALYST || TARGET_OS_TV NSString *cn1NibName = nil; #else NSString *cn1NibName = @"CodenameOne_GLViewController"; @@ -537,7 +542,12 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceAppl - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { +#if TARGET_OS_TV + // The legacy openURL:sourceApplication:annotation: delegate is unavailable on tvOS. + return NO; +#else return [self application:application openURL:url sourceApplication:nil annotation:nil]; +#endif } - (void)applicationWillResignActive:(UIApplication *)application @@ -601,7 +611,11 @@ -(void)application:(UIApplication *)application performFetchWithCompletionHandle #ifdef CN1_INCLUDE_NOTIFICATIONS -- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0), tvos(10.0)) +// UNNotificationContent.userInfo is unavailable on tvOS, so the foreground +// presentation handler (which reads userInfo to route local/push payloads) is +// omitted there -- it is an optional UNUserNotificationCenterDelegate method. +#if !TARGET_OS_TV +- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0), tvos(10.0)) { if (@available(iOS 10, *)) { if( [notification.request.content.userInfo valueForKey:@"__ios_id__"] != NULL) @@ -628,10 +642,13 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNot [self cn1RoutePush:userInfo]; #endif - + } +#endif // !TARGET_OS_TV (willPresentNotification) +// UNNotificationResponse (notification action responses) is unavailable on tvOS. +#if !TARGET_OS_TV - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { if (@available(iOS 10, *)) { if( [response.notification.request.content.userInfo valueForKey:@"__ios_id__"] != NULL) @@ -660,12 +677,15 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNoti #endif } +#endif // !TARGET_OS_TV (didReceiveNotificationResponse) #endif #ifdef INCLUDE_CN1_PUSH -UNNotificationResponse* currentNotificationResponse = nil; +// UNNotificationResponse type is unavailable on tvOS; hold it as id so the push +// content plumbing compiles (the tvOS-specific text-response branch is guarded). +id currentNotificationResponse = nil; - (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { const unsigned *tokenBytes = [deviceToken bytes]; NSString *tokenAsString = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x", @@ -722,12 +742,15 @@ -(void)cn1RoutePush:(NSDictionary*)userInfo withAction:(NSString*)actionId withC } if (actionId != nil) { com_codename1_push_PushContent_setActionId___java_lang_String(CN1_THREAD_GET_STATE_PASS_ARG fromNSString(CN1_THREAD_GET_STATE_PASS_ARG actionId)); +#if !TARGET_OS_TV + // UNTextInputNotificationResponse (text-input notification actions) is unavailable on tvOS. if (currentNotificationResponse != nil && [currentNotificationResponse isKindOfClass:[UNTextInputNotificationResponse class]]) { UNTextInputNotificationResponse* textResponse = (UNTextInputNotificationResponse*)currentNotificationResponse; if (textResponse.userText != nil) { com_codename1_push_PushContent_setTextResponse___java_lang_String(CN1_THREAD_GET_STATE_PASS_ARG fromNSString(CN1_THREAD_GET_STATE_PASS_ARG textResponse.userText)); } } +#endif } pushReceivedCount=0; if( [apsInfo valueForKey:@"alert"] != NULL) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h index 612b20ea23..0f1e12037d 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h @@ -32,10 +32,11 @@ #import "ExecutableOp.h" #import "PaintOp.h" #import "GLUIImage.h" -// MessageUI (mail/SMS composer) is unavailable on watchOS; the email/SMS native -// methods are #if !TARGET_OS_WATCH-guarded in IOSNative.m, and the matching -// delegate conformances below are likewise dropped on the watch slice. -#if !TARGET_OS_WATCH +// MessageUI (mail/SMS composer) is unavailable on watchOS, and on tvOS it ships +// only a link stub with no composer headers; the email/SMS native methods are +// guarded the same way in IOSNative.m, and the matching delegate conformances +// below are likewise dropped on those slices. +#if !TARGET_OS_WATCH && !TARGET_OS_TV #import #import #endif @@ -255,18 +256,24 @@ void CN1DismissLaunchPlaceholder(void); -(void)drawFrame:(CGRect)rect; @end #else -@interface CodenameOne_GLViewController : UIViewController= 130000 @@ -664,7 +665,7 @@ BOOL isVKBAlwaysOpen() { if(isIOS8() && !isIPad() && displayWidth > displayHeight) { return NO; } -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV else if (!isIOS8() && !isIPad() && ([[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeLeft || [[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeRight)) { // iOS 7 needs a more specific check to find out if we are in landscape mode return NO; @@ -862,6 +863,7 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { || utf.keyboardType == UIKeyboardTypeNumberPad || (utf.returnKeyType == UIReturnKeyNext && isVKBAlwaysOpen())) && !isIPad()) { //add navigation toolbar to the top of the keyboard +#if !TARGET_OS_TV if(showToolbar) { #ifndef CN1_USE_ARC UIToolbar *toolbar = [[[UIToolbar alloc] init] autorelease]; @@ -933,6 +935,7 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { [toolbar setItems:itemsArray]; [utf setInputAccessoryView:toolbar]; } +#endif } } else { CN1UITextView* utv = [[CN1UITextView alloc] initWithFrame:rect]; @@ -943,7 +946,11 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { // scrollViewShouldScrollToTop: when exactly one scroll view has // scrollsToTop=YES, and UITextView's internal scroll view defaults // to YES. + #if !TARGET_OS_TV + #if !TARGET_OS_TV utv.scrollsToTop = NO; + #endif + #endif [utv setBackgroundColor:[UIColor clearColor]]; [utv.layer setBorderColor:[[UIColor clearColor] CGColor]]; [utv.layer setBorderWidth:0]; @@ -1040,6 +1047,7 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { } } +#if !TARGET_OS_TV if(showToolbar) { //add navigation toolbar to the top of the keyboard #ifndef CN1_USE_ARC @@ -1093,6 +1101,7 @@ void cn1_setStyleDoneButton(CN1_THREAD_STATE_MULTI_ARG UIBarButtonItem* btn) { [toolbar setItems:itemsArray]; [utv setInputAccessoryView:toolbar]; } +#endif } editingComponent.opaque = NO; [[CodenameOne_GLViewController instance].view addSubview:editingComponent]; @@ -1892,7 +1901,7 @@ void Java_com_codename1_impl_ios_IOSNative_nativeDrawShadowMutable(CN1_THREAD_ST //CN1Log(@"String is %@", s); //CN1Log(@"Font is %i", (int)f); //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_stringWidthNativeImpl finished"); -#if TARGET_OS_WATCH +#if TARGET_OS_WATCH || TARGET_OS_TV if (s == nil) { return 0; } if (f == nil) { f = [UIFont systemFontOfSize:16.0]; } return (int)[s sizeWithAttributes:@{NSFontAttributeName: f}].width; @@ -1905,7 +1914,7 @@ void Java_com_codename1_impl_ios_IOSNative_nativeDrawShadowMutable(CN1_THREAD_ST int Java_com_codename1_impl_ios_IOSImplementation_charWidthNativeImpl (void* peer, int chr) { UIFont* f = (BRIDGE_CAST UIFont*)peer; -#if TARGET_OS_WATCH +#if TARGET_OS_WATCH || TARGET_OS_TV if (f == nil) { f = [UIFont systemFontOfSize:16.0]; } return [[NSString stringWithCharacters:((const unichar *)&chr) length:1] sizeWithAttributes:@{NSFontAttributeName: f}].width; #else @@ -2673,8 +2682,10 @@ +(CGAffineTransform) currentMutableTransform { return currentMutableTransform; } -#if defined(CN1_USE_METAL) && TARGET_OS_MACCATALYST +#if defined(CN1_USE_METAL) && (TARGET_OS_MACCATALYST || TARGET_OS_TV) // On Mac Catalyst the iOS XIB never compiles (IBAgent-macOS-UIKit crashes +// on it under Xcode 26) and on tvOS the iOS XIBs are excluded from the bundle +// entirely (TvNativeBuilder), so both pass nil to initWithNibName: and the // on it under Xcode 26), so CodenameOne_GLAppDelegate.m passes nil to // initWithNibName: and the default loadView would hand us a plain // UIView. The rendering pipeline expects [eaglView] to find a METALView @@ -2814,7 +2825,11 @@ - (void)cn1InstallStatusBarTapProxy { cn1StatusBarTapProxy = [[CN1StatusBarTapProxyView alloc] initWithFrame:window.bounds]; cn1StatusBarTapProxy.delegate = self; cn1StatusBarTapProxy.backgroundColor = [UIColor clearColor]; + #if !TARGET_OS_TV + #if !TARGET_OS_TV cn1StatusBarTapProxy.scrollsToTop = YES; + #endif + #endif // userInteractionEnabled must remain YES; iOS skips the // scrollViewShouldScrollToTop: dispatch for views that have it disabled. // Touches in the rest of the screen pass through naturally because the @@ -2862,7 +2877,11 @@ - (void)cn1UpdateStatusBarTapProxyFrame { if (statusBarHeight <= 0) { // Pre-iOS 11, or safe-area insets not yet populated, fall back to // the legacy status-bar frame. + #if TARGET_OS_TV + statusBarHeight = 0; + #else statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height; + #endif } // Floor of 1pt keeps the proxy non-empty so iOS still routes // UIStatusBarTapAction to it when the status bar is hidden. Cap at 80pt @@ -3124,6 +3143,7 @@ - (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent * // once viewDidLoad has run; only attaches on iOS 13.0+ where // UIHoverGestureRecognizer exists. - (void)cn1InstallHoverRecognizer { +#if !TARGET_OS_TV if (@available(iOS 13.0, *)) { UIHoverGestureRecognizer *hover = [[UIHoverGestureRecognizer alloc] initWithTarget:self @@ -3140,9 +3160,10 @@ - (void)cn1InstallHoverRecognizer { [hover release]; #endif } +#endif } -- (void)cn1HandleHover:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) { +- (void)cn1HandleHover:(UIGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) { CGPoint p = [recognizer locationInView:self.view]; int x = (int)(p.x * scaleValue); int y = (int)(p.y * scaleValue); @@ -3170,6 +3191,7 @@ - (void)cn1HandleHover:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios( #endif -(UIImage*)createSplashImage { +#if !TARGET_OS_TV UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; bool isPortrait = (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown); @@ -3237,6 +3259,9 @@ -(UIImage*)createSplashImage { } } return img; +#else + return nil; +#endif } EAGLView* lastFoundEaglView; @@ -3362,6 +3387,8 @@ - (void)awakeFromNib #endif //CN1Log(@"Draw texture extension %i", (int)drawTextureSupported); +#if !TARGET_OS_TV +#if !TARGET_OS_TV // register for keyboard notifications [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) @@ -3372,17 +3399,23 @@ - (void)awakeFromNib selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:self.view.window]; +#endif //detect orientation by statusBarOrientation UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; bool isPortrait = (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown); +#else + bool isPortrait = NO; +#endif #ifdef CN1_USE_SPLASH_SCREEN UIImage *img = [self createSplashImage]; #else UIImage* img = nil; #endif + #if !TARGET_OS_TV [self.view setMultipleTouchEnabled:YES]; + #endif if(img != nil) { float scale = scaleValue; DrawImage* dr; @@ -3539,7 +3572,9 @@ - (void)keyboardWillHide:(NSNotification *)n #ifdef __IPHONE_7_0 if (isIOS7()) { prefersStatusBarHidden = NO; + #if !TARGET_OS_TV [self setNeedsStatusBarAppearanceUpdate]; + #endif } #endif [UIView beginAnimations:nil context:NULL]; @@ -3575,7 +3610,9 @@ - (void)keyboardWillShow:(NSNotification *)n // get the size of the keyboard CGRect keyboardEndFrame; + #if !TARGET_OS_TV [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame]; + #endif CGRect keyboardFrame = [self.view convertRect:keyboardEndFrame fromView:nil]; keyboardHeight = keyboardFrame.size.height; @@ -3612,7 +3649,9 @@ - (void)keyboardWillShow:(NSNotification *)n #ifdef __IPHONE_7_0 if (isIOS7()) { prefersStatusBarHidden = YES; + #if !TARGET_OS_TV [self setNeedsStatusBarAppearanceUpdate]; + #endif } #endif viewFrame = setOriginY(keyboardSlideOffset, viewFrame); @@ -4390,7 +4429,11 @@ -(void)drawString:(int)color alpha:(int)alpha font:(UIFont*)font str:(NSString*) CGContextSaveGState(context); CGContextConcatCTM(context, currentMutableTransform); } +#if TARGET_OS_TV + [str drawAtPoint:CGPointMake(x, y) withAttributes:@{NSFontAttributeName: font}]; +#else [str drawAtPoint:CGPointMake(x, y) withFont:font]; +#endif if (currentMutableTransformSet) { CGContextRestoreGState(context); } @@ -4632,7 +4675,11 @@ - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)r com_codename1_impl_ios_IOSImplementation_onGeofenceExit___java_lang_String(CN1_THREAD_GET_STATE_PASS_ARG fromNSString(CN1_THREAD_GET_STATE_PASS_ARG [region identifier])); } +#if TARGET_OS_TV +extern id popoverController; +#else extern UIPopoverController* popoverController; +#endif extern int popoverSupported(); #ifdef INCLUDE_PHOTOLIBRARY_USAGE @@ -4844,6 +4891,7 @@ - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray #endif // MessageUI + AddressBookUI are unavailable on watchOS (and AddressBookUI on Mac -// Catalyst). The native methods that use them are guarded to no-ops on watch. -#if !TARGET_OS_WATCH +// Catalyst); on tvOS MessageUI ships only a link stub with no composer headers +// and AddressBookUI is absent. The native methods that use them are guarded to +// no-ops on those slices. +#if !TARGET_OS_WATCH && !TARGET_OS_TV #import #endif -#if !TARGET_OS_MACCATALYST && !TARGET_OS_WATCH +#if !TARGET_OS_MACCATALYST && !TARGET_OS_WATCH && !TARGET_OS_TV // AddressBookUI and the legacy AddressBook C API are unavailable on Mac -// Catalyst. Skip the import; the contacts path falls back to Contacts.framework -// (handled via INCLUDE_CONTACTS_USAGE undef below). +// Catalyst and tvOS. Skip the import; the contacts path falls back to +// Contacts.framework (handled via INCLUDE_CONTACTS_USAGE undef below). #import #endif -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV #import #endif @@ -131,9 +133,10 @@ #include "permission_apis.h" //#import "QRCodeReaderOC.h" #define AUTO_PLAY_VIDEO -// WebKit is unavailable on watchOS; gate the WKWebView path off there (this -// also leaves supportsWKWebKit undefined, disabling the WK usage block below). -#if defined(ENABLE_WKWEBVIEW) && !TARGET_OS_WATCH +// WebKit is unavailable on watchOS and tvOS; gate the WKWebView path off there +// (this also leaves supportsWKWebKit undefined, disabling the WK usage block +// below). tvOS ships neither UIWebView nor WKWebView, so it has no web view. +#if defined(ENABLE_WKWEBVIEW) && !TARGET_OS_WATCH && !TARGET_OS_TV #if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_1) #import #define supportsWKWebKit @@ -568,28 +571,28 @@ JAVA_INT com_codename1_impl_ios_IOSNative_getDisplayHeight__(CN1_THREAD_STATE_MU } JAVA_OBJECT com_codename1_impl_ios_IOSNative_getClipboardString___R_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV POOL_BEGIN(); UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; JAVA_OBJECT str = fromNSString(CN1_THREAD_STATE_PASS_ARG pasteboard.string); POOL_END(); return str; #else - // watchOS has no UIPasteboard. + // watchOS/tvOS have no UIPasteboard. return JAVA_NULL; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_setClipboardString___java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT str) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV POOL_BEGIN(); NSString* ns = toNSString(CN1_THREAD_STATE_PASS_ARG str); UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.string = ns; POOL_END(); #else - // watchOS has no UIPasteboard. -#endif // !TARGET_OS_WATCH + // watchOS/tvOS have no UIPasteboard. +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } @@ -1676,6 +1679,18 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isRunningOnWatch__(CN1_THREAD_STAT #endif } +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isRunningOnTV__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) +{ + // Resolved entirely at compile time: the tvOS slice returns true, every + // other slice (iOS, Mac Catalyst, watchOS, simulator) returns false so + // behaviour is byte-for-byte identical on iOS. +#if TARGET_OS_TV + return JAVA_TRUE; +#else + return JAVA_FALSE; +#endif +} + void com_codename1_impl_ios_IOSNative_setMacWindowDarkAppearance___boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_BOOLEAN dark) { #if TARGET_OS_MACCATALYST if (@available(iOS 13.0, *)) { @@ -2340,7 +2355,7 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canExecute___java_lang_String(CN1_ void com_codename1_impl_ios_IOSNative_execute___java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT n1) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block NSString* ns = toNSString(CN1_THREAD_STATE_PASS_ARG n1); #ifdef CN1_USE_ARC [ns retain]; @@ -2372,8 +2387,8 @@ void com_codename1_impl_ios_IOSNative_execute___java_lang_String(CN1_THREAD_STAT POOL_END(); }); #else - // watchOS has no UIApplication openURL / UIDocumentInteractionController. -#endif // !TARGET_OS_WATCH + // watchOS/tvOS have no UIDocumentInteractionController. +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_flashBacklight___int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT n1) @@ -2408,7 +2423,7 @@ void com_codename1_impl_ios_IOSNative_restoreMinimizedApplication__(CN1_THREAD_S void com_codename1_impl_ios_IOSNative_lockOrientation___boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_BOOLEAN n1) { //XMLVM_BEGIN_WRAPPER[com_codename1_impl_ios_IOSNative_lockOrientation___boolean] -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if(n1) { orientationLock = 1; dispatch_async(dispatch_get_main_queue(), ^{ @@ -2431,9 +2446,9 @@ void com_codename1_impl_ios_IOSNative_lockOrientation___boolean(CN1_THREAD_STATE }); } #else - // watchOS has no device orientation / UIViewController rotation. + // watchOS/tvOS have no device orientation / UIViewController rotation. orientationLock = n1 ? 1 : 2; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV //XMLVM_END_WRAPPER } @@ -3206,15 +3221,15 @@ void com_codename1_impl_ios_IOSNative_retainPeer___long(CN1_THREAD_STATE_MULTI_A }); #endif } -#if TARGET_OS_WATCH -// watchOS has neither UIWebView nor WKWebView. Disabling the UIWebView path +#if TARGET_OS_WATCH || TARGET_OS_TV +// watchOS and tvOS have no UIWebView. Disabling the UIWebView path // here lets every browser function below compile to its existing fallback // (the WKWebView path is already gated on supportsWKWebKit, which is not -// defined on watch). The browser symbols still exist so the runtime links. +// defined on watch/tv). The browser symbols still exist so the runtime links. #ifndef NO_UIWEBVIEW #define NO_UIWEBVIEW #endif -#endif // TARGET_OS_WATCH +#endif // TARGET_OS_WATCH || TARGET_OS_TV #ifndef NO_UIWEBVIEW UIWebView* com_codename1_impl_ios_IOSNative_createBrowserComponent = nil; #endif @@ -3805,7 +3820,7 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_getBrowserURL___long(CN1_THREAD_STA return returnString; } -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV void registerVideoCallback(CN1_THREAD_STATE_MULTI_ARG MPMoviePlayerController *moviePlayer, JAVA_INT callbackId) { id observer = [[NSNotificationCenter defaultCenter] addObserverForName:MPMoviePlayerPlaybackDidFinishNotification object:moviePlayer queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { @@ -3825,7 +3840,7 @@ void registerVideoCallback(CN1_THREAD_STATE_MULTI_ARG MPMoviePlayerController *m }]; com_codename1_impl_ios_IOSImplementation_bindNSObserverPeerToMediaCallback___long_int(CN1_THREAD_GET_STATE_PASS_ARG (JAVA_LONG)((BRIDGE_CAST void*)observer), callbackId); } -#endif // !TARGET_OS_WATCH (registerVideoCallback / MPMoviePlayerController) +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV (registerVideoCallback / MPMoviePlayerController) void registerVideoCallbackAV(CN1_THREAD_STATE_MULTI_ARG AVPlayer *moviePlayer, JAVA_INT callbackId) { #ifdef CN1_USE_AVKIT @@ -3891,7 +3906,7 @@ BOOL useAVKit() { return NO; } JAVA_LONG createVideoComponentFromStringMP(JAVA_OBJECT str, JAVA_INT onCompletionCallbackId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block MPMoviePlayerController* moviePlayerInstance; dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); @@ -3920,7 +3935,7 @@ JAVA_LONG createVideoComponentFromStringMP(JAVA_OBJECT str, JAVA_INT onCompletio return (JAVA_LONG)((BRIDGE_CAST void*)moviePlayerInstance); #else return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } void addPlaybackToAudioSession() { AVAudioSessionCategory cat = [[AVAudioSession sharedInstance] category]; @@ -3982,7 +3997,7 @@ void com_codename1_impl_ios_IOSNative_removeNotificationCenterObserver___long(CN JAVA_LONG createNativeVideoComponentFromStringMP(JAVA_OBJECT str, JAVA_INT onCompletionCallbackId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block MPMoviePlayerViewController* moviePlayerInstance; dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN() @@ -4003,7 +4018,7 @@ JAVA_LONG createNativeVideoComponentFromStringMP(JAVA_OBJECT str, JAVA_INT onCom return (JAVA_LONG)((BRIDGE_CAST void*)moviePlayerInstance); #else return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_LONG createNativeVideoComponentFromStringAV(JAVA_OBJECT str, JAVA_INT onCompletionCallbackId) { #ifdef CN1_USE_AVKIT @@ -4043,7 +4058,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createNativeVideoComponent___java_lan } JAVA_LONG createVideoComponentMP(JAVA_OBJECT dataObject, JAVA_INT onCompletionCallbackId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block MPMoviePlayerController* moviePlayerInstance; dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); @@ -4081,7 +4096,7 @@ JAVA_LONG createVideoComponentMP(JAVA_OBJECT dataObject, JAVA_INT onCompletionCa return (JAVA_LONG)((BRIDGE_CAST void*)moviePlayerInstance); #else return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_LONG createVideoComponentAV(JAVA_OBJECT dataObject, JAVA_INT onCompletionCallbackId) { #ifdef CN1_USE_AVKIT @@ -4162,7 +4177,7 @@ JAVA_LONG createNativeVideoComponentAV(JAVA_OBJECT dataObject, JAVA_INT onComple #endif } JAVA_LONG createNativeVideoComponentMP(JAVA_OBJECT dataObject, JAVA_INT onCompletionCallbackId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block MPMoviePlayerViewController* moviePlayerInstance; dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); @@ -4196,7 +4211,7 @@ JAVA_LONG createNativeVideoComponentMP(JAVA_OBJECT dataObject, JAVA_INT onComple return (JAVA_LONG)((BRIDGE_CAST void*)moviePlayerInstance); #else return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_LONG com_codename1_impl_ios_IOSNative_createNativeVideoComponent___byte_1ARRAY_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT dataObject, JAVA_INT onCompletionCallbackId) { @@ -4212,7 +4227,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createNativeVideoComponent___byte_1AR } JAVA_LONG createVideoComponentNSDataMP(JAVA_LONG nsData, JAVA_INT onCompletionCallbackId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block MPMoviePlayerController* moviePlayerInstance; dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); @@ -4245,7 +4260,7 @@ JAVA_LONG createVideoComponentNSDataMP(JAVA_LONG nsData, JAVA_INT onCompletionCa return (JAVA_LONG)((BRIDGE_CAST void*)moviePlayerInstance); #else return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_LONG createVideoComponentNSDataAV(JAVA_LONG nsData, JAVA_INT onCompletionCallbackId) { @@ -4288,7 +4303,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createVideoComponentNSData___long_int } JAVA_LONG createNativeVideoComponentNSDataMP(JAVA_LONG nsData, JAVA_INT onCompletionCallbackId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV __block MPMoviePlayerViewController* moviePlayerInstance; dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); @@ -4315,7 +4330,7 @@ JAVA_LONG createNativeVideoComponentNSDataMP(JAVA_LONG nsData, JAVA_INT onComple return (JAVA_LONG)((BRIDGE_CAST void*)moviePlayerInstance); #else return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_LONG createNativeVideoComponentNSDataAV(JAVA_LONG nsData, JAVA_INT onCompletionCallbackId) { @@ -4358,7 +4373,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createNativeVideoComponentNSData___lo #endif } -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV void launchMailAppOnDevice(JAVA_OBJECT recipients, JAVA_OBJECT subject, JAVA_OBJECT content){ // Recipient. NSMutableArray * recipientsArray = [[NSMutableArray alloc] init]; @@ -4384,12 +4399,12 @@ void launchMailAppOnDevice(JAVA_OBJECT recipients, JAVA_OBJECT subject, JAVA_OBJ }); } -#endif // !TARGET_OS_WATCH (launchMailAppOnDevice) +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV (launchMailAppOnDevice) void com_codename1_impl_ios_IOSNative_sendEmailMessage___java_lang_String_1ARRAY_java_lang_String_java_lang_String_java_lang_String_1ARRAY_java_lang_String_1ARRAY_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT recipients, JAVA_OBJECT subject, JAVA_OBJECT content, JAVA_OBJECT attachment, JAVA_OBJECT attachmentMimeType, JAVA_BOOLEAN htmlMail) { -#if TARGET_OS_WATCH - // No MessageUI on watchOS; email composition is a no-op. +#if TARGET_OS_WATCH || TARGET_OS_TV + // No MessageUI on watchOS/tvOS; email composition is a no-op. return; #else if (![MFMailComposeViewController canSendMail]) { @@ -4478,10 +4493,10 @@ void com_codename1_impl_ios_IOSNative_sendEmailMessage___java_lang_String_1ARRAY releaseCN1(CN1_THREAD_GET_STATE_PASS_ARG attachmentMimeType); POOL_END(); }); -#endif // !TARGET_OS_WATCH (sendEmailMessage) +#endif // !(TARGET_OS_WATCH || TARGET_OS_TV) (sendEmailMessage) } -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV MPMoviePlayerController* getMPPlayer(JAVA_LONG peer) { NSObject* obj = (BRIDGE_CAST NSObject*)peer; MPMoviePlayerController* m = nil;; @@ -4547,9 +4562,9 @@ void startVideoComponentAV(JAVA_LONG peer) { }); #endif } -#endif // !TARGET_OS_WATCH (MPMoviePlayerController / AVKit video helpers) +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV (MPMoviePlayerController / AVKit video helpers) -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV void com_codename1_impl_ios_IOSNative_startVideoComponent___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { if (useAVKit()) { startVideoComponentAV(peer); @@ -4976,7 +4991,7 @@ void com_codename1_impl_ios_IOSNative_setVideoFullScreen___long_boolean(CN1_THRE JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isVideoFullScreen___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { return NO; } JAVA_LONG com_codename1_impl_ios_IOSNative_getVideoViewPeer___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { return 0; } void com_codename1_impl_ios_IOSNative_showNativePlayerController___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) {} -#endif // !TARGET_OS_WATCH (MPMoviePlayer / AVKit video peer functions) +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV (MPMoviePlayer / AVKit video peer functions) JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isDarkMode___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { #if !TARGET_OS_WATCH @@ -5458,7 +5473,7 @@ void com_codename1_impl_ios_IOSNative_bonjourPublishStop___long(CN1_THREAD_STATE } JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isLargerTextEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 7.0, *)) { CGFloat baseSize = [UIFont systemFontSize]; UIFont *preferred = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; @@ -5467,13 +5482,13 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isLargerTextEnabled___R_boolean(CN return JAVA_FALSE; } #else - // watchOS has no UIFont systemFontSize. + // watchOS/tvOS have no UIFont systemFontSize. return JAVA_FALSE; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_FLOAT com_codename1_impl_ios_IOSNative_getLargerTextScale___R_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 7.0, *)) { CGFloat baseSize = [UIFont systemFontSize]; UIFont *preferred = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; @@ -5485,9 +5500,9 @@ JAVA_FLOAT com_codename1_impl_ios_IOSNative_getLargerTextScale___R_float(CN1_THR return 1.0f; } #else - // watchOS has no UIFont systemFontSize. + // watchOS/tvOS have no UIFont systemFontSize. return 1.0f; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } #ifdef INCLUDE_LOCATION_USAGE @@ -5606,12 +5621,18 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_getLocationTimeStamp___long(CN1_THREA } #if !TARGET_OS_WATCH +#if TARGET_OS_TV +// UIPopoverController is unavailable on tvOS; hold it as id (the pickers/popovers it backs are tvOS-absent). +id popoverController; +#else UIPopoverController* popoverController; +#endif #endif // !TARGET_OS_WATCH void com_codename1_impl_ios_IOSNative_captureCamera___boolean_int_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_BOOLEAN movie, JAVA_INT quality, JAVA_INT duration) { // UIImagePickerController / UIPopoverController / presentModalViewController are -// all unavailable on watchOS; camera capture is a no-op there. -#if defined(INCLUDE_CAMERA_USAGE) && !TARGET_OS_WATCH +// all unavailable on watchOS and tvOS; camera capture is a no-op there (tvOS +// has no camera). +#if defined(INCLUDE_CAMERA_USAGE) && !TARGET_OS_WATCH && !TARGET_OS_TV dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); UIImagePickerControllerSourceType sourceType = UIImagePickerControllerSourceTypeCamera; // default @@ -5936,39 +5957,39 @@ void com_codename1_impl_ios_IOSNative_startUpdatingLocation___long_int(CN1_THREA case 0 : // HIGH PRIORITY l.desiredAccuracy = kCLLocationAccuracyBest; l.distanceFilter = kCLDistanceFilterNone; -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (isIOS7()) { l.pausesLocationUpdatesAutomatically = NO; } -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV break; case 1: // MEDIUM PRIORITY l.desiredAccuracy = kCLLocationAccuracyHundredMeters; l.distanceFilter = 100; -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (isIOS7()) { l.pausesLocationUpdatesAutomatically = YES; } -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV break; case 2 : // LOW PRIORITY l.desiredAccuracy = kCLLocationAccuracyThreeKilometers; l.distanceFilter = 3000; -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (isIOS7()) { l.pausesLocationUpdatesAutomatically = YES; } -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV break; default : l.desiredAccuracy = kCLLocationAccuracyHundredMeters; l.distanceFilter = kCLDistanceFilterNone; -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (isIOS7()) { l.pausesLocationUpdatesAutomatically = NO; } -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV break; } @@ -5995,34 +6016,38 @@ void com_codename1_impl_ios_IOSNative_startUpdatingLocation___long_int(CN1_THREA //[l setAllowsBackgroundLocationUpdates:YES]; } #endif +#if !TARGET_OS_TV [l startUpdatingLocation]; +#endif // !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_stopUpdatingLocation___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { CLLocationManager* l = (BRIDGE_CAST CLLocationManager*)((void *)peer); +#if !TARGET_OS_TV [l stopUpdatingLocation]; +#endif // !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_startUpdatingBackgroundLocation___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV CLLocationManager* l = (BRIDGE_CAST CLLocationManager*)((void *)peer); l.delegate = [CodenameOne_GLViewController instance]; [l startMonitoringSignificantLocationChanges]; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_stopUpdatingBackgroundLocation___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV CLLocationManager* l = (BRIDGE_CAST CLLocationManager*)((void *)peer); [l stopMonitoringSignificantLocationChanges]; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } //native void addGeofencing(long peer, double lat, double lng, double radius, long expiration, String id); void com_codename1_impl_ios_IOSNative_addGeofencing___long_double_double_double_long_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObj, JAVA_LONG peer, JAVA_DOUBLE lat, JAVA_DOUBLE lng, JAVA_DOUBLE radius, JAVA_LONG expires, JAVA_OBJECT geoId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV CLLocationManager* l = (BRIDGE_CAST CLLocationManager*)((void *)peer); l.delegate = [CodenameOne_GLViewController instance]; CLLocationCoordinate2D center = CLLocationCoordinate2DMake(lat, lng); @@ -6033,20 +6058,20 @@ void com_codename1_impl_ios_IOSNative_addGeofencing___long_double_double_double_ #ifndef CN1_USE_ARC [region release]; #endif -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } // native void removeGeofencing(String id); void com_codename1_impl_ios_IOSNative_removeGeofencing___long_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG peer, JAVA_OBJECT geoId) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV CLLocationManager* l = (BRIDGE_CAST CLLocationManager*)((void *)peer); for (CLRegion *region in [l monitoredRegions]) { if ([[region identifier] isEqualToString:toNSString(CN1_THREAD_GET_STATE_PASS_ARG geoId)]) { [l stopMonitoringForRegion:region]; } } -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } #ifdef INCLUDE_CONTACTS_USAGE @@ -6644,7 +6669,7 @@ void com_codename1_impl_ios_IOSNative_updatePersonWithRecordID___int_com_codenam #endif } -#if defined(CN1_USE_METAL) && TARGET_OS_MACCATALYST +#if defined(CN1_USE_METAL) && (TARGET_OS_MACCATALYST || TARGET_OS_TV) // Reads the Metal renderer's persistent screenTexture back into a CGImage. // screenTexture is exactly the frame presentFramebuffer blits into the // CAMetalLayer drawable, so it IS the genuine on-screen pixel content. Unlike @@ -6682,7 +6707,15 @@ static CGImageRef cn1_copyMetalScreenTextureImage(METALView *mv) { [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:w height:h mipmapped:NO]; desc.usage = MTLTextureUsageShaderRead; +#if TARGET_OS_TV + // tvOS is unified-memory only (like iOS): MTLStorageModeManaged and + // -synchronizeResource: are unavailable. Stage into a Shared texture, which + // the CPU can getBytes from directly once the blit completes -- no + // synchronize step is needed. + desc.storageMode = MTLStorageModeShared; +#else desc.storageMode = MTLStorageModeManaged; +#endif id staging = [device newTextureWithDescriptor:desc]; if (staging == nil) { return NULL; @@ -6696,7 +6729,11 @@ static CGImageRef cn1_copyMetalScreenTextureImage(METALView *mv) { toTexture:staging destinationSlice:0 destinationLevel:0 destinationOrigin:MTLOriginMake(0, 0, 0)]; +#if !TARGET_OS_TV + // Managed storage (Catalyst) must be synchronized to become CPU-visible; + // Shared storage (tvOS) is already CPU-visible, so this would be invalid. [blit synchronizeResource:staging]; +#endif [blit endEncoding]; [cb commit]; [cb waitUntilCompleted]; @@ -6841,7 +6878,7 @@ static BOOL cn1_renderViewIntoContext(UIView *renderView, UIView *rootView, CGCo } } #endif -#if defined(CN1_USE_METAL) && TARGET_OS_MACCATALYST +#if defined(CN1_USE_METAL) && (TARGET_OS_MACCATALYST || TARGET_OS_TV) // The Metal screen view: capture from the renderer's screenTexture, the // exact pixels presented to the drawable. On headless Mac Catalyst the // display link never presents, so -drawViewHierarchyInRect: below would @@ -7246,7 +7283,7 @@ void com_codename1_impl_ios_IOSNative_dial___java_lang_String(CN1_THREAD_STATE_M void com_codename1_impl_ios_IOSNative_sendSMS___java_lang_String_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT number, JAVA_OBJECT text) { -#if TARGET_OS_MACCATALYST || TARGET_OS_WATCH +#if TARGET_OS_MACCATALYST || TARGET_OS_WATCH || TARGET_OS_TV // SMS hardware is absent on Mac / watchOS (no MessageUI on watch); // MFMessageComposeViewController canSendText returns NO. Short-circuit. return; @@ -7341,8 +7378,10 @@ void com_codename1_impl_ios_IOSNative_registerPush__(CN1_THREAD_STATE_MULTI_ARG } } } else { +#if !TARGET_OS_TV [[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; +#endif // !TARGET_OS_TV } } }); @@ -7372,14 +7411,14 @@ void com_codename1_impl_ios_IOSNative_setBadgeNumber___int(CN1_THREAD_STATE_MULT //#endif } -#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH +#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH && !TARGET_OS_TV static NSMutableArray* pushActions; static NSMutableArray* currentCategoryActions; static NSSet* pushCategories; static NSString* currentCategoryId; #endif void com_codename1_impl_ios_IOSNative_registerPushAction___java_lang_String_java_lang_String_java_lang_String_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT identifier, JAVA_OBJECT title, JAVA_OBJECT placeholderText, JAVA_OBJECT replyButtonText) { -#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH +#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 10, *)) { if (pushActions == nil) { pushActions = [[NSMutableArray alloc] init]; @@ -7401,7 +7440,7 @@ void com_codename1_impl_ios_IOSNative_registerPushAction___java_lang_String_java void com_codename1_impl_ios_IOSNative_startPushActionCategory___java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT identifier) { -#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH +#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 10, *)) { currentCategoryId = toNSString(CN1_THREAD_GET_STATE_PASS_ARG identifier); if (currentCategoryActions != nil) { @@ -7413,7 +7452,7 @@ void com_codename1_impl_ios_IOSNative_startPushActionCategory___java_lang_String } void com_codename1_impl_ios_IOSNative_endPushActionCategory__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { -#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH +#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 10, *)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:currentCategoryId actions:currentCategoryActions intentIdentifiers:@[] options:UNNotificationCategoryOptionNone]; @@ -7426,7 +7465,7 @@ void com_codename1_impl_ios_IOSNative_endPushActionCategory__(CN1_THREAD_STATE_M } void com_codename1_impl_ios_IOSNative_addPushActionToCategory___java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT identifier) { -#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH +#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 10, *)) { UNNotificationAction *action = nil; NSString *nsId = toNSString(CN1_THREAD_GET_STATE_PASS_ARG identifier); @@ -7446,7 +7485,7 @@ void com_codename1_impl_ios_IOSNative_addPushActionToCategory___java_lang_String } void com_codename1_impl_ios_IOSNative_registerPushCategories__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { -#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH +#if defined(INCLUDE_CN1_PUSH2) && !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 10, *)) { if (pushCategories != nil) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; @@ -9029,8 +9068,8 @@ JAVA_BOOLEAN java_util_TimeZone_isTimezoneDST___java_lang_String_long(CN1_THREAD } JAVA_OBJECT com_codename1_impl_ios_IOSNative_getUserAgentString___java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT callbackId) { -#if TARGET_OS_WATCH - // watchOS has neither UIWebView nor WKWebView to query a user agent from. +#if TARGET_OS_WATCH || TARGET_OS_TV + // watchOS/tvOS have neither UIWebView nor WKWebView to query a user agent from. return JAVA_NULL; #else __block JAVA_OBJECT c = nil; @@ -9067,7 +9106,7 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_getUserAgentString___java_lang_Stri }); return c; -#endif // TARGET_OS_WATCH +#endif // TARGET_OS_WATCH || TARGET_OS_TV } bool datepickerPopover = NO; @@ -9079,13 +9118,13 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_getUserAgentString___java_lang_Stri int stringPickerSelection; NSDate* currentDatePickerDate; JAVA_LONG currentDatePickerDuration=-1; -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV extern UIPopoverController* popoverControllerInstance; extern UIView *currentActionSheet; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV JAVA_LONG defaultDatePickerDate; -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV void showPopupPickerView(CN1_THREAD_STATE_MULTI_ARG UIView *pickerView) { int SCREEN_HEIGHT = [CodenameOne_GLViewController instance].view.bounds.size.height; int SCREEN_WIDTH = [CodenameOne_GLViewController instance].view.bounds.size.width; @@ -9715,14 +9754,14 @@ void com_codename1_impl_ios_IOSNative_printDocument___java_lang_String_java_lang repaintUI(); }); } -#else // TARGET_OS_WATCH: no UIPickerView / UIDatePicker / UIActivityViewController / UIPrintInteractionController on the watch. +#else // TARGET_OS_WATCH || TARGET_OS_TV: no UIPickerView / UIDatePicker / UIActivityViewController / UIPrintInteractionController on the watch/tv. void com_codename1_impl_ios_IOSNative_openStringPicker___java_lang_String_1ARRAY_int_int_int_int_int_int_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT stringArray, JAVA_INT selection, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h, JAVA_INT preferredWidth, JAVA_INT preferredHeight) {} void com_codename1_impl_ios_IOSNative_openDatePicker___int_long_int_int_int_int_int_int_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT type, JAVA_LONG time, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h, JAVA_INT preferredWidth, JAVA_INT preferredHeightArg, JAVA_INT minuteStep) {} void com_codename1_impl_ios_IOSNative_socialShare___java_lang_String_long_com_codename1_ui_geom_Rectangle(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT text, JAVA_LONG imagePeer, JAVA_OBJECT rectangle) {} void com_codename1_impl_ios_IOSNative_socialShareWithCallback___java_lang_String_long_com_codename1_ui_geom_Rectangle_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT text, JAVA_LONG imagePeer, JAVA_OBJECT rectangle, JAVA_INT callbackId) {} JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isPrintingAvailable__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { return NO; } void com_codename1_impl_ios_IOSNative_printDocument___java_lang_String_java_lang_String_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT path, JAVA_OBJECT mimeType, JAVA_INT callbackId) {} -#endif // !TARGET_OS_WATCH (UIPickerView / share / print) +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV (UIPickerView / share / print) extern BOOL isVKBAlwaysOpen(); @@ -10942,6 +10981,10 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isRunningOnWatch___R_boolean(CN1_T return com_codename1_impl_ios_IOSNative_isRunningOnWatch__(CN1_THREAD_STATE_PASS_ARG instanceObject); } +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isRunningOnTV___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { + return com_codename1_impl_ios_IOSNative_isRunningOnTV__(CN1_THREAD_STATE_PASS_ARG instanceObject); +} + JAVA_LONG com_codename1_impl_ios_IOSNative_createNSData___java_lang_String_R_long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT file) { return com_codename1_impl_ios_IOSNative_createNSData___java_lang_String(CN1_THREAD_STATE_PASS_ARG instanceObject, file); } @@ -11697,8 +11740,13 @@ static void cn1CancelScheduledLocalNotificationById(NSString *nsId) { __block NSMutableArray *matches = [NSMutableArray array]; [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { for (UNNotificationRequest *request in requests) { +#if !TARGET_OS_TV NSString *uid = [NSString stringWithFormat:@"%@", [request.content.userInfo valueForKey:@"__ios_id__"]]; if ([nsId isEqualToString:uid] || [nsId isEqualToString:request.identifier]) { +#else + // tvOS has no UNNotificationContent.userInfo; match on the request identifier only. + if ([nsId isEqualToString:request.identifier]) { +#endif // !TARGET_OS_TV [matches addObject:request.identifier]; } } @@ -11707,7 +11755,9 @@ static void cn1CancelScheduledLocalNotificationById(NSString *nsId) { dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC))); if ([matches count] > 0) { [center removePendingNotificationRequestsWithIdentifiers:matches]; +#if !TARGET_OS_TV [center removeDeliveredNotificationsWithIdentifiers:matches]; +#endif // !TARGET_OS_TV } } #endif @@ -11784,14 +11834,18 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str } if (@available(iOS 10, *)) { UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; +#if !TARGET_OS_TV content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil]; content.body = [NSString localizedUserNotificationStringForKey:body arguments:nil]; +#endif // !TARGET_OS_TV if (alertSound != NULL) { NSString *soundName = toNSString(CN1_THREAD_STATE_PASS_ARG alertSound); if (soundName != nil && [soundName length] > 0) { #if TARGET_OS_WATCH // UNNotificationSound soundNamed: is unavailable on watchOS. content.sound = [UNNotificationSound defaultSound]; +#elif TARGET_OS_TV + // UNNotificationSound is unavailable on tvOS. #else content.sound = [UNNotificationSound soundNamed:soundName]; #endif @@ -11800,7 +11854,9 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str if (badgeNumber >= 0) { content.badge = [NSNumber numberWithInt:badgeNumber]; } +#if !TARGET_OS_TV content.userInfo = dict; +#endif // !TARGET_OS_TV UNNotificationTrigger *trigger = cn1CreateNotificationTrigger(fireDate, repeatType); UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:notificationIdString content:content trigger:trigger]; @@ -11840,7 +11896,13 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_cancelLocalNotification___java_lang_S // Enriched local notifications, permission, BGTaskScheduler and shared content // --------------------------------------------------------------------------- -#ifdef CN1_INCLUDE_NOTIFICATIONS2 +#if defined(CN1_INCLUDE_NOTIFICATIONS2) && TARGET_OS_TV +// tvOS has no UNNotificationCategory / UNNotificationAction; categories are a no-op. +static NSString* cn1RegisterLocalNotificationCategory(NSString *categoryId, NSString *actionsEncoded) API_AVAILABLE(ios(10.0)) { + return nil; +} +#endif +#if defined(CN1_INCLUDE_NOTIFICATIONS2) && !TARGET_OS_TV // Builds and registers a UNNotificationCategory from the packed actions string. Field // separator is U+0001 and record separator is U+0002 (see IOSImplementation). static NSString* cn1RegisterLocalNotificationCategory(NSString *categoryId, NSString *actionsEncoded) API_AVAILABLE(ios(10.0)) { @@ -11875,7 +11937,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_cancelLocalNotification___java_lang_S }]; return categoryId; } -#endif +#endif // CN1_INCLUDE_NOTIFICATIONS2 && !TARGET_OS_TV JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification2___java_lang_String_java_lang_String_java_lang_String_java_lang_String_int_long_int_boolean_java_lang_String_java_lang_String_boolean_java_lang_String_java_lang_String( CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT notificationId, JAVA_OBJECT alertTitle, JAVA_OBJECT alertBody, JAVA_OBJECT alertSound, JAVA_INT badgeNumber, JAVA_LONG fireDate, JAVA_INT repeatType, JAVA_BOOLEAN foreground, @@ -11894,14 +11956,18 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification2___java_lang_St } UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; +#if !TARGET_OS_TV content.title = [NSString localizedUserNotificationStringForKey:title arguments:nil]; content.body = [NSString localizedUserNotificationStringForKey:body arguments:nil]; +#endif // !TARGET_OS_TV if (alertSound != NULL) { NSString *soundName = toNSString(CN1_THREAD_STATE_PASS_ARG alertSound); if (soundName != nil && [soundName length] > 0) { #if TARGET_OS_WATCH // UNNotificationSound soundNamed: is unavailable on watchOS. content.sound = [UNNotificationSound defaultSound]; +#elif TARGET_OS_TV + // UNNotificationSound is unavailable on tvOS. #else content.sound = [UNNotificationSound soundNamed:soundName]; #endif @@ -11910,6 +11976,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification2___java_lang_St if (badgeNumber >= 0) { content.badge = [NSNumber numberWithInt:badgeNumber]; } +#if !TARGET_OS_TV content.userInfo = dict; if (threadId != NULL) { NSString *t = toNSString(CN1_THREAD_STATE_PASS_ARG threadId); @@ -11917,6 +11984,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification2___java_lang_St content.threadIdentifier = t; } } +#endif // !TARGET_OS_TV if (timeSensitive) { if (@available(iOS 15.0, *)) { content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive; @@ -11925,9 +11993,12 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification2___java_lang_St NSString *cat = categoryId == NULL ? nil : toNSString(CN1_THREAD_STATE_PASS_ARG categoryId); NSString *acts = actionsEncoded == NULL ? nil : toNSString(CN1_THREAD_STATE_PASS_ARG actionsEncoded); NSString *registered = cn1RegisterLocalNotificationCategory(cat, acts); +#if !TARGET_OS_TV if (registered != nil) { content.categoryIdentifier = registered; } +#endif // !TARGET_OS_TV +#if !TARGET_OS_TV if (imageAttachmentPath != NULL) { NSString *imgPath = toNSString(CN1_THREAD_STATE_PASS_ARG imageAttachmentPath); if (imgPath != nil && [imgPath length] > 0) { @@ -11950,6 +12021,7 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification2___java_lang_St } } } +#endif // !TARGET_OS_TV (UNNotificationAttachment) UNNotificationTrigger *trigger = cn1CreateNotificationTrigger(fireDate, repeatType); UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:notificationIdString content:content trigger:trigger]; @@ -11989,12 +12061,12 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_requestNotificationPermission___int(C if (@available(iOS 12.0, *)) { if (settings.authorizationStatus == UNAuthorizationStatusProvisional) { level = 3; } } -#if !TARGET_OS_WATCH - // UNAuthorizationStatusEphemeral is unavailable on watchOS. +#if !TARGET_OS_WATCH && !TARGET_OS_TV + // UNAuthorizationStatusEphemeral is unavailable on watchOS/tvOS. if (@available(iOS 14.0, *)) { if (settings.authorizationStatus == UNAuthorizationStatusEphemeral) { level = 4; } } -#endif +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV BOOL g = (level == 2 || level == 3 || level == 4); com_codename1_impl_ios_IOSImplementation_notificationPermissionResult___boolean_int(CN1_THREAD_GET_STATE_PASS_ARG g ? JAVA_TRUE : JAVA_FALSE, level); }]; @@ -13089,9 +13161,12 @@ JAVA_INT com_codename1_impl_ios_IOSNative_generateRsaKeyPair___int_byte_1ARRAY_b // by stopBiometricAuthentication(). Memory management is manual because the // iOS port builds with CLANG_ENABLE_OBJC_ARC=NO (see ARC memory in plan). +#if !TARGET_OS_TV static LAContext *cn1_biometricsContext = nil; +#endif // !TARGET_OS_TV static NSString *cn1_keychainAccessGroup = nil; +#if !TARGET_OS_TV static LAContext *cn1_ensureContext(void) { if (cn1_biometricsContext == nil) { cn1_biometricsContext = [[LAContext alloc] init]; @@ -13105,9 +13180,10 @@ static void cn1_resetContext(void) { cn1_biometricsContext = nil; } } +#endif // !TARGET_OS_TV JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isBiometricsSupported__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (NSClassFromString(@"LAContext") == NULL) { return JAVA_FALSE; } @@ -13116,9 +13192,9 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isBiometricsSupported__(CN1_THREAD BOOL ok = [ctx canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]; return ok ? JAVA_TRUE : JAVA_FALSE; #else - // watchOS has no LAPolicyDeviceOwnerAuthenticationWithBiometrics. + // watchOS/tvOS have no LAPolicyDeviceOwnerAuthenticationWithBiometrics. return JAVA_FALSE; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canAuthenticateBiometric__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { @@ -13126,7 +13202,7 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_canAuthenticateBiometric__(CN1_THR } JAVA_INT com_codename1_impl_ios_IOSNative_getAvailableBiometricTypes__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV if (NSClassFromString(@"LAContext") == NULL) { return 0; } @@ -13148,13 +13224,13 @@ JAVA_INT com_codename1_impl_ios_IOSNative_getAvailableBiometricTypes__(CN1_THREA } return mask; #else - // watchOS has no LAPolicyDeviceOwnerAuthenticationWithBiometrics. + // watchOS/tvOS have no LAPolicyDeviceOwnerAuthenticationWithBiometrics. return 0; -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_authenticateBiometric___int_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_INT requestId, JAVA_OBJECT reason) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV POOL_BEGIN(); NSString *nsReason = (reason == JAVA_NULL) ? @"Authenticate" : toNSString(CN1_THREAD_STATE_PASS_ARG reason); // Each authenticate call gets a fresh context so a prior stopAuthentication @@ -13177,18 +13253,20 @@ void com_codename1_impl_ios_IOSNative_authenticateBiometric___int_java_lang_Stri }); POOL_END(); #else - // watchOS has no LAPolicyDeviceOwnerAuthenticationWithBiometrics; report failure. + // watchOS/tvOS have no LAPolicyDeviceOwnerAuthenticationWithBiometrics; report failure. com_codename1_impl_ios_IOSBiometrics_nativeAuthError___int_int_java_lang_String(getThreadLocalData(), requestId, -1, JAVA_NULL); -#endif // !TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH && !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_stopBiometricAuthentication__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me) { +#if !TARGET_OS_TV if (cn1_biometricsContext != nil) { if (@available(iOS 9.0, *)) { [cn1_biometricsContext invalidate]; } cn1_resetContext(); } +#endif // !TARGET_OS_TV } void com_codename1_impl_ios_IOSNative_setSecureStorageAccessGroup___java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT me, JAVA_OBJECT accessGroup) { diff --git a/Ports/iOSPort/nativeSources/NetworkConnectionImpl.m b/Ports/iOSPort/nativeSources/NetworkConnectionImpl.m index 586f6888bf..192987c331 100644 --- a/Ports/iOSPort/nativeSources/NetworkConnectionImpl.m +++ b/Ports/iOSPort/nativeSources/NetworkConnectionImpl.m @@ -53,7 +53,7 @@ - (id)init - (void*)openConnection:(NSString*)url timeout:(int)timeout { dispatch_async(dispatch_get_main_queue(), ^{ -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; #endif }); @@ -73,7 +73,7 @@ - (void*)openConnection:(NSString*)url timeout:(int)timeout { } - (void)connect { -#if TARGET_OS_WATCH +#if TARGET_OS_WATCH || TARGET_OS_TV // NSURLConnection's synchronous delegate initializer is unavailable on // watchOS (NSURLSession is the supported API). Networking via this legacy // path is a no-op on the watch slice for now. @@ -240,7 +240,7 @@ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)err connectionError((BRIDGE_CAST void*)self, [error localizedDescription]); connections--; if(connections < 1) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; #endif } @@ -254,7 +254,7 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { connectionComplete((BRIDGE_CAST void*)self); connections--; if(connections < 1) { -#if !TARGET_OS_WATCH +#if !TARGET_OS_WATCH && !TARGET_OS_TV [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; #endif } diff --git a/Ports/iOSPort/nativeSources/TVOS_PORT.md b/Ports/iOSPort/nativeSources/TVOS_PORT.md new file mode 100644 index 0000000000..229c91d828 --- /dev/null +++ b/Ports/iOSPort/nativeSources/TVOS_PORT.md @@ -0,0 +1,140 @@ +# Apple TV (tvOS) port + +This document describes how the Codename One iOS port is built for **Apple TV +(tvOS)** and tracks the native-source guarding work. It is the tvOS analog of +`WATCHOS_PORT.md`. + +## Strategy: tvOS is the Mac Catalyst model, not the watch model + +Unlike watchOS (which has no UIKit / Metal / UIView and therefore needs the +dedicated Core Graphics backend in `CN1CGGraphics`), **tvOS is very close to +iOS**: it has UIKit, UIView, `UIApplicationMain` and Metal. The one rendering +difference is that **OpenGL ES is not used at runtime on tvOS** (the framework +headers ship in the SDK but the GL pipeline is unavailable), so the tvOS slice +renders through the existing **Metal backend** (`CN1_USE_METAL`) exactly like the +Mac Catalyst slice does. + +Consequently the tvOS app: + +- is a **separate `appletvos` Xcode target** (`
TV`) added by + `TvNativeBuilder` (it cannot be a variant of the `iphoneos` app target the way + Mac Catalyst is, because the SDK differs), compiled from the **same** + ParparVM-generated sources; +- builds with `CN1_USE_METAL` (the iOS default) and **excludes the OpenGL-only + implementation files** (`CN1ES2compat.m`, `CN1ES1compat.m`, `EAGLView.m`) plus + the iOS XIBs — identical to the Mac Catalyst `EXCLUDED_SOURCE_FILE_NAMES`; +- reuses the shared `UIApplicationMain` entry, the app delegate and the Metal + view controller (no SwiftUI shell and no duplicate-`main` rename, unlike the + watch target); +- weak-links the genuinely-absent frameworks (`WebKit`, and `OpenGLES`/`GLKit` + which are present-but-unused) via ParparVM `-Doptional.frameworks`. + +The tvOS SDK **does** ship the `OpenGLES`, `GLKit` and (a link-stub-only) +`MessageUI` frameworks, so — unlike the Mac Catalyst slice — **no GLKit/OpenGLES +stub headers are needed**. `MessageUI` ships only a `.tbd` with no composer +headers, so the mail/SMS composer is guarded out (as on watchOS). + +Enable it with `tvNative.enabled=true`, or implicitly by declaring +`codename1.tvMain` in `codenameone_settings.properties`. `CN.isTV()` returns +`true` at runtime (backed by the `TARGET_OS_TV` native flag). + +## Native guarding pattern + +Where the iOS code uses an API that is **absent on tvOS**, guard it with +`#if !TARGET_OS_TV` (or broaden an existing `#if !TARGET_OS_WATCH` to +`#if !TARGET_OS_WATCH && !TARGET_OS_TV` when the API is absent on both, and an +`#if TARGET_OS_WATCH` no-op branch to `#if TARGET_OS_WATCH || TARGET_OS_TV`). +This mirrors the established `TARGET_OS_WATCH` / `TARGET_OS_MACCATALYST` guards. + +### tvOS-absent APIs to guard (the working list) + +Verified absent on the `appletvos`/`appletvsimulator` 26.x SDK: + +- **MessageUI** composer (`MFMailComposeViewController`, + `MFMessageComposeViewController`) — *guarded* (CodenameOne_GLViewController.h/.m, + IOSNative.m: email/SMS methods route to the watch no-op path). +- **AddressBookUI / AddressBook** — *guarded* (IOSNative.m import). +- **UIImagePickerController**, **UIDocumentInteractionController**, + **UIPopoverController**, **UIActionSheet**, **UIPickerView**, **UIDatePicker** + delegates/peers — *guarding in progress* (CodenameOne_GLViewController.h + conformance list + the date/picker peer methods in + CodenameOne_GLViewController.m). +- **WebKit / UIWebView** — to guard (no web view on tvOS). +- **Status bar / device orientation / `UIApplication openURL`** — to guard + (no status bar or orientation on tvOS; the remote replaces touch). + +### Native compile status (verified locally against tvOS 26 simulator SDK) + +Building the `HelloCodenameOneTV` target surfaces the tvOS-absent APIs in waves. +Done so far (compile clean): the builder/target generation, `IOSNative.m`, +`CodenameOne_GLAppDelegate.m`, `NetworkConnectionImpl.m`, `UIWebViewEventDelegate.*`, +`DrawStringTextureCache.m`, and the sample's `LocalNotificationNativeImpl.m`. + +* **`IOSNative.m`** (was 144 errors): the tvOS-absent native features there + (CLLocation region monitoring, `MPMoviePlayer*`, `UIPasteboard`, orientation, + `statusBar*`, telephony) overlap almost exactly with what watchOS lacks, so the + `!TARGET_OS_WATCH` / `TARGET_OS_WATCH` guards were broadened to also fire on + `TARGET_OS_TV`. NOTE: this also disables a few features tvOS *does* support + (e.g. `UITextField`/`UITextView`, AudioToolbox) — re-enabling those surgically + is a follow-up; the broad guard gets the slice compiling + rendering first. +**Correction to the earlier IOSNative.m note:** treating tvOS like watchOS for +IOSNative.m was wrong — tvOS *supports* most of what watchOS lacks (CIFilter, +vImage/Accelerate, UIView capture, UITextField, audio, UNUserNotificationCenter). +The correct model is **tvOS ≈ iOS**: follow the iOS code path and guard only the +genuinely tvOS-absent APIs with `#if !TARGET_OS_TV`. The blanket guard-broadening +was reverted. The genuine IOSNative.m remainder is ~6 localized features: +`UIWebView` (legacy browser peer — guard like UIWebViewEventDelegate), +`MPMoviePlayerController`/`MPMoviePlayerViewController` (deprecated media player), +`UIPasteboard` (no clipboard), device orientation +(`UIInterfaceOrientation*`/`statusBarOrientation`/`attemptRotationToDeviceOrientation`), +`UIDocumentInteractionController`, and `scrollsToTop`. Plus `LAContext` +(LocalAuthentication) and the `UNNotificationAction`/`UNNotificationCategory` +push-action registration are tvOS-absent. + +* **`CodenameOne_GLViewController.m`** — COMPILES on tvOS (the ~63 surgical guards + landed: orientation, status bar, toolbar input-accessory, keyboard, hover, + pickers/datePicker/imagePicker/actionSheet/documentInteraction delegates, + legacy text -> `sizeWithAttributes:`, `UIPopoverController` -> `id`). + Unlike IOSNative, this file must follow the **iOS** path (tvOS has UIView/Metal), + so the guards are surgical `#if !TARGET_OS_TV`, NOT the watch broadening. The + sites cluster as: device orientation (`UIInterfaceOrientation*`, + `statusBarOrientation`), status bar (`statusBarFrame`, + `setNeedsStatusBarAppearanceUpdate`), `UIToolbar`/`UIBarStyleBlackTranslucent` + input-accessory, on-screen keyboard notifications (`UIKeyboard*`), + `scrollsToTop`, `setMultipleTouchEnabled`, `UIHoverGestureRecognizer`, legacy + `sizeWithFont:` / `drawAtPoint:withFont:` (use `sizeWithAttributes:` / + `drawAtPoint:withAttributes:`), `systemBackgroundColor` (fall back to + `whiteColor`), and the `UIImagePickerController` / `UIDatePicker` / + `UIPickerView` / `UIActionSheet` delegate methods. IMPORTANT: `UIPopoverController` + (`popoverController` / `popoverControllerInstance`) is unavailable on tvOS and + is referenced file-wide — change its type to `id` rather than block-guarding, + or the picker delegate guards cascade into "undeclared identifier" errors. + +### Build recipe (local, no `~/.m2` collision) + +The `ios-source` build bundles the *installed* `codenameone-ios` artifact's +`nativeSources`, so edits under `Ports/iOSPort/nativeSources/` only reach the +generated project after the iOS port is rebuilt+installed (use an isolated repo: +`-Dmaven.repo.local=/path/to/iso -nsu`). For a fast edit loop, sync edited +sources straight into the generated `
-src/` (but do NOT overwrite the +generated `CN1ES2compat.h` — the builder uncomments `#define CN1_USE_METAL` there; +clobbering it drops the slice to the legacy GL text path). Build with arm64 +(Apple-silicon tvOS sim) and a high error limit to see the full wave: + +``` +./scripts/build-ios-app.sh # generates the
TV target +xcodebuild -project <...>/HelloCodenameOne.xcodeproj -target HelloCodenameOneTV \ + -sdk appletvsimulator -arch arm64 CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES \ + OTHER_CFLAGS="-ferror-limit=0" build +``` + +Once it compiles + links, run it via simctl on an "Apple TV 4K" simulator and +seed `scripts/ios/screenshots-tv/` from the captured frames (the `build-ios-tv` +CI job is non-blocking until then). + +## Input + +tvOS is remote/focus-driven (no touchscreen). Codename One's existing D-pad focus +traversal (`Form.updateFocus`, `GAME_UP/DOWN/LEFT/RIGHT`) drives navigation; the +Siri-remote → `GAME_*` mapping (UIPress / the focus engine) is a follow-up on top +of the rendering slice (the screenshot pipeline does not require input). diff --git a/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.h b/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.h index 5c01229af7..d25355e850 100644 --- a/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.h +++ b/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.h @@ -23,9 +23,9 @@ #import #include "TargetConditionals.h" -// UIWebViewDelegate / WebKit are unavailable on watchOS; the browser peer is -// excluded from the watch slice and this header is empty there. -#if !TARGET_OS_WATCH +// UIWebViewDelegate / WebKit are unavailable on watchOS and tvOS; the browser +// peer is dropped on those slices and this header is empty there. +#if !TARGET_OS_WATCH && !TARGET_OS_TV #import #import "CodenameOne_GLViewController.h" #ifdef ENABLE_WKWEBVIEW diff --git a/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m b/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m index 1b2fb449e7..d43a8872dc 100644 --- a/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m +++ b/Ports/iOSPort/nativeSources/UIWebViewEventDelegate.m @@ -21,6 +21,10 @@ * need additional information or have any questions. */ +#include "TargetConditionals.h" +// UIWebView / WebKit are unavailable on tvOS; the legacy browser-peer delegate +// is dropped on the tvOS slice (matching how it is excluded on watchOS). +#if !TARGET_OS_TV #import "UIWebViewEventDelegate.h" #include "com_codename1_impl_ios_IOSImplementation.h" #include "com_codename1_ui_events_BrowserNavigationCallback.h" @@ -161,3 +165,5 @@ - (void)userContentController:(WKUserContentController *)userContentController d @end + +#endif // !TARGET_OS_TV diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 3863ca23d8..c67b13c956 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -344,6 +344,11 @@ public boolean isWatch() { return nativeInstance.isRunningOnWatch(); } + @Override + public boolean isTV() { + return nativeInstance.isRunningOnTV(); + } + @Override public void addCookie(Cookie c) { if(isUseNativeCookieStore()) { @@ -9407,6 +9412,9 @@ public String[] getPlatformOverrides() { if(isWatch()) { return new String[] {"watch", "ios", "applewatch"}; } + if(isTV()) { + return new String[] {"tv", "ios", "appletv"}; + } if(isTablet()) { return new String[] {"tablet", "ios", "ipad"}; } else { diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index cae89d825c..64f304a38e 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -157,6 +157,11 @@ native void fillGradient(int kind, int stopCount, float[] positions, float[] pre // returning false with zero runtime cost. native boolean isRunningOnWatch(); + // Returns true when the binary is running on the tvOS slice. Implemented + // natively via the TARGET_OS_TV compile-time check so the iOS slice keeps + // returning false with zero runtime cost. + native boolean isRunningOnTV(); + // Mac native (Catalyst): set the host window title bar text from the current form title. native void setWindowTitle(String title); diff --git a/README.md b/README.md index c5fe089bf5..7ee0b33aa5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ It's a complete mobile platform featuring virtual machines, simulator, design to #### 🌟   Codename One is the only platform that.. - Has Write Once Run Anywhere support with no special hardware requirements and 100% code reuse -- Compiles Java or Kotlin into native code for iOS, UWP (Universal Windows Platform), Android and even JavaScript (with seamless PWA and Thread support) +- Compiles Java or Kotlin into native code for iOS, UWP (Universal Windows Platform), Android and even JavaScript (with seamless PWA and Thread support), including the TV form factors Apple TV (tvOS) and Android TV / Google TV - Is Open Source and Free with an enterprise grade commercial offering - Is Easy to use with 100% portable Drag and Drop GUI builder - Has Full access to underlying native OS capabilities using the native OS programming language (e.g. Objective-C) without compromising portability diff --git a/docs/developer-guide/TVPlatforms.asciidoc b/docs/developer-guide/TVPlatforms.asciidoc new file mode 100644 index 0000000000..244260c935 --- /dev/null +++ b/docs/developer-guide/TVPlatforms.asciidoc @@ -0,0 +1,104 @@ +== Television (Apple TV and Android TV) + +Codename One can build and run your application UI on the living-room TV +platforms: Apple TV (tvOS) and Android TV / Google TV. As with the phone and +wearable targets, the same Java/Kotlin code base drives the TV app -- you write +Codename One UI as usual and the build pipeline produces the appropriate TV +artifact for each platform. + +The two platforms reach the TV through different mechanisms: + +* *Android TV / Google TV is Android.* An Android TV app is an ordinary Android + app (the *same APK*) that adds a small amount of manifest metadata: a Leanback + launcher category so the app appears on the TV home screen, the + `android.software.leanback` feature, and an optional banner. The existing + Codename One Android port renders the UI through exactly the same pipeline it + uses on phones and tablets. +* *Apple TV runs tvOS.* tvOS is a UIKit relative of iOS reached through the same + ParparVM (Java bytecode -> C -> native) pipeline used for iPhone/iPad, built + against the `appletvos` SDK with a tvOS application target. tvOS has no + touchscreen -- navigation is driven by the Siri remote and the focus engine. + +In both cases the build is *additive*: with the TV hints turned off your +phone/tablet build is unchanged. + +=== Detecting the TV Form Factor + +Use `CN.isTV()` (or `Display.getInstance().isTV()`) to branch your UI for the +TV at runtime. This is the living-room analog of the existing `isTablet()`, +`isDesktop()` and `isWatch()` checks: + +[source,java] +---- +Form f = new Form(BoxLayout.y()); +if (CN.isTV()) { + // 10-foot UI: larger fonts, generous spacing, focus-driven navigation + f.add(new Label("Hello TV")); +} else { + // Full phone/tablet layout + f.add(new Label("Hello")); +} +f.show(); +---- + +Codename One's existing D-pad/arrow focus traversal (the same mechanism that +powers `GAME_UP`/`GAME_DOWN`/`GAME_LEFT`/`GAME_RIGHT` and component focus) drives +remote navigation on the TV, so focusable components such as `Button` are +navigable with the remote out of the box. + +=== Adapting Styling with CSS Media Queries + +The TV (and watch) form factors integrate with the CSS `@media` device-type +mechanism. Rules inside a `device-tv` (or `device-watch`) media block are +selected at runtime when `Display.isTV()` (respectively `isWatch()`) returns +`true` -- exactly the way `device-tablet` / `device-desktop` already work: + +[source,css] +---- +Label { + color: black; +} + +@media device-tv { + Label { + /* Larger type for the 10-foot UI */ + font-size: 3mm; + color: white; + } +} +---- + +=== Building for Android TV / Google TV + +Enable the Android TV manifest metadata with a single build hint in +`codenameone_settings.properties`: + +[source,properties] +---- +codename1.arg.android.tv=true +---- + +This makes the build: + +* add `` + to the launcher activity so the app appears on the Android TV home screen; +* declare `` and make `android.hardware.touchscreen` optional + so the app installs on touchless TVs; +* generate a 320×180 launcher banner (`@drawable/tv_banner`) from the app icon. + +The resulting APK still installs and runs on phones and tablets. + +=== Building for Apple TV (tvOS) + +Enable the tvOS application target with the `tvNative.*` build hints (analogous +to the `watchNative.*` hints used for Apple Watch): + +[source,properties] +---- +codename1.arg.tvNative.enabled=true +---- + +The tvOS build produces a standalone Apple TV app. Because tvOS uses a separate +App ID and provisioning profile from your iOS app, you supply tvOS signing +material the same way you do for the other Apple targets. diff --git a/docs/developer-guide/css.asciidoc b/docs/developer-guide/css.asciidoc index 65ad9a0080..dc7aa12375 100644 --- a/docs/developer-guide/css.asciidoc +++ b/docs/developer-guide/css.asciidoc @@ -1145,7 +1145,7 @@ You can use media queries to target styles to specific platforms, devices, and d . `platform-xxx` - Target a specific platform. For example, `platform-and`, `platform-ios`, `platform-mac`, `platform-win`. . `density-xxx` - Target a specific device density. For example, `density--low`, `density-low`, `density-medium`, `density-high`, `density--high`, `density-hd`, `density-2hd`, and `density-560`. -. `device-xxx` - Target a specific device type. For example, `device-desktop`, `device-tablet`, `device-phone`. +. `device-xxx` - Target a specific device type. For example, `device-desktop`, `device-tablet`, `device-phone`, `device-tv` (Apple TV / Android TV), and `device-watch` (Apple Watch / Wear OS). The `device-tv` and `device-watch` variants are selected at runtime when `Display.isTV()` / `Display.isWatch()` returns `true`, letting you adapt styling for the 10-foot TV UI or the small wearable screen. Also to the Codename One specific media tokens above, the CSS compilers also recognize standard dark-mode media queries using `prefers-color-scheme: dark`. Rules inside these blocks are compiled into `$Dark` UIIDs automatically (for example, `Button` becomes `$DarkButton`, `Button.selected` becomes `$DarkButton.selected`). diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index b6d7acf2d5..23f0b3c6d1 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -139,6 +139,8 @@ include::Working-With-Linux.asciidoc[] include::Wearables.asciidoc[] +include::TVPlatforms.asciidoc[] + include::Working-With-CodenameOne-Sources.asciidoc[] include::Skin-Designer.asciidoc[] diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt index 42844bbd26..f770e813f4 100644 --- a/docs/developer-guide/languagetool-accept.txt +++ b/docs/developer-guide/languagetool-accept.txt @@ -65,6 +65,7 @@ TeaVM teavm teavmdbg LWUIT +Leanback GUIBuilder UIBuilder UDID diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index 9d59735734..061547340d 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -1195,6 +1195,27 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc } } + // Android TV / Google TV support. android.tv=true marks this as an + // Android TV app: the Leanback launcher category makes the app appear on + // the TV home screen, the leanback software feature plus an optional + // touchscreen declaration advertise TV compatibility, and a 320x180 + // banner is generated from the app icon (see below). The same APK still + // runs on phones/tablets and CN.isTV() returns true at runtime via + // PackageManager.FEATURE_TELEVISION/leanback. With the hint off the + // manifest is unchanged. + String tvLeanbackCategory = ""; + String tvActivityBanner = ""; + if ("true".equals(request.getArg("android.tv", "false"))) { + tvLeanbackCategory = " \n"; + tvActivityBanner = " android:banner=\"@drawable/tv_banner\"\n"; + if (!xPermissions.contains("android.software.leanback")) { + xPermissions += " \n"; + } + if (!xPermissions.contains("android.hardware.touchscreen")) { + xPermissions += " \n"; + } + } + if (playServicesAds) { minSDK = maxInt("21", minSDK); } @@ -2069,6 +2090,23 @@ public void usesClassMethod(String cls, String method) { createIconFile(new File(drawableXXhdpiDir, "icon.png"), iconImage, 144, 144); createIconFile(new File(drawableXXXhdpiDir, "icon.png"), iconImage, 192, 192); + if ("true".equals(request.getArg("android.tv", "false"))) { + // Android TV / Google TV launcher banner (320x180, xhdpi). The + // app icon is centered (scaled, not stretched) on a solid dark + // background so it isn't distorted; the Leanback launcher needs + // a banner for the app to appear on the TV home screen. + BufferedImage banner = new BufferedImage(320, 180, BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D bannerGraphics = banner.createGraphics(); + bannerGraphics.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, + java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR); + bannerGraphics.setColor(new java.awt.Color(0x20, 0x20, 0x20)); + bannerGraphics.fillRect(0, 0, 320, 180); + java.awt.Image scaledIcon = getScaledInstance(iconImage, 144, 144); + bannerGraphics.drawImage(scaledIcon, (320 - 144) / 2, (180 - 144) / 2, null); + bannerGraphics.dispose(); + ImageIO.write(banner, "png", new File(drawableXhdpiDir, "tv_banner.png")); + } + if (enableAdaptiveIcons) { createIconFile(new File(mipmapMdpiDir, "ic_launcher.png"), iconImage, 48, 48); createIconFile(new File(mipmapHdpiDir, "ic_launcher.png"), iconImage, 72, 72); @@ -2883,10 +2921,13 @@ public void usesClassMethod(String cls, String method) { + " android:theme=\""+activityTheme+"\"\n" + " android:configChanges=\"orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout\"\n" + " android:launchMode=\""+launchMode+"\"\n" - + " android:label=\"" + xmlizedDisplayName + "\" >\n" + + " android:label=\"" + xmlizedDisplayName + "\"\n" + + tvActivityBanner + + " >\n" + " \n" + " \n" + " \n" + + tvLeanbackCategory + " \n" + request.getArg("android.xintent_filter", "") + " \n" diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index ee76f884d4..c48e54e4a1 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -64,6 +64,12 @@ public class IPhoneBuilder extends Executor { // watchNative.enabled hint is set, keeping the iOS build unchanged. private final WatchNativeBuilder watchNativeBuilder = new WatchNativeBuilder(this); + // tvNative.* delegate: adds an Apple TV (tvOS) target. tvOS is handled like + // the Mac Catalyst slice (Metal + GL stub headers + GL-only sources excluded) + // but as a separate appletvos target. Inert unless tvNative.enabled (or + // codename1.tvMain) is set, keeping the iOS build unchanged. + private final TvNativeBuilder tvNativeBuilder = new TvNativeBuilder(this); + private boolean enableGalleryMultiselect; private boolean usePhotoKitForMultigallery; private boolean enableWKWebView, disableUIWebView; @@ -337,6 +343,15 @@ public boolean build(File sourceZip, BuildRequest request) throws BuildException ensureXcodeprojInstalled(); } + // tvNative: parse + prep. tvOS has no OpenGL ES, so (like Mac Catalyst) + // force Metal on; the tvOS app is a separate appletvos target wired via + // the xcodeproj gem post-generate. + tvNativeBuilder.parseHints(request); + if (tvNativeBuilder.isEnabled()) { + useMetal = true; + ensureXcodeprojInstalled(); + } + log("Request Args: "); log("-----------------"); for (String arg : request.getArgs()) { @@ -2332,6 +2347,11 @@ public void usesClassMethod(String cls, String method) { // sources link on both the iOS app target and the watch // target. (macNative already widens the set when both apply.) parparCmd.add(watchNativeBuilder.parparvmOptionalFrameworksArg()); + } else if (tvNativeBuilder.isEnabled()) { + // Weak-link the tvOS-incompatible frameworks (OpenGL ES, GLKit, + // WebKit, MessageUI, AddressBook) so the shared sources link on + // both the iOS app target and the tvOS target. + parparCmd.add(tvNativeBuilder.parparvmOptionalFrameworksArg()); } parparCmd.add("-Xmx384m"); parparCmd.add("-jar"); @@ -3002,6 +3022,12 @@ public void usesClassMethod(String cls, String method) { watchNativeBuilder.applyXcodeSettings(request, tmpFile, buildVersion); } + if (tvNativeBuilder.isEnabled()) { + File appSrcDir = new File(tmpFile, "dist/" + request.getMainClass() + "-src"); + tvNativeBuilder.writeTvInfoPlist(request, appSrcDir); + tvNativeBuilder.applyXcodeSettings(request, tmpFile, buildVersion); + } + } catch (Exception ex) { throw new BuildException("Failed to inject into plist"); } diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TvNativeBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TvNativeBuilder.java new file mode 100644 index 0000000000..dc431ad39a --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/TvNativeBuilder.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.apache.tools.ant.BuildException; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Helper extracted from {@link IPhoneBuilder} that owns the Apple TV (tvOS) + * native build path. Activated by the build hint {@code tvNative.enabled=true} + * (or implicitly when {@code codename1.tvMain} is declared). + * + *

Unlike the Apple Watch port (which has no UIKit / Metal and therefore ships + * a dedicated Core Graphics backend), tvOS is much closer to iOS: it has UIKit, + * UIView, {@code UIApplicationMain} and Metal -- it simply lacks OpenGL ES / + * GLKit. tvOS is therefore handled exactly like the Mac Catalyst slice + * ({@link MacNativeBuilder}): the build runs with {@code CN1_USE_METAL} (the + * iOS default), the OpenGL-only source files are excluded, GLKit / OpenGLES + * umbrella imports resolve to stub headers, and the absent frameworks are + * weak-linked. The shared {@code UIApplicationMain} entry, the Metal view + * controller and the UIKit peers are reused as-is. + * + *

Because tvOS uses a different SDK ({@code appletvos}) it cannot be a + * variant of the {@code iphoneos} app target the way Mac Catalyst is; so -- like + * the watch builder -- this delegate adds a second Xcode target that + * compiles the same ParparVM-generated sources (minus the GL-only files) for + * tvOS. Every change is additive: with the hint off the iOS build is + * byte-for-byte unchanged. + */ +class TvNativeBuilder { + private final IPhoneBuilder owner; + + // Parsed hints. + private boolean enabled; + private String bundleId; + private String minDeploymentTarget; // TVOS_DEPLOYMENT_TARGET + private String teamId; + private String displayName; + // Fully-qualified tvOS lifecycle entry class (codename1.tvMain). Optional; + // the tvOS app reuses the shared UIApplicationMain entry (the phone main + // class) so a distinct value is only a tree-shaking root / auto-enable + // trigger. Empty when neither tvMain nor tvNative.mainClass is set. + private String tvMain; + + // OpenGL-only source files with no tvOS substitute (tvOS has no OpenGL ES / + // GLKit). Excluded from the tvOS target exactly as MacNativeBuilder excludes + // them from the Mac Catalyst slice; the rendering-op .m files take their + // internal `#elif defined(CN1_USE_METAL)` branch on tvOS. The four iOS XIBs + // are excluded for the same reason they are on Mac (IBAgent UIKit errors / + // the runtime never loads them by name on the non-iPhone slice). + private static final String EXCLUDED_TV_SOURCES = + "CN1ES2compat.m CN1ES1compat.m EAGLView.m " + + "CodenameOne_GLViewController.xib MainWindow.xib " + + "CodenameOne_METALViewController.xib MainWindowMETAL.xib"; + + // Frameworks the iOS port links that are unavailable on tvOS; ParparVM + // weak-links these (see -Doptional.frameworks) so the iOS slice is unchanged + // while the tvOS slice tolerates the absent symbols. The tvOS SDK actually + // ships the OpenGLES / GLKit / MessageUI headers (deprecated), so only the + // genuinely-absent frameworks need weak-linking. OpenGL ES is not used at + // runtime (the slice renders via Metal); WebKit has no tvOS equivalent. + private static final String TV_OPTIONAL_FRAMEWORKS = + "WebKit.framework;OpenGLES.framework;GLKit.framework"; + + TvNativeBuilder(IPhoneBuilder owner) { + this.owner = owner; + } + + boolean isEnabled() { + return enabled; + } + + /** + * Parse the {@code tvNative.*} hint family. The tvOS slice auto-enables when + * the project declares a {@code codename1.tvMain} entry, so the dual app is + * produced as part of the regular iPhone build; {@code tvNative.enabled=true} + * forces it on even without a distinct tvMain. + */ + void parseHints(BuildRequest request) { + tvMain = request.getArg("tvMain", + request.getArg("tvNative.mainClass", "")).trim(); + enabled = "true".equals(request.getArg("tvNative.enabled", "false")) + || tvMain.length() > 0; + if (!enabled) { + return; + } + if (tvMain.length() == 0) { + tvMain = request.getMainClass(); + } + bundleId = request.getArg("tvNative.bundleId", + request.getPackageName() + ".tvos"); + // tvOS 13 is a safe modern floor: Metal is fully supported and the focus + // engine / UIKit surface the port relies on are all present. + minDeploymentTarget = request.getArg("tvNative.minDeploymentTarget", "13.0"); + teamId = request.getArg("tvNative.teamId", + request.getArg("ios.release.teamId", + request.getArg("ios.teamId", + request.getArg("ios.debug.teamId", "")))); + displayName = request.getArg("tvNative.displayName", + request.getDisplayName() != null ? request.getDisplayName() : request.getMainClass()); + } + + String getMinDeploymentTarget() { + return minDeploymentTarget; + } + + /** Fully-qualified tvOS lifecycle entry class (or the phone main class). */ + String getTvMain() { + return tvMain; + } + + /** + * Frameworks the ParparVM translator should weak-link so the iOS slice still + * links normally while the tvOS slice tolerates absent symbols. + */ + String parparvmOptionalFrameworksArg() { + return "-Doptional.frameworks=" + TV_OPTIONAL_FRAMEWORKS; + } + + /** + * Write the tvOS app's Info.plist into {@code appSrcDir}. tvOS needs only the + * standard CFBundle keys plus the arm64 device capability; the app icon is + * left unset (ASSETCATALOG_COMPILER_APPICON_NAME empty) so the build does not + * require a tvOS Brand Assets set, mirroring how the watch target ships its + * own minimal plist. + */ + void writeTvInfoPlist(BuildRequest request, File appSrcDir) throws IOException { + appSrcDir.mkdirs(); + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("\n"); + sb.append("\n\n"); + plistString(sb, "CFBundleDisplayName", displayName); + plistString(sb, "CFBundleExecutable", "$(EXECUTABLE_NAME)"); + plistString(sb, "CFBundleIdentifier", "$(PRODUCT_BUNDLE_IDENTIFIER)"); + plistString(sb, "CFBundleName", "$(PRODUCT_NAME)"); + plistString(sb, "CFBundlePackageType", "$(PRODUCT_BUNDLE_PACKAGE_TYPE)"); + plistString(sb, "CFBundleShortVersionString", + request.getVersion() == null ? "1.0" : request.getVersion()); + plistString(sb, "CFBundleVersion", "1"); + sb.append(" UIRequiredDeviceCapabilities\n \n") + .append(" arm64\n \n"); + sb.append("\n\n"); + File plist = new File(appSrcDir, request.getMainClass() + "-TV-Info.plist"); + owner.createFile(plist, sb.toString().getBytes(StandardCharsets.UTF_8)); + } + + private static void plistString(StringBuilder sb, String key, String value) { + sb.append(" ").append(key).append("\n ") + .append(value == null ? "" : value).append("\n"); + } + + /** + * Add and configure the tvOS app target in the generated Xcode project via + * the Ruby {@code xcodeproj} gem. Creates the target, compiles the shared + * sources (minus {@link #EXCLUDED_TV_SOURCES}) for {@code appletvos} with the + * Metal backend, points it at the tvOS Info.plist + GL stub headers, mirrors + * the iOS bundle resources, and weak-links / drops the absent GL frameworks. + * The tvOS app reuses the shared {@code UIApplicationMain} entry, so unlike + * the watch target there is no SwiftUI shell or duplicate-{@code main} rename. + */ + void applyXcodeSettings(BuildRequest request, File tmpFile, String buildVersion) + throws BuildException { + File hooksDir = new File(tmpFile, "hooks"); + hooksDir.mkdir(); + File scriptFile = new File(hooksDir, "apply_tv_native_settings.rb"); + String mainClass = request.getMainClass(); + String tvTargetName = mainClass + "TV"; + String projectFile = new File(tmpFile, "dist/" + mainClass + ".xcodeproj").getAbsolutePath(); + String infoPlistPath = mainClass + "-src/" + mainClass + "-TV-Info.plist"; + String resolvedTeamId = owner.sanitizeTeamId(teamId, "tvNative.teamId"); + + StringBuilder s = new StringBuilder(); + s.append("#!/usr/bin/env ruby\n") + .append("require 'xcodeproj'\n") + .append("project_file = '").append(IPhoneBuilder.escapeRubyStr(projectFile)).append("'\n") + .append("xcproj = Xcodeproj::Project.open(project_file)\n") + .append("app_target = xcproj.targets.find { |t| t.name == '") + .append(IPhoneBuilder.escapeRubyStr(mainClass)).append("' }\n") + .append("abort('Unable to find app target ").append(IPhoneBuilder.escapeRubyStr(mainClass)) + .append("') unless app_target\n") + .append("tv_name = '").append(IPhoneBuilder.escapeRubyStr(tvTargetName)).append("'\n") + .append("tv_target = xcproj.targets.find { |t| t.name == tv_name }\n") + .append("if tv_target.nil?\n") + .append(" tv_target = xcproj.new_target(:application, tv_name, :tvos, '") + .append(IPhoneBuilder.escapeRubyStr(minDeploymentTarget)).append("')\n") + .append("end\n") + // Compile the shared ParparVM sources for tvOS, minus the OpenGL- + // only files. Reuse the app target's compile sources so we track + // exactly what was generated (incl. the translated Stub + main()). + .append("excluded = %w[").append(EXCLUDED_TV_SOURCES).append("]\n") + .append("app_target.source_build_phase.files.to_a.each do |bf|\n") + .append(" ref = bf.file_ref\n") + .append(" next unless ref && ref.path\n") + .append(" base = File.basename(ref.path)\n") + .append(" next if excluded.include?(base)\n") + .append(" unless tv_target.source_build_phase.files_references.include?(ref)\n") + .append(" tv_target.source_build_phase.add_file_reference(ref)\n") + .append(" end\n") + .append("end\n") + // Build settings for the tvOS slice. + .append("tv_target.build_configurations.each do |config|\n") + .append(" bs = config.build_settings\n") + .append(" bs['SDKROOT'] = 'appletvos'\n") + .append(" bs['TVOS_DEPLOYMENT_TARGET'] = '") + .append(IPhoneBuilder.escapeRubyStr(minDeploymentTarget)).append("'\n") + .append(" bs['TARGETED_DEVICE_FAMILY'] = '3'\n") + .append(" bs['PRODUCT_BUNDLE_IDENTIFIER'] = '") + .append(IPhoneBuilder.escapeRubyStr(bundleId)).append("'\n") + .append(" bs['PRODUCT_NAME'] = '$(TARGET_NAME)'\n") + .append(" bs['INFOPLIST_FILE'] = '") + .append(IPhoneBuilder.escapeRubyStr(infoPlistPath)).append("'\n") + .append(" bs['MARKETING_VERSION'] = '") + .append(IPhoneBuilder.escapeRubyStr(request.getVersion() == null ? "1.0" : request.getVersion())).append("'\n") + .append(" bs['CURRENT_PROJECT_VERSION'] = '") + .append(IPhoneBuilder.escapeRubyStr(buildVersion == null ? "1" : buildVersion)).append("'\n") + .append(" bs['GCC_PREFIX_HEADER'] = '") + .append(IPhoneBuilder.escapeRubyStr(mainClass + "-src/" + mainClass + "-Prefix.pch")).append("'\n") + .append(" bs['EXCLUDED_SOURCE_FILE_NAMES'] = '").append(EXCLUDED_TV_SOURCES).append("'\n") + // The CN1 sources compile without ARC, matching the iOS port. + .append(" bs['CLANG_ENABLE_OBJC_ARC'] = 'NO'\n") + // No tvOS Brand Assets set is generated; leave the app-icon unset + // so actool does not fail the build (dev/screenshot builds only). + .append(" bs['ASSETCATALOG_COMPILER_APPICON_NAME'] = ''\n") + .append(" bs['SKIP_INSTALL'] = 'YES'\n"); + if (resolvedTeamId != null && !resolvedTeamId.isEmpty()) { + s.append(" bs['DEVELOPMENT_TEAM'] = '").append(resolvedTeamId).append("'\n"); + } + s.append("end\n"); + + // The iOS XIBs / OpenGLES.framework have no tvOS equivalent. Drop GL + // framework refs from the tvOS target (GLKit/OpenGLES are absent on tvOS; + // the GL types come from the stub headers, the rendering uses Metal). + s.append("gl = %w[OpenGLES.framework GLKit.framework]\n") + .append("tv_target.frameworks_build_phase.files.to_a.each do |bf|\n") + .append(" ref = bf.file_ref\n") + .append(" next unless ref && ref.path\n") + .append(" bf.remove_from_project if gl.include?(File.basename(ref.path))\n") + .append("end\n"); + + // Add the generated tvOS Info.plist file reference to the project group so + // INFOPLIST_FILE resolves. + s.append("tv_src = '").append(IPhoneBuilder.escapeRubyStr(mainClass)).append("-src'\n"); + + // Mirror the iOS app's bundle resources into the tvOS target so the CN1 + // runtime finds its theme + assets at runtime (the native theme .res, the + // app theme/CN1Resource.res, material-design-font.ttf, bundled images). + // Skip the iOS UI / icon assets: the asset catalog has no tvOS-applicable + // content and storyboards/xibs are the iOS UI. + s.append("res_skip = %w[.xcassets .storyboard .xib]\n") + .append("app_target.resources_build_phase.files.to_a.each do |bf|\n") + .append(" ref = bf.file_ref\n") + .append(" next unless ref && ref.path\n") + .append(" next if res_skip.any? { |ext| ref.path.to_s.end_with?(ext) }\n") + .append(" unless tv_target.resources_build_phase.files_references.include?(ref)\n") + .append(" tv_target.resources_build_phase.add_file_reference(ref)\n") + .append(" end\n") + .append("end\n"); + + s.append("xcproj.save\n"); + + try { + owner.createFile(scriptFile, s.toString().getBytes(StandardCharsets.UTF_8)); + owner.exec(hooksDir, "chmod", "0755", scriptFile.getAbsolutePath()); + if (!owner.exec(hooksDir, scriptFile.getAbsolutePath())) { + throw new BuildException("Failed to apply tvNative Xcode settings via xcodeproj"); + } + owner.log("[tvNative] Added tvOS target " + tvTargetName + + " (standalone, tvOS " + minDeploymentTarget + ", Metal)"); + } catch (BuildException ex) { + throw ex; + } catch (Exception ex) { + throw new BuildException("Failed to apply tvNative Xcode settings", ex); + } + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java index 1facc69dd4..c1761beda5 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java @@ -911,6 +911,16 @@ private void doAndroidLocalBuild(File tmpProjectDir, Properties props, File dist r.putArgument("watchMain", cn1WatchMain.trim()); } } + // tvMain: optional separate lifecycle entry point for the Apple TV + // (tvOS) slice, declared as codename1.tvMain. Like watchMain it may + // point at the same class as mainName; a distinct value also auto-enables + // the tvOS target. Reaches TvNativeBuilder via request.getArg("tvMain"). + { + String cn1TvMain = props.getProperty("codename1.tvMain"); + if (cn1TvMain != null && cn1TvMain.trim().length() > 0) { + r.putArgument("tvMain", cn1TvMain.trim()); + } + } r.setVersion(props.getProperty("codename1.version")); String iconPath = props.getProperty("codename1.icon"); File iconFile = new File(iconPath); @@ -1163,6 +1173,16 @@ private void doIOSLocalBuild(File tmpProjectDir, Properties props, File distJar) r.putArgument("watchMain", cn1WatchMain.trim()); } } + // tvMain: optional separate lifecycle entry point for the Apple TV + // (tvOS) slice, declared as codename1.tvMain. Like watchMain it may + // point at the same class as mainName; a distinct value also auto-enables + // the tvOS target. Reaches TvNativeBuilder via request.getArg("tvMain"). + { + String cn1TvMain = props.getProperty("codename1.tvMain"); + if (cn1TvMain != null && cn1TvMain.trim().length() > 0) { + r.putArgument("tvMain", cn1TvMain.trim()); + } + } r.setVersion(props.getProperty("codename1.version")); String iconPath = props.getProperty("codename1.icon"); File iconFile = new File(iconPath); @@ -1282,6 +1302,16 @@ private void doWindowsNativeLocalBuild(File tmpProjectDir, Properties props, Fil r.putArgument("watchMain", cn1WatchMain.trim()); } } + // tvMain: optional separate lifecycle entry point for the Apple TV + // (tvOS) slice, declared as codename1.tvMain. Like watchMain it may + // point at the same class as mainName; a distinct value also auto-enables + // the tvOS target. Reaches TvNativeBuilder via request.getArg("tvMain"). + { + String cn1TvMain = props.getProperty("codename1.tvMain"); + if (cn1TvMain != null && cn1TvMain.trim().length() > 0) { + r.putArgument("tvMain", cn1TvMain.trim()); + } + } r.setVersion(props.getProperty("codename1.version")); r.setVendor(props.getProperty("codename1.vendor")); r.setType("windows"); @@ -1375,6 +1405,16 @@ private void doLinuxNativeLocalBuild(File tmpProjectDir, Properties props, File r.putArgument("watchMain", cn1WatchMain.trim()); } } + // tvMain: optional separate lifecycle entry point for the Apple TV + // (tvOS) slice, declared as codename1.tvMain. Like watchMain it may + // point at the same class as mainName; a distinct value also auto-enables + // the tvOS target. Reaches TvNativeBuilder via request.getArg("tvMain"). + { + String cn1TvMain = props.getProperty("codename1.tvMain"); + if (cn1TvMain != null && cn1TvMain.trim().length() > 0) { + r.putArgument("tvMain", cn1TvMain.trim()); + } + } r.setVersion(props.getProperty("codename1.version")); r.setVendor(props.getProperty("codename1.vendor")); r.setType("linux"); @@ -1444,6 +1484,16 @@ private void doJavaScriptLocalBuild(File tmpProjectDir, Properties props, File d r.putArgument("watchMain", cn1WatchMain.trim()); } } + // tvMain: optional separate lifecycle entry point for the Apple TV + // (tvOS) slice, declared as codename1.tvMain. Like watchMain it may + // point at the same class as mainName; a distinct value also auto-enables + // the tvOS target. Reaches TvNativeBuilder via request.getArg("tvMain"). + { + String cn1TvMain = props.getProperty("codename1.tvMain"); + if (cn1TvMain != null && cn1TvMain.trim().length() > 0) { + r.putArgument("tvMain", cn1TvMain.trim()); + } + } r.setVersion(props.getProperty("codename1.version")); String iconPath = props.getProperty("codename1.icon"); if (iconPath != null) { diff --git a/scripts/hellocodenameone/common/codenameone_settings.properties b/scripts/hellocodenameone/common/codenameone_settings.properties index 96a2035069..d087ea2ddb 100644 --- a/scripts/hellocodenameone/common/codenameone_settings.properties +++ b/scripts/hellocodenameone/common/codenameone_settings.properties @@ -25,6 +25,7 @@ codename1.kotlin=false codename1.languageLevel=5 codename1.mainName=HelloCodenameOne codename1.watchMain=com.codenameone.examples.hellocodenameone.HelloCodenameOneWatch +codename1.tvMain=com.codenameone.examples.hellocodenameone.HelloCodenameOne codename1.packageName=com.codenameone.examples.hellocodenameone codename1.rim.certificatePassword= codename1.rim.signtoolCsk= diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m index 1fb61420c0..145538e967 100644 --- a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m +++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_LocalNotificationNativeImpl.m @@ -8,6 +8,9 @@ -(void)clearScheduledLocalNotifications:(NSString*)param{ if (param == nil) { return; } + // tvOS has no delivered-notification management (removeDelivered*) and a + // reduced UserNotifications surface; this maintenance call is a no-op there. +#if !TARGET_OS_TV if (@available(iOS 10.0, *)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; dispatch_semaphore_t sem = dispatch_semaphore_create(0); @@ -27,6 +30,7 @@ -(void)clearScheduledLocalNotifications:(NSString*)param{ [center removeDeliveredNotificationsWithIdentifiers:matches]; } } +#endif } -(int)getScheduledLocalNotificationCount:(NSString*)param{ @@ -34,6 +38,7 @@ -(int)getScheduledLocalNotificationCount:(NSString*)param{ return 0; } __block int count = 0; +#if !TARGET_OS_TV if (@available(iOS 10.0, *)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; dispatch_semaphore_t sem = dispatch_semaphore_create(0); @@ -48,6 +53,7 @@ -(int)getScheduledLocalNotificationCount:(NSString*)param{ }]; dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC))); } +#endif return count; } diff --git a/scripts/ios/screenshots-tv/AdsScreen.png b/scripts/ios/screenshots-tv/AdsScreen.png new file mode 100644 index 0000000000..00d9cfe61b Binary files /dev/null and b/scripts/ios/screenshots-tv/AdsScreen.png differ diff --git a/scripts/ios/screenshots-tv/AnimateHierarchyScreenshotTest.png b/scripts/ios/screenshots-tv/AnimateHierarchyScreenshotTest.png new file mode 100644 index 0000000000..67dee24470 Binary files /dev/null and b/scripts/ios/screenshots-tv/AnimateHierarchyScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/AnimateLayoutScreenshotTest.png b/scripts/ios/screenshots-tv/AnimateLayoutScreenshotTest.png new file mode 100644 index 0000000000..f825d175cd Binary files /dev/null and b/scripts/ios/screenshots-tv/AnimateLayoutScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/AnimateUnlayoutScreenshotTest.png b/scripts/ios/screenshots-tv/AnimateUnlayoutScreenshotTest.png new file mode 100644 index 0000000000..f5748f2371 Binary files /dev/null and b/scripts/ios/screenshots-tv/AnimateUnlayoutScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/ButtonTheme_dark.png b/scripts/ios/screenshots-tv/ButtonTheme_dark.png new file mode 100644 index 0000000000..6f8d7609ff Binary files /dev/null and b/scripts/ios/screenshots-tv/ButtonTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/ButtonTheme_light.png b/scripts/ios/screenshots-tv/ButtonTheme_light.png new file mode 100644 index 0000000000..377abead8c Binary files /dev/null and b/scripts/ios/screenshots-tv/ButtonTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/ChatInput_dark.png b/scripts/ios/screenshots-tv/ChatInput_dark.png new file mode 100644 index 0000000000..ef01e02b03 Binary files /dev/null and b/scripts/ios/screenshots-tv/ChatInput_dark.png differ diff --git a/scripts/ios/screenshots-tv/ChatInput_light.png b/scripts/ios/screenshots-tv/ChatInput_light.png new file mode 100644 index 0000000000..1626053f8a Binary files /dev/null and b/scripts/ios/screenshots-tv/ChatInput_light.png differ diff --git a/scripts/ios/screenshots-tv/ChatView_dark.png b/scripts/ios/screenshots-tv/ChatView_dark.png new file mode 100644 index 0000000000..e70972aca4 Binary files /dev/null and b/scripts/ios/screenshots-tv/ChatView_dark.png differ diff --git a/scripts/ios/screenshots-tv/ChatView_light.png b/scripts/ios/screenshots-tv/ChatView_light.png new file mode 100644 index 0000000000..ae911471dc Binary files /dev/null and b/scripts/ios/screenshots-tv/ChatView_light.png differ diff --git a/scripts/ios/screenshots-tv/CheckBoxRadioTheme_dark.png b/scripts/ios/screenshots-tv/CheckBoxRadioTheme_dark.png new file mode 100644 index 0000000000..edc0dd80c9 Binary files /dev/null and b/scripts/ios/screenshots-tv/CheckBoxRadioTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/CheckBoxRadioTheme_light.png b/scripts/ios/screenshots-tv/CheckBoxRadioTheme_light.png new file mode 100644 index 0000000000..dff95fd522 Binary files /dev/null and b/scripts/ios/screenshots-tv/CheckBoxRadioTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/ComponentReplaceFadeScreenshotTest.png b/scripts/ios/screenshots-tv/ComponentReplaceFadeScreenshotTest.png new file mode 100644 index 0000000000..d7164ae492 Binary files /dev/null and b/scripts/ios/screenshots-tv/ComponentReplaceFadeScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/ComponentReplaceFlipScreenshotTest.png b/scripts/ios/screenshots-tv/ComponentReplaceFlipScreenshotTest.png new file mode 100644 index 0000000000..a0dd9ec27a Binary files /dev/null and b/scripts/ios/screenshots-tv/ComponentReplaceFlipScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/ComponentReplaceSlideScreenshotTest.png b/scripts/ios/screenshots-tv/ComponentReplaceSlideScreenshotTest.png new file mode 100644 index 0000000000..c0e6cfea0f Binary files /dev/null and b/scripts/ios/screenshots-tv/ComponentReplaceSlideScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/CoverHorizontalTransitionTest.png b/scripts/ios/screenshots-tv/CoverHorizontalTransitionTest.png new file mode 100644 index 0000000000..4db92e40ba Binary files /dev/null and b/scripts/ios/screenshots-tv/CoverHorizontalTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/DesktopMode.png b/scripts/ios/screenshots-tv/DesktopMode.png new file mode 100644 index 0000000000..65cdaf71c6 Binary files /dev/null and b/scripts/ios/screenshots-tv/DesktopMode.png differ diff --git a/scripts/ios/screenshots-tv/DialogTheme_dark.png b/scripts/ios/screenshots-tv/DialogTheme_dark.png new file mode 100644 index 0000000000..d2c36ea84a Binary files /dev/null and b/scripts/ios/screenshots-tv/DialogTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/DialogTheme_light.png b/scripts/ios/screenshots-tv/DialogTheme_light.png new file mode 100644 index 0000000000..3250db7c24 Binary files /dev/null and b/scripts/ios/screenshots-tv/DialogTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/FadeTransitionTest.png b/scripts/ios/screenshots-tv/FadeTransitionTest.png new file mode 100644 index 0000000000..aba4a42f28 Binary files /dev/null and b/scripts/ios/screenshots-tv/FadeTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/FlipTransitionTest.png b/scripts/ios/screenshots-tv/FlipTransitionTest.png new file mode 100644 index 0000000000..d54e03e73b Binary files /dev/null and b/scripts/ios/screenshots-tv/FlipTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/FloatingActionButtonTheme_dark.png b/scripts/ios/screenshots-tv/FloatingActionButtonTheme_dark.png new file mode 100644 index 0000000000..8acb4a6e68 Binary files /dev/null and b/scripts/ios/screenshots-tv/FloatingActionButtonTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/FloatingActionButtonTheme_light.png b/scripts/ios/screenshots-tv/FloatingActionButtonTheme_light.png new file mode 100644 index 0000000000..9c1ba428a4 Binary files /dev/null and b/scripts/ios/screenshots-tv/FloatingActionButtonTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/Gpu3DAnimation.png b/scripts/ios/screenshots-tv/Gpu3DAnimation.png new file mode 100644 index 0000000000..5ce2fd6ab3 Binary files /dev/null and b/scripts/ios/screenshots-tv/Gpu3DAnimation.png differ diff --git a/scripts/ios/screenshots-tv/Gpu3DCube.png b/scripts/ios/screenshots-tv/Gpu3DCube.png new file mode 100644 index 0000000000..add543d530 Binary files /dev/null and b/scripts/ios/screenshots-tv/Gpu3DCube.png differ diff --git a/scripts/ios/screenshots-tv/Gpu3DModel.png b/scripts/ios/screenshots-tv/Gpu3DModel.png new file mode 100644 index 0000000000..3d428a87e1 Binary files /dev/null and b/scripts/ios/screenshots-tv/Gpu3DModel.png differ diff --git a/scripts/ios/screenshots-tv/Gpu3DTexturedCube.png b/scripts/ios/screenshots-tv/Gpu3DTexturedCube.png new file mode 100644 index 0000000000..8f1c68f9e1 Binary files /dev/null and b/scripts/ios/screenshots-tv/Gpu3DTexturedCube.png differ diff --git a/scripts/ios/screenshots-tv/ImageViewerNavigationModes.png b/scripts/ios/screenshots-tv/ImageViewerNavigationModes.png new file mode 100644 index 0000000000..e135807b64 Binary files /dev/null and b/scripts/ios/screenshots-tv/ImageViewerNavigationModes.png differ diff --git a/scripts/ios/screenshots-tv/LightweightPickerButtons.png b/scripts/ios/screenshots-tv/LightweightPickerButtons.png new file mode 100644 index 0000000000..733edc892f Binary files /dev/null and b/scripts/ios/screenshots-tv/LightweightPickerButtons.png differ diff --git a/scripts/ios/screenshots-tv/LightweightPickerButtons_above_center.png b/scripts/ios/screenshots-tv/LightweightPickerButtons_above_center.png new file mode 100644 index 0000000000..1fc1b0e0eb Binary files /dev/null and b/scripts/ios/screenshots-tv/LightweightPickerButtons_above_center.png differ diff --git a/scripts/ios/screenshots-tv/LightweightPickerButtons_below_right.png b/scripts/ios/screenshots-tv/LightweightPickerButtons_below_right.png new file mode 100644 index 0000000000..38375a6b52 Binary files /dev/null and b/scripts/ios/screenshots-tv/LightweightPickerButtons_below_right.png differ diff --git a/scripts/ios/screenshots-tv/LightweightPickerButtons_between_mixed.png b/scripts/ios/screenshots-tv/LightweightPickerButtons_between_mixed.png new file mode 100644 index 0000000000..d49f946038 Binary files /dev/null and b/scripts/ios/screenshots-tv/LightweightPickerButtons_between_mixed.png differ diff --git a/scripts/ios/screenshots-tv/ListTheme_dark.png b/scripts/ios/screenshots-tv/ListTheme_dark.png new file mode 100644 index 0000000000..af3f51660b Binary files /dev/null and b/scripts/ios/screenshots-tv/ListTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/ListTheme_light.png b/scripts/ios/screenshots-tv/ListTheme_light.png new file mode 100644 index 0000000000..07f64259e0 Binary files /dev/null and b/scripts/ios/screenshots-tv/ListTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/LottieAnimatedScreenshotTest.png b/scripts/ios/screenshots-tv/LottieAnimatedScreenshotTest.png new file mode 100644 index 0000000000..4dadc9176d Binary files /dev/null and b/scripts/ios/screenshots-tv/LottieAnimatedScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/MainActivity.png b/scripts/ios/screenshots-tv/MainActivity.png new file mode 100644 index 0000000000..37d0d3c032 Binary files /dev/null and b/scripts/ios/screenshots-tv/MainActivity.png differ diff --git a/scripts/ios/screenshots-tv/MediaPlayback.png b/scripts/ios/screenshots-tv/MediaPlayback.png new file mode 100644 index 0000000000..621168488b Binary files /dev/null and b/scripts/ios/screenshots-tv/MediaPlayback.png differ diff --git a/scripts/ios/screenshots-tv/MorphTransitionScrolledSourceTest.png b/scripts/ios/screenshots-tv/MorphTransitionScrolledSourceTest.png new file mode 100644 index 0000000000..ce2588ab38 Binary files /dev/null and b/scripts/ios/screenshots-tv/MorphTransitionScrolledSourceTest.png differ diff --git a/scripts/ios/screenshots-tv/MorphTransitionSnapshotTest.png b/scripts/ios/screenshots-tv/MorphTransitionSnapshotTest.png new file mode 100644 index 0000000000..6dee9b0569 Binary files /dev/null and b/scripts/ios/screenshots-tv/MorphTransitionSnapshotTest.png differ diff --git a/scripts/ios/screenshots-tv/MorphTransitionTest.png b/scripts/ios/screenshots-tv/MorphTransitionTest.png new file mode 100644 index 0000000000..57a9fe3317 Binary files /dev/null and b/scripts/ios/screenshots-tv/MorphTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/MotionShowcaseScreenshotTest.png b/scripts/ios/screenshots-tv/MotionShowcaseScreenshotTest.png new file mode 100644 index 0000000000..025fd62a38 Binary files /dev/null and b/scripts/ios/screenshots-tv/MotionShowcaseScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/MultiButtonTheme_dark.png b/scripts/ios/screenshots-tv/MultiButtonTheme_dark.png new file mode 100644 index 0000000000..13ce97ffe4 Binary files /dev/null and b/scripts/ios/screenshots-tv/MultiButtonTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/MultiButtonTheme_light.png b/scripts/ios/screenshots-tv/MultiButtonTheme_light.png new file mode 100644 index 0000000000..e9ba140c79 Binary files /dev/null and b/scripts/ios/screenshots-tv/MultiButtonTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/PaletteOverrideTheme_dark.png b/scripts/ios/screenshots-tv/PaletteOverrideTheme_dark.png new file mode 100644 index 0000000000..63f197b429 Binary files /dev/null and b/scripts/ios/screenshots-tv/PaletteOverrideTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/PaletteOverrideTheme_light.png b/scripts/ios/screenshots-tv/PaletteOverrideTheme_light.png new file mode 100644 index 0000000000..afa77b2d5e Binary files /dev/null and b/scripts/ios/screenshots-tv/PaletteOverrideTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/PickerTheme_dark.png b/scripts/ios/screenshots-tv/PickerTheme_dark.png new file mode 100644 index 0000000000..a4c3be3b1e Binary files /dev/null and b/scripts/ios/screenshots-tv/PickerTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/PickerTheme_light.png b/scripts/ios/screenshots-tv/PickerTheme_light.png new file mode 100644 index 0000000000..85d0aa0002 Binary files /dev/null and b/scripts/ios/screenshots-tv/PickerTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/PullToRefreshSpinnerScreenshotTest.png b/scripts/ios/screenshots-tv/PullToRefreshSpinnerScreenshotTest.png new file mode 100644 index 0000000000..2f6a8bf5d7 Binary files /dev/null and b/scripts/ios/screenshots-tv/PullToRefreshSpinnerScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/README.md b/scripts/ios/screenshots-tv/README.md new file mode 100644 index 0000000000..35d8998c93 --- /dev/null +++ b/scripts/ios/screenshots-tv/README.md @@ -0,0 +1,10 @@ +# Apple TV (tvOS) screenshot goldens + +Reference PNGs for the `build-ios-tv` CI job (`scripts/run-tv-ui-tests.sh`), +captured from the `hellocodenameone` `cn1ss` suite running on the tvOS simulator +(1920x1080) and streamed through `Cn1ssScreenshotServer`. + +This set is seeded once the tvOS native slice compiles end-to-end (see +`Ports/iOSPort/nativeSources/TVOS_PORT.md`), the same way the watch goldens in +`../screenshots-watch` were bootstrapped in CI. Until then the `build-ios-tv` +job runs non-blocking. diff --git a/scripts/ios/screenshots-tv/SVGAnimatedScreenshotTest.png b/scripts/ios/screenshots-tv/SVGAnimatedScreenshotTest.png new file mode 100644 index 0000000000..f4e8081451 Binary files /dev/null and b/scripts/ios/screenshots-tv/SVGAnimatedScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/SVGStatic.png b/scripts/ios/screenshots-tv/SVGStatic.png new file mode 100644 index 0000000000..67df3262f5 Binary files /dev/null and b/scripts/ios/screenshots-tv/SVGStatic.png differ diff --git a/scripts/ios/screenshots-tv/Sheet.png b/scripts/ios/screenshots-tv/Sheet.png new file mode 100644 index 0000000000..20166b0780 Binary files /dev/null and b/scripts/ios/screenshots-tv/Sheet.png differ diff --git a/scripts/ios/screenshots-tv/SheetSlideUpAnimationScreenshotTest.png b/scripts/ios/screenshots-tv/SheetSlideUpAnimationScreenshotTest.png new file mode 100644 index 0000000000..aaa655f6b4 Binary files /dev/null and b/scripts/ios/screenshots-tv/SheetSlideUpAnimationScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/ShowcaseTheme_dark.png b/scripts/ios/screenshots-tv/ShowcaseTheme_dark.png new file mode 100644 index 0000000000..92c35db935 Binary files /dev/null and b/scripts/ios/screenshots-tv/ShowcaseTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/ShowcaseTheme_light.png b/scripts/ios/screenshots-tv/ShowcaseTheme_light.png new file mode 100644 index 0000000000..7643447895 Binary files /dev/null and b/scripts/ios/screenshots-tv/ShowcaseTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/SlideFadeTitleTransitionTest.png b/scripts/ios/screenshots-tv/SlideFadeTitleTransitionTest.png new file mode 100644 index 0000000000..e0f3b7721d Binary files /dev/null and b/scripts/ios/screenshots-tv/SlideFadeTitleTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/SlideHorizontalBackTransitionTest.png b/scripts/ios/screenshots-tv/SlideHorizontalBackTransitionTest.png new file mode 100644 index 0000000000..d9f49e1527 Binary files /dev/null and b/scripts/ios/screenshots-tv/SlideHorizontalBackTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/SlideHorizontalTransitionTest.png b/scripts/ios/screenshots-tv/SlideHorizontalTransitionTest.png new file mode 100644 index 0000000000..469b4c62ef Binary files /dev/null and b/scripts/ios/screenshots-tv/SlideHorizontalTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/SlideVerticalTransitionTest.png b/scripts/ios/screenshots-tv/SlideVerticalTransitionTest.png new file mode 100644 index 0000000000..8d323b17ae Binary files /dev/null and b/scripts/ios/screenshots-tv/SlideVerticalTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/SmoothScrollScreenshotTest.png b/scripts/ios/screenshots-tv/SmoothScrollScreenshotTest.png new file mode 100644 index 0000000000..d336e558a9 Binary files /dev/null and b/scripts/ios/screenshots-tv/SmoothScrollScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/SpanLabelTheme_dark.png b/scripts/ios/screenshots-tv/SpanLabelTheme_dark.png new file mode 100644 index 0000000000..693767a15a Binary files /dev/null and b/scripts/ios/screenshots-tv/SpanLabelTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/SpanLabelTheme_light.png b/scripts/ios/screenshots-tv/SpanLabelTheme_light.png new file mode 100644 index 0000000000..01bc95b266 Binary files /dev/null and b/scripts/ios/screenshots-tv/SpanLabelTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/StatusBarTapDiagnosticScreenshotTest.png b/scripts/ios/screenshots-tv/StatusBarTapDiagnosticScreenshotTest.png new file mode 100644 index 0000000000..2821ffdffd Binary files /dev/null and b/scripts/ios/screenshots-tv/StatusBarTapDiagnosticScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/StickyHeaderFadeTransitionScreenshotTest.png b/scripts/ios/screenshots-tv/StickyHeaderFadeTransitionScreenshotTest.png new file mode 100644 index 0000000000..3e0862be6e Binary files /dev/null and b/scripts/ios/screenshots-tv/StickyHeaderFadeTransitionScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/StickyHeaderScreenshotTest.png b/scripts/ios/screenshots-tv/StickyHeaderScreenshotTest.png new file mode 100644 index 0000000000..161ff05b50 Binary files /dev/null and b/scripts/ios/screenshots-tv/StickyHeaderScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/StickyHeaderSlideTransitionScreenshotTest.png b/scripts/ios/screenshots-tv/StickyHeaderSlideTransitionScreenshotTest.png new file mode 100644 index 0000000000..e41533d96e Binary files /dev/null and b/scripts/ios/screenshots-tv/StickyHeaderSlideTransitionScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/SwitchTheme_dark.png b/scripts/ios/screenshots-tv/SwitchTheme_dark.png new file mode 100644 index 0000000000..9f3a581b4a Binary files /dev/null and b/scripts/ios/screenshots-tv/SwitchTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/SwitchTheme_light.png b/scripts/ios/screenshots-tv/SwitchTheme_light.png new file mode 100644 index 0000000000..6b388d702a Binary files /dev/null and b/scripts/ios/screenshots-tv/SwitchTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/TabsAnimatedIndicatorScreenshotTest.png b/scripts/ios/screenshots-tv/TabsAnimatedIndicatorScreenshotTest.png new file mode 100644 index 0000000000..7b5f41e09a Binary files /dev/null and b/scripts/ios/screenshots-tv/TabsAnimatedIndicatorScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/TabsBehavior.png b/scripts/ios/screenshots-tv/TabsBehavior.png new file mode 100644 index 0000000000..13809d0a69 Binary files /dev/null and b/scripts/ios/screenshots-tv/TabsBehavior.png differ diff --git a/scripts/ios/screenshots-tv/TabsTheme_dark.png b/scripts/ios/screenshots-tv/TabsTheme_dark.png new file mode 100644 index 0000000000..1ac5d25f3b Binary files /dev/null and b/scripts/ios/screenshots-tv/TabsTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/TabsTheme_light.png b/scripts/ios/screenshots-tv/TabsTheme_light.png new file mode 100644 index 0000000000..716ae96f7c Binary files /dev/null and b/scripts/ios/screenshots-tv/TabsTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/TensileBounceScreenshotTest.png b/scripts/ios/screenshots-tv/TensileBounceScreenshotTest.png new file mode 100644 index 0000000000..f5564504b1 Binary files /dev/null and b/scripts/ios/screenshots-tv/TensileBounceScreenshotTest.png differ diff --git a/scripts/ios/screenshots-tv/TextAreaAlignmentStates.png b/scripts/ios/screenshots-tv/TextAreaAlignmentStates.png new file mode 100644 index 0000000000..940ad8ef83 Binary files /dev/null and b/scripts/ios/screenshots-tv/TextAreaAlignmentStates.png differ diff --git a/scripts/ios/screenshots-tv/TextFieldTheme_dark.png b/scripts/ios/screenshots-tv/TextFieldTheme_dark.png new file mode 100644 index 0000000000..cc31d008f1 Binary files /dev/null and b/scripts/ios/screenshots-tv/TextFieldTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/TextFieldTheme_light.png b/scripts/ios/screenshots-tv/TextFieldTheme_light.png new file mode 100644 index 0000000000..e365d2c008 Binary files /dev/null and b/scripts/ios/screenshots-tv/TextFieldTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/ToastBarTopPosition.png b/scripts/ios/screenshots-tv/ToastBarTopPosition.png new file mode 100644 index 0000000000..273280cfdd Binary files /dev/null and b/scripts/ios/screenshots-tv/ToastBarTopPosition.png differ diff --git a/scripts/ios/screenshots-tv/ToolbarTheme_dark.png b/scripts/ios/screenshots-tv/ToolbarTheme_dark.png new file mode 100644 index 0000000000..9d560a7fe0 Binary files /dev/null and b/scripts/ios/screenshots-tv/ToolbarTheme_dark.png differ diff --git a/scripts/ios/screenshots-tv/ToolbarTheme_light.png b/scripts/ios/screenshots-tv/ToolbarTheme_light.png new file mode 100644 index 0000000000..b9192624c4 Binary files /dev/null and b/scripts/ios/screenshots-tv/ToolbarTheme_light.png differ diff --git a/scripts/ios/screenshots-tv/UncoverHorizontalTransitionTest.png b/scripts/ios/screenshots-tv/UncoverHorizontalTransitionTest.png new file mode 100644 index 0000000000..21024d421f Binary files /dev/null and b/scripts/ios/screenshots-tv/UncoverHorizontalTransitionTest.png differ diff --git a/scripts/ios/screenshots-tv/ValidatorLightweightPicker.png b/scripts/ios/screenshots-tv/ValidatorLightweightPicker.png new file mode 100644 index 0000000000..4674a5495e Binary files /dev/null and b/scripts/ios/screenshots-tv/ValidatorLightweightPicker.png differ diff --git a/scripts/ios/screenshots-tv/chart-bar-stacked.png b/scripts/ios/screenshots-tv/chart-bar-stacked.png new file mode 100644 index 0000000000..bd96cba822 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-bar-stacked.png differ diff --git a/scripts/ios/screenshots-tv/chart-bar.png b/scripts/ios/screenshots-tv/chart-bar.png new file mode 100644 index 0000000000..8f710e116a Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-bar.png differ diff --git a/scripts/ios/screenshots-tv/chart-bubble.png b/scripts/ios/screenshots-tv/chart-bubble.png new file mode 100644 index 0000000000..e86b82c6a8 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-bubble.png differ diff --git a/scripts/ios/screenshots-tv/chart-combined-xy.png b/scripts/ios/screenshots-tv/chart-combined-xy.png new file mode 100644 index 0000000000..c404f9e7e2 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-combined-xy.png differ diff --git a/scripts/ios/screenshots-tv/chart-cubic-line.png b/scripts/ios/screenshots-tv/chart-cubic-line.png new file mode 100644 index 0000000000..25cedf376e Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-cubic-line.png differ diff --git a/scripts/ios/screenshots-tv/chart-doughnut.png b/scripts/ios/screenshots-tv/chart-doughnut.png new file mode 100644 index 0000000000..c3c6c74ca0 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-doughnut.png differ diff --git a/scripts/ios/screenshots-tv/chart-line.png b/scripts/ios/screenshots-tv/chart-line.png new file mode 100644 index 0000000000..81fd6a2062 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-line.png differ diff --git a/scripts/ios/screenshots-tv/chart-pie.png b/scripts/ios/screenshots-tv/chart-pie.png new file mode 100644 index 0000000000..1697f11dcf Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-pie.png differ diff --git a/scripts/ios/screenshots-tv/chart-radar.png b/scripts/ios/screenshots-tv/chart-radar.png new file mode 100644 index 0000000000..351dbd566b Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-radar.png differ diff --git a/scripts/ios/screenshots-tv/chart-range-bar.png b/scripts/ios/screenshots-tv/chart-range-bar.png new file mode 100644 index 0000000000..7f7d012f70 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-range-bar.png differ diff --git a/scripts/ios/screenshots-tv/chart-rotated-pie.png b/scripts/ios/screenshots-tv/chart-rotated-pie.png new file mode 100644 index 0000000000..7e2df5b12d Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-rotated-pie.png differ diff --git a/scripts/ios/screenshots-tv/chart-scatter.png b/scripts/ios/screenshots-tv/chart-scatter.png new file mode 100644 index 0000000000..ffb434c962 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-scatter.png differ diff --git a/scripts/ios/screenshots-tv/chart-time.png b/scripts/ios/screenshots-tv/chart-time.png new file mode 100644 index 0000000000..ddf83bf41f Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-time.png differ diff --git a/scripts/ios/screenshots-tv/chart-transform.png b/scripts/ios/screenshots-tv/chart-transform.png new file mode 100644 index 0000000000..201b4baff6 Binary files /dev/null and b/scripts/ios/screenshots-tv/chart-transform.png differ diff --git a/scripts/ios/screenshots-tv/css-gradients.png b/scripts/ios/screenshots-tv/css-gradients.png new file mode 100644 index 0000000000..771ff74377 Binary files /dev/null and b/scripts/ios/screenshots-tv/css-gradients.png differ diff --git a/scripts/ios/screenshots-tv/graphics-affine-scale.png b/scripts/ios/screenshots-tv/graphics-affine-scale.png new file mode 100644 index 0000000000..a95b0ad714 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-affine-scale.png differ diff --git a/scripts/ios/screenshots-tv/graphics-clip-under-rotation.png b/scripts/ios/screenshots-tv/graphics-clip-under-rotation.png new file mode 100644 index 0000000000..6baf0cde62 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-clip-under-rotation.png differ diff --git a/scripts/ios/screenshots-tv/graphics-clip.png b/scripts/ios/screenshots-tv/graphics-clip.png new file mode 100644 index 0000000000..613a7a3cc8 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-clip.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-arc.png b/scripts/ios/screenshots-tv/graphics-draw-arc.png new file mode 100644 index 0000000000..753bd062b0 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-arc.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-gradient-stops.png b/scripts/ios/screenshots-tv/graphics-draw-gradient-stops.png new file mode 100644 index 0000000000..3375af92a6 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-gradient-stops.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-gradient.png b/scripts/ios/screenshots-tv/graphics-draw-gradient.png new file mode 100644 index 0000000000..aa056236af Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-gradient.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-image-rect.png b/scripts/ios/screenshots-tv/graphics-draw-image-rect.png new file mode 100644 index 0000000000..2347fbfc93 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-image-rect.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-line.png b/scripts/ios/screenshots-tv/graphics-draw-line.png new file mode 100644 index 0000000000..c1fae27be6 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-line.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-rect.png b/scripts/ios/screenshots-tv/graphics-draw-rect.png new file mode 100644 index 0000000000..9add36a9c2 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-rect.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-round-rect.png b/scripts/ios/screenshots-tv/graphics-draw-round-rect.png new file mode 100644 index 0000000000..11d25615d3 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-round-rect.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-shape.png b/scripts/ios/screenshots-tv/graphics-draw-shape.png new file mode 100644 index 0000000000..fa2484511b Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-shape.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-string-decorated.png b/scripts/ios/screenshots-tv/graphics-draw-string-decorated.png new file mode 100644 index 0000000000..8b5a053493 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-string-decorated.png differ diff --git a/scripts/ios/screenshots-tv/graphics-draw-string.png b/scripts/ios/screenshots-tv/graphics-draw-string.png new file mode 100644 index 0000000000..1305196972 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-draw-string.png differ diff --git a/scripts/ios/screenshots-tv/graphics-empty-clip.png b/scripts/ios/screenshots-tv/graphics-empty-clip.png new file mode 100644 index 0000000000..05f83d5710 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-empty-clip.png differ diff --git a/scripts/ios/screenshots-tv/graphics-fill-arc.png b/scripts/ios/screenshots-tv/graphics-fill-arc.png new file mode 100644 index 0000000000..01c3466827 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-fill-arc.png differ diff --git a/scripts/ios/screenshots-tv/graphics-fill-polygon.png b/scripts/ios/screenshots-tv/graphics-fill-polygon.png new file mode 100644 index 0000000000..fd6084155d Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-fill-polygon.png differ diff --git a/scripts/ios/screenshots-tv/graphics-fill-rect.png b/scripts/ios/screenshots-tv/graphics-fill-rect.png new file mode 100644 index 0000000000..900ac50ef8 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-fill-rect.png differ diff --git a/scripts/ios/screenshots-tv/graphics-fill-round-rect.png b/scripts/ios/screenshots-tv/graphics-fill-round-rect.png new file mode 100644 index 0000000000..18c3cc2baf Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-fill-round-rect.png differ diff --git a/scripts/ios/screenshots-tv/graphics-fill-shape.png b/scripts/ios/screenshots-tv/graphics-fill-shape.png new file mode 100644 index 0000000000..83ae903822 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-fill-shape.png differ diff --git a/scripts/ios/screenshots-tv/graphics-fill-triangle.png b/scripts/ios/screenshots-tv/graphics-fill-triangle.png new file mode 100644 index 0000000000..97dc4d8872 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-fill-triangle.png differ diff --git a/scripts/ios/screenshots-tv/graphics-gaussian-blur.png b/scripts/ios/screenshots-tv/graphics-gaussian-blur.png new file mode 100644 index 0000000000..8ddfb1f3a8 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-gaussian-blur.png differ diff --git a/scripts/ios/screenshots-tv/graphics-inscribed-triangle-grid.png b/scripts/ios/screenshots-tv/graphics-inscribed-triangle-grid.png new file mode 100644 index 0000000000..43f77ea3b4 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-inscribed-triangle-grid.png differ diff --git a/scripts/ios/screenshots-tv/graphics-large-stroke-dirty-clip.png b/scripts/ios/screenshots-tv/graphics-large-stroke-dirty-clip.png new file mode 100644 index 0000000000..e30c1e520a Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-large-stroke-dirty-clip.png differ diff --git a/scripts/ios/screenshots-tv/graphics-rotate.png b/scripts/ios/screenshots-tv/graphics-rotate.png new file mode 100644 index 0000000000..94735d056f Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-rotate.png differ diff --git a/scripts/ios/screenshots-tv/graphics-scale.png b/scripts/ios/screenshots-tv/graphics-scale.png new file mode 100644 index 0000000000..cd1789fb67 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-scale.png differ diff --git a/scripts/ios/screenshots-tv/graphics-stroke-test.png b/scripts/ios/screenshots-tv/graphics-stroke-test.png new file mode 100644 index 0000000000..d8b2404012 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-stroke-test.png differ diff --git a/scripts/ios/screenshots-tv/graphics-tile-image.png b/scripts/ios/screenshots-tv/graphics-tile-image.png new file mode 100644 index 0000000000..480220ae62 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-tile-image.png differ diff --git a/scripts/ios/screenshots-tv/graphics-transform-camera.png b/scripts/ios/screenshots-tv/graphics-transform-camera.png new file mode 100644 index 0000000000..2ee376ac25 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-transform-camera.png differ diff --git a/scripts/ios/screenshots-tv/graphics-transform-perspective.png b/scripts/ios/screenshots-tv/graphics-transform-perspective.png new file mode 100644 index 0000000000..557b672552 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-transform-perspective.png differ diff --git a/scripts/ios/screenshots-tv/graphics-transform-rotation.png b/scripts/ios/screenshots-tv/graphics-transform-rotation.png new file mode 100644 index 0000000000..1fdf2dff15 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-transform-rotation.png differ diff --git a/scripts/ios/screenshots-tv/graphics-transform-translation.png b/scripts/ios/screenshots-tv/graphics-transform-translation.png new file mode 100644 index 0000000000..6d2070e363 Binary files /dev/null and b/scripts/ios/screenshots-tv/graphics-transform-translation.png differ diff --git a/scripts/ios/screenshots-tv/kotlin.png b/scripts/ios/screenshots-tv/kotlin.png new file mode 100644 index 0000000000..d8132162f6 Binary files /dev/null and b/scripts/ios/screenshots-tv/kotlin.png differ diff --git a/scripts/ios/screenshots-tv/landscape.png b/scripts/ios/screenshots-tv/landscape.png new file mode 100644 index 0000000000..65cdaf71c6 Binary files /dev/null and b/scripts/ios/screenshots-tv/landscape.png differ diff --git a/scripts/run-tv-ui-tests.sh b/scripts/run-tv-ui-tests.sh new file mode 100755 index 0000000000..0d727a2a20 --- /dev/null +++ b/scripts/run-tv-ui-tests.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# Run the Codename One UI screenshot suite on the tvOS simulator and compare the +# captured frames to the Apple TV golden set (scripts/ios/screenshots-tv). +# +# Apple TV reuses the iOS UIApplicationMain entry and the Metal renderer (tvOS +# has UIKit + Metal, just no OpenGL ES), so the

TV target is built like a +# regular iOS app for the appletvsimulator SDK and launched via simctl. It +# streams each screenshot to the host-side Cn1ssScreenshotServer over +# ws://127.0.0.1:8765 -- the same transport the iOS / watch jobs use -- so the +# comparison/report tooling in scripts/lib/cn1ss.sh is reused verbatim. +# +# Usage: run-tv-ui-tests.sh [scheme] +set -euo pipefail + +rt_log() { printf '%s %s\n' "[run-tv-ui-tests]" "$*" >&2; } + +WORKSPACE_PATH="${1:?Usage: $0 [scheme]}" +SCHEME="${2:-}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +CN1SS_HELPER_SOURCE_DIR="$SCRIPT_DIR/common/java" +source "$SCRIPT_DIR/lib/cn1ss.sh" + +: "${ARTIFACTS_DIR:=$REPO_ROOT/artifacts/tv-ui-tests}" +mkdir -p "$ARTIFACTS_DIR" + +# --- Xcode / project resolution ------------------------------------------- +if [ -z "${XCODE_APP:-}" ]; then + XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)" +fi +if [ ! -x "${XCODE_APP:-}/Contents/Developer/usr/bin/xcodebuild" ]; then + XCODE_APP="/Applications/Xcode.app" +fi +export DEVELOPER_DIR="${DEVELOPER_DIR:-$XCODE_APP/Contents/Developer}" +rt_log "Using DEVELOPER_DIR=$DEVELOPER_DIR" +if ! command -v xcodebuild >/dev/null 2>&1; then + rt_log "xcodebuild not found (DEVELOPER_DIR=$DEVELOPER_DIR)"; exit 3 +fi + +# The tvOS target builds via -project (its target carries no CocoaPods deps). +if [[ "$WORKSPACE_PATH" == *.xcodeproj ]]; then + PROJECT_PATH="$WORKSPACE_PATH" +else + PROJECT_DIR="$(dirname "$WORKSPACE_PATH")" + base="$(basename "$WORKSPACE_PATH" .xcworkspace)" + PROJECT_PATH="$PROJECT_DIR/$base.xcodeproj" +fi +if [ ! -d "$PROJECT_PATH" ]; then + rt_log "Could not resolve .xcodeproj from '$WORKSPACE_PATH' (looked for $PROJECT_PATH)"; exit 3 +fi +[ -z "$SCHEME" ] && SCHEME="$(basename "$PROJECT_PATH" .xcodeproj)" +TV_TARGET="${SCHEME}TV" +rt_log "Project=$PROJECT_PATH tvTarget=$TV_TARGET" + +# --- Pick an Apple TV simulator -------------------------------------------- +# Screenshots are pixel-compared, so the device must match the one the goldens +# were captured on: Apple TV renders at 1920x1080. Prefer "Apple TV 4K"; fall +# back to any Apple TV (override with CN1SS_TV_UDID / CN1SS_TV_MODEL). +TV_MODEL_PREF="${CN1SS_TV_MODEL:-Apple TV 4K}" +TV_UDID="${CN1SS_TV_UDID:-}" +DEVLIST="$(xcrun simctl list devices available 2>/dev/null | grep -iE 'Apple TV')" +if [ -z "$TV_UDID" ]; then + TV_UDID="$(printf '%s\n' "$DEVLIST" | grep -i "$TV_MODEL_PREF" | grep -oE '\([0-9A-F-]{36}\)' | head -1 | tr -d '()')" +fi +if [ -z "$TV_UDID" ]; then + rt_log "No '$TV_MODEL_PREF' simulator found; falling back to any available Apple TV" + TV_UDID="$(printf '%s\n' "$DEVLIST" | grep -oE '\([0-9A-F-]{36}\)' | head -1 | tr -d '()')" +fi +if [ -z "$TV_UDID" ]; then + rt_log "No Apple TV simulator available. Install a tvOS runtime in Xcode."; exit 4 +fi +rt_log "Apple TV simulators available:"; printf '%s\n' "$DEVLIST" | sed 's/^/ /' >&2 +rt_log "Using Apple TV simulator $TV_UDID (pref '$TV_MODEL_PREF')" +xcrun simctl boot "$TV_UDID" 2>/dev/null || true +xcrun simctl bootstatus "$TV_UDID" -b 2>/dev/null || true + +# --- Build the tvOS target -------------------------------------------------- +BUILD_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/cn1-tv-build-XXXXXX")" +rt_log "Building $TV_TARGET for appletvsimulator -> $BUILD_ROOT" +# Build for arm64 explicitly: the macos-15 runner is Apple Silicon and its +# tvOS simulator is arm64, so the app must be arm64 to launch. It also keeps +# the NEON-only IOSSimd.m off the x86_64 slice (a destination-less simulator +# `build` otherwise resolves the active arch to x86_64 and fails to compile). +xcodebuild -project "$PROJECT_PATH" -target "$TV_TARGET" \ + -sdk appletvsimulator -configuration Debug \ + ARCHS=arm64 ONLY_ACTIVE_ARCH=NO CODE_SIGNING_ALLOWED=NO SYMROOT="$BUILD_ROOT" build \ + > "$ARTIFACTS_DIR/tv-build.log" 2>&1 || { + rt_log "tvOS build FAILED (see $ARTIFACTS_DIR/tv-build.log)"; tail -40 "$ARTIFACTS_DIR/tv-build.log" >&2; exit 5; } + +APP_PATH="$(/usr/bin/find "$BUILD_ROOT" -name "${TV_TARGET}.app" -maxdepth 3 | head -1)" +[ -z "$APP_PATH" ] && { rt_log "Built tvOS .app not found under $BUILD_ROOT"; exit 5; } +BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Info.plist" 2>/dev/null)" +[ -z "$BUNDLE_ID" ] && { rt_log "Could not read CFBundleIdentifier from $APP_PATH"; exit 5; } +rt_log "Built $APP_PATH (bundle $BUNDLE_ID)" + +# --- Screenshot capture: host WS sink + the streaming tvOS app -------------- +JAVA_BIN="${JAVA17_BIN:-$(command -v java)}" +cn1ss_setup "$JAVA_BIN" "$CN1SS_HELPER_SOURCE_DIR" + +SS_TMP="$(mktemp -d "${TMPDIR:-/tmp}/cn1-tv-ss-XXXXXX")" +WS_RAW_DIR="$SS_TMP/ws"; PREVIEW_DIR="$SS_TMP/previews" +mkdir -p "$WS_RAW_DIR" "$PREVIEW_DIR" + +cleanup() { cn1ss_stop_ws_server 2>/dev/null || true; xcrun simctl terminate "$TV_UDID" "$BUNDLE_ID" 2>/dev/null || true; } +trap cleanup EXIT + +cn1ss_start_ws_server "$WS_RAW_DIR" || { rt_log "Failed to start Cn1ssScreenshotServer"; exit 6; } +rt_log "WS sink on port ${CN1SS_WS_PORT:-8765} -> $WS_RAW_DIR" + +xcrun simctl terminate "$TV_UDID" "$BUNDLE_ID" 2>/dev/null || true +xcrun simctl install "$TV_UDID" "$APP_PATH" +# Capture the app's stdout/stderr (the CN1SS:* suite markers and any native +# exception / crash) so a no-screenshot run is diagnosable instead of silent. +APP_CONSOLE="$ARTIFACTS_DIR/tv-app-console.log" +: > "$APP_CONSOLE" +( xcrun simctl launch --console-pty "$TV_UDID" "$BUNDLE_ID" >>"$APP_CONSOLE" 2>&1 ) & +APP_CONSOLE_PID=$! +rt_log "Launched tvOS app (console -> $APP_CONSOLE); waiting for the suite to stream screenshots..." + +MAX_WAIT="${CN1SS_TV_TIMEOUT:-1200}" +TV_REF_DIR="${SCREENSHOT_REF_DIR:-$SCRIPT_DIR/ios/screenshots-tv}" +EXPECTED="$(/usr/bin/find "$TV_REF_DIR" -name '*.png' 2>/dev/null | wc -l | tr -d ' ')" +rt_log "Expecting $EXPECTED screenshots (golden set)" +prev=-1; stable=0; waited=0 +while [ "$waited" -lt "$MAX_WAIT" ]; do + sleep 8; waited=$((waited+8)) + cur="$(/usr/bin/find "$WS_RAW_DIR" -name '*.png' 2>/dev/null | wc -l | tr -d ' ')" + if [ "$EXPECTED" -gt 0 ] && [ "$cur" -ge "$EXPECTED" ]; then + stable=$((stable+1)); [ "$stable" -ge 2 ] && break + continue + fi + if [ "$cur" = "$prev" ] && [ "$cur" -gt 0 ]; then + stable=$((stable+1)); [ "$stable" -ge 10 ] && break + else stable=0; fi + prev="$cur" + # The suite emits CN1SS:SUITE:FINISHED when done; bail early on that (covers + # the seed run where EXPECTED=0) or on an obvious native crash, instead of + # blocking the full MAX_WAIT. + if grep -qa "CN1SS:SUITE:FINISHED" "$APP_CONSOLE" 2>/dev/null; then + rt_log "Suite reported FINISHED after ${waited}s"; break + fi + if grep -qaE "Fatal|Terminating app due to uncaught exception|EXC_BAD|did crash|libsystem_kernel" "$APP_CONSOLE" 2>/dev/null; then + rt_log "Detected app crash/fatal in console after ${waited}s"; break + fi +done +rt_log "Capture settled: $(/usr/bin/find "$WS_RAW_DIR" -name '*.png' 2>/dev/null | wc -l | tr -d ' ') of $EXPECTED screenshots after ${waited}s" + +# Always surface the app console + any crash report so a zero-screenshot run is +# diagnosable from the uploaded artifacts. +rt_log "----- tvOS app console (tail) -----" +tail -60 "$APP_CONSOLE" 2>/dev/null | sed 's/^/[tv-app] /' || true +rt_log "----- end app console -----" +CRASH_DIR="$HOME/Library/Logs/DiagnosticReports" +SIM_CRASH_DIR="$HOME/Library/Developer/CoreSimulator/Devices/$TV_UDID/data/Library/Logs/DiagnosticReports" +for d in "$SIM_CRASH_DIR" "$CRASH_DIR"; do + [ -d "$d" ] || continue + /usr/bin/find "$d" -name 'HelloCodenameOneTV*' -newermt '-10 minutes' 2>/dev/null | while IFS= read -r cr; do + rt_log "----- crash report: $cr -----"; sed -n '1,40p' "$cr" | sed 's/^/[crash] /' + cp "$cr" "$ARTIFACTS_DIR/" 2>/dev/null || true + done +done + +# --- Compare against the tvOS golden set + emit report ---------------------- +REF_DIR="${SCREENSHOT_REF_DIR:-$SCRIPT_DIR/ios/screenshots-tv}" +REF_DIR="$(cd "$REF_DIR" && pwd)" +declare -a ACTUAL=() +while IFS= read -r png; do + name="$(basename "$png" .png)" + ACTUAL+=("$name=$png") +done < <(/usr/bin/find "$WS_RAW_DIR" -name '*.png' | sort) + +cp -f "$WS_RAW_DIR"/*.png "$ARTIFACTS_DIR/" 2>/dev/null || true + +COMPARE_JSON="$SS_TMP/compare.json"; SUMMARY_OUT="$SS_TMP/summary.txt"; COMMENT_OUT="$SS_TMP/comment.md" + +export CN1SS_FAIL_ON_MISMATCH="${CN1SS_FAIL_ON_MISMATCH:-1}" +export CN1SS_ALLOWED_MISSING="${CN1SS_ALLOWED_MISSING:-0}" +set +e +CN1SS_SUCCESS_MESSAGE="${CN1SS_SUCCESS_MESSAGE:-Apple TV (tvOS, Metal) screenshots match the goldens.}" \ +cn1ss_process_and_report \ + "Apple TV (tvOS / Metal)" \ + "$COMPARE_JSON" "$SUMMARY_OUT" "$COMMENT_OUT" \ + "$REF_DIR" "$PREVIEW_DIR" "$ARTIFACTS_DIR" \ + "${ACTUAL[@]}" +gate_rc=$? +set -e + +cp -f "$COMPARE_JSON" "$SUMMARY_OUT" "$COMMENT_OUT" "$ARTIFACTS_DIR/" 2>/dev/null || true +cn1ss_post_pr_comment "$COMMENT_OUT" "$PREVIEW_DIR" || true + +rc="$gate_rc" +if [ -f "$SUMMARY_OUT" ] && grep -q "^missing_expected|" "$SUMMARY_OUT"; then + me="$(grep -c "^missing_expected|" "$SUMMARY_OUT" 2>/dev/null || echo 0)" + rt_log "FATAL: $me screenshot(s) streamed with no stored golden (missing_expected) -- add them to scripts/ios/screenshots-tv." + [ "$rc" -eq 0 ] && rc=17 +fi +[ "${#ACTUAL[@]}" -gt 0 ] || rc=1 +rt_log "exit rc=$rc (gate_rc=$gate_rc, mismatch_fail=${CN1SS_FAIL_ON_MISMATCH}, allowed_missing=${CN1SS_ALLOWED_MISSING})" +exit "$rc"