ScreenOrientationListener.java revision e5d81f57cb97b3b6b7fccc9c5610d21eb81db09d
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser;
6
7import android.annotation.SuppressLint;
8import android.content.ComponentCallbacks;
9import android.content.Context;
10import android.content.res.Configuration;
11import android.hardware.display.DisplayManager;
12import android.hardware.display.DisplayManager.DisplayListener;
13import android.os.Build;
14import android.util.Log;
15import android.view.Surface;
16import android.view.WindowManager;
17
18import com.google.common.annotations.VisibleForTesting;
19
20import org.chromium.base.ObserverList;
21import org.chromium.base.ThreadUtils;
22import org.chromium.ui.gfx.DeviceDisplayInfo;
23
24/**
25 * ScreenOrientationListener is a class that informs its observers when the
26 * screen orientation changes.
27 */
28@VisibleForTesting
29public class ScreenOrientationListener {
30
31    /**
32     * Observes changes in screen orientation.
33     */
34    public interface ScreenOrientationObserver {
35        /**
36         * Called whenever the screen orientation changes.
37         *
38         * @param orientation The orientation angle of the screen.
39         */
40        void onScreenOrientationChanged(int orientation);
41    }
42
43    /**
44     * ScreenOrientationListenerBackend is an interface that abstract the
45     * mechanism used for the actual screen orientation listening. The reason
46     * being that from Android API Level 17 DisplayListener will be used. Before
47     * that, an unreliable solution based on onConfigurationChanged has to be
48     * used.
49     */
50    private interface ScreenOrientationListenerBackend {
51
52        /**
53         * Starts to listen for screen orientation changes. This will be called
54         * when the first observer is added.
55         */
56        void startListening();
57
58        /**
59         * Stops to listen for screen orientation changes. This will be called
60         * when the last observer is removed.
61         */
62        void stopListening();
63    }
64
65    /**
66     * ScreenOrientationConfigurationListener implements ScreenOrientationListenerBackend
67     * to use ComponentCallbacks in order to listen for screen orientation
68     * changes.
69     *
70     * This method is known to not correctly detect 180 degrees changes but it
71     * is the only method that will work before API Level 17 (excluding polling).
72     */
73    private class ScreenOrientationConfigurationListener
74            implements ScreenOrientationListenerBackend, ComponentCallbacks {
75
76        // ScreenOrientationListenerBackend implementation:
77
78        @Override
79        public void startListening() {
80            mAppContext.registerComponentCallbacks(this);
81        }
82
83        @Override
84        public void stopListening() {
85            mAppContext.unregisterComponentCallbacks(this);
86        }
87
88        // ComponentCallbacks implementation:
89
90        @Override
91        public void onConfigurationChanged(Configuration newConfig) {
92            notifyObservers();
93        }
94
95        @Override
96        public void onLowMemory() {
97        }
98    }
99
100    /**
101     * ScreenOrientationDisplayListener implements ScreenOrientationListenerBackend
102     * to use DisplayListener in order to listen for screen orientation changes.
103     *
104     * This method is reliable but DisplayListener is only available for API Level 17+.
105     */
106    @SuppressLint("NewApi")
107    private class ScreenOrientationDisplayListener
108            implements ScreenOrientationListenerBackend, DisplayListener {
109
110        // ScreenOrientationListenerBackend implementation:
111
112        @Override
113        public void startListening() {
114            DisplayManager displayManager =
115                    (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE);
116            displayManager.registerDisplayListener(this, null);
117        }
118
119        @Override
120        public void stopListening() {
121            DisplayManager displayManager =
122                    (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE);
123            displayManager.unregisterDisplayListener(this);
124        }
125
126        // DisplayListener implementation:
127
128        @Override
129        public void onDisplayAdded(int displayId) {
130        }
131
132        @Override
133        public void onDisplayRemoved(int displayId) {
134        }
135
136        @Override
137        public void onDisplayChanged(int displayId) {
138            notifyObservers();
139        }
140
141    }
142
143    private static final String TAG = "ScreenOrientationListener";
144
145    // List of observers to notify when the screen orientation changes.
146    private final ObserverList<ScreenOrientationObserver> mObservers =
147            new ObserverList<ScreenOrientationObserver>();
148
149    // mOrientation will be updated every time the orientation changes. When not
150    // listening for changes, the value will be invalid and will be updated when
151    // starting to listen again.
152    private int mOrientation;
153
154    // Current application context derived from the first context being received.
155    private Context mAppContext;
156
157    private ScreenOrientationListenerBackend mBackend;
158
159    private static ScreenOrientationListener sInstance;
160
161    /**
162     * Returns a ScreenOrientationListener implementation based on the device's
163     * supported API level.
164     */
165    public static ScreenOrientationListener getInstance() {
166        ThreadUtils.assertOnUiThread();
167
168        if (sInstance == null) {
169            sInstance = new ScreenOrientationListener();
170        }
171
172        return sInstance;
173    }
174
175    private ScreenOrientationListener() {
176        mBackend = Build.VERSION.SDK_INT >= 17 ?
177                new ScreenOrientationDisplayListener() :
178                new ScreenOrientationConfigurationListener();
179    }
180
181    /**
182     * Creates a ScreenOrientationConfigurationListener backend regardless of
183     * the current SDK.
184     */
185    @VisibleForTesting
186    void injectConfigurationListenerBackendForTest() {
187        mBackend = new ScreenOrientationConfigurationListener();
188    }
189
190    /**
191     * Add |observer| in the ScreenOrientationListener observer list and
192     * immediately call |onScreenOrientationChanged| on it with the current
193     * orientation value.
194     *
195     * @param observer The observer that will get notified.
196     * @param context The context associated with this observer.
197     */
198    public void addObserver(ScreenOrientationObserver observer, Context context) {
199        if (mAppContext == null) {
200            mAppContext = context.getApplicationContext();
201        }
202
203        assert mAppContext == context.getApplicationContext();
204        assert mAppContext != null;
205
206        if (!mObservers.addObserver(observer)) {
207            Log.w(TAG, "Adding an observer that is already present!");
208            return;
209        }
210
211        // If we got our first observer, we should start listening.
212        if (mObservers.size() == 1) {
213            updateOrientation();
214            mBackend.startListening();
215        }
216
217        // We need to send the current value to the added observer as soon as
218        // possible but outside of the current stack.
219        final ScreenOrientationObserver obs = observer;
220        ThreadUtils.assertOnUiThread();
221        ThreadUtils.postOnUiThread(new Runnable() {
222            @Override
223            public void run() {
224                obs.onScreenOrientationChanged(mOrientation);
225            }
226        });
227    }
228
229    /**
230     * Remove the |observer| from the ScreenOrientationListener observer list.
231     *
232     * @param observer The observer that will no longer receive notification.
233     */
234    public void removeObserver(ScreenOrientationObserver observer) {
235        if (!mObservers.removeObserver(observer)) {
236            Log.w(TAG, "Removing an inexistent observer!");
237            return;
238        }
239
240        if (mObservers.isEmpty()) {
241            // The last observer was removed, we should just stop listening.
242            mBackend.stopListening();
243        }
244    }
245
246    /**
247     * This should be called by classes extending ScreenOrientationListener when
248     * it is possible that there is a screen orientation change. If there is an
249     * actual change, the observers will get notified.
250     */
251    private void notifyObservers() {
252        int previousOrientation = mOrientation;
253        updateOrientation();
254
255        DeviceDisplayInfo.create(mAppContext).updateNativeSharedDisplayInfo();
256
257        if (mOrientation == previousOrientation) {
258            return;
259        }
260
261        for (ScreenOrientationObserver observer : mObservers) {
262            observer.onScreenOrientationChanged(mOrientation);
263        }
264    }
265
266    /**
267     * Updates |mOrientation| based on the default display rotation.
268     */
269    private void updateOrientation() {
270        WindowManager windowManager =
271                (WindowManager) mAppContext.getSystemService(Context.WINDOW_SERVICE);
272
273        switch (windowManager.getDefaultDisplay().getRotation()) {
274            case Surface.ROTATION_0:
275                mOrientation = 0;
276                break;
277            case Surface.ROTATION_90:
278                mOrientation = 90;
279                break;
280            case Surface.ROTATION_180:
281                mOrientation = 180;
282                break;
283            case Surface.ROTATION_270:
284                mOrientation = -90;
285                break;
286            default:
287                throw new IllegalStateException(
288                        "Display.getRotation() shouldn't return that value");
289        }
290    }
291}
292