content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.layoutlib.bridge.impl;
  
  import com.android.ide.common.rendering.api.HardwareConfig;
  import com.android.ide.common.rendering.api.LayoutLog;
  import com.android.ide.common.rendering.api.RenderParams;
  import com.android.ide.common.rendering.api.RenderResources;
  import com.android.ide.common.rendering.api.Result;
  import com.android.layoutlib.bridge.Bridge;
  import com.android.layoutlib.bridge.android.BridgeContext;
  import com.android.resources.Density;
  import com.android.resources.ScreenOrientation;
  import com.android.resources.ScreenRound;
  import com.android.resources.ScreenSize;
  import com.android.tools.layoutlib.annotations.VisibleForTesting;
  
  import android.animation.PropertyValuesHolder_Accessor;
  import android.content.res.Configuration;
  import android.os.HandlerThread_Delegate;
  import android.util.DisplayMetrics;
  import android.view.IWindowManager;
  import android.view.IWindowManagerImpl;
  import android.view.Surface;
  import android.view.ViewConfiguration_Accessor;
  import android.view.WindowManagerGlobal_Delegate;
  import android.view.inputmethod.InputMethodManager_Accessor;
  
  import java.util.Locale;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.locks.ReentrantLock;
  
  import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED;
  import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT;
  import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
  
  /**
   * Base class for rendering action.
   *
   * It provides life-cycle methods to init and stop the rendering.
   * The most important methods are:
   * {@link #init(long)} and {@link #acquire(long)} to start a rendering and {@link #release()}
   * after the rendering.
   *
   *
   * @param <T> the {@link RenderParams} implementation
   *
   */
  public abstract class RenderAction<T extends RenderParams> {
  
      /**
       * The current context being rendered. This is set through {@link #acquire(long)} and
       * {@link #init(long)}, and unset in {@link #release()}.
       */
      @VisibleForTesting
      static BridgeContext sCurrentContext = null;
  
      private final T mParams;
  
      private BridgeContext mContext;
  
      /**
       * Creates a renderAction.
       * <p>
       * This <b>must</b> be followed by a call to {@link RenderAction#init(long)}, which act as a
       * call to {@link RenderAction#acquire(long)}
       *
       * @param params the RenderParams. This must be a copy that the action can keep
       *
       */
      protected RenderAction(T params) {
          mParams = params;
      }
  
      /**
       * Initializes and acquires the scene, creating various Android objects such as context,
       * inflater, and parser.
       *
       * @param timeout the time to wait if another rendering is happening.
       *
       * @return whether the scene was prepared
       *
       * @see #acquire(long)
       * @see #release()
       */
      public Result init(long timeout) {
          // acquire the lock. if the result is null, lock was just acquired, otherwise, return
          // the result.
          Result result = acquireLock(timeout);
          if (result != null) {
              return result;
          }
  
          HardwareConfig hardwareConfig = mParams.getHardwareConfig();
  
          // setup the display Metrics.
          DisplayMetrics metrics = new DisplayMetrics();
          metrics.densityDpi = metrics.noncompatDensityDpi =
                  hardwareConfig.getDensity().getDpiValue();
  
          metrics.density = metrics.noncompatDensity =
                  metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
  
          metrics.scaledDensity = metrics.noncompatScaledDensity = metrics.density;
  
          metrics.widthPixels = metrics.noncompatWidthPixels = hardwareConfig.getScreenWidth();
          metrics.heightPixels = metrics.noncompatHeightPixels = hardwareConfig.getScreenHeight();
          metrics.xdpi = metrics.noncompatXdpi = hardwareConfig.getXdpi();
          metrics.ydpi = metrics.noncompatYdpi = hardwareConfig.getYdpi();
  
          RenderResources resources = mParams.getResources();
  
          // build the context
          mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources,
                  mParams.getAssets(), mParams.getLayoutlibCallback(), getConfiguration(mParams),
                  mParams.getTargetSdkVersion(), mParams.isRtlSupported());
  
          setUp();
  
          return SUCCESS.createResult();
      }
  
      /**
       * Prepares the scene for action.
       * <p>
       * This call is blocking if another rendering/inflating is currently happening, and will return
       * whether the preparation worked.
       *
       * The preparation can fail if another rendering took too long and the timeout was elapsed.
       *
       * More than one call to this from the same thread will have no effect and will return
       * {@link Result.Status#SUCCESS}.
       *
       * After scene actions have taken place, only one call to {@link #release()} must be
       * done.
       *
       * @param timeout the time to wait if another rendering is happening.
       *
       * @return whether the scene was prepared
       *
       * @see #release()
       *
       * @throws IllegalStateException if {@link #init(long)} was never called.
       */
      public Result acquire(long timeout) {
          if (mContext == null) {
              throw new IllegalStateException("After scene creation, #init() must be called");
          }
  
          // acquire the lock. if the result is null, lock was just acquired, otherwise, return
          // the result.
          Result result = acquireLock(timeout);
          if (result != null) {
              return result;
          }
  
          setUp();
  
          return SUCCESS.createResult();
      }
  
      /**
       * Acquire the lock so that the scene can be acted upon.
       * <p>
       * This returns null if the lock was just acquired, otherwise it returns
       * {@link Result.Status#SUCCESS} if the lock already belonged to that thread, or another
       * instance (see {@link Result#getStatus()}) if an error occurred.
       *
       * @param timeout the time to wait if another rendering is happening.
       * @return null if the lock was just acquire or another result depending on the state.
       *
       * @throws IllegalStateException if the current context is different than the one owned by
       *      the scene.
       */
      private Result acquireLock(long timeout) {
          ReentrantLock lock = Bridge.getLock();
          if (!lock.isHeldByCurrentThread()) {
              try {
                  boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
  
                  if (!acquired) {
                      return ERROR_TIMEOUT.createResult();
                  }
              } catch (InterruptedException e) {
                  return ERROR_LOCK_INTERRUPTED.createResult();
              }
          } else {
              // This thread holds the lock already. Checks that this wasn't for a different context.
              // If this is called by init, mContext will be null and so should sCurrentContext
              // anyway
              if (mContext != sCurrentContext) {
                  throw new IllegalStateException("Acquiring different scenes from same thread without releases");
              }
              return SUCCESS.createResult();
          }
  
          return null;
      }
  
      /**
       * Cleans up the scene after an action.
       */
      public void release() {
          ReentrantLock lock = Bridge.getLock();
  
          // with the use of finally blocks, it is possible to find ourself calling this
          // without a successful call to prepareScene. This test makes sure that unlock() will
          // not throw IllegalMonitorStateException.
          if (lock.isHeldByCurrentThread()) {
              tearDown();
              lock.unlock();
          }
      }
  
      /**
       * Sets up the session for rendering.
       * <p/>
       * The counterpart is {@link #tearDown()}.
       */
      private void setUp() {
          // setup the ParserFactory
          ParserFactory.setParserFactory(mParams.getLayoutlibCallback());
  
          // make sure the Resources object references the context (and other objects) for this
          // scene
          mContext.initResources();
          sCurrentContext = mContext;
  
          // Set-up WindowManager
          // FIXME: find those out, and possibly add them to the render params
          boolean hasNavigationBar = true;
          //noinspection ConstantConditions
          IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(),
                  getContext().getMetrics(), Surface.ROTATION_0, hasNavigationBar);
          WindowManagerGlobal_Delegate.setWindowManagerService(iwm);
  
          LayoutLog currentLog = mParams.getLog();
          Bridge.setLog(currentLog);
          mContext.getRenderResources().setLogger(currentLog);
      }
  
      /**
       * Tear down the session after rendering.
       * <p/>
       * The counterpart is {@link #setUp()}.
       */
      private void tearDown() {
          // The context may be null, if there was an error during init().
          if (mContext != null) {
              // Make sure to remove static references, otherwise we could not unload the lib
              mContext.disposeResources();
          }
  
          if (sCurrentContext != null) {
              // quit HandlerThread created during this session.
              HandlerThread_Delegate.cleanUp(sCurrentContext);
          }
  
          // clear the stored ViewConfiguration since the map is per density and not per context.
          ViewConfiguration_Accessor.clearConfigurations();
  
          // remove the InputMethodManager
          InputMethodManager_Accessor.tearDownEditMode();
  
          sCurrentContext = null;
  
          Bridge.setLog(null);
          if (mContext != null) {
              mContext.getRenderResources().setLogger(null);
          }
          ParserFactory.setParserFactory(null);
  
          PropertyValuesHolder_Accessor.clearClassCaches();
      }
  
      public static BridgeContext getCurrentContext() {
          return sCurrentContext;
      }
  
      protected T getParams() {
          return mParams;
      }
  
      protected BridgeContext getContext() {
          return mContext;
      }
  
      /**
       * Returns the log associated with the session.
       * @return the log or null if there are none.
       */
      public LayoutLog getLog() {
          if (mParams != null) {
              return mParams.getLog();
          }
  
          return null;
      }
  
      /**
       * Checks that the lock is owned by the current thread and that the current context is the one
       * from this scene.
       *
       * @throws IllegalStateException if the current context is different than the one owned by
       *      the scene, or if {@link #acquire(long)} was not called.
       */
      protected void checkLock() {
          ReentrantLock lock = Bridge.getLock();
          if (!lock.isHeldByCurrentThread()) {
              throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
          }
          if (sCurrentContext != mContext) {
              throw new IllegalStateException("Thread acquired a scene but is rendering a different one");
          }
      }
  
      // VisibleForTesting
      public static Configuration getConfiguration(RenderParams params) {
          Configuration config = new Configuration();
  
          HardwareConfig hardwareConfig = params.getHardwareConfig();
  
          ScreenSize screenSize = hardwareConfig.getScreenSize();
          if (screenSize != null) {
              switch (screenSize) {
                  case SMALL:
                      config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_SMALL;
                      break;
                  case NORMAL:
                      config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_NORMAL;
                      break;
                  case LARGE:
                      config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_LARGE;
                      break;
                  case XLARGE:
                      config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_XLARGE;
                      break;
              }
          }
  
          Density density = hardwareConfig.getDensity();
          if (density == null) {
              density = Density.MEDIUM;
          }
  
          config.screenWidthDp = hardwareConfig.getScreenWidth() * 160 / density.getDpiValue();
          config.screenHeightDp = hardwareConfig.getScreenHeight() * 160 / density.getDpiValue();
          if (config.screenHeightDp < config.screenWidthDp) {
              //noinspection SuspiciousNameCombination
              config.smallestScreenWidthDp = config.screenHeightDp;
          } else {
              config.smallestScreenWidthDp = config.screenWidthDp;
          }
          config.densityDpi = density.getDpiValue();
  
          // never run in compat mode:
          config.compatScreenWidthDp = config.screenWidthDp;
          config.compatScreenHeightDp = config.screenHeightDp;
  
          ScreenOrientation orientation = hardwareConfig.getOrientation();
          if (orientation != null) {
              switch (orientation) {
              case PORTRAIT:
                  config.orientation = Configuration.ORIENTATION_PORTRAIT;
                  break;
              case LANDSCAPE:
                  config.orientation = Configuration.ORIENTATION_LANDSCAPE;
                  break;
              case SQUARE:
                  //noinspection deprecation
                  config.orientation = Configuration.ORIENTATION_SQUARE;
                  break;
              }
          } else {
              config.orientation = Configuration.ORIENTATION_UNDEFINED;
          }
  
          ScreenRound roundness = hardwareConfig.getScreenRoundness();
          if (roundness != null) {
              switch (roundness) {
                  case ROUND:
                      config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES;
                      break;
                  case NOTROUND:
                      config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_NO;
              }
          } else {
              config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_UNDEFINED;
          }
          String locale = params.getLocale();
          if (locale != null && !locale.isEmpty()) config.locale = new Locale(locale);
  
          // TODO: fill in more config info.
  
          return config;
      }
  }
  