Skip to content
Draft
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
<profileable android:shell="true"/>
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden"
android:exported="true"
android:label="@string/app_name_short"
android:screenOrientation="nosensor"
android:theme="@style/Theme.JetpackCamera">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
44 changes: 44 additions & 0 deletions app/src/main/java/com/google/jetpackcamera/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.google.jetpackcamera
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.hardware.Camera
import android.net.Uri
import android.os.Build
Expand All @@ -31,6 +32,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -81,6 +83,26 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

private const val TAG = "MainActivity"
private const val LARGE_SCREEN_MIN_WIDTH_DP = 600

/**
* Resolves the Activity's [ActivityInfo] screen orientation policy for the current window posture.
*
* Full-screen compact handhelds (`!isInMultiWindowMode && smallestScreenWidthDp < 600`) lock the
* window to [ActivityInfo.SCREEN_ORIENTATION_NOSENSOR] so the UI counter-rotates via sensor
* gravity without triggering a system window rotation. Multi-window panes and large screens
* (`smallestScreenWidthDp >= 600`) use [ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED] so the OS
* does not apply 1:1 portrait compatibility letterboxing.
*/
@VisibleForTesting
internal fun resolveRequestedOrientation(
isInMultiWindowMode: Boolean,
smallestScreenWidthDp: Int
): Int = if (!isInMultiWindowMode && smallestScreenWidthDp < LARGE_SCREEN_MIN_WIDTH_DP) {
ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
} else {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}

/**
* Activity for the JetpackCameraApp.
Expand All @@ -103,6 +125,10 @@ class MainActivity : ComponentActivity() {
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT)
)
super.onCreate(savedInstanceState)
updateRequestedOrientation(resources.configuration)
addOnMultiWindowModeChangedListener { info ->
updateRequestedOrientation(resources.configuration, info.isInMultiWindowMode)
}
cameraLaunchConfigProvider.setIntent(intent)
var uiState: MainActivityUiState by mutableStateOf(Loading)

Expand Down Expand Up @@ -194,6 +220,24 @@ class MainActivity : ComponentActivity() {
}
}

override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateRequestedOrientation(newConfig)
}

private fun updateRequestedOrientation(
configuration: Configuration,
multiWindowMode: Boolean = isInMultiWindowMode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Calling isInMultiWindowMode directly as a default parameter value will cause a NoSuchMethodError crash on devices running API levels below 24 (Android 7.0), as this property was introduced in API 24. Since the app's minimum SDK version is likely lower (e.g., API 21), this call must be guarded with an API level check.

        multiWindowMode: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            isInMultiWindowMode
        } else {
            false
        }

) {
val targetOrientation = resolveRequestedOrientation(
isInMultiWindowMode = multiWindowMode,
smallestScreenWidthDp = configuration.smallestScreenWidthDp
)
if (requestedOrientation != targetOrientation) {
requestedOrientation = targetOrientation
}
}

private val debugSettings: DebugSettings
get() = cameraLaunchConfigProvider.config.value.debugSettings

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.jetpackcamera

import android.content.pm.ActivityInfo
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

/**
* Unit tests for [resolveRequestedOrientation].
*/
@RunWith(JUnit4::class)
class MainActivityOrientationTest {

@Test
fun resolveRequestedOrientation_fullScreenCompactPhone_locksToNoSensor() {
assertThat(
resolveRequestedOrientation(
isInMultiWindowMode = false,
smallestScreenWidthDp = 411
)
).isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)

assertThat(
resolveRequestedOrientation(
isInMultiWindowMode = false,
smallestScreenWidthDp = 599
)
).isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)
}

@Test
fun resolveRequestedOrientation_multiWindowCompact_allowsUnspecifiedOrientation() {
assertThat(
resolveRequestedOrientation(
isInMultiWindowMode = true,
smallestScreenWidthDp = 411
)
).isEqualTo(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
}

@Test
fun resolveRequestedOrientation_largeScreenOrFoldable_allowsUnspecifiedOrientation() {
assertThat(
resolveRequestedOrientation(
isInMultiWindowMode = false,
smallestScreenWidthDp = 600
)
).isEqualTo(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)

assertThat(
resolveRequestedOrientation(
isInMultiWindowMode = true,
smallestScreenWidthDp = 673
)
).isEqualTo(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
}
}
Loading
Loading