164a55af0ac700baecb0877235eb42caac59a3560Jeff Brown/* 264a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * Copyright (C) 2012 The Android Open Source Project 364a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * 464a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * Licensed under the Apache License, Version 2.0 (the "License"); 564a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * you may not use this file except in compliance with the License. 664a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * You may obtain a copy of the License at 764a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * 864a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * http://www.apache.org/licenses/LICENSE-2.0 964a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * 1064a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * Unless required by applicable law or agreed to in writing, software 1164a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * distributed under the License is distributed on an "AS IS" BASIS, 1264a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1364a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * See the License for the specific language governing permissions and 1464a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * limitations under the License. 1564a55af0ac700baecb0877235eb42caac59a3560Jeff Brown */ 1664a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 1764a55af0ac700baecb0877235eb42caac59a3560Jeff Brownpackage com.android.server.display; 1864a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 1964a55af0ac700baecb0877235eb42caac59a3560Jeff Brownimport android.content.Context; 204ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brownimport android.os.Handler; 2164a55af0ac700baecb0877235eb42caac59a3560Jeff Brownimport android.os.IBinder; 22e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brownimport android.os.Looper; 2327f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brownimport android.os.SystemProperties; 244ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brownimport android.util.SparseArray; 2592130f6407dc51c58b3b941d28a6daf4e04b8d62Jeff Brownimport android.view.Display; 26e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brownimport android.view.DisplayEventReceiver; 2764a55af0ac700baecb0877235eb42caac59a3560Jeff Brownimport android.view.Surface; 283866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopianimport android.view.SurfaceControl; 293866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopianimport android.view.SurfaceControl.PhysicalDisplayInfo; 3064a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 314ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brownimport java.io.PrintWriter; 324ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 3364a55af0ac700baecb0877235eb42caac59a3560Jeff Brown/** 3464a55af0ac700baecb0877235eb42caac59a3560Jeff Brown * A display adapter for the local displays managed by Surface Flinger. 35bd6e1500aedc5461e832f69e76341bff0e55fa2bJeff Brown * <p> 364ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown * Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock. 37bd6e1500aedc5461e832f69e76341bff0e55fa2bJeff Brown * </p> 3864a55af0ac700baecb0877235eb42caac59a3560Jeff Brown */ 394ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brownfinal class LocalDisplayAdapter extends DisplayAdapter { 40bd6e1500aedc5461e832f69e76341bff0e55fa2bJeff Brown private static final String TAG = "LocalDisplayAdapter"; 4164a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 424ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown private static final int[] BUILT_IN_DISPLAY_IDS_TO_SCAN = new int[] { 433866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN, 443866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian SurfaceControl.BUILT_IN_DISPLAY_ID_HDMI, 454ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown }; 464ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 474ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown private final SparseArray<LocalDisplayDevice> mDevices = 484ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown new SparseArray<LocalDisplayDevice>(); 4966692500344cab2f53cdb6ee1545c567fff7cb16Jeff Brown private HotplugDisplayEventReceiver mHotplugReceiver; 504ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 513866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian private final SurfaceControl.PhysicalDisplayInfo mTempPhys = new SurfaceControl.PhysicalDisplayInfo(); 524ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 5366692500344cab2f53cdb6ee1545c567fff7cb16Jeff Brown // Called with SyncRoot lock held. 544ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown public LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, 554ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown Context context, Handler handler, Listener listener) { 564ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown super(syncRoot, context, handler, listener, TAG); 5764a55af0ac700baecb0877235eb42caac59a3560Jeff Brown } 5864a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 5964a55af0ac700baecb0877235eb42caac59a3560Jeff Brown @Override 604ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown public void registerLocked() { 614ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown super.registerLocked(); 6266692500344cab2f53cdb6ee1545c567fff7cb16Jeff Brown 6366692500344cab2f53cdb6ee1545c567fff7cb16Jeff Brown mHotplugReceiver = new HotplugDisplayEventReceiver(getHandler().getLooper()); 644ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 654ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown for (int builtInDisplayId : BUILT_IN_DISPLAY_IDS_TO_SCAN) { 66e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall tryConnectDisplayLocked(builtInDisplayId); 67e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } 68e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } 69e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall 70e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall private void tryConnectDisplayLocked(int builtInDisplayId) { 713866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian IBinder displayToken = SurfaceControl.getBuiltInDisplay(builtInDisplayId); 723866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian if (displayToken != null && SurfaceControl.getDisplayInfo(displayToken, mTempPhys)) { 73e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall LocalDisplayDevice device = mDevices.get(builtInDisplayId); 74e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall if (device == null) { 75e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall // Display was added. 76e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall device = new LocalDisplayDevice(displayToken, builtInDisplayId, mTempPhys); 77e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall mDevices.put(builtInDisplayId, device); 78e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall sendDisplayDeviceEventLocked(device, DISPLAY_DEVICE_EVENT_ADDED); 79e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } else if (device.updatePhysicalDisplayInfoLocked(mTempPhys)) { 80e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall // Display properties changed. 81e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall sendDisplayDeviceEventLocked(device, DISPLAY_DEVICE_EVENT_CHANGED); 824ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 83e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } else { 84e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall // The display is no longer available. Ignore the attempt to add it. 85e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall // If it was connected but has already been disconnected, we'll get a 86e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall // disconnect event that will remove it from mDevices. 87e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } 88e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } 89e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall 90e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall private void tryDisconnectDisplayLocked(int builtInDisplayId) { 91e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall LocalDisplayDevice device = mDevices.get(builtInDisplayId); 92e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall if (device != null) { 93e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall // Display was removed. 94e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall mDevices.remove(builtInDisplayId); 95e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall sendDisplayDeviceEventLocked(device, DISPLAY_DEVICE_EVENT_REMOVED); 964ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 9764a55af0ac700baecb0877235eb42caac59a3560Jeff Brown } 9864a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 9964a55af0ac700baecb0877235eb42caac59a3560Jeff Brown private final class LocalDisplayDevice extends DisplayDevice { 1004ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown private final int mBuiltInDisplayId; 1013866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian private final SurfaceControl.PhysicalDisplayInfo mPhys; 10264a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 1034ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown private DisplayDeviceInfo mInfo; 1044ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown private boolean mHavePendingChanges; 1059e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown private boolean mBlanked; 1064ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 1074ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown public LocalDisplayDevice(IBinder displayToken, int builtInDisplayId, 1083866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian SurfaceControl.PhysicalDisplayInfo phys) { 109bd6e1500aedc5461e832f69e76341bff0e55fa2bJeff Brown super(LocalDisplayAdapter.this, displayToken); 1104ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mBuiltInDisplayId = builtInDisplayId; 1113866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian mPhys = new SurfaceControl.PhysicalDisplayInfo(phys); 1124ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 1134ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 1143866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian public boolean updatePhysicalDisplayInfoLocked(SurfaceControl.PhysicalDisplayInfo phys) { 1154ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown if (!mPhys.equals(phys)) { 1164ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mPhys.copyFrom(phys); 1174ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mHavePendingChanges = true; 1184ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown return true; 1194ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 1204ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown return false; 12164a55af0ac700baecb0877235eb42caac59a3560Jeff Brown } 12264a55af0ac700baecb0877235eb42caac59a3560Jeff Brown 12364a55af0ac700baecb0877235eb42caac59a3560Jeff Brown @Override 1244ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown public void applyPendingDisplayDeviceInfoChangesLocked() { 1254ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown if (mHavePendingChanges) { 1264ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo = null; 1274ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mHavePendingChanges = false; 128bd6e1500aedc5461e832f69e76341bff0e55fa2bJeff Brown } 12964a55af0ac700baecb0877235eb42caac59a3560Jeff Brown } 1304ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 1314ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown @Override 1324ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown public DisplayDeviceInfo getDisplayDeviceInfoLocked() { 1334ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown if (mInfo == null) { 1344ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo = new DisplayDeviceInfo(); 1354ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo.width = mPhys.width; 1364ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo.height = mPhys.height; 1374ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo.refreshRate = mPhys.refreshRate; 13877aebfdbae489c3712ae3f9bca29d01fb1f09dc2Jeff Brown 139f0681b34dffc1510cbd9c3da5c3a7e695553fa8dJeff Brown // Assume that all built-in displays that have secure output (eg. HDCP) also 14077aebfdbae489c3712ae3f9bca29d01fb1f09dc2Jeff Brown // support compositing from gralloc protected buffers. 141f0681b34dffc1510cbd9c3da5c3a7e695553fa8dJeff Brown if (mPhys.secure) { 142f0681b34dffc1510cbd9c3da5c3a7e695553fa8dJeff Brown mInfo.flags = DisplayDeviceInfo.FLAG_SECURE 143f0681b34dffc1510cbd9c3da5c3a7e695553fa8dJeff Brown | DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS; 144f0681b34dffc1510cbd9c3da5c3a7e695553fa8dJeff Brown } 14577aebfdbae489c3712ae3f9bca29d01fb1f09dc2Jeff Brown 1463866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian if (mBuiltInDisplayId == SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) { 1474ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo.name = getContext().getResources().getString( 1484ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown com.android.internal.R.string.display_manager_built_in_display_name); 14977aebfdbae489c3712ae3f9bca29d01fb1f09dc2Jeff Brown mInfo.flags |= DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY 15027f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown | DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT; 15192130f6407dc51c58b3b941d28a6daf4e04b8d62Jeff Brown mInfo.type = Display.TYPE_BUILT_IN; 152cbad976b2a36a0895ca94510d5208a86f66cf596Jeff Brown mInfo.densityDpi = (int)(mPhys.density * 160 + 0.5f); 153cbad976b2a36a0895ca94510d5208a86f66cf596Jeff Brown mInfo.xDpi = mPhys.xDpi; 154cbad976b2a36a0895ca94510d5208a86f66cf596Jeff Brown mInfo.yDpi = mPhys.yDpi; 155d728bf514f257670fcb9aa22c6eaf97626072c93Jeff Brown mInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL; 1564ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } else { 15792130f6407dc51c58b3b941d28a6daf4e04b8d62Jeff Brown mInfo.type = Display.TYPE_HDMI; 1584ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown mInfo.name = getContext().getResources().getString( 1594ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown com.android.internal.R.string.display_manager_hdmi_display_name); 160d728bf514f257670fcb9aa22c6eaf97626072c93Jeff Brown mInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL; 161cbad976b2a36a0895ca94510d5208a86f66cf596Jeff Brown mInfo.setAssumedDensityForExternalDisplay(mPhys.width, mPhys.height); 16227f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown 16327f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown // For demonstration purposes, allow rotation of the external display. 16427f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown // In the future we might allow the user to configure this directly. 16527f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown if ("portrait".equals(SystemProperties.get("persist.demo.hdmirotation"))) { 16627f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown mInfo.rotation = Surface.ROTATION_270; 16727f1d674bf9fb53af7facdcb746912e036d5bf75Jeff Brown } 1684ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 1694ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 1704ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown return mInfo; 1714ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 1724ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown 1734ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown @Override 1749e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown public void blankLocked() { 1759e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown mBlanked = true; 1763866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian SurfaceControl.blankDisplay(getDisplayTokenLocked()); 1779e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown } 1789e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown 1799e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown @Override 1809e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown public void unblankLocked() { 1819e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown mBlanked = false; 1823866f0d581ceaa165710feeee9f37fe1b0d7067dMathias Agopian SurfaceControl.unblankDisplay(getDisplayTokenLocked()); 1839e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown } 1849e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown 1859e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown @Override 1864ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown public void dumpLocked(PrintWriter pw) { 1874ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown super.dumpLocked(pw); 1884ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown pw.println("mBuiltInDisplayId=" + mBuiltInDisplayId); 1894ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown pw.println("mPhys=" + mPhys); 1909e316a1a2a8d734315bbd56a85308f9657a92913Jeff Brown pw.println("mBlanked=" + mBlanked); 1914ed8fe75e1dde1a2b9576f3862aecc5a572c56b5Jeff Brown } 19264a55af0ac700baecb0877235eb42caac59a3560Jeff Brown } 193e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown 194e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown private final class HotplugDisplayEventReceiver extends DisplayEventReceiver { 195e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown public HotplugDisplayEventReceiver(Looper looper) { 196e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown super(looper); 197e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown } 198e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown 199e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown @Override 200e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown public void onHotplug(long timestampNanos, int builtInDisplayId, boolean connected) { 201e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown synchronized (getSyncRoot()) { 202e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall if (connected) { 203e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall tryConnectDisplayLocked(builtInDisplayId); 204e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } else { 205e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall tryDisconnectDisplayLocked(builtInDisplayId); 206e244db42da1cb07f53a1fc921b45d498000b56e8Jesse Hall } 207e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown } 208e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown } 209e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown } 210e87bf030766198bf5e1fe846167dba766e27fb3fJeff Brown}