How can I globally detect when the screen rotation changes?
Asked Answered
A

3

10

Question

In an Android service, I want to detect whenever the screen rotation changes. By rotation, I don't just mean portrait versus landscape; I mean any change to the screen rotation. Examples of such changes are changes to:

  • Portrait
  • Reverse portrait
  • Landscape
  • Reverse landscape

Note that this question is not about changes to the device orientation. It's only about the screen orientation/rotation.

What I've Tried

  • Listening for Configuration changes via ACTION_CONFIGURATION_CHANGED. This only covers changes between portrait and landscape, so 180° changes don't trigger this.

Why I'm Doing This

I'm developing a custom screen orientation management app.

Allerie answered 20/5, 2015 at 12:18 Comment(0)
A
4

Use the hidden API

You can do this using a hidden API called IWindowManager.watchRotation(IRotationWatcher). From my testing, it seems to take a callback that is called every time the screen rotation changes. The callback also seems to be given the current screen rotation.

Being a hidden API, you can't just call it directly. How to use hidden APIs is a topic of its own. And, of course, this might not be as reliable from a maintainability perspective as normal APIs.

Also, onRotationChanged isn't called in the same thread you used to call watchRotation. You'll probably want to delegate some work to a different thread such as the UI thread once you're in onRotationChanged.

Compatibility

Tested and working in API 14 - 28.

Example

Here's one way to get it to work:

  1. Copy android.view.IRotationWatcher into your app. Make sure to keep it in its original package. This seems to cause the development tools to think your code has access to it while also causing the operating system to still use the real one rather than your own copy.
  2. Use reflection to call watchRotation:

    try {
        Class<?> serviceManager = Class.forName("android.os.ServiceManager");
        IBinder serviceBinder = (IBinder)serviceManager.getMethod("getService", String.class).invoke(serviceManager, "window");
        Class<?> stub = Class.forName("android.view.IWindowManager$Stub");
        Object windowManagerService = stub.getMethod("asInterface", IBinder.class).invoke(stub, serviceBinder);
        Method watchRotation;
        if (Build.VERSION.SDK_INT >= 26)
            watchRotation = windowManagerService.getClass().getMethod("watchRotation", IRotationWatcher.class, int.class);
        else
            watchRotation = windowManagerService.getClass().getMethod("watchRotation", IRotationWatcher.class);
    
        //This method seems to have been introduced in Android 4.3, so don't expect to always find it
        Method removeRotationWatcher = null;
        try {
            removeRotationWatcher = windowManagerService.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class);
        }
        catch (NoSuchMethodException ignored) {}
    
        IRotationWatcher.Stub screenRotationChanged = new IRotationWatcher.Stub() {
            @Override
            public void onRotationChanged(int rotation) throws RemoteException {
                //Do what you want here
                //WARNING: This isn't called in the same thread you were in when you called watchRotation
            }
        };
    
        //Start monitoring for changes
        if (Build.VERSION.SDK_INT >= 26)
            watchRotation.invoke(windowManagerService, screenRotationChanged, Display.DEFAULT_DISPLAY);
        else
            watchRotation.invoke(windowManagerService, screenRotationChanged);
    
        //Stop monitoring for changes when you're done
        if (removeRotationWatcher != null) {
            removeRotationWatcher.invoke(windowManagerService, screenRotationChanged);
    } catch (ClassNotFoundException | ClassCastException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    
Allerie answered 22/5, 2015 at 10:41 Comment(4)
onRotationChanged is never called for meTerramycin
@Terramycin Have you read "//Stop monitoring for changes when you're done" in the code? You've got to move that code portion to an appropriate place. (I just commented it for testing.)Rogatory
My new source of IRotationWatcher as grep.com is down: code.yawk.at/android/android-9.0.0_r35/android/view/…Rogatory
I'm getting events on Android 4.3, 5 and 13.Rogatory
H
5

The approved answer will work, but if you want a higher resolution of detection (or support further back to API 3), try OrientationEventListener, which can report the orientation of the phone in degrees.

mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

OrientationEventListener orientationEventListener = new OrientationEventListener(this,
        SensorManager.SENSOR_DELAY_NORMAL) {
    @Override
    public void onOrientationChanged(int orientation) {
        Display display = mWindowManager.getDefaultDisplay();
        int rotation = display.getRotation();
        if(rotation != mLastRotation){
             //rotation changed
             if (rotation == Surface.ROTATION_90){} // check rotations here
             if (rotation == Surface.ROTATION_270){} //
        }
        mLastRotation = rotation;
    }
};

if (orientationEventListener.canDetectOrientation()) {
    orientationEventListener.enable();
}
Hurd answered 20/5, 2015 at 12:28 Comment(1)
Yeah; I've seen OrientationEventListener, but it monitors the orientation of the device. As I said in my question, "this question is not about changes to the device orientation". This doesn't cover situations such as the orientation changing as a result of switching apps, toggling auto rotation, or changing USER_ROTATION.Allerie
A
4

Use the hidden API

You can do this using a hidden API called IWindowManager.watchRotation(IRotationWatcher). From my testing, it seems to take a callback that is called every time the screen rotation changes. The callback also seems to be given the current screen rotation.

Being a hidden API, you can't just call it directly. How to use hidden APIs is a topic of its own. And, of course, this might not be as reliable from a maintainability perspective as normal APIs.

Also, onRotationChanged isn't called in the same thread you used to call watchRotation. You'll probably want to delegate some work to a different thread such as the UI thread once you're in onRotationChanged.

Compatibility

Tested and working in API 14 - 28.

Example

Here's one way to get it to work:

  1. Copy android.view.IRotationWatcher into your app. Make sure to keep it in its original package. This seems to cause the development tools to think your code has access to it while also causing the operating system to still use the real one rather than your own copy.
  2. Use reflection to call watchRotation:

    try {
        Class<?> serviceManager = Class.forName("android.os.ServiceManager");
        IBinder serviceBinder = (IBinder)serviceManager.getMethod("getService", String.class).invoke(serviceManager, "window");
        Class<?> stub = Class.forName("android.view.IWindowManager$Stub");
        Object windowManagerService = stub.getMethod("asInterface", IBinder.class).invoke(stub, serviceBinder);
        Method watchRotation;
        if (Build.VERSION.SDK_INT >= 26)
            watchRotation = windowManagerService.getClass().getMethod("watchRotation", IRotationWatcher.class, int.class);
        else
            watchRotation = windowManagerService.getClass().getMethod("watchRotation", IRotationWatcher.class);
    
        //This method seems to have been introduced in Android 4.3, so don't expect to always find it
        Method removeRotationWatcher = null;
        try {
            removeRotationWatcher = windowManagerService.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class);
        }
        catch (NoSuchMethodException ignored) {}
    
        IRotationWatcher.Stub screenRotationChanged = new IRotationWatcher.Stub() {
            @Override
            public void onRotationChanged(int rotation) throws RemoteException {
                //Do what you want here
                //WARNING: This isn't called in the same thread you were in when you called watchRotation
            }
        };
    
        //Start monitoring for changes
        if (Build.VERSION.SDK_INT >= 26)
            watchRotation.invoke(windowManagerService, screenRotationChanged, Display.DEFAULT_DISPLAY);
        else
            watchRotation.invoke(windowManagerService, screenRotationChanged);
    
        //Stop monitoring for changes when you're done
        if (removeRotationWatcher != null) {
            removeRotationWatcher.invoke(windowManagerService, screenRotationChanged);
    } catch (ClassNotFoundException | ClassCastException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    
Allerie answered 22/5, 2015 at 10:41 Comment(4)
onRotationChanged is never called for meTerramycin
@Terramycin Have you read "//Stop monitoring for changes when you're done" in the code? You've got to move that code portion to an appropriate place. (I just commented it for testing.)Rogatory
My new source of IRotationWatcher as grep.com is down: code.yawk.at/android/android-9.0.0_r35/android/view/…Rogatory
I'm getting events on Android 4.3, 5 and 13.Rogatory
T
-2

Another way (instead of the one that the college above provided) is to check rotation angle manually in onCreate

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
    Display display = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    int rotation = display.getRotation();
    if (rotation == Surface.ROTATION_180) { // reverse portrait
        setContentView(R.layout.main_reverse_portrait);
    } else {  // for all other orientations
        setContentView(R.layout.main);
    }
    ...
}
Timorous answered 20/5, 2015 at 12:28 Comment(1)
That doesn't tell me when the screen orientation changes; it only tells me the current state at that one point in time.Allerie

© 2022 - 2024 — McMap. All rights reserved.