On supporting foldables
Tags: android, jetpack, foldable
Published: 2020-07-19 15:25:20

Foldable devices promise exciting new ways to present the content of your apps. Due to quite exclusive price tags the few existing models have not yet gained mass adoption. This will likely change soon as a couple of affordable devices is expected to hit market later this year. But how do we utilize new features? How can we make our apps ready for devices that are not here yet? Also, we can't buy them all, can we? :-) So how do we test these capabilities? Android has, contrary to the iPhone, had the need to support different form factors (almost) from the beginning. Consequently, different display sizes, pixel densities and aspect ratios do not challenge us. Consider this (deliberately) incomplete list of platform features:

- Fragments
- Alternative resources
- Vector Drawables
- Multi Resume
- Multi-Window
- Display Cutouts
- Activity Group
- ...

Wait, what? What the heck are Activity Groups? Relax, if you have not heard of this there's no need to bother. `ActivityGroup` is an abandoned predecessor to the fragments concept. The class has been present since api level 1 but was deprecated in Android 3.2. The idea is to present more than one activity at a time. For example, `android.app.TabActivity` (also deprecated in api level 13) extends `android.app.ActivityGroup`. What's interesting: you can use `ActivityGroup` to show several activities on screen at the same time:

![Two activities](https://dev-to-uploads.s3.amazonaws.com/i/d4k57cg1xhsg8gpnuni6.png)

The screenshot is not very exciting, but take a look at [the code](https://github.com/tkuenneth/android-demos/tree/master/misc/ActivityGroupDemo):

```kotlin
class MainActivity : ActivityGroup() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    first.addView(startActivity(R.string.first, Color.DKGRAY, Color.LTGRAY).decorView)
    second.addView(startActivity(R.string.second, Color.YELLOW, Color.BLUE).decorView)
  }

  private fun startActivity(text: Int, textColor: Int, layoutColor: Int): Window {
    val id1 = getString(text)
    val intent = Intent(this, ChildActivity::class.java)
    intent.putExtra(KEY_TEXT, id1)
    intent.putExtra(KEY_TEXT_COLOR, textColor)
    intent.putExtra(KEY_LAYOUT_COLOR, layoutColor)
    return localActivityManager.startActivity(id1, intent)
  }
}
```

`ChildActivity` is an ordinary activity that needs to be declared in the manifest. `android.app.LocalActivityManager` can load them and make them available as `Window`s, which in turn can easily be included in view hierarchies. So why am I telling you this? If a foldable device is unfolded the user may either use the screen real estate for one app (tablet mode), or desire to have two related apps side by side. Samsung calls this App Pair, and Microsoft's upcoming Surface Duo is expected to feature something called App Groups. 

Now, you may be thinking *But Android offers Multi Window Mode and its implementation as split screen*. True, but here the grouping process is started by the user. If an activity wants to accompany another one this needs to be declared in the manifest. As for as I know (please do use the comments to correct me) there is no general approach for launchers and other shell-like apps. Unfortunately, `LocalActivityManager` isn't, either. Because as *Local* implies only activities of the app can be launched. Trying to summon foreign activities produces security-related exceptions. But this happens deep inside Android, while resolving packages.

## Jetpack Window Manager

But we don't want to write a launcher, do we? Still, we may want to know more about the capabilities of a foldable device. That is where Jetpack Window Manager comes in handy. Currently in 1.0.0-alpha01, the component [aims to](https://developer.android.com/jetpack/androidx/releases/window)

> help application developers support new device form factors and
> provide a common API surface for different Window Manager
> features on both old and new platform versions. 

To use it we need to add the following line to our `dependencies` section in the *build.gradle* file of the *app* module:

```Groovy
 implementation "androidx.window:window:1.0.0-alpha01"
```

The class `androidx.window.WindowManager` is the main interaction point with this library. Use an instance of this class to poll the current state of the device and display, or to register callbacks for changes in the corresponding states. I will illustrate this with a sample project called [WindowManagerDemo](https://github.com/tkuenneth/android-demos/tree/master/misc/WindowManagerDemo). First we need to set things up:

```Kotlin
val backend = ExtensionWindowBackend.getInstance(this)
val wm = WindowManager(this, backend)
```
The second line gets us an instance of the `WindowManager` class initialized with and connected to the provided visual context. In my example `this` refers to my activity. The second parameter points to an interface called `WindowBackend`. That's where the actual information comes from. You can pass a custom implementation for testing, or `null` to use the default one (`ExtensionWindowBackend`). I made this explicitly to illustrate the workflow. Now let's take a look at some of the values we can query:

```Kotlin
val posture = when (wm.deviceState.posture) {
  DeviceState.POSTURE_CLOSED -> "CLOSED"
  DeviceState.POSTURE_HALF_OPENED -> "HALF_OPENED"
  DeviceState.POSTURE_OPENED -> "OPENED"
  DeviceState.POSTURE_FLIPPED -> "FLIPPED"
  // DeviceState.POSTURE_UNKNOWN
  else -> "UNKNOWN"
}
textview.append("Posture: $posture")
```
For example, we can get the posture of a foldable device with a flexible screen or multiple physical screens. Devices with a single rigid display will always report `POSTURE_UNKNOWN`. To determine if a device has a hinge or one flexible display, we can access a list of `DisplayFeature`s. A display feature [is a](https://developer.android.com/reference/androidx/window/DisplayFeature)

> distinctive physical attribute located within the display 
> panel of the device. It can intrude into the application window
> space and create a visual distortion, visual or touch 
> discontinuity, make some area invisible or create a logical 
> divider or separation in the screen space.

We get the list through `windowLayoutInfo.displayFeatures`. Please make sure to execute this code only after your activity has a window attached and the layout pass has happened. In my example the corresponding lines reside in `doOnLayout()`.

![Configuring a foldable AVD](https://dev-to-uploads.s3.amazonaws.com/i/giemdhpcwt3b02uj0xjo.png)

If you launch my app in an ordinary emulator the list of display features is empty. You can, however, configure the emulator with foldable support, as displayed in the screenshot above. One would expect that then at east some features are reported. At the tine of writing this article, they aren't. `ExtensionWindowBackend` contains a private method called `initExtension()`. It invokes `ExtensionHelper.getExtensionImpl()`, which, in turn, calls 
`ExtensionCompat.getExtensionVersion()`. This fails, the code being executed, `ExtensionProvider.getApiVersion()`, throws a `NoClassDefFoundError`. `ExtensionProvider` appears to not be there. What a ride... Luckily, Microsoft has [recently updated their Surface Duo Emulator](https://devblogs.microsoft.com/surface-duo/microsoft-surface-duo-emulator-updated-with-jetpack-window-manager-preview/) to support Jetpack Window Manager.

![WindowManagerDemo in the Surface Duo Emulator](https://dev-to-uploads.s3.amazonaws.com/i/wvhvj7227km6cir4wr9a.png)

## Summary

So far Jetpack Window Manager is quite small, yet extremely useful. It is a great relief that Microsoft seems to be utilizing this, because if there is something our ecosystem should not face is fragmentation the the foldable area. Things might have been more easy if Google had not chosen to completely abandon the tablet market. And a foldable by Googles seems not to be on the horizon either.