Folding legacy apps
Tags: android, java, foldables
Published: 2022-01-22 11:04:26

This series focuses on foldable and large-screen devices. Most of its articles use Kotlin, which has become the preferred programming language for Android apps quite a few years ago. Yet, there are still a lot of Android apps based on Java. Referring to them as legacy apps doesn't mean they no longer are under active development, or, at least in maintenance mode. Therefore, shouldn't we make sure that they provide a great experience on new device categories, too? We certainly should. So, in this article I show you how to make a Java Android app using `View`s behave well on foldables and large screens.

### Jetpack WindowManager

With *Jetpack WindowManager*, you can query certain device characteristics related to foldables. For example, the library tells you if a device has a hinge and if that hinge blocks certain areas of the screen. This, for example, is the case for the dual screen device *Surface Duo* and its successor.

*Jetpack WindowManager* uses a Kotlin-friendly api based on `Flow`s. Fortunately, it gives Java some love, too. To use the library in your Java app, you need to add an implementation dependency in your module-level *build.gradle* file:

```groovy
implementation "androidx.window:window-java:1.0.0-rc01"
```

The next step is make sure your app receives information from *Jetpack WindowManager*. First, let's declare an instance variable:

```java
private WindowInfoTrackerCallbackAdapter adapter;
```

It is initialized in `onCreate()`:

```java
adapter = new WindowInfoTrackerCallbackAdapter(
    WindowInfoTracker.Companion.getOrCreate(
        this
    )
);
```

Now it is time to wire it up with our code. For reasons of brevity I am using old-style `Activity` callbacks, but please consider using *Jetpack Lifecycle* instead.

```java
@Override
protected void onStart() {
  super.onStart();
  adapter.addWindowLayoutInfoListener(this,
      ContextCompat.getMainExecutor(this),
      callback);
  …
}

@Override
protected void onStop() {
  super.onStop();
  adapter.removeWindowLayoutInfoListener(callback);
  …
}
```

So, what is `callback`?

```java
private final Consumer<WindowLayoutInfo> callback =
    (windowLayoutInfo -> {
  final var windowMetrics = WindowMetricsCalculator.getOrCreate()
      .computeCurrentWindowMetrics(this);
  final var windowWidth = windowMetrics.getBounds().width();
  final var windowHeight = windowMetrics.getBounds().height();
  final var leftPaneParams = binding.leftPane.getLayoutParams();
  final var rightPaneParams = binding.rightPane.getLayoutParams();
  final var hingeParams = binding.hinge.getLayoutParams();
  var hasFoldingFeature = false;
  List<DisplayFeature> displayFeatures
        = windowLayoutInfo.getDisplayFeatures();
  …
});
```

First, we setup a couple of variables. `WindowMetricsCalculator` helps us obtain window metrics. So, `windowWidth` and `windowHeight` contain the width and the height. What about `leftPane`, `rightPane`, and `hinge`?

### Layout structure

`leftPane` and `rightPane` represent the content of your app, presented as two columns. `hinge` is placed between them. All three are children of a `LinearLayout`, whose orientation will change if needed. I'll show you soon. 

Now, you may be thinking *But my app doesn't use such a structure*.

I know. Most legacy apps are not optimized for tablets. But that is the beauty of this simple approach. Just make the root of your user interface `leftPane`, add `hinge` and `rightPane`, and wrap all three in a `LinearLayout`. If you app heavily relies on `Fragment`s, a more substantial rewowrk may be due.

Now, let's look at how to process `displayFeatures`.

```java
for (DisplayFeature displayFeature : displayFeatures) {
    FoldingFeature foldingFeature =
        (FoldingFeature) displayFeature;
    if (foldingFeature != null) {
      hasFoldingFeature = true;
      boolean isVertical = foldingFeature.getOrientation()
          == FoldingFeature.Orientation.VERTICAL;
      final var foldingFeatureBounds = foldingFeature.getBounds();
      hingeParams.width = foldingFeatureBounds.width();
      hingeParams.height = foldingFeatureBounds.height();
      if (isVertical) {
        binding.parent.setOrientation(LinearLayout.HORIZONTAL);
        leftPaneParams.width = foldingFeatureBounds.left;
        leftPaneParams.height =
            LinearLayout.LayoutParams.MATCH_PARENT;
        rightPaneParams.width = windowWidth
                                - foldingFeatureBounds.right;
        rightPaneParams.height =
           LinearLayout.LayoutParams.MATCH_PARENT;
      } else {
        int[] intArray = new int[2];
        binding.leftPane.getLocationOnScreen(intArray);
        binding.parent.setOrientation(LinearLayout.VERTICAL);
        leftPaneParams.width =
            LinearLayout.LayoutParams.MATCH_PARENT;
        leftPaneParams.height =
            foldingFeatureBounds.top - intArray[1];
        rightPaneParams.width =
            LinearLayout.LayoutParams.MATCH_PARENT;
        rightPaneParams.height = windowHeight
                                 - foldingFeatureBounds.bottom;
      }
    }
  }
  …
```

So, what is happening here? If we find a hinge, we

- set the sizes of `leftPane`, `rightPane`, and `hinge` accordingly
- configure the orientation of the `LieaerLayout` based on the configuration of the hinge

Did you notice `getLocationOnScreen()`? This code is executed when the orientation of the hinge is *horizontal*. Then, `leftPane` and `rightPane` are arranged vertically, with `hinge` between them. Although both screens usually have an equal size, one screen will contain the statusbar and an appbar, so the area for the content will be smaller. I found that calculation the offset this way is most reliable.

Our last step is to deal with the situation that there is no hinge. This is the case for normal smartphones and tablets.

```java
if (!hasFoldingFeature) {
    final float density =
       getResources().getDisplayMetrics().density;
    final float dp = windowMetrics.getBounds().width() / density;
    binding.parent.setOrientation(LinearLayout.HORIZONTAL);
    hingeParams.width = 0;
    hingeParams.height = 0;
    if (dp >= 600) {
      leftPaneParams.width = windowWidth / 2;
      leftPaneParams.height =
          LinearLayout.LayoutParams.MATCH_PARENT;
      rightPaneParams.width = windowWidth / 2;
      rightPaneParams.height =
          LinearLayout.LayoutParams.MATCH_PARENT;
    } else {
      leftPaneParams.width =
          LinearLayout.LayoutParams.MATCH_PARENT;
      leftPaneParams.height =
          LinearLayout.LayoutParams.MATCH_PARENT;
      rightPaneParams.width = 0;
      rightPaneParams.height = 0;
    }
}
```

I decide if I want to use two column mode by calculation the screen width in density independent pixels. If the computed value is less than 600 I configure a single column layout. Otherwise `leftPane` and `rightPane` will be of equal size. The size of the hinge is set to 0 in any case. 

### Conclusion

In this article I showed you how to incorporate *Jetpack WindowManager* into Java Android apps. Updating an existing layout to support a two column mode on foldables and large-screen devices is amazingly simple in many cases.

Have you made a legacy app look good on foldables? Please share your thoughts in the comments.