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 com.google.common.annotations.VisibleForTesting; 19 20 import org.chromium.base.ObserverList; 21 import org.chromium.base.ThreadUtils; 22 import 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 29 public 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 */ onScreenOrientationChanged(int orientation)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 */ startListening()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 */ stopListening()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 startListening()79 public void startListening() { 80 mAppContext.registerComponentCallbacks(this); 81 } 82 83 @Override stopListening()84 public void stopListening() { 85 mAppContext.unregisterComponentCallbacks(this); 86 } 87 88 // ComponentCallbacks implementation: 89 90 @Override onConfigurationChanged(Configuration newConfig)91 public void onConfigurationChanged(Configuration newConfig) { 92 notifyObservers(); 93 } 94 95 @Override onLowMemory()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 startListening()113 public void startListening() { 114 DisplayManager displayManager = 115 (DisplayManager) mAppContext.getSystemService(Context.DISPLAY_SERVICE); 116 displayManager.registerDisplayListener(this, null); 117 } 118 119 @Override stopListening()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 onDisplayAdded(int displayId)129 public void onDisplayAdded(int displayId) { 130 } 131 132 @Override onDisplayRemoved(int displayId)133 public void onDisplayRemoved(int displayId) { 134 } 135 136 @Override onDisplayChanged(int displayId)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 */ getInstance()165 public static ScreenOrientationListener getInstance() { 166 ThreadUtils.assertOnUiThread(); 167 168 if (sInstance == null) { 169 sInstance = new ScreenOrientationListener(); 170 } 171 172 return sInstance; 173 } 174 ScreenOrientationListener()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 injectConfigurationListenerBackendForTest()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 */ addObserver(ScreenOrientationObserver observer, Context context)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 */ removeObserver(ScreenOrientationObserver observer)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 */ notifyObservers()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 */ updateOrientation()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