1/* 2 * Copyright (C) 2010 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 17package com.android.layoutlib.bridge.impl; 18 19import com.android.ide.common.rendering.api.HardwareConfig; 20import com.android.ide.common.rendering.api.LayoutLog; 21import com.android.ide.common.rendering.api.RenderParams; 22import com.android.ide.common.rendering.api.RenderResources; 23import com.android.ide.common.rendering.api.RenderResources.FrameworkResourceIdProvider; 24import com.android.ide.common.rendering.api.Result; 25import com.android.layoutlib.bridge.Bridge; 26import com.android.layoutlib.bridge.android.BridgeContext; 27import com.android.resources.Density; 28import com.android.resources.ResourceType; 29import com.android.resources.ScreenOrientation; 30import com.android.resources.ScreenRound; 31import com.android.resources.ScreenSize; 32 33import android.content.res.Configuration; 34import android.os.HandlerThread_Delegate; 35import android.util.DisplayMetrics; 36import android.view.ViewConfiguration_Accessor; 37import android.view.inputmethod.InputMethodManager; 38import android.view.inputmethod.InputMethodManager_Accessor; 39import android.widget.SimpleMonthView_Delegate; 40 41import java.util.Locale; 42import java.util.concurrent.TimeUnit; 43import java.util.concurrent.locks.ReentrantLock; 44 45import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED; 46import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT; 47import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; 48 49/** 50 * Base class for rendering action. 51 * 52 * It provides life-cycle methods to init and stop the rendering. 53 * The most important methods are: 54 * {@link #init(long)} and {@link #acquire(long)} to start a rendering and {@link #release()} 55 * after the rendering. 56 * 57 * 58 * @param <T> the {@link RenderParams} implementation 59 * 60 */ 61public abstract class RenderAction<T extends RenderParams> extends FrameworkResourceIdProvider { 62 63 /** 64 * The current context being rendered. This is set through {@link #acquire(long)} and 65 * {@link #init(long)}, and unset in {@link #release()}. 66 */ 67 private static BridgeContext sCurrentContext = null; 68 69 private final T mParams; 70 71 private BridgeContext mContext; 72 73 /** 74 * Creates a renderAction. 75 * <p> 76 * This <b>must</b> be followed by a call to {@link RenderAction#init(long)}, which act as a 77 * call to {@link RenderAction#acquire(long)} 78 * 79 * @param params the RenderParams. This must be a copy that the action can keep 80 * 81 */ 82 protected RenderAction(T params) { 83 mParams = params; 84 } 85 86 /** 87 * Initializes and acquires the scene, creating various Android objects such as context, 88 * inflater, and parser. 89 * 90 * @param timeout the time to wait if another rendering is happening. 91 * 92 * @return whether the scene was prepared 93 * 94 * @see #acquire(long) 95 * @see #release() 96 */ 97 public Result init(long timeout) { 98 // acquire the lock. if the result is null, lock was just acquired, otherwise, return 99 // the result. 100 Result result = acquireLock(timeout); 101 if (result != null) { 102 return result; 103 } 104 105 HardwareConfig hardwareConfig = mParams.getHardwareConfig(); 106 107 // setup the display Metrics. 108 DisplayMetrics metrics = new DisplayMetrics(); 109 metrics.densityDpi = metrics.noncompatDensityDpi = 110 hardwareConfig.getDensity().getDpiValue(); 111 112 metrics.density = metrics.noncompatDensity = 113 metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT; 114 115 metrics.scaledDensity = metrics.noncompatScaledDensity = metrics.density; 116 117 metrics.widthPixels = metrics.noncompatWidthPixels = hardwareConfig.getScreenWidth(); 118 metrics.heightPixels = metrics.noncompatHeightPixels = hardwareConfig.getScreenHeight(); 119 metrics.xdpi = metrics.noncompatXdpi = hardwareConfig.getXdpi(); 120 metrics.ydpi = metrics.noncompatYdpi = hardwareConfig.getYdpi(); 121 122 RenderResources resources = mParams.getResources(); 123 124 // build the context 125 mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources, 126 mParams.getAssets(), mParams.getLayoutlibCallback(), getConfiguration(), 127 mParams.getTargetSdkVersion(), mParams.isRtlSupported()); 128 129 setUp(); 130 131 return SUCCESS.createResult(); 132 } 133 134 135 /** 136 * Prepares the scene for action. 137 * <p> 138 * This call is blocking if another rendering/inflating is currently happening, and will return 139 * whether the preparation worked. 140 * 141 * The preparation can fail if another rendering took too long and the timeout was elapsed. 142 * 143 * More than one call to this from the same thread will have no effect and will return 144 * {@link Result.Status#SUCCESS}. 145 * 146 * After scene actions have taken place, only one call to {@link #release()} must be 147 * done. 148 * 149 * @param timeout the time to wait if another rendering is happening. 150 * 151 * @return whether the scene was prepared 152 * 153 * @see #release() 154 * 155 * @throws IllegalStateException if {@link #init(long)} was never called. 156 */ 157 public Result acquire(long timeout) { 158 if (mContext == null) { 159 throw new IllegalStateException("After scene creation, #init() must be called"); 160 } 161 162 // acquire the lock. if the result is null, lock was just acquired, otherwise, return 163 // the result. 164 Result result = acquireLock(timeout); 165 if (result != null) { 166 return result; 167 } 168 169 setUp(); 170 171 return SUCCESS.createResult(); 172 } 173 174 /** 175 * Acquire the lock so that the scene can be acted upon. 176 * <p> 177 * This returns null if the lock was just acquired, otherwise it returns 178 * {@link Result.Status#SUCCESS} if the lock already belonged to that thread, or another 179 * instance (see {@link Result#getStatus()}) if an error occurred. 180 * 181 * @param timeout the time to wait if another rendering is happening. 182 * @return null if the lock was just acquire or another result depending on the state. 183 * 184 * @throws IllegalStateException if the current context is different than the one owned by 185 * the scene. 186 */ 187 private Result acquireLock(long timeout) { 188 ReentrantLock lock = Bridge.getLock(); 189 if (!lock.isHeldByCurrentThread()) { 190 try { 191 boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); 192 193 if (!acquired) { 194 return ERROR_TIMEOUT.createResult(); 195 } 196 } catch (InterruptedException e) { 197 return ERROR_LOCK_INTERRUPTED.createResult(); 198 } 199 } else { 200 // This thread holds the lock already. Checks that this wasn't for a different context. 201 // If this is called by init, mContext will be null and so should sCurrentContext 202 // anyway 203 if (mContext != sCurrentContext) { 204 throw new IllegalStateException("Acquiring different scenes from same thread without releases"); 205 } 206 return SUCCESS.createResult(); 207 } 208 209 return null; 210 } 211 212 /** 213 * Cleans up the scene after an action. 214 */ 215 public void release() { 216 ReentrantLock lock = Bridge.getLock(); 217 218 // with the use of finally blocks, it is possible to find ourself calling this 219 // without a successful call to prepareScene. This test makes sure that unlock() will 220 // not throw IllegalMonitorStateException. 221 if (lock.isHeldByCurrentThread()) { 222 tearDown(); 223 lock.unlock(); 224 } 225 } 226 227 /** 228 * Sets up the session for rendering. 229 * <p/> 230 * The counterpart is {@link #tearDown()}. 231 */ 232 private void setUp() { 233 // setup the ParserFactory 234 ParserFactory.setParserFactory(mParams.getLayoutlibCallback().getParserFactory()); 235 236 // make sure the Resources object references the context (and other objects) for this 237 // scene 238 mContext.initResources(); 239 sCurrentContext = mContext; 240 241 // create an InputMethodManager 242 InputMethodManager.getInstance(); 243 244 LayoutLog currentLog = mParams.getLog(); 245 Bridge.setLog(currentLog); 246 mContext.getRenderResources().setFrameworkResourceIdProvider(this); 247 mContext.getRenderResources().setLogger(currentLog); 248 } 249 250 /** 251 * Tear down the session after rendering. 252 * <p/> 253 * The counterpart is {@link #setUp()}. 254 */ 255 private void tearDown() { 256 // The context may be null, if there was an error during init(). 257 if (mContext != null) { 258 // Make sure to remove static references, otherwise we could not unload the lib 259 mContext.disposeResources(); 260 } 261 262 if (sCurrentContext != null) { 263 // quit HandlerThread created during this session. 264 HandlerThread_Delegate.cleanUp(sCurrentContext); 265 } 266 267 // clear the stored ViewConfiguration since the map is per density and not per context. 268 ViewConfiguration_Accessor.clearConfigurations(); 269 270 // remove the InputMethodManager 271 InputMethodManager_Accessor.resetInstance(); 272 273 sCurrentContext = null; 274 275 Bridge.setLog(null); 276 if (mContext != null) { 277 mContext.getRenderResources().setFrameworkResourceIdProvider(null); 278 mContext.getRenderResources().setLogger(null); 279 } 280 ParserFactory.setParserFactory(null); 281 SimpleMonthView_Delegate.clearCache(); 282 } 283 284 public static BridgeContext getCurrentContext() { 285 return sCurrentContext; 286 } 287 288 protected T getParams() { 289 return mParams; 290 } 291 292 protected BridgeContext getContext() { 293 return mContext; 294 } 295 296 /** 297 * Returns the log associated with the session. 298 * @return the log or null if there are none. 299 */ 300 public LayoutLog getLog() { 301 if (mParams != null) { 302 return mParams.getLog(); 303 } 304 305 return null; 306 } 307 308 /** 309 * Checks that the lock is owned by the current thread and that the current context is the one 310 * from this scene. 311 * 312 * @throws IllegalStateException if the current context is different than the one owned by 313 * the scene, or if {@link #acquire(long)} was not called. 314 */ 315 protected void checkLock() { 316 ReentrantLock lock = Bridge.getLock(); 317 if (!lock.isHeldByCurrentThread()) { 318 throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); 319 } 320 if (sCurrentContext != mContext) { 321 throw new IllegalStateException("Thread acquired a scene but is rendering a different one"); 322 } 323 } 324 325 private Configuration getConfiguration() { 326 Configuration config = new Configuration(); 327 328 HardwareConfig hardwareConfig = mParams.getHardwareConfig(); 329 330 ScreenSize screenSize = hardwareConfig.getScreenSize(); 331 if (screenSize != null) { 332 switch (screenSize) { 333 case SMALL: 334 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_SMALL; 335 break; 336 case NORMAL: 337 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_NORMAL; 338 break; 339 case LARGE: 340 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_LARGE; 341 break; 342 case XLARGE: 343 config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_XLARGE; 344 break; 345 } 346 } 347 348 Density density = hardwareConfig.getDensity(); 349 if (density == null) { 350 density = Density.MEDIUM; 351 } 352 353 config.screenWidthDp = hardwareConfig.getScreenWidth() / density.getDpiValue(); 354 config.screenHeightDp = hardwareConfig.getScreenHeight() / density.getDpiValue(); 355 if (config.screenHeightDp < config.screenWidthDp) { 356 //noinspection SuspiciousNameCombination 357 config.smallestScreenWidthDp = config.screenHeightDp; 358 } else { 359 config.smallestScreenWidthDp = config.screenWidthDp; 360 } 361 config.densityDpi = density.getDpiValue(); 362 363 // never run in compat mode: 364 config.compatScreenWidthDp = config.screenWidthDp; 365 config.compatScreenHeightDp = config.screenHeightDp; 366 367 ScreenOrientation orientation = hardwareConfig.getOrientation(); 368 if (orientation != null) { 369 switch (orientation) { 370 case PORTRAIT: 371 config.orientation = Configuration.ORIENTATION_PORTRAIT; 372 break; 373 case LANDSCAPE: 374 config.orientation = Configuration.ORIENTATION_LANDSCAPE; 375 break; 376 case SQUARE: 377 //noinspection deprecation 378 config.orientation = Configuration.ORIENTATION_SQUARE; 379 break; 380 } 381 } else { 382 config.orientation = Configuration.ORIENTATION_UNDEFINED; 383 } 384 385 try { 386 ScreenRound roundness = hardwareConfig.getScreenRoundness(); 387 if (roundness != null) { 388 switch (roundness) { 389 case ROUND: 390 config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES; 391 break; 392 case NOTROUND: 393 config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_NO; 394 } 395 } else { 396 config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_UNDEFINED; 397 } 398 } catch (NoSuchMethodError ignored) { 399 // getScreenRoundness was added in later stages of API 15. So, it's not present on some 400 // preview releases of API 15. 401 // TODO: Remove the try catch around Oct 2015. 402 } 403 String locale = getParams().getLocale(); 404 if (locale != null && !locale.isEmpty()) config.locale = new Locale(locale); 405 406 // TODO: fill in more config info. 407 408 return config; 409 } 410 411 412 // --- FrameworkResourceIdProvider methods 413 414 @Override 415 public Integer getId(ResourceType resType, String resName) { 416 return Bridge.getResourceId(resType, resName); 417 } 418} 419