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 5 package org.chromium.content.browser; 6 7 import android.annotation.SuppressLint; 8 import android.content.ComponentCallbacks; 9 import android.content.Context; 10 import android.content.res.Configuration; 11 import android.hardware.display.DisplayManager; 12 import android.hardware.display.DisplayManager.DisplayListener; 13 import android.os.Build; 14 import android.util.Log; 15 import android.view.Surface; 16 import android.view.WindowManager; 17 18 import org.chromium.base.ObserverList; 19 import org.chromium.base.ThreadUtils; 20 import org.chromium.base.VisibleForTesting; 21 import 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 28 public 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 */ onScreenOrientationChanged(int orientation)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 */ startListening()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 */ stopListening()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 */ startAccurateListening()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 */ stopAccurateListening()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 startListening()97 public void startListening() { 98 mAppContext.registerComponentCallbacks(this); 99 } 100 101 @Override stopListening()102 public void stopListening() { 103 mAppContext.unregisterComponentCallbacks(this); 104 } 105 106 @Override startAccurateListening()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 stopAccurateListening()131 public void stopAccurateListening() { 132 --mAccurateCount; 133 assert mAccurateCount >= 0; 134 } 135 136 // ComponentCallbacks implementation: 137 138 @Override onConfigurationChanged(Configuration newConfig)139 public void onConfigurationChanged(Configuration newConfig) { 140 notifyObservers(); 141 } 142 143 @Override onLowMemory()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 startListening()161 public void startListening() { 162 DisplayManager displayManager = 163 (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE); 164 displayManager.registerDisplayListener(this, null); 165 } 166 167 @Override stopListening()168 public void stopListening() { 169 DisplayManager displayManager = 170 (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE); 171 displayManager.unregisterDisplayListener(this); 172 } 173 174 @Override startAccurateListening()175 public void startAccurateListening() { 176 // Always accurate. Do nothing. 177 } 178 179 @Override stopAccurateListening()180 public void stopAccurateListening() { 181 // Always accurate. Do nothing. 182 } 183 184 // DisplayListener implementation: 185 186 @Override onDisplayAdded(int displayId)187 public void onDisplayAdded(int displayId) { 188 } 189 190 @Override onDisplayRemoved(int displayId)191 public void onDisplayRemoved(int displayId) { 192 } 193 194 @Override onDisplayChanged(int displayId)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 */ getInstance()223 public static ScreenOrientationListener getInstance() { 224 ThreadUtils.assertOnUiThread(); 225 226 if (sInstance == null) { 227 sInstance = new ScreenOrientationListener(); 228 } 229 230 return sInstance; 231 } 232 ScreenOrientationListener()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 */ addObserver(ScreenOrientationObserver observer, Context context)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 */ removeObserver(ScreenOrientationObserver observer)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 */ startAccurateListening()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 */ stopAccurateListening()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 */ notifyObservers()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 */ updateOrientation()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