diff --git a/docs/en/apis/sparkling-sdk-android.md b/docs/en/apis/sparkling-sdk-android.md index c0d94298..f3ce6b9c 100644 --- a/docs/en/apis/sparkling-sdk-android.md +++ b/docs/en/apis/sparkling-sdk-android.md @@ -86,12 +86,43 @@ Configuration object passed to both container types. |----------|-------------| | `scheme` | The `hybrid://...` URL to load. | | `sparklingUIProvider` | Implements `SparklingUIProvider` for custom loading/error/toolbar views. | +| `screenOrientationPolicy` | Optional typed orientation policy for a full-page `SparklingActivity`. | | `threadStrategy` | Optional per-container `SparklingThreadStrategy`. Overrides the global default. | | `hybridSchemeParam` | Parsed scheme parameters (auto-populated from `scheme`). | | `lynxViewport` | Optional `SparklingLynxViewport(widthPx, heightPx)` fixed viewport in physical pixels. Programmatic configuration overrides parsed scheme dimensions. | | `containerId` | Unique container identifier (auto-generated). | | `resourceFetcherConfig` | Optional per-page typed resource fetchers. Overrides the global factory. | +## Screen orientation + +`SparklingScreenOrientationPolicy` provides the Java-friendly `SYSTEM`, +`PORTRAIT`, and `LANDSCAPE` values for full-page containers: + +```java +SparklingContext sparklingContext = new SparklingContext(); +sparklingContext.setScreenOrientationPolicy( + SparklingScreenOrientationPolicy.LANDSCAPE); +``` + +An optional application-wide default can be set with +`SparklingHybridConfig.Builder.setDefaultScreenOrientationPolicy(...)`. +Resolution order is: + +1. `SparklingContext.screenOrientationPolicy`, including an explicit `SYSTEM`; +2. the canonical scheme `screen_orientation` value; +3. the global default; +4. Android's existing system/default behavior when all values are unset. + +The canonical `portrait` and `landscape` values map to the corresponding typed +policies. Unknown canonical values preserve the existing `SYSTEM` behavior +instead of falling through to the global default. `SparklingActivity` applies +the resolved policy through Android's public `requestedOrientation` API before +creating its content. + +The policy intentionally does not rotate an Activity that hosts an embedded +`SparklingView`. An embedded view does not own its host Activity; the host must +apply any desired orientation policy itself. + ## Typed resource fetchers Use `SparklingResourceFetcherConfig` to provide Lynx generic, media, and diff --git a/docs/zh/apis/sparkling-sdk-android.md b/docs/zh/apis/sparkling-sdk-android.md index 351981d2..7128592a 100644 --- a/docs/zh/apis/sparkling-sdk-android.md +++ b/docs/zh/apis/sparkling-sdk-android.md @@ -79,11 +79,40 @@ Sparkling 创建的 `LynxView` 也必须使用完全相同的 density 构造** |------|------| | `scheme` | 要加载的 `hybrid://...` URL。 | | `sparklingUIProvider` | 实现 `SparklingUIProvider` 以自定义加载/错误/工具栏视图。 | +| `screenOrientationPolicy` | 全页 `SparklingActivity` 可选的类型安全方向策略。 | | `threadStrategy` | 可选的容器级 `SparklingThreadStrategy`,优先于全局默认值。 | | `hybridSchemeParam` | 解析后的 scheme 参数(从 `scheme` 自动填充)。 | | `lynxViewport` | 可选的 `SparklingLynxViewport(widthPx, heightPx)`,以物理像素指定固定 viewport。程序化配置会覆盖 scheme 中解析的尺寸。 | | `containerId` | 唯一的容器标识符(自动生成)。 | +## 屏幕方向 + +`SparklingScreenOrientationPolicy` 为全页容器提供 Java 友好的 `SYSTEM`、 +`PORTRAIT` 和 `LANDSCAPE`: + +```java +SparklingContext sparklingContext = new SparklingContext(); +sparklingContext.setScreenOrientationPolicy( + SparklingScreenOrientationPolicy.LANDSCAPE); +``` + +宿主也可以通过 +`SparklingHybridConfig.Builder.setDefaultScreenOrientationPolicy(...)` +设置可选的应用级默认值。解析优先级为: + +1. `SparklingContext.screenOrientationPolicy`,包括显式设置的 `SYSTEM`; +2. canonical scheme 的 `screen_orientation`; +3. 全局默认值; +4. 全部未设置时沿用 Android 当前的系统/默认行为。 + +canonical scheme 中的 `portrait` 和 `landscape` 会映射到对应的类型安全策略。 +未知 canonical 值继续保持现有的 `SYSTEM` 行为,不会回退到全局默认值。 +`SparklingActivity` 在创建内容前通过 Android 公开的 `requestedOrientation` +API 应用最终策略。 + +该策略不会旋转承载嵌入式 `SparklingView` 的 Activity。嵌入式 View +不拥有宿主 Activity;需要固定方向时,应由宿主自行应用方向策略。 + 高级宿主如果已经使用 `LynxKitInitParams`,也可以设置其 `lynxViewport` 属性。优先级依次为: init params、`SparklingContext.lynxViewport`、canonical scheme 的 `width` 和 `height`。 三种入口都只接受完整的正数宽高组合。 diff --git a/packages/sparkling-sdk/android/sparkling/src/androidTest/java/com/tiktok/sparkling/SparklingScreenOrientationInstrumentedTest.kt b/packages/sparkling-sdk/android/sparkling/src/androidTest/java/com/tiktok/sparkling/SparklingScreenOrientationInstrumentedTest.kt new file mode 100644 index 00000000..1dc5fad8 --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/androidTest/java/com/tiktok/sparkling/SparklingScreenOrientationInstrumentedTest.kt @@ -0,0 +1,50 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling + +import android.content.Intent +import android.content.pm.ActivityInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SparklingScreenOrientationInstrumentedTest { + @Test + fun fullPageActivityReceivesPerContainerOrientationPolicy() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = + SparklingContext().apply { + containerId = "instrumented-orientation-landscape" + screenOrientationPolicy = SparklingScreenOrientationPolicy.LANDSCAPE + } + SparklingContextTransferStation.saveSparklingContext(context) + val intent = + Intent(instrumentation.targetContext, SparklingActivity::class.java).apply { + putExtra(Sparkling.SPARKLING_CONTEXT_CONTAINER_ID, context.containerId) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + val activity = instrumentation.startActivitySync(intent) as SparklingActivity + + try { + instrumentation.waitForIdleSync() + assertEquals( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, + activity.requestedOrientation, + ) + } finally { + instrumentation.runOnMainSync { + activity.finish() + } + SparklingContextTransferStation.releaseSparklingContext(context.containerId) + assertEquals( + null, + SparklingContextTransferStation.getSparklingContext(context.containerId), + ) + } + } +} diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingActivity.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingActivity.kt index 299f7fec..91ad4994 100644 --- a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingActivity.kt +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingActivity.kt @@ -13,19 +13,29 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.tiktok.sparkling.Sparkling.Companion.SPARKLING_CONTEXT_CONTAINER_ID +import com.tiktok.sparkling.hybridkit.HybridCommon import com.tiktok.sparkling.hybridkit.utils.ColorUtil class SparklingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) val containerId = intent.getStringExtra(SPARKLING_CONTEXT_CONTAINER_ID) val sparklingContext = SparklingContextTransferStation.getSparklingContext(containerId) + applyScreenOrientationPolicy(sparklingContext) + super.onCreate(savedInstanceState) initStatusBar(sparklingContext) setContentView(R.layout.activity_sparkling) initToolBar(sparklingContext) initSparklingFragment(sparklingContext) } + private fun applyScreenOrientationPolicy(sparklingContext: SparklingContext?) { + val policy = + sparklingContext?.resolveScreenOrientationPolicy( + HybridCommon.hybridConfig?.defaultScreenOrientationPolicy, + ) ?: return + requestedOrientation = policy.toRequestedOrientation() + } + private fun initStatusBar(sparklingContext: SparklingContext?) { val param = sparklingContext?.hybridSchemeParam ?: return val controller = WindowInsetsControllerCompat(window, window.decorView) @@ -91,12 +101,6 @@ class SparklingActivity : AppCompatActivity() { if (it.hideNavBar || (it.transStatusBar && !it.showNavBarInTransStatusBar)) { supportActionBar?.hide() } - requestedOrientation = - when (it.screenOrientation) { - "portrait" -> android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - "landscape" -> android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - else -> android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } } val fragment = SparklingFragment.newInstance() diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt index ee1c2601..e504eb43 100644 --- a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingContext.kt @@ -106,4 +106,11 @@ class SparklingContext : HybridContext() { var lynxViewport: SparklingLynxViewport? = null var threadStrategy: SparklingThreadStrategy? = null var resourceFetcherConfig: SparklingResourceFetcherConfig? = null + + /** + * Optional orientation policy for a full-page Sparkling container. + * + * Embedded SparklingViews do not change their host Activity orientation. + */ + var screenOrientationPolicy: SparklingScreenOrientationPolicy? = null } diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingScreenOrientationPolicy.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingScreenOrientationPolicy.kt new file mode 100644 index 00000000..ce27fb91 --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/SparklingScreenOrientationPolicy.kt @@ -0,0 +1,38 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling + +import android.content.pm.ActivityInfo + +/** + * Controls the screen orientation of a full-page [SparklingActivity]. + * + * This policy does not change the Activity that hosts an embedded [SparklingView]. + */ +enum class SparklingScreenOrientationPolicy { + SYSTEM, + PORTRAIT, + LANDSCAPE, +} + +internal fun SparklingContext.resolveScreenOrientationPolicy( + globalDefault: SparklingScreenOrientationPolicy?, +): SparklingScreenOrientationPolicy? = + screenOrientationPolicy + ?: hybridSchemeParam?.screenOrientation?.toLegacyScreenOrientationPolicy() + ?: globalDefault + +internal fun SparklingScreenOrientationPolicy.toRequestedOrientation(): Int = + when (this) { + SparklingScreenOrientationPolicy.SYSTEM -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + SparklingScreenOrientationPolicy.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + SparklingScreenOrientationPolicy.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } + +private fun String.toLegacyScreenOrientationPolicy(): SparklingScreenOrientationPolicy = + when (this) { + "portrait" -> SparklingScreenOrientationPolicy.PORTRAIT + "landscape" -> SparklingScreenOrientationPolicy.LANDSCAPE + else -> SparklingScreenOrientationPolicy.SYSTEM + } diff --git a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingHybridConfig.kt b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingHybridConfig.kt index 6f418497..8ac45d8e 100644 --- a/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingHybridConfig.kt +++ b/packages/sparkling-sdk/android/sparkling/src/main/java/com/tiktok/sparkling/hybridkit/config/SparklingHybridConfig.kt @@ -6,6 +6,7 @@ package com.tiktok.sparkling.hybridkit.config import android.content.Context import android.webkit.WebSettings import android.webkit.WebView +import com.tiktok.sparkling.SparklingScreenOrientationPolicy import com.tiktok.sparkling.hybridkit.HybridContext import com.tiktok.sparkling.hybridkit.service.IKitBridgeService import com.tiktok.sparkling.hybridkit.utils.HybridLogger @@ -17,6 +18,7 @@ open class SparklingHybridConfig private constructor( val bridgeConfig: IBridgeConfig?, val logConfig: LogConfig?, val debugConfig: DebugConfig?, + val defaultScreenOrientationPolicy: SparklingScreenOrientationPolicy?, ) { companion object { inline fun build( @@ -33,6 +35,7 @@ open class SparklingHybridConfig private constructor( private var bridgeConfig: IBridgeConfig? = null private var logConfig: LogConfig? = null private var debugConfig: DebugConfig? = null + private var defaultScreenOrientationPolicy: SparklingScreenOrientationPolicy? = null fun setDebugConfig(debugConfig: DebugConfig) { this.debugConfig = debugConfig @@ -54,7 +57,20 @@ open class SparklingHybridConfig private constructor( this.logConfig = logConfig } - fun build() = SparklingHybridConfig(baseInfoConfig, lynxConfig, webConfig, bridgeConfig, logConfig, debugConfig) + fun setDefaultScreenOrientationPolicy(policy: SparklingScreenOrientationPolicy?) { + defaultScreenOrientationPolicy = policy + } + + fun build() = + SparklingHybridConfig( + baseInfoConfig, + lynxConfig, + webConfig, + bridgeConfig, + logConfig, + debugConfig, + defaultScreenOrientationPolicy, + ) } } diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingActivityTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingActivityTest.kt index ddaed11a..5afdce94 100644 --- a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingActivityTest.kt +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingActivityTest.kt @@ -4,9 +4,15 @@ package com.tiktok.sparkling import android.app.Application +import android.content.Intent +import android.content.pm.ActivityInfo +import com.tiktok.sparkling.hybridkit.HybridCommon +import com.tiktok.sparkling.hybridkit.config.BaseInfoConfig +import com.tiktok.sparkling.hybridkit.config.SparklingHybridConfig import com.tiktok.sparkling.hybridkit.scheme.HybridSchemeParam import com.tiktok.sparkling.hybridkit.utils.ColorUtil import org.junit.After +import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test @@ -32,11 +38,13 @@ class SparklingActivityTest { application.setTheme(R.style.AppTheme_NoActionBar) ColorUtil.appContext = application SparklingContextTransferStation.clearAllContexts() + setGlobalOrientationPolicy(null) } @After fun tearDown() { SparklingContextTransferStation.clearAllContexts() + setGlobalOrientationPolicy(null) } @Test @@ -44,6 +52,10 @@ class SparklingActivityTest { val activity = Robolectric.buildActivity(SparklingActivity::class.java).create().get() assertNotNull(activity) assertNotNull(activity.findViewById(R.id.toolbar)) + assertEquals( + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, + activity.requestedOrientation, + ) } @Test @@ -108,17 +120,17 @@ class SparklingActivityTest { @Test fun onCreateAppliesPortraitOrientation() { + setGlobalOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE) val ctx = SparklingContext().apply { containerId = "container-portrait" - hybridSchemeParam = HybridSchemeParam(screenOrientation = "portrait") + scheme = "hybrid://lynxview_page?screen_orientation=portrait" } - SparklingContextTransferStation.saveSparklingContext(ctx) + Sparkling.build(application, ctx).processSparklingContext(ctx) - val intent = android.content.Intent(application, SparklingActivity::class.java) - intent.putExtra(Sparkling.SPARKLING_CONTEXT_CONTAINER_ID, ctx.containerId) - val activity = Robolectric.buildActivity(SparklingActivity::class.java, intent).create().get() - assertNotNull(activity) + val activity = launch(ctx) + + assertEquals(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, activity.requestedOrientation) } @Test @@ -133,7 +145,64 @@ class SparklingActivityTest { val intent = android.content.Intent(application, SparklingActivity::class.java) intent.putExtra(Sparkling.SPARKLING_CONTEXT_CONTAINER_ID, ctx.containerId) val activity = Robolectric.buildActivity(SparklingActivity::class.java, intent).create().get() - assertNotNull(activity) + assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, activity.requestedOrientation) + } + + @Test + fun onCreateUsesGlobalOrientationDefault() { + setGlobalOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE) + val ctx = + SparklingContext().apply { + containerId = "container-global-landscape" + } + + val activity = launch(ctx) + + assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, activity.requestedOrientation) + } + + @Test + fun onCreateUsesPagePolicyBeforeSchemeAndGlobalDefault() { + setGlobalOrientationPolicy(SparklingScreenOrientationPolicy.PORTRAIT) + val ctx = + SparklingContext().apply { + containerId = "container-page-landscape" + screenOrientationPolicy = SparklingScreenOrientationPolicy.LANDSCAPE + hybridSchemeParam = HybridSchemeParam(screenOrientation = "portrait") + } + + val activity = launch(ctx) + + assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, activity.requestedOrientation) + } + + @Test + fun onCreateLetsExplicitSystemPolicyOverrideGlobalDefault() { + setGlobalOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE) + val ctx = + SparklingContext().apply { + containerId = "container-page-system" + screenOrientationPolicy = SparklingScreenOrientationPolicy.SYSTEM + } + + val activity = launch(ctx) + + assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation) + } + + @Test + fun onCreatePreservesSystemBehaviorForUnknownCanonicalValue() { + setGlobalOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE) + val ctx = + SparklingContext().apply { + containerId = "container-canonical-auto" + scheme = "hybrid://lynxview_page?screen_orientation=auto" + } + Sparkling.build(application, ctx).processSparklingContext(ctx) + + val activity = launch(ctx) + + assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation) } @Test @@ -215,4 +284,21 @@ class SparklingActivityTest { }, ) } + + private fun launch(context: SparklingContext): SparklingActivity { + SparklingContextTransferStation.saveSparklingContext(context) + val intent = + Intent(application, SparklingActivity::class.java).apply { + putExtra(Sparkling.SPARKLING_CONTEXT_CONTAINER_ID, context.containerId) + } + return Robolectric.buildActivity(SparklingActivity::class.java, intent).create().get() + } + + private fun setGlobalOrientationPolicy(policy: SparklingScreenOrientationPolicy?) { + val config = + SparklingHybridConfig.build(BaseInfoConfig(isDebug = false)) { + setDefaultScreenOrientationPolicy(policy) + } + HybridCommon.setHybridConfig(config, application) + } } diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingScreenOrientationJavaApiTest.java b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingScreenOrientationJavaApiTest.java new file mode 100644 index 00000000..e1aa5f8e --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingScreenOrientationJavaApiTest.java @@ -0,0 +1,33 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling; + +import static org.junit.Assert.assertEquals; + +import com.tiktok.sparkling.hybridkit.config.BaseInfoConfig; +import com.tiktok.sparkling.hybridkit.config.SparklingHybridConfig; +import org.junit.Test; + +public class SparklingScreenOrientationJavaApiTest { + @Test + public void typedPoliciesAreAvailableFromJava() { + SparklingContext sparklingContext = new SparklingContext(); + sparklingContext.setScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE); + + SparklingHybridConfig.Builder builder = + new SparklingHybridConfig.Builder(new BaseInfoConfig(false)); + builder.setDefaultScreenOrientationPolicy(SparklingScreenOrientationPolicy.PORTRAIT); + SparklingHybridConfig config = builder.build(); + + assertEquals( + SparklingScreenOrientationPolicy.LANDSCAPE, + sparklingContext.getScreenOrientationPolicy()); + assertEquals( + SparklingScreenOrientationPolicy.PORTRAIT, + config.getDefaultScreenOrientationPolicy()); + assertEquals( + SparklingScreenOrientationPolicy.SYSTEM, + SparklingScreenOrientationPolicy.valueOf("SYSTEM")); + } +} diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingScreenOrientationPolicyTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingScreenOrientationPolicyTest.kt new file mode 100644 index 00000000..8ab648bf --- /dev/null +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/SparklingScreenOrientationPolicyTest.kt @@ -0,0 +1,140 @@ +// Copyright (c) 2026 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +package com.tiktok.sparkling + +import android.content.pm.ActivityInfo +import androidx.fragment.app.FragmentActivity +import com.tiktok.sparkling.hybridkit.scheme.HybridSchemeParam +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33], packageName = "com.tiktok.sparkling") +class SparklingScreenOrientationPolicyTest { + @Test + fun pagePolicyTakesPrecedenceOverSchemeAndGlobalDefault() { + val context = + SparklingContext().apply { + screenOrientationPolicy = SparklingScreenOrientationPolicy.LANDSCAPE + hybridSchemeParam = HybridSchemeParam(screenOrientation = "portrait") + } + + assertEquals( + SparklingScreenOrientationPolicy.LANDSCAPE, + context.resolveScreenOrientationPolicy(SparklingScreenOrientationPolicy.PORTRAIT), + ) + } + + @Test + fun explicitSystemPolicyTakesPrecedenceOverGlobalDefault() { + val context = + SparklingContext().apply { + screenOrientationPolicy = SparklingScreenOrientationPolicy.SYSTEM + hybridSchemeParam = HybridSchemeParam(screenOrientation = "portrait") + } + + assertEquals( + SparklingScreenOrientationPolicy.SYSTEM, + context.resolveScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE), + ) + } + + @Test + fun pagePortraitPolicyTakesPrecedenceOverSchemeAndGlobalDefault() { + val context = + SparklingContext().apply { + screenOrientationPolicy = SparklingScreenOrientationPolicy.PORTRAIT + hybridSchemeParam = HybridSchemeParam(screenOrientation = "landscape") + } + + assertEquals( + SparklingScreenOrientationPolicy.PORTRAIT, + context.resolveScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE), + ) + } + + @Test + fun canonicalSchemePolicyTakesPrecedenceOverGlobalDefault() { + val context = + SparklingContext().apply { + hybridSchemeParam = HybridSchemeParam(screenOrientation = "portrait") + } + + assertEquals( + SparklingScreenOrientationPolicy.PORTRAIT, + context.resolveScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE), + ) + } + + @Test + fun unknownLegacySchemeValuePreservesSystemBehavior() { + val context = + SparklingContext().apply { + hybridSchemeParam = HybridSchemeParam(screenOrientation = "auto") + } + + assertEquals( + SparklingScreenOrientationPolicy.SYSTEM, + context.resolveScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE), + ) + } + + @Test + fun globalDefaultIsUsedWhenPageConfigurationIsAbsent() { + assertEquals( + SparklingScreenOrientationPolicy.LANDSCAPE, + SparklingContext() + .resolveScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE), + ) + } + + @Test + fun unsetConfigurationLeavesAndroidDefaultUntouched() { + assertNull(SparklingContext().resolveScreenOrientationPolicy(null)) + } + + @Test + fun policiesMapToPublicAndroidRequestedOrientationValues() { + assertEquals( + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, + SparklingScreenOrientationPolicy.SYSTEM.toRequestedOrientation(), + ) + assertEquals( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, + SparklingScreenOrientationPolicy.PORTRAIT.toRequestedOrientation(), + ) + assertEquals( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, + SparklingScreenOrientationPolicy.LANDSCAPE.toRequestedOrientation(), + ) + } + + @Test + fun embeddedViewDoesNotChangeHostActivityOrientation() { + val activity = + Robolectric + .buildActivity(FragmentActivity::class.java) + .setup() + .get() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + val context = + SparklingContext().apply { + screenOrientationPolicy = SparklingScreenOrientationPolicy.LANDSCAPE + } + + val view = Sparkling.build(activity, context).createView(withoutPrepare = true) + + assertNotNull(view) + assertEquals( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, + activity.requestedOrientation, + ) + } +} diff --git a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt index 61677f86..f703c2d9 100644 --- a/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt +++ b/packages/sparkling-sdk/android/sparkling/src/test/java/com/tiktok/sparkling/hybridkit/config/HybridConfigTest.kt @@ -5,6 +5,7 @@ import com.lynx.tasm.LynxEnv import com.tiktok.sparkling.SparklingContext import com.tiktok.sparkling.SparklingResourceFetcherConfig import com.tiktok.sparkling.SparklingResourceFetcherFactory +import com.tiktok.sparkling.SparklingScreenOrientationPolicy import com.tiktok.sparkling.SparklingThreadStrategy import com.tiktok.sparkling.hybridkit.lynx.SparklingLynxModuleWrapper import io.mockk.mockk @@ -32,6 +33,7 @@ class HybridConfigTest { assertNull(cfg.bridgeConfig) assertNull(cfg.logConfig) assertNull(cfg.debugConfig) + assertNull(cfg.defaultScreenOrientationPolicy) } @Test @@ -50,6 +52,7 @@ class HybridConfigTest { setBridgeConfig(bridgeCfg) setLogConfig(logCfg) setDebugConfig(debugCfg) + setDefaultScreenOrientationPolicy(SparklingScreenOrientationPolicy.LANDSCAPE) } assertSame(baseInfo, cfg.baseInfoConfig) @@ -58,6 +61,10 @@ class HybridConfigTest { assertSame(bridgeCfg, cfg.bridgeConfig) assertSame(logCfg, cfg.logConfig) assertSame(debugCfg, cfg.debugConfig) + assertEquals( + SparklingScreenOrientationPolicy.LANDSCAPE, + cfg.defaultScreenOrientationPolicy, + ) } @Test