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 org.chromium.base.ObserverList;
19import org.chromium.base.ThreadUtils;
20import org.chromium.base.VisibleForTesting;
21import org.chromium.ui.gfx.DeviceDisplayInfo;
22
23/**
24 * ScreenOrientationListener is a class that informs its observers when the
25 * screen orientation changes.
26 */
27@VisibleForTesting
28public class ScreenOrientationListener {
29
30    /**
31     * Observes changes in screen orientation.
32     */
33    public interface ScreenOrientationObserver {
34        /**
35         * Called whenever the screen orientation changes.
36         *
37         * @param orientation The orientation angle of the screen.
38         */
39        void onScreenOrientationChanged(int orientation);
40    }
41
42    /**
43     * ScreenOrientationListenerBackend is an interface that abstract the
44     * mechanism used for the actual screen orientation listening. The reason
45     * being that from Android API Level 17 DisplayListener will be used. Before
46     * that, an unreliable solution based on onConfigurationChanged has to be
47     * used.
48     */
49    private interface ScreenOrientationListenerBackend {
50
51        /**
52         * Starts to listen for screen orientation changes. This will be called
53         * when the first observer is added.
54         */
55        void startListening();
56
57        /**
58         * Stops to listen for screen orientation changes. This will be called
59         * when the last observer is removed.
60         */
61        void stopListening();
62
63        /**
64         * Toggle the accurate mode if it wasn't already doing so. The backend
65         * will keep track of the number of times this has been called.
66         */
67        void startAccurateListening();
68
69        /**
70         * Request to stop the accurate mode. It will effectively be stopped
71         * only if this method is called as many times as
72         * startAccurateListening().
73         */
74        void stopAccurateListening();
75    }
76
77    /**
78     * ScreenOrientationConfigurationListener implements ScreenOrientationListenerBackend
79     * to use ComponentCallbacks in order to listen for screen orientation
80     * changes.
81     *
82     * This method is known to not correctly detect 180 degrees changes but it
83     * is the only method that will work before API Level 17 (excluding polling).
84     * When toggleAccurateMode() is called, it will start polling in order to
85     * find out if the display has changed.
86     */
87    private class ScreenOrientationConfigurationListener
88            implements ScreenOrientationListenerBackend, ComponentCallbacks {
89
90        private static final long POLLING_DELAY = 500;
91
92        private int mAccurateCount = 0;
93
94        // ScreenOrientationListenerBackend implementation:
95
96        @Override
97        public void startListening() {
98            mAppContext.registerComponentCallbacks(this);
99        }
100
101        @Override
102        public void stopListening() {
103            mAppContext.unregisterComponentCallbacks(this);
104        }
105
106        @Override
107        public void startAccurateListening() {
108            ++mAccurateCount;
109
110            if (mAccurateCount > 1)
111                return;
112
113            // Start polling if we went from 0 to 1. The polling will
114            // automatically stop when mAccurateCount reaches 0.
115            final ScreenOrientationConfigurationListener self = this;
116            ThreadUtils.postOnUiThreadDelayed(new Runnable() {
117                @Override
118                public void run() {
119                    self.onConfigurationChanged(null);
120
121                    if (self.mAccurateCount < 1)
122                        return;
123
124                    ThreadUtils.postOnUiThreadDelayed(this,
125                            ScreenOrientationConfigurationListener.POLLING_DELAY);
126                }
127            }, POLLING_DELAY);
128        }
129
130        @Override
131        public void stopAccurateListening() {
132            --mAccurateCount;
133            assert mAccurateCount >= 0;
134        }
135
136        // ComponentCallbacks implementation:
137
138        @Override
139        public void onConfigurationChanged(Configuration newConfig) {
140            notifyObservers();
141        }
142
143        @Override
144        public void onLowMemory() {
145        }
146    }
147
148    /**
149     * ScreenOrientationDisplayListener implements ScreenOrientationListenerBackend
150     * to use DisplayListener in order to listen for screen orientation changes.
151     *
152     * This method is reliable but DisplayListener is only available for API Level 17+.
153     */
154    @SuppressLint("NewApi")
155    private class ScreenOrientationDisplayListener
156            implements ScreenOrientationListenerBackend, DisplayListener {
157
158        // ScreenOrientationListenerBackend implementation:
159
160        @Override
161        public void startListening() {
162            DisplayManager displayManager =
163                    (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE);
164            displayManager.registerDisplayListener(this, null);
165        }
166
167        @Override
168        public void stopListening() {
169            DisplayManager displayManager =
170                    (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE);
171            displayManager.unregisterDisplayListener(this);
172        }
173
174        @Override
175        public void startAccurateListening() {
176            // Always accurate. Do nothing.
177        }
178
179        @Override
180        public void stopAccurateListening() {
181            // Always accurate. Do nothing.
182        }
183
184        // DisplayListener implementation:
185
186        @Override
187        public void onDisplayAdded(int displayId) {
188        }
189
190        @Override
191        public void onDisplayRemoved(int displayId) {
192        }
193
194        @Override
195        public void onDisplayChanged(int displayId) {
196            notifyObservers();
197        }
198
199    }
200
201    private static final String TAG = "ScreenOrientationListener";
202
203    // List of observers to notify when the screen orientation changes.
204    private final ObserverList<ScreenOrientationObserver> mObservers =
205            new ObserverList<ScreenOrientationObserver>();
206
207    // mOrientation will be updated every time the orientation changes. When not
208    // listening for changes, the value will be invalid and will be updated when
209    // starting to listen again.
210    private int mOrientation;
211
212    // Current application context derived from the first context being received.
213    private Context mAppContext;
214
215    private ScreenOrientationListenerBackend mBackend;
216
217    private static ScreenOrientationListener sInstance;
218
219    /**
220     * Returns a ScreenOrientationListener implementation based on the device's
221     * supported API level.
222     */
223    public static ScreenOrientationListener getInstance() {
224        ThreadUtils.assertOnUiThread();
225
226        if (sInstance == null) {
227            sInstance = new ScreenOrientationListener();
228        }
229
230        return sInstance;
231    }
232
233    private ScreenOrientationListener() {
234        mBackend = Build.VERSION.SDK_INT >= 17 ?
235                new ScreenOrientationDisplayListener() :
236                new ScreenOrientationConfigurationListener();
237    }
238
239    /**
240     * Add |observer| in the ScreenOrientationListener observer list and
241     * immediately call |onScreenOrientationChanged| on it with the current
242     * orientation value.
243     *
244     * @param observer The observer that will get notified.
245     * @param context The context associated with this observer.
246     */
247    public void addObserver(ScreenOrientationObserver observer, Context context) {
248        if (mAppContext == null) {
249            mAppContext = context.getApplicationContext();
250        }
251
252        assert mAppContext == context.getApplicationContext();
253        assert mAppContext != null;
254
255        if (!mObservers.addObserver(observer)) {
256            Log.w(TAG, "Adding an observer that is already present!");
257            return;
258        }
259
260        // If we got our first observer, we should start listening.
261        if (mObservers.size() == 1) {
262            updateOrientation();
263            mBackend.startListening();
264        }
265
266        // We need to send the current value to the added observer as soon as
267        // possible but outside of the current stack.
268        final ScreenOrientationObserver obs = observer;
269        ThreadUtils.assertOnUiThread();
270        ThreadUtils.postOnUiThread(new Runnable() {
271            @Override
272            public void run() {
273                obs.onScreenOrientationChanged(mOrientation);
274            }
275        });
276    }
277
278    /**
279     * Remove the |observer| from the ScreenOrientationListener observer list.
280     *
281     * @param observer The observer that will no longer receive notification.
282     */
283    public void removeObserver(ScreenOrientationObserver observer) {
284        if (!mObservers.removeObserver(observer)) {
285            Log.w(TAG, "Removing an inexistent observer!");
286            return;
287        }
288
289        if (mObservers.isEmpty()) {
290            // The last observer was removed, we should just stop listening.
291            mBackend.stopListening();
292        }
293    }
294
295    /**
296     * Toggle the accurate mode if it wasn't already doing so. The backend will
297     * keep track of the number of times this has been called.
298     */
299    public void startAccurateListening() {
300        mBackend.startAccurateListening();
301    }
302
303    /**
304     * Request to stop the accurate mode. It will effectively be stopped only if
305     * this method is called as many times as startAccurateListening().
306     */
307    public void stopAccurateListening() {
308        mBackend.stopAccurateListening();
309    }
310
311    /**
312     * This should be called by classes extending ScreenOrientationListener when
313     * it is possible that there is a screen orientation change. If there is an
314     * actual change, the observers will get notified.
315     */
316    private void notifyObservers() {
317        int previousOrientation = mOrientation;
318        updateOrientation();
319
320        if (mOrientation == previousOrientation) {
321            return;
322        }
323
324        DeviceDisplayInfo.create(mAppContext).updateNativeSharedDisplayInfo();
325
326        for (ScreenOrientationObserver observer : mObservers) {
327            observer.onScreenOrientationChanged(mOrientation);
328        }
329    }
330
331    /**
332     * Updates |mOrientation| based on the default display rotation.
333     */
334    private void updateOrientation() {
335        WindowManager windowManager =
336                (WindowManager) mAppContext.getSystemService(Context.WINDOW_SERVICE);
337
338        switch (windowManager.getDefaultDisplay().getRotation()) {
339            case Surface.ROTATION_0:
340                mOrientation = 0;
341                break;
342            case Surface.ROTATION_90:
343                mOrientation = 90;
344                break;
345            case Surface.ROTATION_180:
346                mOrientation = 180;
347                break;
348            case Surface.ROTATION_270:
349                mOrientation = -90;
350                break;
351            default:
352                throw new IllegalStateException(
353                        "Display.getRotation() shouldn't return that value");
354        }
355    }
356}
357