java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation

最近遭遇したクラッシュについて、久しぶりにQiita記事を書いてみようと思います :hugging_face:

# クラッシュについて
端的に書くと、

- Android 8.0の端末である
- targetSdkVersionを27以上にしている
- 背景を透過にしている
- 画面の向きを固定している

という4つの条件を満たすと、以下のようなクラッシュが発生します。

```
java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
```

Android 8.1以降では発生しないようです。すなわち、targetSdkVersionを27にしたら、Android 8.0（＝APIレベル26）のみでクラッシュするということです。

|バージョン|APIレベル|
|:--|:--|
|Android 8.0|26|
|Android 8.1|27|

コードで書くと以下のような感じです。

__targetSdkVersionを27以上にしている__

```groovy:app/build.gradle
android {
    defaultConfig {
        targetSdkVersion 27
    }
}
```

__背景を透過にしている__

```xml:styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Translucent" parent="AppTheme">
        <item name="android:windowIsTranslucent">true</item>
    </style>
</resources>
```

__画面の向きを固定している__

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest>
    <application>
        <activity
            android:name=".TranslucentActivity"
            android:configChanges="orientation"
            android:screenOrientation="portrait"
            android:theme="@style/Translucent" />
    </application>
</manifest>
```

上記のTranslucentActivityをAndroid 8.0の端末で開くとクラッシュします。

# 原因
Android 8.0の`Activity#onCreate`には、以下のような処理が入っているため、上記条件を満たすとクラッシュするようになっています。

```java:Activity.java
public class Activity {
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // ...

        if (getApplicationInfo().targetSdkVersion >= O_MR1 && mActivityInfo.isFixedOrientation()) {
            final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window);
            final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta);
            ta.recycle();

            if (isTranslucentOrFloating) {
                throw new IllegalStateException(
                        "Only fullscreen opaque activities can request orientation");
            }
        }
        
        // ...
    }
}
```

しかし、Android 8.1では上述の処理が消されています。

`Activity.java`の[commit log](https://android.googlesource.com/platform/frameworks/base/+log/master/core/java/android/app/Activity.java)を見てみると、だいたい以下のような時系列で修正が入っていそうです。

|commit / tag|説明|
|:--|:--|
|[android-7.1.2_r36](https://android.googlesource.com/platform/frameworks/base/+/android-7.1.2_r36)|Android 7.1.2|
|[3979159](https://android.googlesource.com/platform/frameworks/base/+/39791594560b2326625b663ed6796882900c220f%5E%21/core/java/android/app/Activity.java)<br>[d1ac18c](https://android.googlesource.com/platform/frameworks/base/+/d1ac18c7c9eca1b07120be598dc6859b188baeb3%5E%21/core/java/android/app/Activity.java)|該当箇所の追加|
|[android-8.0.0_r1](https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r36)<br>[android-8.0.0_r36](https://android.googlesource.com/platform/frameworks/base/+/android-8.0.0_r36)|Android 8.0|
|[a89b183](https://android.googlesource.com/platform/frameworks/base/+/a89b183ef9323f4426941b207e2378064b5c7de3%5E%21/core/java/android/app/Activity.java)<br>[e83f34c](https://android.googlesource.com/platform/frameworks/base/+/e83f34cde79c51efb66f31f2770c2e8e572e96db%5E%21/core/java/android/app/Activity.java)<br>[d4ecffa](https://android.googlesource.com/platform/frameworks/base/+/d4ecffae67fa0dea03c581ca26f76b87a14be763%5E%21/core/java/android/app/Activity.java)|該当箇所の削除|
|[android-8.1.0_r1](https://android.googlesource.com/platform/frameworks/base/+/android-8.1.0_r1)|Android 8.1|
[a4ceea0](https://android.googlesource.com/platform/frameworks/base/+/a4ceea026d6373e9be4b1daf3aa4ed93de4157cf%5E%21/core/java/android/app/Activity.java)|（同様に削除）|

APIレベルとタグの関係は以下のページを参考にしました
https://source.android.com/setup/start/build-numbers

## バグなのか？仕様なのか？
完全に予想ですが、うしろのActivityが見えているようなActivityの場合、orientationを指定するとうしろのActivityも同じorientationにならなければならず、その時にうしろのActivityはorientationを固定していても画面回転が起こってしまうというのを防ぎたかったのかなと思います。
とはいえ、Android 8.1では削除されている事実からすると、少なくともクラッシュするという挙動はAndroid開発者たちには受け入れられなかったようです。以下で話し合われているのは`Activity#onCreate`の部分ではないですが、そのようなやりとりがあります。
https://issuetracker.google.com/issues/68454482

# 解決方法
当たり前ですが、クラッシュしてしまう4つの条件のうち、いずれかを満たさないようにすれば解決できます。とはいえ、Android 8.0を使わせないとか、一生targetSdkVersionを27以上にしないとかはできないと思うので、以下のような解決策になると思います。

## 画面回転に対応する
本来は画面回転に対応するのがベストだと思います！とはいえこの問題に遭遇する人は画面回転に対応していないアプリを開発している気がします :sweat_smile:

## orientationをbehindする
遷移元のActivityが自分のアプリに限定されるのであれば、遷移元のActivityの向きを固定することができます。そうした場合、orientationをbehindにすると、うしろに透けているActivityと同じ向きになるため、そのまま固定されます。
（透明なActivityの場合、orientation未指定でもbehind指定時と同じ挙動になるようですが、behindを指定しておくと明示できて良い気がします）

## targetSdkVersionを一瞬下げる
完全に力技です :triumph: :triumph: :triumph: 
適当に触った感じだと正常に動作していそうですが、動作は保証できないです :bow:

```kotlin:TranslucentActivity.kt
class TranslucentActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {
            val targetSdkVersion = applicationInfo.targetSdkVersion
            applicationInfo.targetSdkVersion = Build.VERSION_CODES.O
            super.onCreate(savedInstanceState)
            applicationInfo.targetSdkVersion = targetSdkVersion
        } else {
            super.onCreate(savedInstanceState)
        }
        setContentView(R.layout.activity_translucent)
    }
}
```

# まとめ
targetSdkVersionを27に上げた場合、APIレベル27（Android8.1）で動作確認すれば大丈夫かと思いきや、そうでもないようですね :sweat_smile:
とはいえ今回の場合はActivityを起動した瞬間にクラッシュするので、自動テストを充実させて防ぎたいものです :muscle:

何かご意見ご感想、間違い指摘などがあればコメントください :dancer: