1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.hardware.display; 18 19 20 import static android.hardware.display.DisplayManager.EventsMask; 21 import static android.view.Display.HdrCapabilities.HdrType; 22 23 import android.Manifest; 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.RequiresPermission; 28 import android.app.PropertyInvalidatedCache; 29 import android.compat.annotation.UnsupportedAppUsage; 30 import android.content.Context; 31 import android.content.pm.ParceledListSlice; 32 import android.content.res.Resources; 33 import android.graphics.ColorSpace; 34 import android.graphics.Point; 35 import android.hardware.OverlayProperties; 36 import android.hardware.display.DisplayManager.DisplayListener; 37 import android.hardware.graphics.common.DisplayDecorationSupport; 38 import android.media.projection.IMediaProjection; 39 import android.media.projection.MediaProjection; 40 import android.os.Handler; 41 import android.os.HandlerExecutor; 42 import android.os.IBinder; 43 import android.os.Looper; 44 import android.os.Message; 45 import android.os.RemoteException; 46 import android.os.ServiceManager; 47 import android.os.Trace; 48 import android.util.Log; 49 import android.util.Pair; 50 import android.util.SparseArray; 51 import android.view.Display; 52 import android.view.DisplayAdjustments; 53 import android.view.DisplayInfo; 54 import android.view.Surface; 55 56 import com.android.internal.annotations.VisibleForTesting; 57 58 import java.lang.annotation.Retention; 59 import java.lang.annotation.RetentionPolicy; 60 import java.util.Collections; 61 import java.util.List; 62 import java.util.Objects; 63 import java.util.concurrent.CopyOnWriteArrayList; 64 import java.util.concurrent.Executor; 65 import java.util.concurrent.atomic.AtomicLong; 66 67 /** 68 * Manager communication with the display manager service on behalf of 69 * an application process. You're probably looking for {@link DisplayManager}. 70 * 71 * @hide 72 */ 73 public final class DisplayManagerGlobal { 74 private static final String TAG = "DisplayManager"; 75 private static final boolean DEBUG = false; 76 77 // True if display info and display ids should be cached. 78 // 79 // FIXME: The cache is currently disabled because it's unclear whether we have the 80 // necessary guarantees that the caches will always be flushed before clients 81 // attempt to observe their new state. For example, depending on the order 82 // in which the binder transactions take place, we might have a problem where 83 // an application could start processing a configuration change due to a display 84 // orientation change before the display info cache has actually been invalidated. 85 private static final boolean USE_CACHE = false; 86 87 @IntDef(prefix = {"EVENT_DISPLAY_"}, flag = true, value = { 88 EVENT_DISPLAY_ADDED, 89 EVENT_DISPLAY_CHANGED, 90 EVENT_DISPLAY_REMOVED, 91 EVENT_DISPLAY_BRIGHTNESS_CHANGED, 92 EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED, 93 }) 94 @Retention(RetentionPolicy.SOURCE) 95 public @interface DisplayEvent {} 96 97 public static final int EVENT_DISPLAY_ADDED = 1; 98 public static final int EVENT_DISPLAY_CHANGED = 2; 99 public static final int EVENT_DISPLAY_REMOVED = 3; 100 public static final int EVENT_DISPLAY_BRIGHTNESS_CHANGED = 4; 101 public static final int EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED = 5; 102 103 @UnsupportedAppUsage 104 private static DisplayManagerGlobal sInstance; 105 106 // Guarded by mLock 107 private boolean mDispatchNativeCallbacks = false; 108 private float mNativeCallbackReportedRefreshRate; 109 private final Object mLock = new Object(); 110 111 @UnsupportedAppUsage 112 private final IDisplayManager mDm; 113 114 private DisplayManagerCallback mCallback; 115 private @EventsMask long mRegisteredEventsMask = 0; 116 private final CopyOnWriteArrayList<DisplayListenerDelegate> mDisplayListeners = 117 new CopyOnWriteArrayList<>(); 118 119 private final SparseArray<DisplayInfo> mDisplayInfoCache = new SparseArray<>(); 120 private final ColorSpace mWideColorSpace; 121 private final OverlayProperties mOverlayProperties; 122 private int[] mDisplayIdCache; 123 124 private int mWifiDisplayScanNestCount; 125 126 @VisibleForTesting DisplayManagerGlobal(IDisplayManager dm)127 public DisplayManagerGlobal(IDisplayManager dm) { 128 mDm = dm; 129 try { 130 mWideColorSpace = 131 ColorSpace.get( 132 ColorSpace.Named.values()[mDm.getPreferredWideGamutColorSpaceId()]); 133 mOverlayProperties = mDm.getOverlaySupport(); 134 } catch (RemoteException ex) { 135 throw ex.rethrowFromSystemServer(); 136 } 137 } 138 139 private PropertyInvalidatedCache<Integer, DisplayInfo> mDisplayCache = 140 new PropertyInvalidatedCache<Integer, DisplayInfo>( 141 8, // size of display cache 142 CACHE_KEY_DISPLAY_INFO_PROPERTY) { 143 @Override 144 public DisplayInfo recompute(Integer id) { 145 try { 146 return mDm.getDisplayInfo(id); 147 } catch (RemoteException ex) { 148 throw ex.rethrowFromSystemServer(); 149 } 150 } 151 }; 152 153 /** 154 * Gets an instance of the display manager global singleton. 155 * 156 * @return The display manager instance, may be null early in system startup 157 * before the display manager has been fully initialized. 158 */ 159 @UnsupportedAppUsage getInstance()160 public static DisplayManagerGlobal getInstance() { 161 synchronized (DisplayManagerGlobal.class) { 162 if (sInstance == null) { 163 IBinder b = ServiceManager.getService(Context.DISPLAY_SERVICE); 164 if (b != null) { 165 sInstance = new DisplayManagerGlobal(IDisplayManager.Stub.asInterface(b)); 166 } 167 } 168 return sInstance; 169 } 170 } 171 172 /** 173 * Get information about a particular logical display. 174 * 175 * @param displayId The logical display id. 176 * @return Information about the specified display, or null if it does not exist. 177 * This object belongs to an internal cache and should be treated as if it were immutable. 178 */ 179 @UnsupportedAppUsage getDisplayInfo(int displayId)180 public DisplayInfo getDisplayInfo(int displayId) { 181 synchronized (mLock) { 182 return getDisplayInfoLocked(displayId); 183 } 184 } 185 186 /** 187 * Gets information about a particular logical display 188 * See {@link getDisplayInfo}, but assumes that {@link mLock} is held 189 */ getDisplayInfoLocked(int displayId)190 private @Nullable DisplayInfo getDisplayInfoLocked(int displayId) { 191 DisplayInfo info = null; 192 if (mDisplayCache != null) { 193 info = mDisplayCache.query(displayId); 194 } else { 195 try { 196 info = mDm.getDisplayInfo(displayId); 197 } catch (RemoteException ex) { 198 ex.rethrowFromSystemServer(); 199 } 200 } 201 if (info == null) { 202 return null; 203 } 204 205 registerCallbackIfNeededLocked(); 206 207 if (DEBUG) { 208 Log.d(TAG, "getDisplayInfo: displayId=" + displayId + ", info=" + info); 209 } 210 return info; 211 } 212 213 /** 214 * Gets all currently valid logical display ids. 215 * 216 * @return An array containing all display ids. 217 */ 218 @UnsupportedAppUsage getDisplayIds()219 public int[] getDisplayIds() { 220 return getDisplayIds(/* includeDisabled= */ false); 221 } 222 223 /** 224 * Gets all currently valid logical display ids. 225 * 226 * @param includeDisabled True if the returned list of displays includes disabled displays. 227 * @return An array containing all display ids. 228 */ getDisplayIds(boolean includeDisabled)229 public int[] getDisplayIds(boolean includeDisabled) { 230 try { 231 synchronized (mLock) { 232 if (USE_CACHE) { 233 if (mDisplayIdCache != null) { 234 return mDisplayIdCache; 235 } 236 } 237 238 int[] displayIds = mDm.getDisplayIds(includeDisabled); 239 if (USE_CACHE) { 240 mDisplayIdCache = displayIds; 241 } 242 registerCallbackIfNeededLocked(); 243 return displayIds; 244 } 245 } catch (RemoteException ex) { 246 throw ex.rethrowFromSystemServer(); 247 } 248 } 249 250 /** 251 * Check if specified UID's content is present on display and should be granted access to it. 252 * 253 * @param uid UID to be checked. 254 * @param displayId id of the display where presence of the content is checked. 255 * @return {@code true} if UID is present on display, {@code false} otherwise. 256 */ isUidPresentOnDisplay(int uid, int displayId)257 public boolean isUidPresentOnDisplay(int uid, int displayId) { 258 try { 259 return mDm.isUidPresentOnDisplay(uid, displayId); 260 } catch (RemoteException ex) { 261 throw ex.rethrowFromSystemServer(); 262 } 263 } 264 265 /** 266 * Gets information about a logical display. 267 * 268 * The display metrics may be adjusted to provide compatibility 269 * for legacy applications or limited screen areas. 270 * 271 * @param displayId The logical display id. 272 * @param daj The compatibility info and activityToken. 273 * @return The display object, or null if there is no display with the given id. 274 */ getCompatibleDisplay(int displayId, DisplayAdjustments daj)275 public Display getCompatibleDisplay(int displayId, DisplayAdjustments daj) { 276 DisplayInfo displayInfo = getDisplayInfo(displayId); 277 if (displayInfo == null) { 278 return null; 279 } 280 return new Display(this, displayId, displayInfo, daj); 281 } 282 283 /** 284 * Gets information about a logical display. 285 * 286 * The display metrics may be adjusted to provide compatibility 287 * for legacy applications or limited screen areas. 288 * 289 * @param displayId The logical display id. 290 * @param resources Resources providing compatibility info. 291 * @return The display object, or null if there is no display with the given id. 292 */ getCompatibleDisplay(int displayId, Resources resources)293 public Display getCompatibleDisplay(int displayId, Resources resources) { 294 DisplayInfo displayInfo = getDisplayInfo(displayId); 295 if (displayInfo == null) { 296 return null; 297 } 298 return new Display(this, displayId, displayInfo, resources); 299 } 300 301 /** 302 * Gets information about a logical display without applying any compatibility metrics. 303 * 304 * @param displayId The logical display id. 305 * @return The display object, or null if there is no display with the given id. 306 */ 307 @UnsupportedAppUsage getRealDisplay(int displayId)308 public Display getRealDisplay(int displayId) { 309 return getCompatibleDisplay(displayId, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS); 310 } 311 312 /** 313 * Register a listener for display-related changes. 314 * 315 * @param listener The listener that will be called when display changes occur. 316 * @param handler Handler for the thread that will be receiving the callbacks. May be null. 317 * If null, listener will use the handler for the current thread, and if still null, 318 * the handler for the main thread. 319 * If that is still null, a runtime exception will be thrown. 320 */ registerDisplayListener(@onNull DisplayListener listener, @Nullable Handler handler, @EventsMask long eventsMask)321 public void registerDisplayListener(@NonNull DisplayListener listener, 322 @Nullable Handler handler, @EventsMask long eventsMask) { 323 Looper looper = getLooperForHandler(handler); 324 Handler springBoard = new Handler(looper); 325 registerDisplayListener(listener, new HandlerExecutor(springBoard), eventsMask); 326 } 327 328 /** 329 * Register a listener for display-related changes. 330 * 331 * @param listener The listener that will be called when display changes occur. 332 * @param executor Executor for the thread that will be receiving the callbacks. Cannot be null. 333 */ registerDisplayListener(@onNull DisplayListener listener, @NonNull Executor executor, @EventsMask long eventsMask)334 public void registerDisplayListener(@NonNull DisplayListener listener, 335 @NonNull Executor executor, @EventsMask long eventsMask) { 336 if (listener == null) { 337 throw new IllegalArgumentException("listener must not be null"); 338 } 339 340 if (eventsMask == 0) { 341 throw new IllegalArgumentException("The set of events to listen to must not be empty."); 342 } 343 344 synchronized (mLock) { 345 int index = findDisplayListenerLocked(listener); 346 if (index < 0) { 347 mDisplayListeners.add(new DisplayListenerDelegate(listener, executor, eventsMask)); 348 registerCallbackIfNeededLocked(); 349 } else { 350 mDisplayListeners.get(index).setEventsMask(eventsMask); 351 } 352 updateCallbackIfNeededLocked(); 353 } 354 } 355 unregisterDisplayListener(DisplayListener listener)356 public void unregisterDisplayListener(DisplayListener listener) { 357 if (listener == null) { 358 throw new IllegalArgumentException("listener must not be null"); 359 } 360 361 synchronized (mLock) { 362 int index = findDisplayListenerLocked(listener); 363 if (index >= 0) { 364 DisplayListenerDelegate d = mDisplayListeners.get(index); 365 d.clearEvents(); 366 mDisplayListeners.remove(index); 367 updateCallbackIfNeededLocked(); 368 } 369 } 370 } 371 getLooperForHandler(@ullable Handler handler)372 private static Looper getLooperForHandler(@Nullable Handler handler) { 373 Looper looper = handler != null ? handler.getLooper() : Looper.myLooper(); 374 if (looper == null) { 375 looper = Looper.getMainLooper(); 376 } 377 if (looper == null) { 378 throw new RuntimeException("Could not get Looper for the UI thread."); 379 } 380 return looper; 381 } 382 findDisplayListenerLocked(DisplayListener listener)383 private int findDisplayListenerLocked(DisplayListener listener) { 384 final int numListeners = mDisplayListeners.size(); 385 for (int i = 0; i < numListeners; i++) { 386 if (mDisplayListeners.get(i).mListener == listener) { 387 return i; 388 } 389 } 390 return -1; 391 } 392 393 @EventsMask calculateEventsMaskLocked()394 private int calculateEventsMaskLocked() { 395 int mask = 0; 396 final int numListeners = mDisplayListeners.size(); 397 for (int i = 0; i < numListeners; i++) { 398 mask |= mDisplayListeners.get(i).mEventsMask; 399 } 400 if (mDispatchNativeCallbacks) { 401 mask |= DisplayManager.EVENT_FLAG_DISPLAY_ADDED 402 | DisplayManager.EVENT_FLAG_DISPLAY_CHANGED 403 | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED; 404 } 405 return mask; 406 } 407 registerCallbackIfNeededLocked()408 private void registerCallbackIfNeededLocked() { 409 if (mCallback == null) { 410 mCallback = new DisplayManagerCallback(); 411 updateCallbackIfNeededLocked(); 412 } 413 } 414 updateCallbackIfNeededLocked()415 private void updateCallbackIfNeededLocked() { 416 int mask = calculateEventsMaskLocked(); 417 if (mask != mRegisteredEventsMask) { 418 try { 419 mDm.registerCallbackWithEventMask(mCallback, mask); 420 mRegisteredEventsMask = mask; 421 } catch (RemoteException ex) { 422 throw ex.rethrowFromSystemServer(); 423 } 424 } 425 } 426 handleDisplayEvent(int displayId, @DisplayEvent int event)427 private void handleDisplayEvent(int displayId, @DisplayEvent int event) { 428 final DisplayInfo info; 429 synchronized (mLock) { 430 if (USE_CACHE) { 431 mDisplayInfoCache.remove(displayId); 432 433 if (event == EVENT_DISPLAY_ADDED || event == EVENT_DISPLAY_REMOVED) { 434 mDisplayIdCache = null; 435 } 436 } 437 438 info = getDisplayInfoLocked(displayId); 439 if (event == EVENT_DISPLAY_CHANGED && mDispatchNativeCallbacks) { 440 // Choreographer only supports a single display, so only dispatch refresh rate 441 // changes for the default display. 442 if (displayId == Display.DEFAULT_DISPLAY) { 443 // We can likely save a binder hop if we attach the refresh rate onto the 444 // listener. 445 DisplayInfo display = getDisplayInfoLocked(displayId); 446 if (display != null 447 && mNativeCallbackReportedRefreshRate != display.getRefreshRate()) { 448 mNativeCallbackReportedRefreshRate = display.getRefreshRate(); 449 // Signal native callbacks if we ever set a refresh rate. 450 nSignalNativeCallbacks(mNativeCallbackReportedRefreshRate); 451 } 452 } 453 } 454 } 455 // Accepting an Executor means the listener may be synchronously invoked, so we must 456 // not be holding mLock when we do so 457 for (DisplayListenerDelegate listener : mDisplayListeners) { 458 listener.sendDisplayEvent(displayId, event, info); 459 } 460 } 461 startWifiDisplayScan()462 public void startWifiDisplayScan() { 463 synchronized (mLock) { 464 if (mWifiDisplayScanNestCount++ == 0) { 465 registerCallbackIfNeededLocked(); 466 try { 467 mDm.startWifiDisplayScan(); 468 } catch (RemoteException ex) { 469 throw ex.rethrowFromSystemServer(); 470 } 471 } 472 } 473 } 474 stopWifiDisplayScan()475 public void stopWifiDisplayScan() { 476 synchronized (mLock) { 477 if (--mWifiDisplayScanNestCount == 0) { 478 try { 479 mDm.stopWifiDisplayScan(); 480 } catch (RemoteException ex) { 481 throw ex.rethrowFromSystemServer(); 482 } 483 } else if (mWifiDisplayScanNestCount < 0) { 484 Log.wtf(TAG, "Wifi display scan nest count became negative: " 485 + mWifiDisplayScanNestCount); 486 mWifiDisplayScanNestCount = 0; 487 } 488 } 489 } 490 connectWifiDisplay(String deviceAddress)491 public void connectWifiDisplay(String deviceAddress) { 492 if (deviceAddress == null) { 493 throw new IllegalArgumentException("deviceAddress must not be null"); 494 } 495 496 try { 497 mDm.connectWifiDisplay(deviceAddress); 498 } catch (RemoteException ex) { 499 throw ex.rethrowFromSystemServer(); 500 } 501 } 502 pauseWifiDisplay()503 public void pauseWifiDisplay() { 504 try { 505 mDm.pauseWifiDisplay(); 506 } catch (RemoteException ex) { 507 throw ex.rethrowFromSystemServer(); 508 } 509 } 510 resumeWifiDisplay()511 public void resumeWifiDisplay() { 512 try { 513 mDm.resumeWifiDisplay(); 514 } catch (RemoteException ex) { 515 throw ex.rethrowFromSystemServer(); 516 } 517 } 518 519 @UnsupportedAppUsage disconnectWifiDisplay()520 public void disconnectWifiDisplay() { 521 try { 522 mDm.disconnectWifiDisplay(); 523 } catch (RemoteException ex) { 524 throw ex.rethrowFromSystemServer(); 525 } 526 } 527 renameWifiDisplay(String deviceAddress, String alias)528 public void renameWifiDisplay(String deviceAddress, String alias) { 529 if (deviceAddress == null) { 530 throw new IllegalArgumentException("deviceAddress must not be null"); 531 } 532 533 try { 534 mDm.renameWifiDisplay(deviceAddress, alias); 535 } catch (RemoteException ex) { 536 throw ex.rethrowFromSystemServer(); 537 } 538 } 539 forgetWifiDisplay(String deviceAddress)540 public void forgetWifiDisplay(String deviceAddress) { 541 if (deviceAddress == null) { 542 throw new IllegalArgumentException("deviceAddress must not be null"); 543 } 544 545 try { 546 mDm.forgetWifiDisplay(deviceAddress); 547 } catch (RemoteException ex) { 548 throw ex.rethrowFromSystemServer(); 549 } 550 } 551 552 @UnsupportedAppUsage getWifiDisplayStatus()553 public WifiDisplayStatus getWifiDisplayStatus() { 554 try { 555 return mDm.getWifiDisplayStatus(); 556 } catch (RemoteException ex) { 557 throw ex.rethrowFromSystemServer(); 558 } 559 } 560 561 /** 562 * Sets the HDR types that have been disabled by user. 563 * @param userDisabledHdrTypes the HDR types to disable. The HDR types are any of 564 */ setUserDisabledHdrTypes(@drType int[] userDisabledHdrTypes)565 public void setUserDisabledHdrTypes(@HdrType int[] userDisabledHdrTypes) { 566 try { 567 mDm.setUserDisabledHdrTypes(userDisabledHdrTypes); 568 } catch (RemoteException ex) { 569 throw ex.rethrowFromSystemServer(); 570 } 571 } 572 573 /** 574 * Sets whether or not the user disabled HDR types are returned from 575 * {@link Display#getHdrCapabilities}. 576 * 577 * @param areUserDisabledHdrTypesAllowed If true, the user-disabled 578 * types are ignored and returned, if the display supports them. If 579 * false, the user-disabled types are taken into consideration and 580 * are never returned, even if the display supports them. 581 */ setAreUserDisabledHdrTypesAllowed(boolean areUserDisabledHdrTypesAllowed)582 public void setAreUserDisabledHdrTypesAllowed(boolean areUserDisabledHdrTypesAllowed) { 583 try { 584 mDm.setAreUserDisabledHdrTypesAllowed(areUserDisabledHdrTypesAllowed); 585 } catch (RemoteException ex) { 586 throw ex.rethrowFromSystemServer(); 587 } 588 } 589 590 /** 591 * Returns whether or not the user-disabled HDR types are returned from 592 * {@link Display#getHdrCapabilities}. 593 */ areUserDisabledHdrTypesAllowed()594 public boolean areUserDisabledHdrTypesAllowed() { 595 try { 596 return mDm.areUserDisabledHdrTypesAllowed(); 597 } catch (RemoteException ex) { 598 throw ex.rethrowFromSystemServer(); 599 } 600 } 601 602 /** 603 * Returns the HDR formats disabled by the user. 604 * 605 */ getUserDisabledHdrTypes()606 public int[] getUserDisabledHdrTypes() { 607 try { 608 return mDm.getUserDisabledHdrTypes(); 609 } catch (RemoteException ex) { 610 throw ex.rethrowFromSystemServer(); 611 } 612 } 613 614 /** 615 * Overrides HDR modes for a display device. 616 * 617 */ 618 @RequiresPermission(Manifest.permission.ACCESS_SURFACE_FLINGER) overrideHdrTypes(int displayId, int[] modes)619 public void overrideHdrTypes(int displayId, int[] modes) { 620 try { 621 mDm.overrideHdrTypes(displayId, modes); 622 } catch (RemoteException ex) { 623 throw ex.rethrowFromSystemServer(); 624 } 625 } 626 627 requestColorMode(int displayId, int colorMode)628 public void requestColorMode(int displayId, int colorMode) { 629 try { 630 mDm.requestColorMode(displayId, colorMode); 631 } catch (RemoteException ex) { 632 throw ex.rethrowFromSystemServer(); 633 } 634 } 635 createVirtualDisplay(@onNull Context context, MediaProjection projection, @NonNull VirtualDisplayConfig virtualDisplayConfig, VirtualDisplay.Callback callback, @Nullable Executor executor)636 public VirtualDisplay createVirtualDisplay(@NonNull Context context, MediaProjection projection, 637 @NonNull VirtualDisplayConfig virtualDisplayConfig, VirtualDisplay.Callback callback, 638 @Nullable Executor executor) { 639 VirtualDisplayCallback callbackWrapper = new VirtualDisplayCallback(callback, executor); 640 IMediaProjection projectionToken = projection != null ? projection.getProjection() : null; 641 int displayId; 642 try { 643 displayId = mDm.createVirtualDisplay(virtualDisplayConfig, callbackWrapper, 644 projectionToken, context.getPackageName()); 645 } catch (RemoteException ex) { 646 throw ex.rethrowFromSystemServer(); 647 } 648 return createVirtualDisplayWrapper(virtualDisplayConfig, callbackWrapper, 649 displayId); 650 } 651 652 /** 653 * Create a VirtualDisplay wrapper object for a newly created virtual display ; to be called 654 * once the display has been created in system_server. 655 */ 656 @Nullable createVirtualDisplayWrapper(VirtualDisplayConfig virtualDisplayConfig, IVirtualDisplayCallback callbackWrapper, int displayId)657 public VirtualDisplay createVirtualDisplayWrapper(VirtualDisplayConfig virtualDisplayConfig, 658 IVirtualDisplayCallback callbackWrapper, int displayId) { 659 if (displayId < 0) { 660 Log.e(TAG, "Could not create virtual display: " + virtualDisplayConfig.getName()); 661 return null; 662 } 663 Display display = getRealDisplay(displayId); 664 if (display == null) { 665 Log.wtf(TAG, "Could not obtain display info for newly created " 666 + "virtual display: " + virtualDisplayConfig.getName()); 667 try { 668 mDm.releaseVirtualDisplay(callbackWrapper); 669 } catch (RemoteException ex) { 670 throw ex.rethrowFromSystemServer(); 671 } 672 return null; 673 } 674 return new VirtualDisplay(this, display, callbackWrapper, 675 virtualDisplayConfig.getSurface()); 676 } 677 setVirtualDisplaySurface(IVirtualDisplayCallback token, Surface surface)678 public void setVirtualDisplaySurface(IVirtualDisplayCallback token, Surface surface) { 679 try { 680 mDm.setVirtualDisplaySurface(token, surface); 681 setVirtualDisplayState(token, surface != null); 682 } catch (RemoteException ex) { 683 throw ex.rethrowFromSystemServer(); 684 } 685 } 686 resizeVirtualDisplay(IVirtualDisplayCallback token, int width, int height, int densityDpi)687 public void resizeVirtualDisplay(IVirtualDisplayCallback token, 688 int width, int height, int densityDpi) { 689 try { 690 mDm.resizeVirtualDisplay(token, width, height, densityDpi); 691 } catch (RemoteException ex) { 692 throw ex.rethrowFromSystemServer(); 693 } 694 } 695 releaseVirtualDisplay(IVirtualDisplayCallback token)696 public void releaseVirtualDisplay(IVirtualDisplayCallback token) { 697 try { 698 mDm.releaseVirtualDisplay(token); 699 } catch (RemoteException ex) { 700 throw ex.rethrowFromSystemServer(); 701 } 702 } 703 setVirtualDisplayState(IVirtualDisplayCallback token, boolean isOn)704 void setVirtualDisplayState(IVirtualDisplayCallback token, boolean isOn) { 705 try { 706 mDm.setVirtualDisplayState(token, isOn); 707 } catch (RemoteException ex) { 708 throw ex.rethrowFromSystemServer(); 709 } 710 } 711 712 /** 713 * Gets the stable device display size, in pixels. 714 */ getStableDisplaySize()715 public Point getStableDisplaySize() { 716 try { 717 return mDm.getStableDisplaySize(); 718 } catch (RemoteException ex) { 719 throw ex.rethrowFromSystemServer(); 720 } 721 } 722 723 /** 724 * Retrieves brightness change events. 725 */ getBrightnessEvents(String callingPackage)726 public List<BrightnessChangeEvent> getBrightnessEvents(String callingPackage) { 727 try { 728 ParceledListSlice<BrightnessChangeEvent> events = 729 mDm.getBrightnessEvents(callingPackage); 730 if (events == null) { 731 return Collections.emptyList(); 732 } 733 return events.getList(); 734 } catch (RemoteException ex) { 735 throw ex.rethrowFromSystemServer(); 736 } 737 } 738 739 /** 740 * Retrieves Brightness Info for the specified display. 741 */ getBrightnessInfo(int displayId)742 public BrightnessInfo getBrightnessInfo(int displayId) { 743 try { 744 return mDm.getBrightnessInfo(displayId); 745 } catch (RemoteException ex) { 746 throw ex.rethrowFromSystemServer(); 747 } 748 } 749 750 /** 751 * Gets the preferred wide gamut color space for all displays. 752 * The wide gamut color space is returned from composition pipeline 753 * based on hardware capability. 754 * 755 * @hide 756 */ getPreferredWideGamutColorSpace()757 public ColorSpace getPreferredWideGamutColorSpace() { 758 return mWideColorSpace; 759 } 760 761 /** 762 * Gets the overlay properties for all displays. 763 * 764 * @hide 765 */ getOverlaySupport()766 public OverlayProperties getOverlaySupport() { 767 return mOverlayProperties; 768 } 769 770 /** 771 * Sets the global brightness configuration for a given user. 772 * 773 * @hide 774 */ setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId, String packageName)775 public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId, 776 String packageName) { 777 try { 778 mDm.setBrightnessConfigurationForUser(c, userId, packageName); 779 } catch (RemoteException ex) { 780 throw ex.rethrowFromSystemServer(); 781 } 782 } 783 784 /** 785 * Sets the brightness configuration for a given display. 786 * 787 * @hide 788 */ setBrightnessConfigurationForDisplay(BrightnessConfiguration c, String uniqueDisplayId, int userId, String packageName)789 public void setBrightnessConfigurationForDisplay(BrightnessConfiguration c, 790 String uniqueDisplayId, int userId, String packageName) { 791 try { 792 mDm.setBrightnessConfigurationForDisplay(c, uniqueDisplayId, userId, packageName); 793 } catch (RemoteException ex) { 794 throw ex.rethrowFromSystemServer(); 795 } 796 } 797 798 /** 799 * Gets the brightness configuration for a given display or null if one hasn't been set. 800 * 801 * @hide 802 */ getBrightnessConfigurationForDisplay(String uniqueDisplayId, int userId)803 public BrightnessConfiguration getBrightnessConfigurationForDisplay(String uniqueDisplayId, 804 int userId) { 805 try { 806 return mDm.getBrightnessConfigurationForDisplay(uniqueDisplayId, userId); 807 } catch (RemoteException ex) { 808 throw ex.rethrowFromSystemServer(); 809 } 810 } 811 812 /** 813 * Gets the global brightness configuration for a given user or null if one hasn't been set. 814 * 815 * @hide 816 */ getBrightnessConfigurationForUser(int userId)817 public BrightnessConfiguration getBrightnessConfigurationForUser(int userId) { 818 try { 819 return mDm.getBrightnessConfigurationForUser(userId); 820 } catch (RemoteException ex) { 821 throw ex.rethrowFromSystemServer(); 822 } 823 } 824 825 /** 826 * Gets the default brightness configuration or null if one hasn't been configured. 827 * 828 * @hide 829 */ getDefaultBrightnessConfiguration()830 public BrightnessConfiguration getDefaultBrightnessConfiguration() { 831 try { 832 return mDm.getDefaultBrightnessConfiguration(); 833 } catch (RemoteException ex) { 834 throw ex.rethrowFromSystemServer(); 835 } 836 } 837 838 /** 839 * Gets the last requested minimal post processing setting for the display with displayId. 840 * 841 * @hide 842 */ isMinimalPostProcessingRequested(int displayId)843 public boolean isMinimalPostProcessingRequested(int displayId) { 844 try { 845 return mDm.isMinimalPostProcessingRequested(displayId); 846 } catch (RemoteException ex) { 847 throw ex.rethrowFromSystemServer(); 848 } 849 } 850 851 /** 852 * Temporarily sets the brightness of the display. 853 * <p> 854 * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission. 855 * </p> 856 * 857 * @param brightness The brightness value from 0.0f to 1.0f. 858 * 859 * @hide Requires signature permission. 860 */ setTemporaryBrightness(int displayId, float brightness)861 public void setTemporaryBrightness(int displayId, float brightness) { 862 try { 863 mDm.setTemporaryBrightness(displayId, brightness); 864 } catch (RemoteException ex) { 865 throw ex.rethrowFromSystemServer(); 866 } 867 } 868 869 870 /** 871 * Sets the brightness of the display. 872 * 873 * @param brightness The brightness value from 0.0f to 1.0f. 874 * 875 * @hide 876 */ setBrightness(int displayId, float brightness)877 public void setBrightness(int displayId, float brightness) { 878 try { 879 mDm.setBrightness(displayId, brightness); 880 } catch (RemoteException ex) { 881 throw ex.rethrowFromSystemServer(); 882 } 883 } 884 885 /** 886 * Report whether/how the display supports DISPLAY_DECORATION. 887 * 888 * @param displayId The display whose support is being queried. 889 * 890 * @hide 891 */ getDisplayDecorationSupport(int displayId)892 public DisplayDecorationSupport getDisplayDecorationSupport(int displayId) { 893 try { 894 return mDm.getDisplayDecorationSupport(displayId); 895 } catch (RemoteException ex) { 896 throw ex.rethrowFromSystemServer(); 897 } 898 } 899 900 /** 901 * Gets the brightness of the display. 902 * 903 * @param displayId The display from which to get the brightness 904 * 905 * @hide 906 */ getBrightness(int displayId)907 public float getBrightness(int displayId) { 908 try { 909 return mDm.getBrightness(displayId); 910 } catch (RemoteException ex) { 911 throw ex.rethrowFromSystemServer(); 912 } 913 } 914 915 /** 916 * Temporarily sets the auto brightness adjustment factor. 917 * <p> 918 * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission. 919 * </p> 920 * 921 * @param adjustment The adjustment factor from -1.0 to 1.0. 922 * 923 * @hide Requires signature permission. 924 */ setTemporaryAutoBrightnessAdjustment(float adjustment)925 public void setTemporaryAutoBrightnessAdjustment(float adjustment) { 926 try { 927 mDm.setTemporaryAutoBrightnessAdjustment(adjustment); 928 } catch (RemoteException ex) { 929 throw ex.rethrowFromSystemServer(); 930 } 931 } 932 933 /** 934 * Returns the minimum brightness curve, which guarantess that any brightness curve that dips 935 * below it is rejected by the system. 936 * This prevent auto-brightness from setting the screen so dark as to prevent the user from 937 * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable 938 * in that ambient brightness. 939 * 940 * @return The minimum brightness curve (as lux values and their corresponding nits values). 941 */ getMinimumBrightnessCurve()942 public Pair<float[], float[]> getMinimumBrightnessCurve() { 943 try { 944 Curve curve = mDm.getMinimumBrightnessCurve(); 945 return Pair.create(curve.getX(), curve.getY()); 946 } catch (RemoteException ex) { 947 throw ex.rethrowFromSystemServer(); 948 } 949 } 950 951 /** 952 * Retrieves ambient brightness stats. 953 */ getAmbientBrightnessStats()954 public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() { 955 try { 956 ParceledListSlice<AmbientBrightnessDayStats> stats = mDm.getAmbientBrightnessStats(); 957 if (stats == null) { 958 return Collections.emptyList(); 959 } 960 return stats.getList(); 961 } catch (RemoteException ex) { 962 throw ex.rethrowFromSystemServer(); 963 } 964 } 965 966 /** 967 * Sets the default display mode, according to the refresh rate and the resolution chosen by the 968 * user. 969 */ setUserPreferredDisplayMode(int displayId, Display.Mode mode)970 public void setUserPreferredDisplayMode(int displayId, Display.Mode mode) { 971 try { 972 mDm.setUserPreferredDisplayMode(displayId, mode); 973 } catch (RemoteException ex) { 974 throw ex.rethrowFromSystemServer(); 975 } 976 } 977 978 /** 979 * Returns the user preferred display mode. 980 */ getUserPreferredDisplayMode(int displayId)981 public Display.Mode getUserPreferredDisplayMode(int displayId) { 982 try { 983 return mDm.getUserPreferredDisplayMode(displayId); 984 } catch (RemoteException ex) { 985 throw ex.rethrowFromSystemServer(); 986 } 987 } 988 989 /** 990 * Returns the system preferred display mode. 991 */ getSystemPreferredDisplayMode(int displayId)992 public Display.Mode getSystemPreferredDisplayMode(int displayId) { 993 try { 994 return mDm.getSystemPreferredDisplayMode(displayId); 995 } catch (RemoteException ex) { 996 throw ex.rethrowFromSystemServer(); 997 } 998 } 999 1000 /** 1001 * Sets the {@link HdrConversionMode} for the device. 1002 */ setHdrConversionMode(@onNull HdrConversionMode hdrConversionMode)1003 public void setHdrConversionMode(@NonNull HdrConversionMode hdrConversionMode) { 1004 try { 1005 mDm.setHdrConversionMode(hdrConversionMode); 1006 } catch (RemoteException ex) { 1007 throw ex.rethrowFromSystemServer(); 1008 } 1009 } 1010 1011 /** 1012 * Returns the {@link HdrConversionMode} of the device, which is set by the user. 1013 * The HDR conversion mode chosen by user is returned irrespective of whether HDR conversion 1014 * is disabled by an app. 1015 */ getHdrConversionModeSetting()1016 public HdrConversionMode getHdrConversionModeSetting() { 1017 try { 1018 return mDm.getHdrConversionModeSetting(); 1019 } catch (RemoteException ex) { 1020 throw ex.rethrowFromSystemServer(); 1021 } 1022 } 1023 1024 /** 1025 * Returns the {@link HdrConversionMode} of the device. 1026 */ getHdrConversionMode()1027 public HdrConversionMode getHdrConversionMode() { 1028 try { 1029 return mDm.getHdrConversionMode(); 1030 } catch (RemoteException ex) { 1031 throw ex.rethrowFromSystemServer(); 1032 } 1033 } 1034 1035 /** 1036 * Returns the HDR output types supported by the device. 1037 */ getSupportedHdrOutputTypes()1038 public @HdrType int[] getSupportedHdrOutputTypes() { 1039 try { 1040 return mDm.getSupportedHdrOutputTypes(); 1041 } catch (RemoteException ex) { 1042 throw ex.rethrowFromSystemServer(); 1043 } 1044 } 1045 1046 /** 1047 * When enabled the app requested display resolution and refresh rate is always selected 1048 * in DisplayModeDirector regardless of user settings and policies for low brightness, low 1049 * battery etc. 1050 */ setShouldAlwaysRespectAppRequestedMode(boolean enabled)1051 public void setShouldAlwaysRespectAppRequestedMode(boolean enabled) { 1052 try { 1053 mDm.setShouldAlwaysRespectAppRequestedMode(enabled); 1054 } catch (RemoteException ex) { 1055 throw ex.rethrowFromSystemServer(); 1056 } 1057 } 1058 1059 /** 1060 * Returns whether DisplayModeDirector is running in a mode which always selects the app 1061 * requested display mode and ignores user settings and policies for low brightness, low 1062 * battery etc. 1063 */ shouldAlwaysRespectAppRequestedMode()1064 public boolean shouldAlwaysRespectAppRequestedMode() { 1065 try { 1066 return mDm.shouldAlwaysRespectAppRequestedMode(); 1067 } catch (RemoteException ex) { 1068 throw ex.rethrowFromSystemServer(); 1069 } 1070 } 1071 1072 /** 1073 * Sets the refresh rate switching type. 1074 * 1075 * @hide 1076 */ setRefreshRateSwitchingType(@isplayManager.SwitchingType int newValue)1077 public void setRefreshRateSwitchingType(@DisplayManager.SwitchingType int newValue) { 1078 try { 1079 mDm.setRefreshRateSwitchingType(newValue); 1080 } catch (RemoteException ex) { 1081 throw ex.rethrowFromSystemServer(); 1082 } 1083 } 1084 1085 /** 1086 * Returns the refresh rate switching type. 1087 * 1088 * @hide 1089 */ 1090 @DisplayManager.SwitchingType getRefreshRateSwitchingType()1091 public int getRefreshRateSwitchingType() { 1092 try { 1093 return mDm.getRefreshRateSwitchingType(); 1094 } catch (RemoteException ex) { 1095 throw ex.rethrowFromSystemServer(); 1096 } 1097 } 1098 1099 private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { 1100 @Override onDisplayEvent(int displayId, @DisplayEvent int event)1101 public void onDisplayEvent(int displayId, @DisplayEvent int event) { 1102 if (DEBUG) { 1103 Log.d(TAG, "onDisplayEvent: displayId=" + displayId + ", event=" + eventToString( 1104 event)); 1105 } 1106 handleDisplayEvent(displayId, event); 1107 } 1108 } 1109 1110 private static final class DisplayListenerDelegate { 1111 public final DisplayListener mListener; 1112 public volatile long mEventsMask; 1113 1114 private final DisplayInfo mDisplayInfo = new DisplayInfo(); 1115 private final Executor mExecutor; 1116 private AtomicLong mGenerationId = new AtomicLong(1); 1117 DisplayListenerDelegate(DisplayListener listener, @NonNull Executor executor, @EventsMask long eventsMask)1118 DisplayListenerDelegate(DisplayListener listener, @NonNull Executor executor, 1119 @EventsMask long eventsMask) { 1120 mExecutor = executor; 1121 mListener = listener; 1122 mEventsMask = eventsMask; 1123 } 1124 sendDisplayEvent(int displayId, @DisplayEvent int event, DisplayInfo info)1125 public void sendDisplayEvent(int displayId, @DisplayEvent int event, DisplayInfo info) { 1126 long generationId = mGenerationId.get(); 1127 Message msg = Message.obtain(null, event, displayId, 0, info); 1128 mExecutor.execute(() -> { 1129 // If the generation id's don't match we were canceled but still need to recycle() 1130 if (generationId == mGenerationId.get()) { 1131 handleMessage(msg); 1132 } 1133 msg.recycle(); 1134 }); 1135 } 1136 clearEvents()1137 public void clearEvents() { 1138 mGenerationId.incrementAndGet(); 1139 } 1140 setEventsMask(@ventsMask long newEventsMask)1141 public void setEventsMask(@EventsMask long newEventsMask) { 1142 mEventsMask = newEventsMask; 1143 } 1144 handleMessage(Message msg)1145 private void handleMessage(Message msg) { 1146 if (DEBUG) { 1147 Trace.beginSection( 1148 "DisplayListenerDelegate(" + eventToString(msg.what) 1149 + ", display=" + msg.arg1 1150 + ", listener=" + mListener.getClass() + ")"); 1151 } 1152 switch (msg.what) { 1153 case EVENT_DISPLAY_ADDED: 1154 if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) { 1155 mListener.onDisplayAdded(msg.arg1); 1156 } 1157 break; 1158 case EVENT_DISPLAY_CHANGED: 1159 if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) { 1160 DisplayInfo newInfo = (DisplayInfo) msg.obj; 1161 if (newInfo != null && !newInfo.equals(mDisplayInfo)) { 1162 mDisplayInfo.copyFrom(newInfo); 1163 mListener.onDisplayChanged(msg.arg1); 1164 } 1165 } 1166 break; 1167 case EVENT_DISPLAY_BRIGHTNESS_CHANGED: 1168 if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) { 1169 mListener.onDisplayChanged(msg.arg1); 1170 } 1171 break; 1172 case EVENT_DISPLAY_REMOVED: 1173 if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) { 1174 mListener.onDisplayRemoved(msg.arg1); 1175 } 1176 break; 1177 case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED: 1178 if ((mEventsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) { 1179 mListener.onDisplayChanged(msg.arg1); 1180 } 1181 break; 1182 } 1183 if (DEBUG) { 1184 Trace.endSection(); 1185 } 1186 } 1187 } 1188 1189 /** 1190 * Assists in dispatching VirtualDisplay lifecycle event callbacks on a given Executor. 1191 */ 1192 public static final class VirtualDisplayCallback extends IVirtualDisplayCallback.Stub { 1193 @Nullable private final VirtualDisplay.Callback mCallback; 1194 @Nullable private final Executor mExecutor; 1195 1196 /** 1197 * Creates a virtual display callback. 1198 * 1199 * @param callback The callback to call for virtual display events, or {@code null} if the 1200 * caller does not wish to receive callback events. 1201 * @param executor The executor to call the {@code callback} on. Must not be {@code null} if 1202 * the callback is not {@code null}. 1203 */ VirtualDisplayCallback(VirtualDisplay.Callback callback, Executor executor)1204 public VirtualDisplayCallback(VirtualDisplay.Callback callback, Executor executor) { 1205 mCallback = callback; 1206 mExecutor = mCallback != null ? Objects.requireNonNull(executor) : null; 1207 } 1208 1209 // These methods are called from the binder thread, but the AIDL is oneway, so it should be 1210 // safe to call the callback on arbitrary executors directly without risking blocking 1211 // the system. 1212 1213 @Override // Binder call onPaused()1214 public void onPaused() { 1215 if (mCallback != null) { 1216 mExecutor.execute(mCallback::onPaused); 1217 } 1218 } 1219 1220 @Override // Binder call onResumed()1221 public void onResumed() { 1222 if (mCallback != null) { 1223 mExecutor.execute(mCallback::onResumed); 1224 } 1225 } 1226 1227 @Override // Binder call onStopped()1228 public void onStopped() { 1229 if (mCallback != null) { 1230 mExecutor.execute(mCallback::onStopped); 1231 } 1232 } 1233 } 1234 1235 /** 1236 * Name of the property containing a unique token which changes every time we update the 1237 * system's display configuration. 1238 */ 1239 public static final String CACHE_KEY_DISPLAY_INFO_PROPERTY = 1240 "cache_key.display_info"; 1241 1242 /** 1243 * Invalidates the contents of the display info cache for all applications. Can only 1244 * be called by system_server. 1245 */ invalidateLocalDisplayInfoCaches()1246 public static void invalidateLocalDisplayInfoCaches() { 1247 PropertyInvalidatedCache.invalidateCache(CACHE_KEY_DISPLAY_INFO_PROPERTY); 1248 } 1249 1250 /** 1251 * Disables the binder call cache. 1252 */ disableLocalDisplayInfoCaches()1253 public void disableLocalDisplayInfoCaches() { 1254 mDisplayCache = null; 1255 } 1256 nSignalNativeCallbacks(float refreshRate)1257 private static native void nSignalNativeCallbacks(float refreshRate); 1258 1259 /** 1260 * Called from AChoreographer via JNI. 1261 * Registers AChoreographer so that refresh rate callbacks can be dispatched from DMS. 1262 * Public for unit testing to be able to call this method. 1263 */ 1264 @VisibleForTesting registerNativeChoreographerForRefreshRateCallbacks()1265 public void registerNativeChoreographerForRefreshRateCallbacks() { 1266 synchronized (mLock) { 1267 mDispatchNativeCallbacks = true; 1268 registerCallbackIfNeededLocked(); 1269 updateCallbackIfNeededLocked(); 1270 DisplayInfo display = getDisplayInfoLocked(Display.DEFAULT_DISPLAY); 1271 if (display != null) { 1272 // We need to tell AChoreographer instances the current refresh rate so that apps 1273 // can get it for free once a callback first registers. 1274 mNativeCallbackReportedRefreshRate = display.getRefreshRate(); 1275 nSignalNativeCallbacks(mNativeCallbackReportedRefreshRate); 1276 } 1277 } 1278 } 1279 1280 /** 1281 * Called from AChoreographer via JNI. 1282 * Unregisters AChoreographer from receiving refresh rate callbacks. 1283 * Public for unit testing to be able to call this method. 1284 */ 1285 @VisibleForTesting unregisterNativeChoreographerForRefreshRateCallbacks()1286 public void unregisterNativeChoreographerForRefreshRateCallbacks() { 1287 synchronized (mLock) { 1288 mDispatchNativeCallbacks = false; 1289 updateCallbackIfNeededLocked(); 1290 } 1291 } 1292 eventToString(@isplayEvent int event)1293 private static String eventToString(@DisplayEvent int event) { 1294 switch (event) { 1295 case EVENT_DISPLAY_ADDED: 1296 return "ADDED"; 1297 case EVENT_DISPLAY_CHANGED: 1298 return "CHANGED"; 1299 case EVENT_DISPLAY_REMOVED: 1300 return "REMOVED"; 1301 case EVENT_DISPLAY_BRIGHTNESS_CHANGED: 1302 return "BRIGHTNESS_CHANGED"; 1303 case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED: 1304 return "HDR_SDR_RATIO_CHANGED"; 1305 } 1306 return "UNKNOWN"; 1307 } 1308 } 1309