diff options
author | Siva Velusamy <vsiva@google.com> | 2012-09-18 14:51:46 -0700 |
---|---|---|
committer | Siva Velusamy <vsiva@google.com> | 2012-09-18 15:09:33 -0700 |
commit | 6837aad30d6c51783ca1dc784ca6bdcc8a3d9f2d (patch) | |
tree | 90af15ddcc9f0bbc3151a0978b3e637f77fdd54a /sdk_common/src | |
parent | 6184f12fa097e1c5bddfe50700b3b0740c736a5a (diff) | |
download | sdk-6837aad30d6c51783ca1dc784ca6bdcc8a3d9f2d.zip sdk-6837aad30d6c51783ca1dc784ca6bdcc8a3d9f2d.tar.gz sdk-6837aad30d6c51783ca1dc784ca6bdcc8a3d9f2d.tar.bz2 |
Rename ide_common to sdk_common
Change-Id: I1b39ee439a532f3f6758be35b569948e2e906665
Diffstat (limited to 'sdk_common/src')
48 files changed, 9666 insertions, 0 deletions
diff --git a/sdk_common/src/com/android/ide/common/rendering/LayoutLibrary.java b/sdk_common/src/com/android/ide/common/rendering/LayoutLibrary.java new file mode 100644 index 0000000..0a353f9 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/rendering/LayoutLibrary.java @@ -0,0 +1,755 @@ +/* + * 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.ide.common.rendering; + +import static com.android.ide.common.rendering.api.Result.Status.ERROR_REFLECTION; + +import com.android.ide.common.rendering.api.Bridge; +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.common.rendering.api.DrawableParams; +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.ide.common.rendering.api.SessionParams.RenderingMode; +import com.android.ide.common.rendering.api.ViewInfo; +import com.android.ide.common.rendering.legacy.ILegacyPullParser; +import com.android.ide.common.rendering.legacy.LegacyCallback; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.sdk.LoadStatus; +import com.android.layoutlib.api.ILayoutBridge; +import com.android.layoutlib.api.ILayoutLog; +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IXmlPullParser; +import com.android.resources.ResourceType; +import com.android.utils.ILogger; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Class to use the Layout library. + * <p/> + * Use {@link #load(String, ILogger)} to load the jar file. + * <p/> + * Use the layout library with: + * {@link #init(String, Map)}, {@link #supports(Capability)}, {@link #createSession(SessionParams)}, + * {@link #dispose()}, {@link #clearCaches(Object)}. + * + * <p/> + * For client wanting to access both new and old (pre API level 5) layout libraries, it is + * important that the following interfaces be used:<br> + * {@link ILegacyPullParser} instead of {@link ILayoutPullParser}<br> + * {@link LegacyCallback} instead of {@link com.android.ide.common.rendering.api.IProjectCallback}. + * <p/> + * These interfaces will ensure that both new and older Layout libraries can be accessed. + */ +@SuppressWarnings("deprecation") +public class LayoutLibrary { + + public final static String CLASS_BRIDGE = "com.android.layoutlib.bridge.Bridge"; //$NON-NLS-1$ + + /** Link to the layout bridge */ + private final Bridge mBridge; + /** Link to a ILayoutBridge in case loaded an older library */ + private final ILayoutBridge mLegacyBridge; + /** Status of the layoutlib.jar loading */ + private final LoadStatus mStatus; + /** Message associated with the {@link LoadStatus}. This is mostly used when + * {@link #getStatus()} returns {@link LoadStatus#FAILED}. + */ + private final String mLoadMessage; + /** classloader used to load the jar file */ + private final ClassLoader mClassLoader; + + // Reflection data for older Layout Libraries. + private Method mViewGetParentMethod; + private Method mViewGetBaselineMethod; + private Method mViewParentIndexOfChildMethod; + private Class<?> mMarginLayoutParamClass; + private Field mLeftMarginField; + private Field mTopMarginField; + private Field mRightMarginField; + private Field mBottomMarginField; + + /** + * Returns the {@link LoadStatus} of the loading of the layoutlib jar file. + */ + public LoadStatus getStatus() { + return mStatus; + } + + /** Returns the message associated with the {@link LoadStatus}. This is mostly used when + * {@link #getStatus()} returns {@link LoadStatus#FAILED}. + */ + public String getLoadMessage() { + return mLoadMessage; + } + + /** + * Returns the classloader used to load the classes in the layoutlib jar file. + */ + public ClassLoader getClassLoader() { + return mClassLoader; + } + + /** + * Loads the layoutlib.jar file located at the given path and returns a {@link LayoutLibrary} + * object representing the result. + * <p/> + * If loading failed {@link #getStatus()} will reflect this, and {@link #getBridge()} will + * return null. + * + * @param layoutLibJarOsPath the path of the jar file + * @param log an optional log file. + * @return a {@link LayoutLibrary} object always. + */ + public static LayoutLibrary load(String layoutLibJarOsPath, ILogger log, String toolName) { + + LoadStatus status = LoadStatus.LOADING; + String message = null; + Bridge bridge = null; + ILayoutBridge legacyBridge = null; + ClassLoader classLoader = null; + + try { + // get the URL for the file. + File f = new File(layoutLibJarOsPath); + if (f.isFile() == false) { + if (log != null) { + log.error(null, "layoutlib.jar is missing!"); //$NON-NLS-1$ + } + } else { + URI uri = f.toURI(); + URL url = uri.toURL(); + + // create a class loader. Because this jar reference interfaces + // that are in the editors plugin, it's important to provide + // a parent class loader. + classLoader = new URLClassLoader( + new URL[] { url }, + LayoutLibrary.class.getClassLoader()); + + // load the class + Class<?> clazz = classLoader.loadClass(CLASS_BRIDGE); + if (clazz != null) { + // instantiate an object of the class. + Constructor<?> constructor = clazz.getConstructor(); + if (constructor != null) { + Object bridgeObject = constructor.newInstance(); + if (bridgeObject instanceof Bridge) { + bridge = (Bridge)bridgeObject; + } else if (bridgeObject instanceof ILayoutBridge) { + legacyBridge = (ILayoutBridge) bridgeObject; + } + } + } + + if (bridge == null && legacyBridge == null) { + status = LoadStatus.FAILED; + message = "Failed to load " + CLASS_BRIDGE; //$NON-NLS-1$ + if (log != null) { + log.error(null, + "Failed to load " + //$NON-NLS-1$ + CLASS_BRIDGE + + " from " + //$NON-NLS-1$ + layoutLibJarOsPath); + } + } else { + // mark the lib as loaded, unless it's overridden below. + status = LoadStatus.LOADED; + + // check the API, only if it's not a legacy bridge + if (bridge != null) { + int api = bridge.getApiLevel(); + if (api > Bridge.API_CURRENT) { + status = LoadStatus.FAILED; + message = String.format( + "This version of the rendering library is more recent than your version of %1$s. Please update %1$s", toolName); + } + } + } + } + } catch (Throwable t) { + status = LoadStatus.FAILED; + Throwable cause = t; + while (cause.getCause() != null) { + cause = cause.getCause(); + } + message = "Failed to load the LayoutLib: " + cause.getMessage(); + // log the error. + if (log != null) { + log.error(t, message); + } + } + + return new LayoutLibrary(bridge, legacyBridge, classLoader, status, message); + } + + // ------ Layout Lib API proxy + + /** + * Returns the API level of the layout library. + */ + public int getApiLevel() { + if (mBridge != null) { + return mBridge.getApiLevel(); + } + + if (mLegacyBridge != null) { + return getLegacyApiLevel(); + } + + return 0; + } + + /** + * Returns the revision of the library inside a given (layoutlib) API level. + * The true version number of the library is {@link #getApiLevel()}.{@link #getRevision()} + */ + public int getRevision() { + if (mBridge != null) { + return mBridge.getRevision(); + } + + return 0; + } + + /** + * Returns whether the LayoutLibrary supports a given {@link Capability}. + * @return true if it supports it. + * + * @see Bridge#getCapabilities() + * + */ + public boolean supports(Capability capability) { + if (mBridge != null) { + return mBridge.getCapabilities().contains(capability); + } + + if (mLegacyBridge != null) { + switch (capability) { + case UNBOUND_RENDERING: + // legacy stops at 4. 5 is new API. + return getLegacyApiLevel() == 4; + } + } + + return false; + } + + /** + * Initializes the Layout Library object. This must be called before any other action is taken + * on the instance. + * + * @param platformProperties The build properties for the platform. + * @param fontLocation the location of the fonts in the SDK target. + * @param enumValueMap map attrName => { map enumFlagName => Integer value }. This is typically + * read from attrs.xml in the SDK target. + * @param log a {@link LayoutLog} object. Can be null. + * @return true if success. + * + * @see Bridge#init(String, Map) + */ + public boolean init(Map<String, String> platformProperties, + File fontLocation, + Map<String, Map<String, Integer>> enumValueMap, + LayoutLog log) { + if (mBridge != null) { + return mBridge.init(platformProperties, fontLocation, enumValueMap, log); + } else if (mLegacyBridge != null) { + return mLegacyBridge.init(fontLocation.getAbsolutePath(), enumValueMap); + } + + return false; + } + + /** + * Prepares the layoutlib to unloaded. + * + * @see Bridge#dispose() + */ + public boolean dispose() { + if (mBridge != null) { + return mBridge.dispose(); + } + + return true; + } + + /** + * Starts a layout session by inflating and rendering it. The method returns a + * {@link RenderSession} on which further actions can be taken. + * <p/> + * Before taking further actions on the scene, it is recommended to use + * {@link #supports(Capability)} to check what the scene can do. + * + * @return a new {@link ILayoutScene} object that contains the result of the scene creation and + * first rendering or null if {@link #getStatus()} doesn't return {@link LoadStatus#LOADED}. + * + * @see Bridge#createSession(SessionParams) + */ + public RenderSession createSession(SessionParams params) { + if (mBridge != null) { + RenderSession session = mBridge.createSession(params); + if (params.getExtendedViewInfoMode() && + mBridge.getCapabilities().contains(Capability.EXTENDED_VIEWINFO) == false) { + // Extended view info was requested but the layoutlib does not support it. + // Add it manually. + List<ViewInfo> infoList = session.getRootViews(); + if (infoList != null) { + for (ViewInfo info : infoList) { + addExtendedViewInfo(info); + } + } + } + + return session; + } else if (mLegacyBridge != null) { + return createLegacySession(params); + } + + return null; + } + + /** + * Renders a Drawable. If the rendering is successful, the result image is accessible through + * {@link Result#getData()}. It is of type {@link BufferedImage} + * @param params the rendering parameters. + * @return the result of the action. + */ + public Result renderDrawable(DrawableParams params) { + if (mBridge != null) { + return mBridge.renderDrawable(params); + } + + return Status.NOT_IMPLEMENTED.createResult(); + } + + /** + * Clears the resource cache for a specific project. + * <p/>This cache contains bitmaps and nine patches that are loaded from the disk and reused + * until this method is called. + * <p/>The cache is not configuration dependent and should only be cleared when a + * resource changes (at this time only bitmaps and 9 patches go into the cache). + * + * @param projectKey the key for the project. + * + * @see Bridge#clearCaches(Object) + */ + public void clearCaches(Object projectKey) { + if (mBridge != null) { + mBridge.clearCaches(projectKey); + } else if (mLegacyBridge != null) { + mLegacyBridge.clearCaches(projectKey); + } + } + + /** + * Utility method returning the parent of a given view object. + * + * @param viewObject the object for which to return the parent. + * + * @return a {@link Result} indicating the status of the action, and if success, the parent + * object in {@link Result#getData()} + */ + public Result getViewParent(Object viewObject) { + if (mBridge != null) { + Result r = mBridge.getViewParent(viewObject); + if (r.isSuccess()) { + return r; + } + } + + return getViewParentWithReflection(viewObject); + } + + /** + * Utility method returning the index of a given view in its parent. + * @param viewObject the object for which to return the index. + * + * @return a {@link Result} indicating the status of the action, and if success, the index in + * the parent in {@link Result#getData()} + */ + public Result getViewIndex(Object viewObject) { + if (mBridge != null) { + Result r = mBridge.getViewIndex(viewObject); + if (r.isSuccess()) { + return r; + } + } + + return getViewIndexReflection(viewObject); + } + + // ------ Implementation + + private LayoutLibrary(Bridge bridge, ILayoutBridge legacyBridge, ClassLoader classLoader, + LoadStatus status, String message) { + mBridge = bridge; + mLegacyBridge = legacyBridge; + mClassLoader = classLoader; + mStatus = status; + mLoadMessage = message; + } + + /** + * Returns the API level of the legacy bridge. + * <p/> + * This handles the case where ILayoutBridge does not have a {@link ILayoutBridge#getApiLevel()} + * (at API level 1). + * <p/> + * {@link ILayoutBridge#getApiLevel()} should never called directly. + * + * @return the api level of {@link #mLegacyBridge}. + */ + private int getLegacyApiLevel() { + int apiLevel = 1; + try { + apiLevel = mLegacyBridge.getApiLevel(); + } catch (AbstractMethodError e) { + // the first version of the api did not have this method + // so this is 1 + } + + return apiLevel; + } + + private RenderSession createLegacySession(SessionParams params) { + if (params.getLayoutDescription() instanceof IXmlPullParser == false) { + throw new IllegalArgumentException("Parser must be of type ILegacyPullParser"); + } + if (params.getProjectCallback() instanceof + com.android.layoutlib.api.IProjectCallback == false) { + throw new IllegalArgumentException("Project callback must be of type ILegacyCallback"); + } + + if (params.getResources() instanceof ResourceResolver == false) { + throw new IllegalArgumentException("RenderResources object must be of type ResourceResolver"); + } + + ResourceResolver resources = (ResourceResolver) params.getResources(); + + int apiLevel = getLegacyApiLevel(); + + // create a log wrapper since the older api requires a ILayoutLog + final LayoutLog log = params.getLog(); + ILayoutLog logWrapper = new ILayoutLog() { + + @Override + public void warning(String message) { + log.warning(null, message, null /*data*/); + } + + @Override + public void error(Throwable t) { + log.error(null, "error!", t, null /*data*/); + } + + @Override + public void error(String message) { + log.error(null, message, null /*data*/); + } + }; + + + // convert the map of ResourceValue into IResourceValue. Super ugly but works. + + Map<String, Map<String, IResourceValue>> projectMap = convertMap( + resources.getProjectResources()); + Map<String, Map<String, IResourceValue>> frameworkMap = convertMap( + resources.getFrameworkResources()); + + ILayoutResult result = null; + + if (apiLevel == 4) { + // Final ILayoutBridge API added support for "render full height" + result = mLegacyBridge.computeLayout( + (IXmlPullParser) params.getLayoutDescription(), + params.getProjectKey(), + params.getScreenWidth(), params.getScreenHeight(), + params.getRenderingMode() == RenderingMode.FULL_EXPAND ? true : false, + params.getDensity().getDpiValue(), params.getXdpi(), params.getYdpi(), + resources.getThemeName(), resources.isProjectTheme(), + projectMap, frameworkMap, + (IProjectCallback) params.getProjectCallback(), + logWrapper); + } else if (apiLevel == 3) { + // api 3 add density support. + result = mLegacyBridge.computeLayout( + (IXmlPullParser) params.getLayoutDescription(), params.getProjectKey(), + params.getScreenWidth(), params.getScreenHeight(), + params.getDensity().getDpiValue(), params.getXdpi(), params.getYdpi(), + resources.getThemeName(), resources.isProjectTheme(), + projectMap, frameworkMap, + (IProjectCallback) params.getProjectCallback(), logWrapper); + } else if (apiLevel == 2) { + // api 2 added boolean for separation of project/framework theme + result = mLegacyBridge.computeLayout( + (IXmlPullParser) params.getLayoutDescription(), params.getProjectKey(), + params.getScreenWidth(), params.getScreenHeight(), + resources.getThemeName(), resources.isProjectTheme(), + projectMap, frameworkMap, + (IProjectCallback) params.getProjectCallback(), logWrapper); + } else { + // First api with no density/dpi, and project theme boolean mixed + // into the theme name. + + // change the string if it's a custom theme to make sure we can + // differentiate them + String themeName = resources.getThemeName(); + if (resources.isProjectTheme()) { + themeName = "*" + themeName; //$NON-NLS-1$ + } + + result = mLegacyBridge.computeLayout( + (IXmlPullParser) params.getLayoutDescription(), params.getProjectKey(), + params.getScreenWidth(), params.getScreenHeight(), + themeName, + projectMap, frameworkMap, + (IProjectCallback) params.getProjectCallback(), logWrapper); + } + + // clean up that is not done by the ILayoutBridge itself + legacyCleanUp(); + + return convertToScene(result); + } + + @SuppressWarnings("unchecked") + private Map<String, Map<String, IResourceValue>> convertMap( + Map<ResourceType, Map<String, ResourceValue>> map) { + Map<String, Map<String, IResourceValue>> result = + new HashMap<String, Map<String, IResourceValue>>(); + + for (Entry<ResourceType, Map<String, ResourceValue>> entry : map.entrySet()) { + // ugly case but works. + result.put(entry.getKey().getName(), + (Map) entry.getValue()); + } + + return result; + } + + /** + * Converts a {@link ILayoutResult} to a {@link RenderSession}. + */ + private RenderSession convertToScene(ILayoutResult result) { + + Result sceneResult; + ViewInfo rootViewInfo = null; + + if (result.getSuccess() == ILayoutResult.SUCCESS) { + sceneResult = Status.SUCCESS.createResult(); + ILayoutViewInfo oldRootView = result.getRootView(); + if (oldRootView != null) { + rootViewInfo = convertToViewInfo(oldRootView); + } + } else { + sceneResult = Status.ERROR_UNKNOWN.createResult(result.getErrorMessage()); + } + + // create a BasicLayoutScene. This will return the given values but return the default + // implementation for all method. + // ADT should gracefully handle the default implementations of LayoutScene + return new StaticRenderSession(sceneResult, rootViewInfo, result.getImage()); + } + + /** + * Converts a {@link ILayoutViewInfo} (and its children) to a {@link ViewInfo}. + */ + private ViewInfo convertToViewInfo(ILayoutViewInfo view) { + // create the view info. + ViewInfo viewInfo = new ViewInfo(view.getName(), view.getViewKey(), + view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + + // then convert the children + ILayoutViewInfo[] children = view.getChildren(); + if (children != null) { + ArrayList<ViewInfo> convertedChildren = new ArrayList<ViewInfo>(children.length); + for (ILayoutViewInfo child : children) { + convertedChildren.add(convertToViewInfo(child)); + } + viewInfo.setChildren(convertedChildren); + } + + return viewInfo; + } + + /** + * Post rendering clean-up that must be done here because it's not done in any layoutlib using + * {@link ILayoutBridge}. + */ + private void legacyCleanUp() { + try { + Class<?> looperClass = mClassLoader.loadClass("android.os.Looper"); //$NON-NLS-1$ + Field threadLocalField = looperClass.getField("sThreadLocal"); //$NON-NLS-1$ + if (threadLocalField != null) { + threadLocalField.setAccessible(true); + // get object. Field is static so no need to pass an object + ThreadLocal<?> threadLocal = (ThreadLocal<?>) threadLocalField.get(null); + if (threadLocal != null) { + threadLocal.remove(); + } + } + } catch (Exception e) { + // do nothing. + } + } + + private Result getViewParentWithReflection(Object viewObject) { + // default implementation using reflection. + try { + if (mViewGetParentMethod == null) { + Class<?> viewClass = Class.forName("android.view.View"); + mViewGetParentMethod = viewClass.getMethod("getParent"); + } + + return Status.SUCCESS.createResult(mViewGetParentMethod.invoke(viewObject)); + } catch (Exception e) { + // Catch all for the reflection calls. + return ERROR_REFLECTION.createResult(null, e); + } + } + + /** + * Utility method returning the index of a given view in its parent. + * @param viewObject the object for which to return the index. + * + * @return a {@link Result} indicating the status of the action, and if success, the index in + * the parent in {@link Result#getData()} + */ + private Result getViewIndexReflection(Object viewObject) { + // default implementation using reflection. + try { + Class<?> viewClass = Class.forName("android.view.View"); + + if (mViewGetParentMethod == null) { + mViewGetParentMethod = viewClass.getMethod("getParent"); + } + + Object parentObject = mViewGetParentMethod.invoke(viewObject); + + if (mViewParentIndexOfChildMethod == null) { + Class<?> viewParentClass = Class.forName("android.view.ViewParent"); + mViewParentIndexOfChildMethod = viewParentClass.getMethod("indexOfChild", + viewClass); + } + + return Status.SUCCESS.createResult( + mViewParentIndexOfChildMethod.invoke(parentObject, viewObject)); + } catch (Exception e) { + // Catch all for the reflection calls. + return ERROR_REFLECTION.createResult(null, e); + } + } + + private void addExtendedViewInfo(ViewInfo info) { + computeExtendedViewInfo(info); + + List<ViewInfo> children = info.getChildren(); + for (ViewInfo child : children) { + addExtendedViewInfo(child); + } + } + + private void computeExtendedViewInfo(ViewInfo info) { + Object viewObject = info.getViewObject(); + Object params = info.getLayoutParamsObject(); + + int baseLine = getViewBaselineReflection(viewObject); + int leftMargin = 0; + int topMargin = 0; + int rightMargin = 0; + int bottomMargin = 0; + + try { + if (mMarginLayoutParamClass == null) { + mMarginLayoutParamClass = Class.forName( + "android.view.ViewGroup$MarginLayoutParams"); + + mLeftMarginField = mMarginLayoutParamClass.getField("leftMargin"); + mTopMarginField = mMarginLayoutParamClass.getField("topMargin"); + mRightMarginField = mMarginLayoutParamClass.getField("rightMargin"); + mBottomMarginField = mMarginLayoutParamClass.getField("bottomMargin"); + } + + if (mMarginLayoutParamClass.isAssignableFrom(params.getClass())) { + + leftMargin = (Integer)mLeftMarginField.get(params); + topMargin = (Integer)mTopMarginField.get(params); + rightMargin = (Integer)mRightMarginField.get(params); + bottomMargin = (Integer)mBottomMarginField.get(params); + } + + } catch (Exception e) { + // just use 'unknown' value. + leftMargin = Integer.MIN_VALUE; + topMargin = Integer.MIN_VALUE; + rightMargin = Integer.MIN_VALUE; + bottomMargin = Integer.MIN_VALUE; + } + + info.setExtendedInfo(baseLine, leftMargin, topMargin, rightMargin, bottomMargin); + } + + /** + * Utility method returning the baseline value for a given view object. This basically returns + * View.getBaseline(). + * + * @param viewObject the object for which to return the index. + * + * @return the baseline value or -1 if not applicable to the view object or if this layout + * library does not implement this method. + */ + private int getViewBaselineReflection(Object viewObject) { + // default implementation using reflection. + try { + if (mViewGetBaselineMethod == null) { + Class<?> viewClass = Class.forName("android.view.View"); + mViewGetBaselineMethod = viewClass.getMethod("getBaseline"); + } + + Object result = mViewGetBaselineMethod.invoke(viewObject); + if (result instanceof Integer) { + return ((Integer)result).intValue(); + } + + } catch (Exception e) { + // Catch all for the reflection calls. + } + + return Integer.MIN_VALUE; + } +} diff --git a/sdk_common/src/com/android/ide/common/rendering/StaticRenderSession.java b/sdk_common/src/com/android/ide/common/rendering/StaticRenderSession.java new file mode 100644 index 0000000..c122c1c --- /dev/null +++ b/sdk_common/src/com/android/ide/common/rendering/StaticRenderSession.java @@ -0,0 +1,63 @@ +/* + * 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.ide.common.rendering; + +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.ViewInfo; + +import java.awt.image.BufferedImage; +import java.util.Collections; +import java.util.List; + +/** + * Static {@link RenderSession} returning a given {@link Result}, {@link ViewInfo} and + * {@link BufferedImage}. + * <p/> + * All other methods are untouched from the base implementation provided by the API. + * <p/> + * This is meant to be used as a wrapper around the static results. No further operations are + * possible. + * + */ +public class StaticRenderSession extends RenderSession { + + private final Result mResult; + private final List<ViewInfo> mRootViewInfo; + private final BufferedImage mImage; + + public StaticRenderSession(Result result, ViewInfo rootViewInfo, BufferedImage image) { + mResult = result; + mRootViewInfo = Collections.singletonList(rootViewInfo); + mImage = image; + } + + @Override + public Result getResult() { + return mResult; + } + + @Override + public List<ViewInfo> getRootViews() { + return mRootViewInfo; + } + + @Override + public BufferedImage getImage() { + return mImage; + } +} diff --git a/sdk_common/src/com/android/ide/common/rendering/legacy/ILegacyPullParser.java b/sdk_common/src/com/android/ide/common/rendering/legacy/ILegacyPullParser.java new file mode 100644 index 0000000..a71e190 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/rendering/legacy/ILegacyPullParser.java @@ -0,0 +1,31 @@ +/* + * 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.ide.common.rendering.legacy; + +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.layoutlib.api.IXmlPullParser; + +/** + * Intermediary interface extending both old and new project pull parsers from the layout lib API. + * + * Clients should use this instead of {@link ILayoutPullParser} or {@link IXmlPullParser}. + * + */ +@SuppressWarnings("deprecation") +public interface ILegacyPullParser extends ILayoutPullParser, IXmlPullParser { + +} diff --git a/sdk_common/src/com/android/ide/common/rendering/legacy/LegacyCallback.java b/sdk_common/src/com/android/ide/common/rendering/legacy/LegacyCallback.java new file mode 100644 index 0000000..67e6a7b --- /dev/null +++ b/sdk_common/src/com/android/ide/common/rendering/legacy/LegacyCallback.java @@ -0,0 +1,59 @@ +/* + * 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.ide.common.rendering.legacy; + +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.resources.ResourceType; +import com.android.util.Pair; + +/** + * Intermediary class implementing parts of both the old and new project call back from the + * layout lib API. + * + * Clients should use this instead of {@link IProjectCallback} to target both old and new + * Layout Libraries. + * + */ +@SuppressWarnings("deprecation") +public abstract class LegacyCallback implements + com.android.ide.common.rendering.api.IProjectCallback, + com.android.layoutlib.api.IProjectCallback { + + // ------ implementation of the old interface using the new interface. + + @Override + public final Integer getResourceValue(String type, String name) { + return getResourceId(ResourceType.getEnum(type), name); + } + + @Override + public final String[] resolveResourceValue(int id) { + Pair<ResourceType, String> info = resolveResourceId(id); + if (info != null) { + return new String[] { info.getSecond(), info.getFirst().getName() }; + } + + return null; + } + + @Override + public final String resolveResourceValue(int[] id) { + return resolveResourceId(id); + } + + // ------ +} diff --git a/sdk_common/src/com/android/ide/common/resources/FrameworkResourceItem.java b/sdk_common/src/com/android/ide/common/resources/FrameworkResourceItem.java new file mode 100644 index 0000000..70bbcef --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/FrameworkResourceItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +/** + * A custom {@link ResourceItem} for resources provided by the framework. + * + * The main change is that {@link #isEditableDirectly()} returns false. + */ +class FrameworkResourceItem extends ResourceItem { + + FrameworkResourceItem(String name) { + super(name); + } + + @Override + public boolean isEditableDirectly() { + return false; + } + + @Override + public String toString() { + return "FrameworkResourceItem [mName=" + getName() + ", mFiles=" //$NON-NLS-1$ //$NON-NLS-2$ + + getSourceFileList() + "]"; //$NON-NLS-1$ + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/FrameworkResources.java b/sdk_common/src/com/android/ide/common/resources/FrameworkResources.java new file mode 100755 index 0000000..fbf5926 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/FrameworkResources.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.resources.ResourceType; +import com.android.utils.ILogger; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * Framework resources repository. + * + * This behaves the same as {@link ResourceRepository} except that it differentiates between + * resources that are public and non public. + * {@link #getResources(ResourceType)} and {@link #hasResourcesOfType(ResourceType)} only return + * public resources. This is typically used to display resource lists in the UI. + * + * {@link #getConfiguredResources(com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration)} + * returns all resources, even the non public ones so that this can be used for rendering. + */ +public class FrameworkResources extends ResourceRepository { + + /** + * Map of {@link ResourceType} to list of items. It is guaranteed to contain a list for all + * possible values of ResourceType. + */ + protected final Map<ResourceType, List<ResourceItem>> mPublicResourceMap = + new EnumMap<ResourceType, List<ResourceItem>>(ResourceType.class); + + public FrameworkResources() { + super(true /*isFrameworkRepository*/); + } + + /** + * Returns a {@link Collection} (always non null, but can be empty) of <b>public</b> + * {@link ResourceItem} matching a given {@link ResourceType}. + * + * @param type the type of the resources to return + * @return a collection of items, possible empty. + */ + @Override + @NonNull + public List<ResourceItem> getResourceItemsOfType(@NonNull ResourceType type) { + return mPublicResourceMap.get(type); + } + + /** + * Returns whether the repository has <b>public</b> resources of a given {@link ResourceType}. + * @param type the type of resource to check. + * @return true if the repository contains resources of the given type, false otherwise. + */ + @Override + public boolean hasResourcesOfType(@NonNull ResourceType type) { + return mPublicResourceMap.get(type).size() > 0; + } + + @Override + @NonNull + protected ResourceItem createResourceItem(@NonNull String name) { + return new FrameworkResourceItem(name); + } + + /** + * Reads the public.xml file in data/res/values/ for a given resource folder and builds up + * a map of public resources. + * + * This map is a subset of the full resource map that only contains framework resources + * that are public. + * + * @param resFolder The root folder of the resources + * @param logger a logger to report issues to + */ + public void loadPublicResources(@NonNull IAbstractFolder resFolder, @Nullable ILogger logger) { + IAbstractFolder valueFolder = resFolder.getFolder(SdkConstants.FD_RES_VALUES); + if (valueFolder.exists() == false) { + return; + } + + IAbstractFile publicXmlFile = valueFolder.getFile("public.xml"); //$NON-NLS-1$ + if (publicXmlFile.exists()) { + Reader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(publicXmlFile.getContents(), + "UTF-8")); //$NON-NLS-1$ + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(reader); + + ResourceType lastType = null; + String lastTypeName = ""; + while (true) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + // As of API 15 there are a number of "java-symbol" entries here + if (!parser.getName().equals("public")) { //$NON-NLS-1$ + continue; + } + + String name = null; + String typeName = null; + for (int i = 0, n = parser.getAttributeCount(); i < n; i++) { + String attribute = parser.getAttributeName(i); + + if (attribute.equals("name")) { //$NON-NLS-1$ + name = parser.getAttributeValue(i); + if (typeName != null) { + // Skip id attribute processing + break; + } + } else if (attribute.equals("type")) { //$NON-NLS-1$ + typeName = parser.getAttributeValue(i); + } + } + + if (name != null && typeName != null) { + ResourceType type = null; + if (typeName.equals(lastTypeName)) { + type = lastType; + } else { + type = ResourceType.getEnum(typeName); + lastType = type; + lastTypeName = typeName; + } + if (type != null) { + ResourceItem match = null; + Map<String, ResourceItem> map = mResourceMap.get(type); + if (map != null) { + match = map.get(name); + } + + if (match != null) { + List<ResourceItem> publicList = mPublicResourceMap.get(type); + if (publicList == null) { + // Pick initial size for the list to hold the public + // resources. We could just use map.size() here, + // but they're usually much bigger; for example, + // in one platform version, there are 1500 drawables + // and 1200 strings but only 175 and 25 public ones + // respectively. + int size; + switch (type) { + case STYLE: size = 500; break; + case ATTR: size = 1000; break; + case DRAWABLE: size = 200; break; + case ID: size = 50; break; + case LAYOUT: + case COLOR: + case STRING: + case ANIM: + case INTERPOLATOR: + size = 30; + break; + default: + size = 10; + break; + } + publicList = new ArrayList<ResourceItem>(size); + mPublicResourceMap.put(type, publicList); + } + + publicList.add(match); + } else { + // log that there's a public resource that doesn't actually + // exist? + } + } else { + // log that there was a reference to a typo that doesn't actually + // exist? + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + } catch (Exception e) { + if (logger != null) { + logger.error(e, "Can't read and parse public attribute list"); + } + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // Nothing to be done here - we don't care if it closed or not. + } + } + } + } + + // put unmodifiable list for all res type in the public resource map + // this will simplify access + for (ResourceType type : ResourceType.values()) { + List<ResourceItem> list = mPublicResourceMap.get(type); + if (list == null) { + list = Collections.emptyList(); + } else { + list = Collections.unmodifiableList(list); + } + + // put the new list in the map + mPublicResourceMap.put(type, list); + } + } +} + diff --git a/sdk_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java b/sdk_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java new file mode 100644 index 0000000..9ff1748 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +import com.android.ide.common.rendering.api.DensityBasedResourceValue; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ValueResourceParser.IValueResourceRepository; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; +import com.android.resources.ResourceType; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Represents a resource file that also generates ID resources. + * <p/> + * This is typically an XML file in res/layout or res/menu + */ +public final class IdGeneratingResourceFile extends ResourceFile + implements IValueResourceRepository { + + private final Map<String, ResourceValue> mIdResources = + new HashMap<String, ResourceValue>(); + + private final Collection<ResourceType> mResourceTypeList; + + private final String mFileName; + + private final ResourceType mFileType; + + private final ResourceValue mFileValue; + + public IdGeneratingResourceFile(IAbstractFile file, ResourceFolder folder, ResourceType type) { + super(file, folder); + + mFileType = type; + + // Set up our resource types + mResourceTypeList = new HashSet<ResourceType>(); + mResourceTypeList.add(mFileType); + mResourceTypeList.add(ResourceType.ID); + + // compute the resource name + mFileName = getFileName(type); + + // Get the resource value of this file as a whole layout + mFileValue = getFileValue(file, folder); + } + + @Override + protected void load(ScanningContext context) { + // Parse the file and look for @+id/ entries + parseFileForIds(context); + + // create the resource items in the repository + updateResourceItems(context); + } + + @Override + protected void update(ScanningContext context) { + // Copy the previous list of ID names + Set<String> oldIdNames = new HashSet<String>(mIdResources.keySet()); + + // reset current content. + mIdResources.clear(); + + // need to parse the file and find the IDs. + if (!parseFileForIds(context)) { + context.requestFullAapt(); + // Continue through to updating the resource item here since it + // will make for example layout rendering more accurate until + // aapt is re-run + } + + // We only need to update the repository if our IDs have changed + Set<String> keySet = mIdResources.keySet(); + assert keySet != oldIdNames; + if (oldIdNames.equals(keySet) == false) { + updateResourceItems(context); + } + } + + @Override + protected void dispose(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // Remove declarations from this file from the repository + repository.removeFile(mResourceTypeList, this); + + // Ask for an ID refresh since we'll be taking away ID generating items + context.requestFullAapt(); + } + + @Override + public Collection<ResourceType> getResourceTypes() { + return mResourceTypeList; + } + + @Override + public boolean hasResources(ResourceType type) { + return (type == mFileType) || (type == ResourceType.ID && !mIdResources.isEmpty()); + } + + @Override + public ResourceValue getValue(ResourceType type, String name) { + // Check to see if they're asking for one of the right types: + if (type != mFileType && type != ResourceType.ID) { + return null; + } + + // If they're looking for a resource of this type with this name give them the whole file + if (type == mFileType && name.equals(mFileName)) { + return mFileValue; + } else { + // Otherwise try to return them an ID + // the map will return null if it's not found + return mIdResources.get(name); + } + } + + /** + * Looks through the file represented for Ids and adds them to + * our id repository + * + * @return true if parsing succeeds and false if it fails + */ + private boolean parseFileForIds(ScanningContext context) { + IdResourceParser parser = new IdResourceParser(this, context, isFramework()); + try { + IAbstractFile file = getFile(); + return parser.parse(mFileType, file.getOsLocation(), file.getContents()); + } catch (IOException e) { + // Pass + } catch (StreamException e) { + // Pass + } + + return false; + } + + /** + * Add the resources represented by this file to the repository + */ + private void updateResourceItems(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // remove this file from all existing ResourceItem. + repository.removeFile(mResourceTypeList, this); + + // First add this as a layout file + ResourceItem item = repository.getResourceItem(mFileType, mFileName); + item.add(this); + + // Now iterate through our IDs and add + for (String idName : mIdResources.keySet()) { + item = repository.getResourceItem(ResourceType.ID, idName); + // add this file to the list of files generating ID resources. + item.add(this); + } + + // Ask the repository for an ID refresh + context.requestFullAapt(); + } + + /** + * Returns the resource value associated with this whole file as a layout resource + * @param file the file handler that represents this file + * @param folder the folder this file is under + * @return a resource value associated with this layout + */ + private ResourceValue getFileValue(IAbstractFile file, ResourceFolder folder) { + // test if there's a density qualifier associated with the resource + DensityQualifier qualifier = folder.getConfiguration().getDensityQualifier(); + + ResourceValue value; + if (qualifier == null) { + value = new ResourceValue(mFileType, mFileName, + file.getOsLocation(), isFramework()); + } else { + value = new DensityBasedResourceValue( + mFileType, mFileName, + file.getOsLocation(), + qualifier.getValue(), + isFramework()); + } + return value; + } + + + /** + * Returns the name of this resource. + */ + private String getFileName(ResourceType type) { + // get the name from the filename. + String name = getFile().getName(); + + int pos = name.indexOf('.'); + if (pos != -1) { + name = name.substring(0, pos); + } + + return name; + } + + @Override + public void addResourceValue(ResourceValue value) { + // Just overwrite collisions. We're only interested in the unique + // IDs declared + mIdResources.put(value.getName(), value); + } + + @Override + public boolean hasResourceValue(ResourceType type, String name) { + if (type == ResourceType.ID) { + return mIdResources.containsKey(name); + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java b/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java new file mode 100644 index 0000000..1de664e --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ValueResourceParser.IValueResourceRepository; +import com.android.resources.ResourceType; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Parser for scanning an id-generating resource file such as a layout or a menu + * file, which registers any ids it encounters with an + * {@link IValueResourceRepository}, and which registers errors with a + * {@link ScanningContext}. + */ +public class IdResourceParser { + private final IValueResourceRepository mRepository; + private final boolean mIsFramework; + private ScanningContext mContext; + + /** + * Creates a new {@link IdResourceParser} + * + * @param repository value repository for registering resource declaration + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @param isFramework true if scanning a framework resource + */ + public IdResourceParser(IValueResourceRepository repository, ScanningContext context, + boolean isFramework) { + mRepository = repository; + mContext = context; + mIsFramework = isFramework; + } + + /** + * Parse the given input and register ids with the given + * {@link IValueResourceRepository}. + * + * @param type the type of resource being scanned + * @param path the full OS path to the file being parsed + * @param input the input stream of the XML to be parsed + * @return true if parsing succeeds and false if it fails + * @throws IOException if reading the contents fails + */ + public boolean parse(ResourceType type, final String path, InputStream input) + throws IOException { + KXmlParser parser = new KXmlParser(); + try { + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + if (input instanceof FileInputStream) { + input = new BufferedInputStream(input); + } + parser.setInput(input, "UTF-8"); //$NON-NLS-1$ + + return parse(type, path, parser); + } catch (XmlPullParserException e) { + String message = e.getMessage(); + + // Strip off position description + int index = message.indexOf("(position:"); //$NON-NLS-1$ (Hardcoded in KXml) + if (index != -1) { + message = message.substring(0, index); + } + + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } catch (RuntimeException e) { + // Some exceptions are thrown by the KXmlParser that are not XmlPullParserExceptions, + // such as this one: + // java.lang.RuntimeException: Undefined Prefix: w in org.kxml2.io.KXmlParser@... + // at org.kxml2.io.KXmlParser.adjustNsp(Unknown Source) + // at org.kxml2.io.KXmlParser.parseStartTag(Unknown Source) + String message = e.getMessage(); + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } + } + + private boolean parse(ResourceType type, String path, KXmlParser parser) + throws XmlPullParserException, IOException { + boolean valid = true; + ResourceRepository resources = mContext.getRepository(); + boolean checkForErrors = !mIsFramework && !mContext.needsFullAapt(); + + while (true) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + for (int i = 0, n = parser.getAttributeCount(); i < n; i++) { + String attribute = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + assert value != null : attribute; + + if (value.startsWith("@")) { //$NON-NLS-1$ + // Gather IDs + if (value.startsWith("@+")) { //$NON-NLS-1$ + // Strip out the @+id/ or @+android:id/ section + String id = value.substring(value.indexOf('/') + 1); + ResourceValue newId = new ResourceValue(ResourceType.ID, id, + mIsFramework); + mRepository.addResourceValue(newId); + } else if (checkForErrors){ + // Validate resource references (unless we're scanning a framework + // resource or if we've already scheduled a full aapt run) + boolean exists = resources.hasResourceItem(value); + if (!exists && !mRepository.hasResourceValue(ResourceType.ID, + value.substring(value.indexOf('/') + 1))) { + String error = String.format( + // Don't localize because the exact pattern matches AAPT's + // output which has hardcoded regexp matching in + // AaptParser. + "%1$s:%2$d: Error: No resource found that matches " + //$NON-NLS-1$ + "the given name (at '%3$s' with value '%4$s')", //$NON-NLS-1$ + path, parser.getLineNumber(), + attribute, value); + mContext.addError(error); + valid = false; + } + } + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + + return valid; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/InlineResourceItem.java b/sdk_common/src/com/android/ide/common/resources/InlineResourceItem.java new file mode 100644 index 0000000..37fdc81 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/InlineResourceItem.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008 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.ide.common.resources; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.resources.ResourceType; + + +/** + * Represents a resource item that has been declared inline in another resource file. + * + * This covers the typical ID declaration of "@+id/foo", but does not cover normal value + * resources declared in strings.xml or other similar value files. + * + * This resource will return {@code true} for {@link #isDeclaredInline()} and {@code false} for + * {@link #isEditableDirectly()}. + */ +public class InlineResourceItem extends ResourceItem { + + private ResourceValue mValue = null; + + /** + * Constructs a new inline ResourceItem. + * @param name the name of the resource as it appears in the XML and R.java files. + */ + public InlineResourceItem(String name) { + super(name); + } + + @Override + public boolean isDeclaredInline() { + return true; + } + + @Override + public boolean isEditableDirectly() { + return false; + } + + @Override + public ResourceValue getResourceValue(ResourceType type, FolderConfiguration referenceConfig, + boolean isFramework) { + assert type == ResourceType.ID; + if (mValue == null) { + mValue = new ResourceValue(type, getName(), isFramework); + } + + return mValue; + } + + @Override + public String toString() { + return "InlineResourceItem [mName=" + getName() + ", mFiles=" //$NON-NLS-1$ //$NON-NLS-2$ + + getSourceFileList() + "]"; //$NON-NLS-1$ + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/IntArrayWrapper.java b/sdk_common/src/com/android/ide/common/resources/IntArrayWrapper.java new file mode 100644 index 0000000..668c677 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/IntArrayWrapper.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 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.ide.common.resources; + +import java.util.Arrays; + + +/** + * Wrapper around a int[] to provide hashCode/equals support. + */ +public final class IntArrayWrapper { + + private int[] mData; + + public IntArrayWrapper(int[] data) { + mData = data; + } + + public void set(int[] data) { + mData = data; + } + + @Override + public int hashCode() { + return Arrays.hashCode(mData); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass().equals(obj.getClass())) { + return Arrays.equals(mData, ((IntArrayWrapper)obj).mData); + } + + return super.equals(obj); + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/MultiResourceFile.java b/sdk_common/src/com/android/ide/common/resources/MultiResourceFile.java new file mode 100644 index 0000000..c9a8bc7 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/MultiResourceFile.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2007 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.ide.common.resources; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ValueResourceParser.IValueResourceRepository; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; +import com.android.resources.ResourceType; + +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Represents a resource file able to declare multiple resources, which could be of + * different {@link ResourceType}. + * <p/> + * This is typically an XML file inside res/values. + */ +public final class MultiResourceFile extends ResourceFile implements IValueResourceRepository { + + private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance(); + + private final Map<ResourceType, Map<String, ResourceValue>> mResourceItems = + new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class); + + private Collection<ResourceType> mResourceTypeList = null; + + public MultiResourceFile(IAbstractFile file, ResourceFolder folder) { + super(file, folder); + } + + // Boolean flag to track whether a named element has been added or removed, thus requiring + // a new ID table to be generated + private boolean mNeedIdRefresh; + + @Override + protected void load(ScanningContext context) { + // need to parse the file and find the content. + parseFile(); + + // create new ResourceItems for the new content. + mResourceTypeList = Collections.unmodifiableCollection(mResourceItems.keySet()); + + // We need an ID generation step + mNeedIdRefresh = true; + + // create/update the resource items. + updateResourceItems(context); + } + + @Override + protected void update(ScanningContext context) { + // Reset the ID generation flag + mNeedIdRefresh = false; + + // Copy the previous version of our list of ResourceItems and types + Map<ResourceType, Map<String, ResourceValue>> oldResourceItems + = new EnumMap<ResourceType, Map<String, ResourceValue>>(mResourceItems); + + // reset current content. + mResourceItems.clear(); + + // need to parse the file and find the content. + parseFile(); + + // create new ResourceItems for the new content. + mResourceTypeList = Collections.unmodifiableCollection(mResourceItems.keySet()); + + // Check to see if any names have changed. If so, mark the flag so updateResourceItems + // can notify the ResourceRepository that an ID refresh is needed + if (oldResourceItems.keySet().equals(mResourceItems.keySet())) { + for (ResourceType type : mResourceTypeList) { + // We just need to check the names of the items. + // If there are new or removed names then we'll have to regenerate IDs + if (mResourceItems.get(type).keySet() + .equals(oldResourceItems.get(type).keySet()) == false) { + mNeedIdRefresh = true; + } + } + } else { + // If our type list is different, obviously the names will be different + mNeedIdRefresh = true; + } + // create/update the resource items. + updateResourceItems(context); + } + + @Override + protected void dispose(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // only remove this file from all existing ResourceItem. + repository.removeFile(mResourceTypeList, this); + + // We'll need an ID refresh because we deleted items + context.requestFullAapt(); + + // don't need to touch the content, it'll get reclaimed as this objects disappear. + // In the mean time other objects may need to access it. + } + + @Override + public Collection<ResourceType> getResourceTypes() { + return mResourceTypeList; + } + + @Override + public boolean hasResources(ResourceType type) { + Map<String, ResourceValue> list = mResourceItems.get(type); + return (list != null && list.size() > 0); + } + + private void updateResourceItems(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // remove this file from all existing ResourceItem. + repository.removeFile(mResourceTypeList, this); + + for (ResourceType type : mResourceTypeList) { + Map<String, ResourceValue> list = mResourceItems.get(type); + + if (list != null) { + Collection<ResourceValue> values = list.values(); + for (ResourceValue res : values) { + ResourceItem item = repository.getResourceItem(type, res.getName()); + + // add this file to the list of files generating this resource item. + item.add(this); + } + } + } + + // If we need an ID refresh, ask the repository for that now + if (mNeedIdRefresh) { + context.requestFullAapt(); + } + } + + /** + * Parses the file and creates a list of {@link ResourceType}. + */ + private void parseFile() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + parser.parse(getFile().getContents(), new ValueResourceParser(this, isFramework())); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } catch (StreamException e) { + } + } + + /** + * Adds a resource item to the list + * @param value The value of the resource. + */ + @Override + public void addResourceValue(ResourceValue value) { + ResourceType resType = value.getResourceType(); + + Map<String, ResourceValue> list = mResourceItems.get(resType); + + // if the list does not exist, create it. + if (list == null) { + list = new HashMap<String, ResourceValue>(); + mResourceItems.put(resType, list); + } else { + // look for a possible value already existing. + ResourceValue oldValue = list.get(value.getName()); + + if (oldValue != null) { + oldValue.replaceWith(value); + return; + } + } + + // empty list or no match found? add the given resource + list.put(value.getName(), value); + } + + @Override + public boolean hasResourceValue(ResourceType type, String name) { + Map<String, ResourceValue> map = mResourceItems.get(type); + return map != null && map.containsKey(name); + } + + @Override + public ResourceValue getValue(ResourceType type, String name) { + // get the list for the given type + Map<String, ResourceValue> list = mResourceItems.get(type); + + if (list != null) { + return list.get(name); + } + + return null; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceDeltaKind.java b/sdk_common/src/com/android/ide/common/resources/ResourceDeltaKind.java new file mode 100644 index 0000000..769b6ea --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ResourceDeltaKind.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +/** + * Enum indicating a type of resource change. + * + * This is similar, and can be easily mapped to Eclipse's integer constants in IResourceDelta. + */ +public enum ResourceDeltaKind { + CHANGED, ADDED, REMOVED; +} diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceFile.java b/sdk_common/src/com/android/ide/common/resources/ResourceFile.java new file mode 100644 index 0000000..378602a --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ResourceFile.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2007 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.ide.common.resources; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.Configurable; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.io.IAbstractFile; +import com.android.resources.ResourceType; + +import java.util.Collection; + +/** + * Represents a Resource file (a file under $Project/res/) + */ +public abstract class ResourceFile implements Configurable { + + private final IAbstractFile mFile; + private final ResourceFolder mFolder; + + protected ResourceFile(IAbstractFile file, ResourceFolder folder) { + mFile = file; + mFolder = folder; + } + + protected abstract void load(ScanningContext context); + protected abstract void update(ScanningContext context); + protected abstract void dispose(ScanningContext context); + + @Override + public FolderConfiguration getConfiguration() { + return mFolder.getConfiguration(); + } + + /** + * Returns the IFile associated with the ResourceFile. + */ + public final IAbstractFile getFile() { + return mFile; + } + + /** + * Returns the parent folder as a {@link ResourceFolder}. + */ + public final ResourceFolder getFolder() { + return mFolder; + } + + public final ResourceRepository getRepository() { + return mFolder.getRepository(); + } + + /** + * Returns whether the resource is a framework resource. + */ + public final boolean isFramework() { + return mFolder.getRepository().isFrameworkRepository(); + } + + /** + * Returns the list of {@link ResourceType} generated by the file. This is never null. + */ + public abstract Collection<ResourceType> getResourceTypes(); + + /** + * Returns whether the file generated a resource of a specific type. + * @param type The {@link ResourceType} + */ + public abstract boolean hasResources(ResourceType type); + + /** + * Returns the value of a resource generated by this file by {@link ResourceType} and name. + * <p/>If no resource match, <code>null</code> is returned. + * @param type the type of the resource. + * @param name the name of the resource. + */ + public abstract ResourceValue getValue(ResourceType type, String name); + + @Override + public String toString() { + return mFile.toString(); + } +} + diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceFolder.java b/sdk_common/src/com/android/ide/common/resources/ResourceFolder.java new file mode 100644 index 0000000..d6464c8 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ResourceFolder.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2007 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.ide.common.resources; + +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.ide.common.resources.configuration.Configurable; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Resource Folder class. Contains list of {@link ResourceFile}s, + * the {@link FolderConfiguration}, and a link to the {@link IAbstractFolder} object. + */ +public final class ResourceFolder implements Configurable { + final ResourceFolderType mType; + final FolderConfiguration mConfiguration; + IAbstractFolder mFolder; + List<ResourceFile> mFiles = null; + Map<String, ResourceFile> mNames = null; + private final ResourceRepository mRepository; + + /** + * Creates a new {@link ResourceFolder} + * @param type The type of the folder + * @param config The configuration of the folder + * @param folder The associated {@link IAbstractFolder} object. + * @param repository The associated {@link ResourceRepository} + */ + protected ResourceFolder(ResourceFolderType type, FolderConfiguration config, + IAbstractFolder folder, ResourceRepository repository) { + mType = type; + mConfiguration = config; + mFolder = folder; + mRepository = repository; + } + + /** + * Processes a file and adds it to its parent folder resource. + * + * @param file the underlying resource file. + * @param kind the file change kind. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @return the {@link ResourceFile} that was created. + */ + public ResourceFile processFile(IAbstractFile file, ResourceDeltaKind kind, + ScanningContext context) { + // look for this file if it's already been created + ResourceFile resFile = getFile(file, context); + + if (resFile == null) { + if (kind != ResourceDeltaKind.REMOVED) { + // create a ResourceFile for it. + + resFile = createResourceFile(file); + resFile.load(context); + + // add it to the folder + addFile(resFile); + } + } else { + if (kind == ResourceDeltaKind.REMOVED) { + removeFile(resFile, context); + } else { + resFile.update(context); + } + } + + return resFile; + } + + private ResourceFile createResourceFile(IAbstractFile file) { + // check if that's a single or multi resource type folder. For now we define this by + // the number of possible resource type output by files in the folder. + // We have a special case for layout/menu folders which can also generate IDs. + // This does + // not make the difference between several resource types from a single file or + // the ability to have 2 files in the same folder generating 2 different types of + // resource. The former is handled by MultiResourceFile properly while we don't + // handle the latter. If we were to add this behavior we'd have to change this call. + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(mType); + + ResourceFile resFile = null; + if (types.size() == 1) { + resFile = new SingleResourceFile(file, this); + } else if (types.contains(ResourceType.LAYOUT)) { + resFile = new IdGeneratingResourceFile(file, this, ResourceType.LAYOUT); + } else if (types.contains(ResourceType.MENU)) { + resFile = new IdGeneratingResourceFile(file, this, ResourceType.MENU); + } else { + resFile = new MultiResourceFile(file, this); + } + return resFile; + } + + /** + * Adds a {@link ResourceFile} to the folder. + * @param file The {@link ResourceFile}. + */ + @VisibleForTesting(visibility=Visibility.PROTECTED) + public void addFile(ResourceFile file) { + if (mFiles == null) { + int initialSize = 16; + if (mRepository.isFrameworkRepository()) { + String name = mFolder.getName(); + // Pick some reasonable initial sizes for framework data structures + // since they are typically (a) large and (b) their sizes are roughly known + // in advance + switch (mType) { + case DRAWABLE: { + // See if it's one of the -mdpi, -hdpi etc folders which + // are large (~1250 items) + int index = name.indexOf('-'); + if (index == -1) { + initialSize = 230; // "drawable" folder + } else { + index = name.indexOf('-', index + 1); + if (index == -1) { + // One of the "drawable-<density>" folders + initialSize = 1260; + } else { + // "drawable-sw600dp-hdpi" etc + initialSize = 30; + } + } + break; + } + case LAYOUT: { + // The main layout folder has about ~185 layouts in it; + // the others are small + if (name.indexOf('-') == -1) { + initialSize = 200; + } + break; + } + case VALUES: { + if (name.indexOf('-') == -1) { + initialSize = 32; + } else { + initialSize = 4; + } + break; + } + case ANIM: initialSize = 85; break; + case COLOR: initialSize = 32; break; + case RAW: initialSize = 4; break; + default: + // Stick with the 16 default + break; + } + } + + mFiles = new ArrayList<ResourceFile>(initialSize); + mNames = new HashMap<String, ResourceFile>(initialSize, 2.0f); + } + + mFiles.add(file); + mNames.put(file.getFile().getName(), file); + } + + protected void removeFile(ResourceFile file, ScanningContext context) { + file.dispose(context); + mFiles.remove(file); + mNames.remove(file.getFile().getName()); + } + + protected void dispose(ScanningContext context) { + if (mFiles != null) { + for (ResourceFile file : mFiles) { + file.dispose(context); + } + + mFiles.clear(); + mNames.clear(); + } + } + + /** + * Returns the {@link IAbstractFolder} associated with this object. + */ + public IAbstractFolder getFolder() { + return mFolder; + } + + /** + * Returns the {@link ResourceFolderType} of this object. + */ + public ResourceFolderType getType() { + return mType; + } + + public ResourceRepository getRepository() { + return mRepository; + } + + /** + * Returns the list of {@link ResourceType}s generated by the files inside this folder. + */ + public Collection<ResourceType> getResourceTypes() { + ArrayList<ResourceType> list = new ArrayList<ResourceType>(); + + if (mFiles != null) { + for (ResourceFile file : mFiles) { + Collection<ResourceType> types = file.getResourceTypes(); + + // loop through those and add them to the main list, + // if they are not already present + for (ResourceType resType : types) { + if (list.indexOf(resType) == -1) { + list.add(resType); + } + } + } + } + + return list; + } + + @Override + public FolderConfiguration getConfiguration() { + return mConfiguration; + } + + /** + * Returns whether the folder contains a file with the given name. + * @param name the name of the file. + */ + public boolean hasFile(String name) { + if (mNames != null && mNames.containsKey(name)) { + return true; + } + + // Note: mNames.containsKey(name) is faster, but doesn't give the same result; this + // method seems to be called on this ResourceFolder before it has been processed, + // so we need to use the file system check instead: + return mFolder.hasFile(name); + } + + /** + * Returns the {@link ResourceFile} matching a {@link IAbstractFile} object. + * + * @param file The {@link IAbstractFile} object. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @return the {@link ResourceFile} or null if no match was found. + */ + private ResourceFile getFile(IAbstractFile file, ScanningContext context) { + assert mFolder.equals(file.getParentFolder()); + + if (mNames != null) { + ResourceFile resFile = mNames.get(file.getName()); + if (resFile != null) { + return resFile; + } + } + + // If the file actually exists, the resource folder may not have been + // scanned yet; add it lazily + if (file.exists()) { + ResourceFile resFile = createResourceFile(file); + resFile.load(context); + addFile(resFile); + return resFile; + } + + return null; + } + + /** + * Returns the {@link ResourceFile} matching a given name. + * @param filename The name of the file to return. + * @return the {@link ResourceFile} or <code>null</code> if no match was found. + */ + public ResourceFile getFile(String filename) { + if (mNames != null) { + ResourceFile resFile = mNames.get(filename); + if (resFile != null) { + return resFile; + } + } + + // If the file actually exists, the resource folder may not have been + // scanned yet; add it lazily + IAbstractFile file = mFolder.getFile(filename); + if (file != null && file.exists()) { + ResourceFile resFile = createResourceFile(file); + resFile.load(new ScanningContext(mRepository)); + addFile(resFile); + return resFile; + } + + return null; + } + + /** + * Returns whether a file in the folder is generating a resource of a specified type. + * @param type The {@link ResourceType} being looked up. + */ + public boolean hasResources(ResourceType type) { + // Check if the folder type is able to generate resource of the type that was asked. + // this is a first check to avoid going through the files. + List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); + + boolean valid = false; + for (ResourceFolderType rft : folderTypes) { + if (rft == mType) { + valid = true; + break; + } + } + + if (valid) { + if (mFiles != null) { + for (ResourceFile f : mFiles) { + if (f.hasResources(type)) { + return true; + } + } + } + } + return false; + } + + @Override + public String toString() { + return mFolder.toString(); + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceItem.java b/sdk_common/src/com/android/ide/common/resources/ResourceItem.java new file mode 100644 index 0000000..49396eb --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ResourceItem.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.resources.ResourceType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * An android resource. + * + * This is a representation of the resource, not of its value(s). It gives access to all + * the source files that generate this particular resource which then can be used to access + * the actual value(s). + * + * @see ResourceFile#getResources(ResourceType, ResourceRepository) + */ +public class ResourceItem implements Comparable<ResourceItem> { + + private final static Comparator<ResourceFile> sComparator = new Comparator<ResourceFile>() { + @Override + public int compare(ResourceFile file1, ResourceFile file2) { + // get both FolderConfiguration and compare them + FolderConfiguration fc1 = file1.getFolder().getConfiguration(); + FolderConfiguration fc2 = file2.getFolder().getConfiguration(); + + return fc1.compareTo(fc2); + } + }; + + private final String mName; + + /** + * List of files generating this ResourceItem. + */ + private final List<ResourceFile> mFiles = new ArrayList<ResourceFile>(); + + /** + * Constructs a new ResourceItem. + * @param name the name of the resource as it appears in the XML and R.java files. + */ + public ResourceItem(String name) { + mName = name; + } + + /** + * Returns the name of the resource. + */ + public final String getName() { + return mName; + } + + /** + * Compares the {@link ResourceItem} to another. + * @param other the ResourceItem to be compared to. + */ + @Override + public int compareTo(ResourceItem other) { + return mName.compareTo(other.mName); + } + + /** + * Returns whether the resource is editable directly. + * <p/> + * This is typically the case for resources that don't have alternate versions, or resources + * of type {@link ResourceType#ID} that aren't declared inline. + */ + public boolean isEditableDirectly() { + return hasAlternates() == false; + } + + /** + * Returns whether the ID resource has been declared inline inside another resource XML file. + * If the resource type is not {@link ResourceType#ID}, this will always return {@code false}. + */ + public boolean isDeclaredInline() { + return false; + } + + /** + * Returns a {@link ResourceValue} for this item based on the given configuration. + * If the ResourceItem has several source files, one will be selected based on the config. + * @param type the type of the resource. This is necessary because ResourceItem doesn't embed + * its type, but ResourceValue does. + * @param referenceConfig the config of the resource item. + * @param isFramework whether the resource is a framework value. Same as the type. + * @return a ResourceValue or null if none match the config. + */ + public ResourceValue getResourceValue(ResourceType type, FolderConfiguration referenceConfig, + boolean isFramework) { + // look for the best match for the given configuration + // the match has to be of type ResourceFile since that's what the input list contains + ResourceFile match = (ResourceFile) referenceConfig.findMatchingConfigurable(mFiles); + + if (match != null) { + // get the value of this configured resource. + return match.getValue(type, mName); + } + + return null; + } + + /** + * Adds a new source file. + * @param file the source file. + */ + protected void add(ResourceFile file) { + mFiles.add(file); + } + + /** + * Removes a file from the list of source files. + * @param file the file to remove + */ + protected void removeFile(ResourceFile file) { + mFiles.remove(file); + } + + /** + * Returns {@code true} if the item has no source file. + * @return + */ + protected boolean hasNoSourceFile() { + return mFiles.size() == 0; + } + + /** + * Reset the item by emptying its source file list. + */ + protected void reset() { + mFiles.clear(); + } + + /** + * Returns the sorted list of {@link ResourceItem} objects for this resource item. + */ + public ResourceFile[] getSourceFileArray() { + ArrayList<ResourceFile> list = new ArrayList<ResourceFile>(); + list.addAll(mFiles); + + Collections.sort(list, sComparator); + + return list.toArray(new ResourceFile[list.size()]); + } + + /** + * Returns the list of source file for this resource. + */ + public List<ResourceFile> getSourceFileList() { + return Collections.unmodifiableList(mFiles); + } + + /** + * Returns if the resource has at least one non-default version. + * + * @see ResourceFile#getConfiguration() + * @see FolderConfiguration#isDefault() + */ + public boolean hasAlternates() { + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault() == false) { + return true; + } + } + + return false; + } + + /** + * Returns whether the resource has a default version, with no qualifier. + * + * @see ResourceFile#getConfiguration() + * @see FolderConfiguration#isDefault() + */ + public boolean hasDefault() { + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault()) { + return true; + } + } + + // We only want to return false if there's no default and more than 0 items. + return (mFiles.size() == 0); + } + + /** + * Returns the number of alternate versions for this resource. + * + * @see ResourceFile#getConfiguration() + * @see FolderConfiguration#isDefault() + */ + public int getAlternateCount() { + int count = 0; + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault() == false) { + count++; + } + } + + return count; + } + + /** + * Returns a formatted string usable in an XML to use for the {@link ResourceItem}. + * @param system Whether this is a system resource or a project resource. + * @return a string in the format @[type]/[name] + */ + public String getXmlString(ResourceType type, boolean system) { + if (type == ResourceType.ID && isDeclaredInline()) { + return (system ? "@android:" : "@+") + type.getName() + "/" + mName; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + return (system ? "@android:" : "@") + type.getName() + "/" + mName; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + public String toString() { + return "ResourceItem [mName=" + mName + ", mFiles=" + mFiles + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java b/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java new file mode 100755 index 0000000..ac0614d --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java @@ -0,0 +1,719 @@ +/* + * Copyright (C) 2007 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.ide.common.resources; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.Configurable; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.io.IAbstractResource; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Base class for resource repository. + * + * A repository is both a file representation of a resource folder and a representation + * of the generated resources, organized by type. + * + * {@link #getResourceFolder(IAbstractFolder)} and {@link #getSourceFiles(ResourceType, String, FolderConfiguration)} + * give access to the folders and files of the resource folder. + * + * {@link #getResources(ResourceType)} gives access to the resources directly. + * + */ +public abstract class ResourceRepository { + + protected final Map<ResourceFolderType, List<ResourceFolder>> mFolderMap = + new EnumMap<ResourceFolderType, List<ResourceFolder>>(ResourceFolderType.class); + + protected final Map<ResourceType, Map<String, ResourceItem>> mResourceMap = + new EnumMap<ResourceType, Map<String, ResourceItem>>( + ResourceType.class); + + private final Map<Map<String, ResourceItem>, Collection<ResourceItem>> mReadOnlyListMap = + new IdentityHashMap<Map<String, ResourceItem>, Collection<ResourceItem>>(); + + private final boolean mFrameworkRepository; + + protected final IntArrayWrapper mWrapper = new IntArrayWrapper(null); + + /** + * Makes a resource repository + * @param isFrameworkRepository whether the repository is for framework resources. + */ + protected ResourceRepository(boolean isFrameworkRepository) { + mFrameworkRepository = isFrameworkRepository; + } + + public boolean isFrameworkRepository() { + return mFrameworkRepository; + } + + /** + * Adds a Folder Configuration to the project. + * @param type The resource type. + * @param config The resource configuration. + * @param folder The workspace folder object. + * @return the {@link ResourceFolder} object associated to this folder. + */ + private ResourceFolder add( + @NonNull ResourceFolderType type, + @NonNull FolderConfiguration config, + @NonNull IAbstractFolder folder) { + // get the list for the resource type + List<ResourceFolder> list = mFolderMap.get(type); + + if (list == null) { + list = new ArrayList<ResourceFolder>(); + + ResourceFolder cf = new ResourceFolder(type, config, folder, this); + list.add(cf); + + mFolderMap.put(type, list); + + return cf; + } + + // look for an already existing folder configuration. + for (ResourceFolder cFolder : list) { + if (cFolder.mConfiguration.equals(config)) { + // config already exist. Nothing to be done really, besides making sure + // the IAbstractFolder object is up to date. + cFolder.mFolder = folder; + return cFolder; + } + } + + // If we arrive here, this means we didn't find a matching configuration. + // So we add one. + ResourceFolder cf = new ResourceFolder(type, config, folder, this); + list.add(cf); + + return cf; + } + + /** + * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}. + * @param type The type of the folder + * @param removedFolder the IAbstractFolder object. + * @param context the scanning context + * @return the {@link ResourceFolder} that was removed, or null if no matches were found. + */ + @Nullable + public ResourceFolder removeFolder( + @NonNull ResourceFolderType type, + @NonNull IAbstractFolder removedFolder, + @Nullable ScanningContext context) { + // get the list of folders for the resource type. + List<ResourceFolder> list = mFolderMap.get(type); + + if (list != null) { + int count = list.size(); + for (int i = 0 ; i < count ; i++) { + ResourceFolder resFolder = list.get(i); + IAbstractFolder folder = resFolder.getFolder(); + if (removedFolder.equals(folder)) { + // we found the matching ResourceFolder. we need to remove it. + list.remove(i); + + // remove its content + resFolder.dispose(context); + + return resFolder; + } + } + } + + return null; + } + + /** + * Returns true if this resource repository contains a resource of the given + * name. + * + * @param url the resource URL + * @return true if the resource is known + */ + public boolean hasResourceItem(@NonNull String url) { + assert url.startsWith("@") : url; + + int typeEnd = url.indexOf('/', 1); + if (typeEnd != -1) { + int nameBegin = typeEnd + 1; + + // Skip @ and @+ + int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$ + + int colon = url.lastIndexOf(':', typeEnd); + if (colon != -1) { + typeBegin = colon + 1; + } + String typeName = url.substring(typeBegin, typeEnd); + ResourceType type = ResourceType.getEnum(typeName); + if (type != null) { + String name = url.substring(nameBegin); + return hasResourceItem(type, name); + } + } + + return false; + } + + /** + * Returns true if this resource repository contains a resource of the given + * name. + * + * @param type the type of resource to look up + * @param name the name of the resource + * @return true if the resource is known + */ + public boolean hasResourceItem(@NonNull ResourceType type, @NonNull String name) { + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map != null) { + + ResourceItem resourceItem = map.get(name); + if (resourceItem != null) { + return true; + } + } + + return false; + } + + /** + * Returns a {@link ResourceItem} matching the given {@link ResourceType} and name. If none + * exist, it creates one. + * + * @param type the resource type + * @param name the name of the resource. + * @return A resource item matching the type and name. + */ + @NonNull + protected ResourceItem getResourceItem(@NonNull ResourceType type, @NonNull String name) { + // looking for an existing ResourceItem with this type and name + ResourceItem item = findDeclaredResourceItem(type, name); + + // create one if there isn't one already, or if the existing one is inlined, since + // clearly we need a non inlined one (the inline one is removed too) + if (item == null || item.isDeclaredInline()) { + ResourceItem oldItem = item != null && item.isDeclaredInline() ? item : null; + + item = createResourceItem(name); + + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map == null) { + if (isFrameworkRepository()) { + // Pick initial size for the maps. Also change the load factor to 1.0 + // to avoid rehashing the whole table when we (as expected) get near + // the known rough size of each resource type map. + int size; + switch (type) { + // Based on counts in API 16. Going back to API 10, the counts + // are roughly 25-50% smaller (e.g. compared to the top 5 types below + // the fractions are 1107 vs 1734, 831 vs 1508, 895 vs 1255, + // 733 vs 1064 and 171 vs 783. + case PUBLIC: size = 1734; break; + case DRAWABLE: size = 1508; break; + case STRING: size = 1255; break; + case ATTR: size = 1064; break; + case STYLE: size = 783; break; + case ID: size = 347; break; + case DECLARE_STYLEABLE: size = 210; break; + case LAYOUT: size = 187; break; + case COLOR: size = 120; break; + case ANIM: size = 95; break; + case DIMEN: size = 81; break; + case BOOL: size = 54; break; + case INTEGER: size = 52; break; + case ARRAY: size = 51; break; + case PLURALS: size = 20; break; + case XML: size = 14; break; + case INTERPOLATOR : size = 13; break; + case ANIMATOR: size = 8; break; + case RAW: size = 4; break; + case MENU: size = 2; break; + case MIPMAP: size = 2; break; + case FRACTION: size = 1; break; + default: + size = 2; + } + map = new HashMap<String, ResourceItem>(size, 1.0f); + } else { + map = new HashMap<String, ResourceItem>(); + } + mResourceMap.put(type, map); + } + + map.put(item.getName(), item); + + if (oldItem != null) { + map.remove(oldItem.getName()); + + } + } + + return item; + } + + /** + * Creates a resource item with the given name. + * @param name the name of the resource + * @return a new ResourceItem (or child class) instance. + */ + @NonNull + protected abstract ResourceItem createResourceItem(@NonNull String name); + + /** + * Processes a folder and adds it to the list of existing folders. + * @param folder the folder to process + * @return the ResourceFolder created from this folder, or null if the process failed. + */ + @Nullable + public ResourceFolder processFolder(@NonNull IAbstractFolder folder) { + // split the name of the folder in segments. + String[] folderSegments = folder.getName().split(SdkConstants.RES_QUALIFIER_SEP); + + // get the enum for the resource type. + ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]); + + if (type != null) { + // get the folder configuration. + FolderConfiguration config = FolderConfiguration.getConfig(folderSegments); + + if (config != null) { + return add(type, config, folder); + } + } + + return null; + } + + /** + * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}. + * @param type The {@link ResourceFolderType} + */ + @Nullable + public List<ResourceFolder> getFolders(@NonNull ResourceFolderType type) { + return mFolderMap.get(type); + } + + @NonNull + public List<ResourceType> getAvailableResourceTypes() { + List<ResourceType> list = new ArrayList<ResourceType>(); + + // For each key, we check if there's a single ResourceType match. + // If not, we look for the actual content to give us the resource type. + + for (ResourceFolderType folderType : mFolderMap.keySet()) { + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType); + if (types.size() == 1) { + // before we add it we check if it's not already present, since a ResourceType + // could be created from multiple folders, even for the folders that only create + // one type of resource (drawable for instance, can be created from drawable/ and + // values/) + if (list.contains(types.get(0)) == false) { + list.add(types.get(0)); + } + } else { + // there isn't a single resource type out of this folder, so we look for all + // content. + List<ResourceFolder> folders = mFolderMap.get(folderType); + if (folders != null) { + for (ResourceFolder folder : folders) { + Collection<ResourceType> folderContent = folder.getResourceTypes(); + + // then we add them, but only if they aren't already in the list. + for (ResourceType folderResType : folderContent) { + if (list.contains(folderResType) == false) { + list.add(folderResType); + } + } + } + } + } + } + + return list; + } + + /** + * Returns a list of {@link ResourceItem} matching a given {@link ResourceType}. + * @param type the type of the resource items to return + * @return a non null collection of resource items + */ + @NonNull + public Collection<ResourceItem> getResourceItemsOfType(@NonNull ResourceType type) { + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map == null) { + return Collections.emptyList(); + } + + Collection<ResourceItem> roList = mReadOnlyListMap.get(map); + if (roList == null) { + roList = Collections.unmodifiableCollection(map.values()); + mReadOnlyListMap.put(map, roList); + } + + return roList; + } + + /** + * Returns whether the repository has resources of a given {@link ResourceType}. + * @param type the type of resource to check. + * @return true if the repository contains resources of the given type, false otherwise. + */ + public boolean hasResourcesOfType(@NonNull ResourceType type) { + Map<String, ResourceItem> items = mResourceMap.get(type); + return (items != null && items.size() > 0); + } + + /** + * Returns the {@link ResourceFolder} associated with a {@link IAbstractFolder}. + * @param folder The {@link IAbstractFolder} object. + * @return the {@link ResourceFolder} or null if it was not found. + */ + @Nullable + public ResourceFolder getResourceFolder(@NonNull IAbstractFolder folder) { + Collection<List<ResourceFolder>> values = mFolderMap.values(); + + if (values.isEmpty()) { // This shouldn't be necessary, but has been observed + try { + loadResources(folder.getParentFolder()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + for (List<ResourceFolder> list : values) { + for (ResourceFolder resFolder : list) { + IAbstractFolder wrapper = resFolder.getFolder(); + if (wrapper.equals(folder)) { + return resFolder; + } + } + } + + return null; + } + + /** + * Returns the {@link ResourceFile} matching the given name, {@link ResourceFolderType} and + * configuration. + * <p/>This only works with files generating one resource named after the file (for instance, + * layouts, bitmap based drawable, xml, anims). + * @return the matching file or <code>null</code> if no match was found. + */ + @Nullable + public ResourceFile getMatchingFile(@NonNull String name, @NonNull ResourceFolderType type, + @NonNull FolderConfiguration config) { + // get the folders for the given type + List<ResourceFolder> folders = mFolderMap.get(type); + + // look for folders containing a file with the given name. + ArrayList<ResourceFolder> matchingFolders = new ArrayList<ResourceFolder>(folders.size()); + + // remove the folders that do not have a file with the given name. + for (int i = 0 ; i < folders.size(); i++) { + ResourceFolder folder = folders.get(i); + + if (folder.hasFile(name) == true) { + matchingFolders.add(folder); + } + } + + // from those, get the folder with a config matching the given reference configuration. + Configurable match = config.findMatchingConfigurable(matchingFolders); + + // do we have a matching folder? + if (match instanceof ResourceFolder) { + // get the ResourceFile from the filename + return ((ResourceFolder)match).getFile(name); + } + + return null; + } + + /** + * Returns the list of source files for a given resource. + * Optionally, if a {@link FolderConfiguration} is given, then only the best + * match for this config is returned. + * + * @param type the type of the resource. + * @param name the name of the resource. + * @param referenceConfig an optional config for which only the best match will be returned. + * + * @return a list of files generating this resource or null if it was not found. + */ + @Nullable + public List<ResourceFile> getSourceFiles(@NonNull ResourceType type, @NonNull String name, + @Nullable FolderConfiguration referenceConfig) { + + Collection<ResourceItem> items = getResourceItemsOfType(type); + + for (ResourceItem item : items) { + if (name.equals(item.getName())) { + if (referenceConfig != null) { + Configurable match = referenceConfig.findMatchingConfigurable( + item.getSourceFileList()); + + if (match instanceof ResourceFile) { + return Collections.singletonList((ResourceFile) match); + } + + return null; + } + return item.getSourceFileList(); + } + } + + return null; + } + + /** + * Returns the resources values matching a given {@link FolderConfiguration}. + * + * @param referenceConfig the configuration that each value must match. + * @return a map with guaranteed to contain an entry for each {@link ResourceType} + */ + @NonNull + public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources( + @NonNull FolderConfiguration referenceConfig) { + return doGetConfiguredResources(referenceConfig); + } + + /** + * Returns the resources values matching a given {@link FolderConfiguration} for the current + * project. + * + * @param referenceConfig the configuration that each value must match. + * @return a map with guaranteed to contain an entry for each {@link ResourceType} + */ + @NonNull + protected final Map<ResourceType, Map<String, ResourceValue>> doGetConfiguredResources( + @NonNull FolderConfiguration referenceConfig) { + + Map<ResourceType, Map<String, ResourceValue>> map = + new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class); + + for (ResourceType key : ResourceType.values()) { + // get the local results and put them in the map + map.put(key, getConfiguredResource(key, referenceConfig)); + } + + return map; + } + + /** + * Returns the sorted list of languages used in the resources. + */ + @NonNull + public SortedSet<String> getLanguages() { + SortedSet<String> set = new TreeSet<String>(); + + Collection<List<ResourceFolder>> folderList = mFolderMap.values(); + for (List<ResourceFolder> folderSubList : folderList) { + for (ResourceFolder folder : folderSubList) { + FolderConfiguration config = folder.getConfiguration(); + LanguageQualifier lang = config.getLanguageQualifier(); + if (lang != null) { + set.add(lang.getShortDisplayValue()); + } + } + } + + return set; + } + + /** + * Returns the sorted list of regions used in the resources with the given language. + * @param currentLanguage the current language the region must be associated with. + */ + @NonNull + public SortedSet<String> getRegions(@NonNull String currentLanguage) { + SortedSet<String> set = new TreeSet<String>(); + + Collection<List<ResourceFolder>> folderList = mFolderMap.values(); + for (List<ResourceFolder> folderSubList : folderList) { + for (ResourceFolder folder : folderSubList) { + FolderConfiguration config = folder.getConfiguration(); + + // get the language + LanguageQualifier lang = config.getLanguageQualifier(); + if (lang != null && lang.getShortDisplayValue().equals(currentLanguage)) { + RegionQualifier region = config.getRegionQualifier(); + if (region != null) { + set.add(region.getShortDisplayValue()); + } + } + } + } + + return set; + } + + /** + * Loads the resources from a resource folder. + * <p/> + * + * @param rootFolder The folder to read the resources from. This is the top level + * resource folder (res/) + * @throws IOException + */ + public void loadResources(@NonNull IAbstractFolder rootFolder) + throws IOException { + ScanningContext context = new ScanningContext(this); + + IAbstractResource[] files = rootFolder.listMembers(); + for (IAbstractResource file : files) { + if (file instanceof IAbstractFolder) { + IAbstractFolder folder = (IAbstractFolder) file; + ResourceFolder resFolder = processFolder(folder); + + if (resFolder != null) { + // now we process the content of the folder + IAbstractResource[] children = folder.listMembers(); + + for (IAbstractResource childRes : children) { + if (childRes instanceof IAbstractFile) { + resFolder.processFile((IAbstractFile) childRes, + ResourceDeltaKind.ADDED, context); + } + } + } + } + } + } + + + protected void removeFile(@NonNull Collection<ResourceType> types, + @NonNull ResourceFile file) { + for (ResourceType type : types) { + removeFile(type, file); + } + } + + protected void removeFile(@NonNull ResourceType type, @NonNull ResourceFile file) { + Map<String, ResourceItem> map = mResourceMap.get(type); + if (map != null) { + Collection<ResourceItem> values = map.values(); + for (ResourceItem item : values) { + item.removeFile(file); + } + } + } + + /** + * Returns a map of (resource name, resource value) for the given {@link ResourceType}. + * <p/>The values returned are taken from the resource files best matching a given + * {@link FolderConfiguration}. + * @param type the type of the resources. + * @param referenceConfig the configuration to best match. + */ + @NonNull + private Map<String, ResourceValue> getConfiguredResource(@NonNull ResourceType type, + @NonNull FolderConfiguration referenceConfig) { + + // get the resource item for the given type + Map<String, ResourceItem> items = mResourceMap.get(type); + if (items == null) { + return new HashMap<String, ResourceValue>(); + } + + // create the map + HashMap<String, ResourceValue> map = new HashMap<String, ResourceValue>(items.size()); + + for (ResourceItem item : items.values()) { + ResourceValue value = item.getResourceValue(type, referenceConfig, + isFrameworkRepository()); + if (value != null) { + map.put(item.getName(), value); + } + } + + return map; + } + + + /** + * Cleans up the repository of resource items that have no source file anymore. + */ + public void postUpdateCleanUp() { + // Since removed files/folders remove source files from existing ResourceItem, loop through + // all resource items and remove the ones that have no source files. + + Collection<Map<String, ResourceItem>> maps = mResourceMap.values(); + for (Map<String, ResourceItem> map : maps) { + Set<String> keySet = map.keySet(); + Iterator<String> iterator = keySet.iterator(); + while (iterator.hasNext()) { + String name = iterator.next(); + ResourceItem resourceItem = map.get(name); + if (resourceItem.hasNoSourceFile()) { + iterator.remove(); + } + } + } + } + + /** + * Looks up an existing {@link ResourceItem} by {@link ResourceType} and name. This + * ignores inline resources. + * @param type the Resource Type. + * @param name the Resource name. + * @return the existing ResourceItem or null if no match was found. + */ + @Nullable + private ResourceItem findDeclaredResourceItem(@NonNull ResourceType type, + @NonNull String name) { + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map != null) { + ResourceItem resourceItem = map.get(name); + if (resourceItem != null && !resourceItem.isDeclaredInline()) { + return resourceItem; + } + } + + return null; + } +} + diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java b/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java new file mode 100644 index 0000000..d742c4a --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ResourceResolver.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.PREFIX_ANDROID; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.SdkConstants.REFERENCE_STYLE; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.resources.ResourceType; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class ResourceResolver extends RenderResources { + + private final Map<ResourceType, Map<String, ResourceValue>> mProjectResources; + private final Map<ResourceType, Map<String, ResourceValue>> mFrameworkResources; + + private final Map<StyleResourceValue, StyleResourceValue> mStyleInheritanceMap = + new HashMap<StyleResourceValue, StyleResourceValue>(); + + private StyleResourceValue mTheme; + + private FrameworkResourceIdProvider mFrameworkProvider; + private LayoutLog mLogger; + private String mThemeName; + private boolean mIsProjectTheme; + + private ResourceResolver( + Map<ResourceType, Map<String, ResourceValue>> projectResources, + Map<ResourceType, Map<String, ResourceValue>> frameworkResources) { + mProjectResources = projectResources; + mFrameworkResources = frameworkResources; + } + + /** + * Creates a new {@link ResourceResolver} object. + * + * @param projectResources the project resources. + * @param frameworkResources the framework resources. + * @param themeName the name of the current theme. + * @param isProjectTheme Is this a project theme? + * @return a new {@link ResourceResolver} + */ + public static ResourceResolver create( + Map<ResourceType, Map<String, ResourceValue>> projectResources, + Map<ResourceType, Map<String, ResourceValue>> frameworkResources, + String themeName, boolean isProjectTheme) { + + ResourceResolver resolver = new ResourceResolver( + projectResources, frameworkResources); + + resolver.computeStyleMaps(themeName, isProjectTheme); + + return resolver; + } + + // ---- Methods to help dealing with older LayoutLibs. + + public String getThemeName() { + return mThemeName; + } + + public boolean isProjectTheme() { + return mIsProjectTheme; + } + + public Map<ResourceType, Map<String, ResourceValue>> getProjectResources() { + return mProjectResources; + } + + public Map<ResourceType, Map<String, ResourceValue>> getFrameworkResources() { + return mFrameworkResources; + } + + // ---- RenderResources Methods + + @Override + public void setFrameworkResourceIdProvider(FrameworkResourceIdProvider provider) { + mFrameworkProvider = provider; + } + + @Override + public void setLogger(LayoutLog logger) { + mLogger = logger; + } + + @Override + public StyleResourceValue getCurrentTheme() { + return mTheme; + } + + @Override + public StyleResourceValue getTheme(String name, boolean frameworkTheme) { + ResourceValue theme = null; + + if (frameworkTheme) { + Map<String, ResourceValue> frameworkStyleMap = mFrameworkResources.get( + ResourceType.STYLE); + theme = frameworkStyleMap.get(name); + } else { + Map<String, ResourceValue> projectStyleMap = mProjectResources.get(ResourceType.STYLE); + theme = projectStyleMap.get(name); + } + + if (theme instanceof StyleResourceValue) { + return (StyleResourceValue) theme; + } + + return null; + } + + @Override + public boolean themeIsParentOf(StyleResourceValue parentTheme, StyleResourceValue childTheme) { + do { + childTheme = mStyleInheritanceMap.get(childTheme); + if (childTheme == null) { + return false; + } else if (childTheme == parentTheme) { + return true; + } + } while (true); + } + + @Override + public ResourceValue getFrameworkResource(ResourceType resourceType, String resourceName) { + return getResource(resourceType, resourceName, mFrameworkResources); + } + + @Override + public ResourceValue getProjectResource(ResourceType resourceType, String resourceName) { + return getResource(resourceType, resourceName, mProjectResources); + } + + @Override + @Deprecated + public ResourceValue findItemInStyle(StyleResourceValue style, String attrName) { + // this method is deprecated because it doesn't know about the namespace of the + // attribute so we search for the project namespace first and then in the + // android namespace if needed. + ResourceValue item = findItemInStyle(style, attrName, false /*isFrameworkAttr*/); + if (item == null) { + item = findItemInStyle(style, attrName, true /*isFrameworkAttr*/); + } + + return item; + } + + @Override + public ResourceValue findItemInStyle(StyleResourceValue style, String itemName, + boolean isFrameworkAttr) { + ResourceValue item = style.findValue(itemName, isFrameworkAttr); + + // if we didn't find it, we look in the parent style (if applicable) + if (item == null && mStyleInheritanceMap != null) { + StyleResourceValue parentStyle = mStyleInheritanceMap.get(style); + if (parentStyle != null) { + return findItemInStyle(parentStyle, itemName, isFrameworkAttr); + } + } + + return item; + } + + @Override + public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) { + if (reference == null) { + return null; + } + if (reference.startsWith(PREFIX_THEME_REF)) { + // no theme? no need to go further! + if (mTheme == null) { + return null; + } + + boolean frameworkOnly = false; + + // eliminate the prefix from the string + if (reference.startsWith(ANDROID_THEME_PREFIX)) { + frameworkOnly = true; + reference = reference.substring(ANDROID_THEME_PREFIX.length()); + } else { + reference = reference.substring(PREFIX_THEME_REF.length()); + } + + // at this point, value can contain type/name (drawable/foo for instance). + // split it to make sure. + String[] segments = reference.split("\\/"); + + // we look for the referenced item name. + String referenceName = null; + + if (segments.length == 2) { + // there was a resType in the reference. If it's attr, we ignore it + // else, we assert for now. + if (ResourceType.ATTR.getName().equals(segments[0])) { + referenceName = segments[1]; + } else { + // At this time, no support for ?type/name where type is not "attr" + return null; + } + } else { + // it's just an item name. + referenceName = segments[0]; + } + + // now we look for android: in the referenceName in order to support format + // such as: ?attr/android:name + if (referenceName.startsWith(PREFIX_ANDROID)) { + frameworkOnly = true; + referenceName = referenceName.substring(PREFIX_ANDROID.length()); + } + + // Now look for the item in the theme, starting with the current one. + ResourceValue item = findItemInStyle(mTheme, referenceName, + forceFrameworkOnly || frameworkOnly); + + if (item == null && mLogger != null) { + mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR, + String.format("Couldn't find theme resource %1$s for the current theme", + reference), + new ResourceValue(ResourceType.ATTR, referenceName, frameworkOnly)); + } + + return item; + } else if (reference.startsWith(PREFIX_RESOURCE_REF)) { + boolean frameworkOnly = false; + + // check for the specific null reference value. + if (REFERENCE_NULL.equals(reference)) { + return null; + } + + // Eliminate the prefix from the string. + if (reference.startsWith(ANDROID_PREFIX)) { + frameworkOnly = true; + reference = reference.substring(ANDROID_PREFIX.length()); + } else { + reference = reference.substring(PREFIX_RESOURCE_REF.length()); + } + + // at this point, value contains type/[android:]name (drawable/foo for instance) + String[] segments = reference.split("\\/"); + + // now we look for android: in the resource name in order to support format + // such as: @drawable/android:name + if (segments[1].startsWith(PREFIX_ANDROID)) { + frameworkOnly = true; + segments[1] = segments[1].substring(PREFIX_ANDROID.length()); + } + + ResourceType type = ResourceType.getEnum(segments[0]); + + // unknown type? + if (type == null) { + return null; + } + + return findResValue(type, segments[1], + forceFrameworkOnly ? true :frameworkOnly); + } + + // Looks like the value didn't reference anything. Return null. + return null; + } + + @Override + public ResourceValue resolveValue(ResourceType type, String name, String value, + boolean isFrameworkValue) { + if (value == null) { + return null; + } + + // get the ResourceValue referenced by this value + ResourceValue resValue = findResValue(value, isFrameworkValue); + + // if resValue is null, but value is not null, this means it was not a reference. + // we return the name/value wrapper in a ResourceValue. the isFramework flag doesn't + // matter. + if (resValue == null) { + return new ResourceValue(type, name, value, isFrameworkValue); + } + + // we resolved a first reference, but we need to make sure this isn't a reference also. + return resolveResValue(resValue); + } + + @Override + public ResourceValue resolveResValue(ResourceValue resValue) { + if (resValue == null) { + return null; + } + + // if the resource value is null, we simply return it. + String value = resValue.getValue(); + if (value == null) { + return resValue; + } + + // else attempt to find another ResourceValue referenced by this one. + ResourceValue resolvedResValue = findResValue(value, resValue.isFramework()); + + // if the value did not reference anything, then we simply return the input value + if (resolvedResValue == null) { + return resValue; + } + + // detect potential loop due to mishandled namespace in attributes + if (resValue == resolvedResValue) { + if (mLogger != null) { + mLogger.error(LayoutLog.TAG_BROKEN, + String.format("Potential stackoverflow trying to resolve '%s'. Render may not be accurate.", value), + null); + } + return resValue; + } + + // otherwise, we attempt to resolve this new value as well + return resolveResValue(resolvedResValue); + } + + // ---- Private helper methods. + + /** + * Searches for, and returns a {@link ResourceValue} by its name, and type. + * @param resType the type of the resource + * @param resName the name of the resource + * @param frameworkOnly if <code>true</code>, the method does not search in the + * project resources + */ + private ResourceValue findResValue(ResourceType resType, String resName, + boolean frameworkOnly) { + // map of ResouceValue for the given type + Map<String, ResourceValue> typeMap; + + // if allowed, search in the project resources first. + if (frameworkOnly == false) { + typeMap = mProjectResources.get(resType); + ResourceValue item = typeMap.get(resName); + if (item != null) { + return item; + } + } + + // now search in the framework resources. + typeMap = mFrameworkResources.get(resType); + ResourceValue item = typeMap.get(resName); + if (item != null) { + return item; + } + + // if it was not found and the type is an id, it is possible that the ID was + // generated dynamically when compiling the framework resources. + // Look for it in the R map. + if (mFrameworkProvider != null && resType == ResourceType.ID) { + if (mFrameworkProvider.getId(resType, resName) != null) { + return new ResourceValue(resType, resName, true); + } + } + + // didn't find the resource anywhere. + if (mLogger != null) { + mLogger.warning(LayoutLog.TAG_RESOURCES_RESOLVE, + "Couldn't resolve resource @" + + (frameworkOnly ? "android:" : "") + resType + "/" + resName, + new ResourceValue(resType, resName, frameworkOnly)); + } + return null; + } + + private ResourceValue getResource(ResourceType resourceType, String resourceName, + Map<ResourceType, Map<String, ResourceValue>> resourceRepository) { + Map<String, ResourceValue> typeMap = resourceRepository.get(resourceType); + if (typeMap != null) { + ResourceValue item = typeMap.get(resourceName); + if (item != null) { + item = resolveResValue(item); + return item; + } + } + + // didn't find the resource anywhere. + return null; + + } + + /** + * Compute style information from the given list of style for the project and framework. + * @param themeName the name of the current theme. + * @param isProjectTheme Is this a project theme? + */ + private void computeStyleMaps(String themeName, boolean isProjectTheme) { + mThemeName = themeName; + mIsProjectTheme = isProjectTheme; + Map<String, ResourceValue> projectStyleMap = mProjectResources.get(ResourceType.STYLE); + Map<String, ResourceValue> frameworkStyleMap = mFrameworkResources.get(ResourceType.STYLE); + + // first, get the theme + ResourceValue theme = null; + + // project theme names have been prepended with a * + if (isProjectTheme) { + theme = projectStyleMap.get(themeName); + } else { + theme = frameworkStyleMap.get(themeName); + } + + if (theme instanceof StyleResourceValue) { + // compute the inheritance map for both the project and framework styles + computeStyleInheritance(projectStyleMap.values(), projectStyleMap, + frameworkStyleMap); + + // Compute the style inheritance for the framework styles/themes. + // Since, for those, the style parent values do not contain 'android:' + // we want to force looking in the framework style only to avoid using + // similarly named styles from the project. + // To do this, we pass null in lieu of the project style map. + computeStyleInheritance(frameworkStyleMap.values(), null /*inProjectStyleMap */, + frameworkStyleMap); + + mTheme = (StyleResourceValue) theme; + } + } + + + + /** + * Compute the parent style for all the styles in a given list. + * @param styles the styles for which we compute the parent. + * @param inProjectStyleMap the map of project styles. + * @param inFrameworkStyleMap the map of framework styles. + * @param outInheritanceMap the map of style inheritance. This is filled by the method. + */ + private void computeStyleInheritance(Collection<ResourceValue> styles, + Map<String, ResourceValue> inProjectStyleMap, + Map<String, ResourceValue> inFrameworkStyleMap) { + for (ResourceValue value : styles) { + if (value instanceof StyleResourceValue) { + StyleResourceValue style = (StyleResourceValue)value; + StyleResourceValue parentStyle = null; + + // first look for a specified parent. + String parentName = style.getParentStyle(); + + // no specified parent? try to infer it from the name of the style. + if (parentName == null) { + parentName = getParentName(value.getName()); + } + + if (parentName != null) { + parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap); + + if (parentStyle != null) { + mStyleInheritanceMap.put(style, parentStyle); + } + } + } + } + } + + + /** + * Computes the name of the parent style, or <code>null</code> if the style is a root style. + */ + private String getParentName(String styleName) { + int index = styleName.lastIndexOf('.'); + if (index != -1) { + return styleName.substring(0, index); + } + + return null; + } + + /** + * Searches for and returns the {@link StyleResourceValue} from a given name. + * <p/>The format of the name can be: + * <ul> + * <li>[android:]<name></li> + * <li>[android:]style/<name></li> + * <li>@[android:]style/<name></li> + * </ul> + * @param parentName the name of the style. + * @param inProjectStyleMap the project style map. Can be <code>null</code> + * @param inFrameworkStyleMap the framework style map. + * @return The matching {@link StyleResourceValue} object or <code>null</code> if not found. + */ + private StyleResourceValue getStyle(String parentName, + Map<String, ResourceValue> inProjectStyleMap, + Map<String, ResourceValue> inFrameworkStyleMap) { + boolean frameworkOnly = false; + + String name = parentName; + + // remove the useless @ if it's there + if (name.startsWith(PREFIX_RESOURCE_REF)) { + name = name.substring(PREFIX_RESOURCE_REF.length()); + } + + // check for framework identifier. + if (name.startsWith(PREFIX_ANDROID)) { + frameworkOnly = true; + name = name.substring(PREFIX_ANDROID.length()); + } + + // at this point we could have the format <type>/<name>. we want only the name as long as + // the type is style. + if (name.startsWith(REFERENCE_STYLE)) { + name = name.substring(REFERENCE_STYLE.length()); + } else if (name.indexOf('/') != -1) { + return null; + } + + ResourceValue parent = null; + + // if allowed, search in the project resources. + if (frameworkOnly == false && inProjectStyleMap != null) { + parent = inProjectStyleMap.get(name); + } + + // if not found, then look in the framework resources. + if (parent == null) { + parent = inFrameworkStyleMap.get(name); + } + + // make sure the result is the proper class type and return it. + if (parent instanceof StyleResourceValue) { + return (StyleResourceValue)parent; + } + + if (mLogger != null) { + mLogger.error(LayoutLog.TAG_RESOURCES_RESOLVE, + String.format("Unable to resolve parent style name: %s", parentName), + null /*data*/); + } + + return null; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/ScanningContext.java b/sdk_common/src/com/android/ide/common/resources/ScanningContext.java new file mode 100644 index 0000000..e4ed275 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ScanningContext.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.common.resources; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link ScanningContext} keeps track of state during a resource file scan, + * such as any parsing errors encountered, whether Android ids have changed, and + * so on. + */ +public class ScanningContext { + private final ResourceRepository mRepository; + private boolean mNeedsFullAapt; + private List<String> mErrors = null; + + /** + * Constructs a new {@link ScanningContext} + * + * @param repository the associated resource repository + */ + public ScanningContext(ResourceRepository repository) { + super(); + mRepository = repository; + } + + /** + * Returns a list of errors encountered during scanning + * + * @return a list of errors encountered during scanning (or null) + */ + public List<String> getErrors() { + return mErrors; + } + + /** + * Adds the given error to the scanning context. The error should use the + * same syntax as real aapt error messages such that the aapt parser can + * properly detect the filename, line number, etc. + * + * @param error the error message, including file name and line number at + * the beginning + */ + public void addError(String error) { + if (mErrors == null) { + mErrors = new ArrayList<String>(); + } + mErrors.add(error); + } + + /** + * Returns the repository associated with this scanning context + * + * @return the associated repository, never null + */ + public ResourceRepository getRepository() { + return mRepository; + } + + /** + * Marks that a full aapt compilation of the resources is necessary because it has + * detected a change that cannot be incrementally handled. + */ + protected void requestFullAapt() { + mNeedsFullAapt = true; + } + + /** + * Returns whether this repository has been marked as "dirty"; if one or + * more of the constituent files have declared that the resource item names + * that they provide have changed. + * + * @return true if a full aapt compilation is required + */ + public boolean needsFullAapt() { + return mNeedsFullAapt; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java b/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java new file mode 100644 index 0000000..6b663e9 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2007 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.ide.common.resources; + +import com.android.ide.common.rendering.api.DensityBasedResourceValue; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.io.IAbstractFile; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceType; + +import java.util.Collection; +import java.util.List; + +import javax.xml.parsers.SAXParserFactory; + +/** + * Represents a resource file describing a single resource. + * <p/> + * This is typically an XML file inside res/anim, res/layout, or res/menu or an image file + * under res/drawable. + */ +public class SingleResourceFile extends ResourceFile { + + private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance(); + static { + sParserFactory.setNamespaceAware(true); + } + + private final String mResourceName; + private final ResourceType mType; + private ResourceValue mValue; + + public SingleResourceFile(IAbstractFile file, ResourceFolder folder) { + super(file, folder); + + // we need to infer the type of the resource from the folder type. + // This is easy since this is a single Resource file. + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folder.getType()); + mType = types.get(0); + + // compute the resource name + mResourceName = getResourceName(mType); + + // test if there's a density qualifier associated with the resource + DensityQualifier qualifier = folder.getConfiguration().getDensityQualifier(); + + if (qualifier == null) { + mValue = new ResourceValue(mType, getResourceName(mType), + file.getOsLocation(), isFramework()); + } else { + mValue = new DensityBasedResourceValue( + mType, + getResourceName(mType), + file.getOsLocation(), + qualifier.getValue(), + isFramework()); + } + } + + @Override + protected void load(ScanningContext context) { + // get a resource item matching the given type and name + ResourceItem item = getRepository().getResourceItem(mType, mResourceName); + + // add this file to the list of files generating this resource item. + item.add(this); + + // Ask for an ID refresh since we're adding an item that will generate an ID + context.requestFullAapt(); + } + + @Override + protected void update(ScanningContext context) { + // when this happens, nothing needs to be done since the file only generates + // a single resources that doesn't actually change (its content is the file path) + } + + @Override + protected void dispose(ScanningContext context) { + // only remove this file from the existing ResourceItem. + getFolder().getRepository().removeFile(mType, this); + + // Ask for an ID refresh since we're removing an item that previously generated an ID + context.requestFullAapt(); + + // don't need to touch the content, it'll get reclaimed as this objects disappear. + // In the mean time other objects may need to access it. + } + + @Override + public Collection<ResourceType> getResourceTypes() { + return FolderTypeRelationship.getRelatedResourceTypes(getFolder().getType()); + } + + @Override + public boolean hasResources(ResourceType type) { + return FolderTypeRelationship.match(type, getFolder().getType()); + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceFile#getValue(com.android.ide.eclipse.common.resources.ResourceType, java.lang.String) + * + * This particular implementation does not care about the type or name since a + * SingleResourceFile represents a file generating only one resource. + * The value returned is the full absolute path of the file in OS form. + */ + @Override + public ResourceValue getValue(ResourceType type, String name) { + return mValue; + } + + /** + * Returns the name of the resources. + */ + private String getResourceName(ResourceType type) { + // get the name from the filename. + String name = getFile().getName(); + + int pos = name.indexOf('.'); + if (pos != -1) { + name = name.substring(0, pos); + } + + return name; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java b/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java new file mode 100644 index 0000000..aabfd35 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ValueResourceParser.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2008 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.ide.common.resources; + +import com.android.ide.common.rendering.api.AttrResourceValue; +import com.android.ide.common.rendering.api.DeclareStyleableResourceValue; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.resources.ResourceType; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * SAX handler to parser value resource files. + */ +public final class ValueResourceParser extends DefaultHandler { + + // TODO: reuse definitions from somewhere else. + private final static String NODE_RESOURCES = "resources"; + private final static String NODE_ITEM = "item"; + private final static String ATTR_NAME = "name"; + private final static String ATTR_TYPE = "type"; + private final static String ATTR_PARENT = "parent"; + private final static String ATTR_VALUE = "value"; + + private final static String DEFAULT_NS_PREFIX = "android:"; + private final static int DEFAULT_NS_PREFIX_LEN = DEFAULT_NS_PREFIX.length(); + + public interface IValueResourceRepository { + void addResourceValue(ResourceValue value); + boolean hasResourceValue(ResourceType type, String name); + } + + private boolean inResources = false; + private int mDepth = 0; + private ResourceValue mCurrentValue = null; + private StyleResourceValue mCurrentStyle = null; + private DeclareStyleableResourceValue mCurrentDeclareStyleable = null; + private AttrResourceValue mCurrentAttr; + private IValueResourceRepository mRepository; + private final boolean mIsFramework; + + public ValueResourceParser(IValueResourceRepository repository, boolean isFramework) { + mRepository = repository; + mIsFramework = isFramework; + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (mCurrentValue != null) { + mCurrentValue.setValue(trimXmlWhitespaces(mCurrentValue.getValue())); + } + + if (inResources && qName.equals(NODE_RESOURCES)) { + inResources = false; + } else if (mDepth == 2) { + mCurrentValue = null; + mCurrentStyle = null; + mCurrentDeclareStyleable = null; + mCurrentAttr = null; + } else if (mDepth == 3) { + mCurrentValue = null; + if (mCurrentDeclareStyleable != null) { + mCurrentAttr = null; + } + } + + mDepth--; + super.endElement(uri, localName, qName); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + try { + mDepth++; + if (inResources == false && mDepth == 1) { + if (qName.equals(NODE_RESOURCES)) { + inResources = true; + } + } else if (mDepth == 2 && inResources == true) { + ResourceType type = getType(qName, attributes); + + if (type != null) { + // get the resource name + String name = attributes.getValue(ATTR_NAME); + if (name != null) { + switch (type) { + case STYLE: + String parent = attributes.getValue(ATTR_PARENT); + mCurrentStyle = new StyleResourceValue(type, name, parent, + mIsFramework); + mRepository.addResourceValue(mCurrentStyle); + break; + case DECLARE_STYLEABLE: + mCurrentDeclareStyleable = new DeclareStyleableResourceValue( + type, name, mIsFramework); + mRepository.addResourceValue(mCurrentDeclareStyleable); + break; + case ATTR: + mCurrentAttr = new AttrResourceValue(type, name, mIsFramework); + mRepository.addResourceValue(mCurrentAttr); + break; + default: + mCurrentValue = new ResourceValue(type, name, mIsFramework); + mRepository.addResourceValue(mCurrentValue); + break; + } + } + } + } else if (mDepth == 3) { + // get the resource name + String name = attributes.getValue(ATTR_NAME); + if (name != null) { + + if (mCurrentStyle != null) { + // is the attribute in the android namespace? + boolean isFrameworkAttr = mIsFramework; + if (name.startsWith(DEFAULT_NS_PREFIX)) { + name = name.substring(DEFAULT_NS_PREFIX_LEN); + isFrameworkAttr = true; + } + + mCurrentValue = new ResourceValue(null, name, mIsFramework); + mCurrentStyle.addValue(mCurrentValue, isFrameworkAttr); + } else if (mCurrentDeclareStyleable != null) { + // is the attribute in the android namespace? + boolean isFramework = mIsFramework; + if (name.startsWith(DEFAULT_NS_PREFIX)) { + name = name.substring(DEFAULT_NS_PREFIX_LEN); + isFramework = true; + } + + mCurrentAttr = new AttrResourceValue(ResourceType.ATTR, name, isFramework); + mCurrentDeclareStyleable.addValue(mCurrentAttr); + + // also add it to the repository. + mRepository.addResourceValue(mCurrentAttr); + + } else if (mCurrentAttr != null) { + // get the enum/flag value + String value = attributes.getValue(ATTR_VALUE); + + try { + // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we + // use Long.decode instead. + mCurrentAttr.addValue(name, (int)(long)Long.decode(value)); + } catch (NumberFormatException e) { + // pass, we'll just ignore this value + } + + } + } + } else if (mDepth == 4 && mCurrentAttr != null) { + // get the enum/flag name + String name = attributes.getValue(ATTR_NAME); + String value = attributes.getValue(ATTR_VALUE); + + try { + // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we + // use Long.decode instead. + mCurrentAttr.addValue(name, (int)(long)Long.decode(value)); + } catch (NumberFormatException e) { + // pass, we'll just ignore this value + } + } + } finally { + super.startElement(uri, localName, qName, attributes); + } + } + + private ResourceType getType(String qName, Attributes attributes) { + String typeValue; + + // if the node is <item>, we get the type from the attribute "type" + if (NODE_ITEM.equals(qName)) { + typeValue = attributes.getValue(ATTR_TYPE); + } else { + // the type is the name of the node. + typeValue = qName; + } + + ResourceType type = ResourceType.getEnum(typeValue); + return type; + } + + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (mCurrentValue != null) { + String value = mCurrentValue.getValue(); + if (value == null) { + mCurrentValue.setValue(new String(ch, start, length)); + } else { + mCurrentValue.setValue(value + new String(ch, start, length)); + } + } + } + + public static String trimXmlWhitespaces(String value) { + if (value == null) { + return null; + } + + // look for carriage return and replace all whitespace around it by just 1 space. + int index; + + while ((index = value.indexOf('\n')) != -1) { + // look for whitespace on each side + int left = index - 1; + while (left >= 0) { + if (Character.isWhitespace(value.charAt(left))) { + left--; + } else { + break; + } + } + + int right = index + 1; + int count = value.length(); + while (right < count) { + if (Character.isWhitespace(value.charAt(right))) { + right++; + } else { + break; + } + } + + // remove all between left and right (non inclusive) and replace by a single space. + String leftString = null; + if (left >= 0) { + leftString = value.substring(0, left + 1); + } + String rightString = null; + if (right < count) { + rightString = value.substring(right); + } + + if (leftString != null) { + value = leftString; + if (rightString != null) { + value += " " + rightString; + } + } else { + value = rightString != null ? rightString : ""; + } + } + + // now we un-escape the string + int length = value.length(); + char[] buffer = value.toCharArray(); + + for (int i = 0 ; i < length ; i++) { + if (buffer[i] == '\\' && i + 1 < length) { + if (buffer[i+1] == 'u') { + if (i + 5 < length) { + // this is unicode char \u1234 + int unicodeChar = Integer.parseInt(new String(buffer, i+2, 4), 16); + + // put the unicode char at the location of the \ + buffer[i] = (char)unicodeChar; + + // offset the rest of the buffer since we go from 6 to 1 char + if (i + 6 < buffer.length) { + System.arraycopy(buffer, i+6, buffer, i+1, length - i - 6); + } + length -= 5; + } + } else { + if (buffer[i+1] == 'n') { + // replace the 'n' char with \n + buffer[i+1] = '\n'; + } + + // offset the buffer to erase the \ + System.arraycopy(buffer, i+1, buffer, i, length - i - 1); + length--; + } + } else if (buffer[i] == '"') { + // if the " was escaped it would have been processed above. + // offset the buffer to erase the " + System.arraycopy(buffer, i+1, buffer, i, length - i - 1); + length--; + + // unlike when unescaping, we want to process the next char too + i--; + } + } + + return new String(buffer, 0, length); + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/Configurable.java b/sdk_common/src/com/android/ide/common/resources/configuration/Configurable.java new file mode 100644 index 0000000..5e7f910 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/Configurable.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + + +/** + * An object that is associated with a {@link FolderConfiguration}. + */ +public interface Configurable { + /** + * Returns the {@link FolderConfiguration} for this object. + */ + public FolderConfiguration getConfiguration(); +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/CountryCodeQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/CountryCodeQualifier.java new file mode 100644 index 0000000..eb7cc0d --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/CountryCodeQualifier.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2008 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Mobile Country Code. + */ +public final class CountryCodeQualifier extends ResourceQualifier { + /** Default pixel density value. This means the property is not set. */ + private final static int DEFAULT_CODE = -1; + + private final static Pattern sCountryCodePattern = Pattern.compile("^mcc(\\d{3})$");//$NON-NLS-1$ + + private final int mCode; + + public static final String NAME = "Mobile Country Code"; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link CountryCodeQualifier} object or <code>null</code> + */ + public static CountryCodeQualifier getQualifier(String segment) { + Matcher m = sCountryCodePattern.matcher(segment); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return null; + } + + CountryCodeQualifier qualifier = new CountryCodeQualifier(code); + return qualifier; + } + + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link CountryCodeQualifier} object. + * @param code the value of the qualifier, as returned by {@link #getCode()}. + */ + public static String getFolderSegment(int code) { + if (code != DEFAULT_CODE && code >= 100 && code <=999) { // code is 3 digit.) { + return String.format("mcc%1$d", code); //$NON-NLS-1$ + } + + return ""; //$NON-NLS-1$ + } + + public CountryCodeQualifier() { + this(DEFAULT_CODE); + } + + public CountryCodeQualifier(int code) { + mCode = code; + } + + public int getCode() { + return mCode; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Country Code"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean isValid() { + return mCode != DEFAULT_CODE; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + CountryCodeQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setCountryCodeQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof CountryCodeQualifier) { + return mCode == ((CountryCodeQualifier)qualifier).mCode; + } + + return false; + } + + @Override + public int hashCode() { + return mCode; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String getFolderSegment() { + return getFolderSegment(mCode); + } + + @Override + public String getShortDisplayValue() { + if (mCode != DEFAULT_CODE) { + return String.format("MCC %1$d", mCode); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + return getShortDisplayValue(); + } + +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/DensityQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/DensityQualifier.java new file mode 100644 index 0000000..a9e4a01 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/DensityQualifier.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import com.android.resources.Density; +import com.android.resources.ResourceEnum; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Pixel Density. + */ +public final class DensityQualifier extends EnumBasedResourceQualifier { + private final static Pattern sDensityLegacyPattern = Pattern.compile("^(\\d+)dpi$");//$NON-NLS-1$ + + public static final String NAME = "Density"; + + private Density mValue = Density.MEDIUM; + + public DensityQualifier() { + // pass + } + + public DensityQualifier(Density value) { + mValue = value; + } + + public Density getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 4; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Density density = Density.getEnum(value); + if (density == null) { + + // attempt to read a legacy value. + Matcher m = sDensityLegacyPattern.matcher(value); + if (m.matches()) { + String v = m.group(1); + + try { + density = Density.getEnum(Integer.parseInt(v)); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number + // which really shouldn't happen since the regexp would have failed. + } + } + } + + if (density != null) { + DensityQualifier qualifier = new DensityQualifier(); + qualifier.mValue = density; + config.setDensityQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + if (qualifier instanceof DensityQualifier) { + // as long as there's a density qualifier, it's always a match. + // The best match will be found later. + return true; + } + + return false; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + DensityQualifier compareQ = (DensityQualifier)compareTo; + DensityQualifier referenceQ = (DensityQualifier)reference; + + if (compareQ.mValue == referenceQ.mValue) { + // what we have is already the best possible match (exact match) + return false; + } else if (mValue == referenceQ.mValue) { + // got new exact value, this is the best! + return true; + } else { + // in all case we're going to prefer the higher dpi. + // if reference is high, we want highest dpi. + // if reference is medium, we'll prefer to scale down high dpi, than scale up low dpi + // if reference if low, we'll prefer to scale down high than medium (2:1 over 4:3) + return mValue.getDpiValue() > compareQ.mValue.getDpiValue(); + } + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/DeviceConfigHelper.java b/sdk_common/src/com/android/ide/common/resources/configuration/DeviceConfigHelper.java new file mode 100644 index 0000000..27eaa01 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/DeviceConfigHelper.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 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.ide.common.resources.configuration; + +import com.android.annotations.Nullable; +import com.android.resources.NightMode; +import com.android.resources.UiMode; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.State; + +public class DeviceConfigHelper { + /** + * Returns a {@link FolderConfiguration} based on the given state + * + * @param state + * The {@link State} of the {@link Device} to base the + * {@link FolderConfiguration} on. Can be null. + * @return A {@link FolderConfiguration} based on the given {@link State}. + * If the given {@link State} is null, the result is also null; + */ + @Nullable + public static FolderConfiguration getFolderConfig(@Nullable State state) { + if (state == null) { + return null; + } + + Hardware hw = state.getHardware(); + + FolderConfiguration config = new FolderConfiguration(); + config.createDefault(); + Screen screen = hw.getScreen(); + config.setDensityQualifier(new DensityQualifier(screen.getPixelDensity())); + config.setNavigationMethodQualifier(new NavigationMethodQualifier(hw.getNav())); + ScreenDimensionQualifier sdq; + if (screen.getXDimension() > screen.getYDimension()) { + sdq = new ScreenDimensionQualifier(screen.getXDimension(), screen.getYDimension()); + } else { + sdq = new ScreenDimensionQualifier(screen.getYDimension(), screen.getXDimension()); + } + config.setScreenDimensionQualifier(sdq); + config.setScreenRatioQualifier(new ScreenRatioQualifier(screen.getRatio())); + config.setScreenSizeQualifier(new ScreenSizeQualifier(screen.getSize())); + config.setTextInputMethodQualifier(new TextInputMethodQualifier(hw.getKeyboard())); + config.setTouchTypeQualifier(new TouchScreenQualifier(screen.getMechanism())); + + config.setKeyboardStateQualifier(new KeyboardStateQualifier(state.getKeyState())); + config.setNavigationStateQualifier(new NavigationStateQualifier(state.getNavState())); + config.setScreenOrientationQualifier( + new ScreenOrientationQualifier(state.getOrientation())); + + config.updateScreenWidthAndHeight(); + + // Setup some default qualifiers + config.setUiModeQualifier(new UiModeQualifier(UiMode.NORMAL)); + config.setNightModeQualifier(new NightModeQualifier(NightMode.NOTNIGHT)); + config.setCountryCodeQualifier(new CountryCodeQualifier()); + config.setLanguageQualifier(new LanguageQualifier()); + config.setNetworkCodeQualifier(new NetworkCodeQualifier()); + config.setRegionQualifier(new RegionQualifier()); + config.setVersionQualifier(new VersionQualifier()); + + return config; + } + + /** + * Returns a {@link FolderConfiguration} based on the {@link State} given by + * the {@link Device} and the state name. + * + * @param d + * The {@link Device} to base the {@link FolderConfiguration} on. + * @param stateName + * The name of the state to base the {@link FolderConfiguration} + * on. + * @return The {@link FolderConfiguration} based on the determined + * {@link State}. If there is no {@link State} with the given state + * name for the given device, null is returned. + */ + @Nullable + public static FolderConfiguration getFolderConfig(Device d, String stateName) { + return getFolderConfig(d.getState(stateName)); + } + + /** + * Returns a {@link FolderConfiguration} based on the default {@link State} + * for the given {@link Device}. + * + * @param d + * The {@link Device} to generate the {@link FolderConfiguration} + * from. + * @return A {@link FolderConfiguration} based on the default {@link State} + * for the given {@link Device} + */ + public static FolderConfiguration getFolderConfig(Device d) { + return getFolderConfig(d.getDefaultState()); + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/EnumBasedResourceQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/EnumBasedResourceQualifier.java new file mode 100644 index 0000000..7bfda2d --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/EnumBasedResourceQualifier.java @@ -0,0 +1,92 @@ +/* + * 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.ide.common.resources.configuration; + +import com.android.resources.ResourceEnum; + +/** + * Base class for {@link ResourceQualifier} whose value is an {@link ResourceEnum}. + * + */ +abstract class EnumBasedResourceQualifier extends ResourceQualifier { + + abstract ResourceEnum getEnumValue(); + + @Override + public boolean isValid() { + return getEnumValue() != null; + } + + @Override + public boolean hasFakeValue() { + return getEnumValue().isFakeValue(); + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof EnumBasedResourceQualifier) { + return getEnumValue() == ((EnumBasedResourceQualifier)qualifier).getEnumValue(); + } + + return false; + } + + @Override + public int hashCode() { + ResourceEnum value = getEnumValue(); + if (value != null) { + return value.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public final String getFolderSegment() { + ResourceEnum value = getEnumValue(); + if (value != null) { + return value.getResourceValue(); + } + + return ""; //$NON-NLS-1$ + } + + + @Override + public String getShortDisplayValue() { + ResourceEnum value = getEnumValue(); + if (value != null) { + return value.getShortDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + ResourceEnum value = getEnumValue(); + if (value != null) { + return value.getLongDisplayValue(); + } + + return ""; //$NON-NLS-1$ + } + +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java b/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java new file mode 100644 index 0000000..e2fe767 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/FolderConfiguration.java @@ -0,0 +1,886 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import com.android.SdkConstants; +import com.android.resources.Density; +import com.android.resources.ResourceFolderType; +import com.android.resources.ScreenOrientation; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Represents the configuration for Resource Folders. All the properties have a default + * value which means that the property is not set. + */ +public final class FolderConfiguration implements Comparable<FolderConfiguration> { + + private final static ResourceQualifier[] DEFAULT_QUALIFIERS; + + static { + // get the default qualifiers. + FolderConfiguration defaultConfig = new FolderConfiguration(); + defaultConfig.createDefault(); + DEFAULT_QUALIFIERS = defaultConfig.getQualifiers(); + } + + + private final ResourceQualifier[] mQualifiers = new ResourceQualifier[INDEX_COUNT]; + + private final static int INDEX_COUNTRY_CODE = 0; + private final static int INDEX_NETWORK_CODE = 1; + private final static int INDEX_LANGUAGE = 2; + private final static int INDEX_REGION = 3; + private final static int INDEX_SMALLEST_SCREEN_WIDTH = 4; + private final static int INDEX_SCREEN_WIDTH = 5; + private final static int INDEX_SCREEN_HEIGHT = 6; + private final static int INDEX_SCREEN_LAYOUT_SIZE = 7; + private final static int INDEX_SCREEN_RATIO = 8; + private final static int INDEX_SCREEN_ORIENTATION = 9; + private final static int INDEX_UI_MODE = 10; + private final static int INDEX_NIGHT_MODE = 11; + private final static int INDEX_PIXEL_DENSITY = 12; + private final static int INDEX_TOUCH_TYPE = 13; + private final static int INDEX_KEYBOARD_STATE = 14; + private final static int INDEX_TEXT_INPUT_METHOD = 15; + private final static int INDEX_NAVIGATION_STATE = 16; + private final static int INDEX_NAVIGATION_METHOD = 17; + private final static int INDEX_SCREEN_DIMENSION = 18; + private final static int INDEX_VERSION = 19; + private final static int INDEX_COUNT = 20; + + /** + * Creates a {@link FolderConfiguration} matching the folder segments. + * @param folderSegments The segments of the folder name. The first segments should contain + * the name of the folder + * @return a FolderConfiguration object, or null if the folder name isn't valid.. + */ + public static FolderConfiguration getConfig(String[] folderSegments) { + FolderConfiguration config = new FolderConfiguration(); + + // we are going to loop through the segments, and match them with the first + // available qualifier. If the segment doesn't match we try with the next qualifier. + // Because the order of the qualifier is fixed, we do not reset the first qualifier + // after each successful segment. + // If we run out of qualifier before processing all the segments, we fail. + + int qualifierIndex = 0; + int qualifierCount = DEFAULT_QUALIFIERS.length; + + for (int i = 1 ; i < folderSegments.length; i++) { + String seg = folderSegments[i]; + if (seg.length() > 0) { + while (qualifierIndex < qualifierCount && + DEFAULT_QUALIFIERS[qualifierIndex].checkAndSet(seg, config) == false) { + qualifierIndex++; + } + + // if we reached the end of the qualifier we didn't find a matching qualifier. + if (qualifierIndex == qualifierCount) { + return null; + } + + } else { + return null; + } + } + + return config; + } + + /** + * Returns the number of {@link ResourceQualifier} that make up a Folder configuration. + */ + public static int getQualifierCount() { + return INDEX_COUNT; + } + + /** + * Sets the config from the qualifiers of a given <var>config</var>. + * <p/>This is equivalent to <code>set(config, false)</code> + * @param config the configuration to set + * + * @see #set(FolderConfiguration, boolean) + */ + public void set(FolderConfiguration config) { + set(config, false /*nonFakeValuesOnly*/); + } + + /** + * Sets the config from the qualifiers of a given <var>config</var>. + * @param config the configuration to set + * @param nonFakeValuesOnly if set to true this ignore qualifiers for which the + * current value is a fake value. + * + * @see ResourceQualifier#hasFakeValue() + */ + public void set(FolderConfiguration config, boolean nonFakeValuesOnly) { + if (config != null) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + ResourceQualifier q = config.mQualifiers[i]; + if (nonFakeValuesOnly == false || q == null || q.hasFakeValue() == false) { + mQualifiers[i] = q; + } + } + } + } + + /** + * Reset the config. + * <p/>This makes qualifiers at all indices <code>null</code>. + */ + public void reset() { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + mQualifiers[i] = null; + } + } + + /** + * Removes the qualifiers from the receiver if they are present (and valid) + * in the given configuration. + */ + public void substract(FolderConfiguration config) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (config.mQualifiers[i] != null && config.mQualifiers[i].isValid()) { + mQualifiers[i] = null; + } + } + } + + /** + * Adds the non-qualifiers from the given config. + * Qualifiers that are null in the given config do not change in the receiver. + */ + public void add(FolderConfiguration config) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (config.mQualifiers[i] != null) { + mQualifiers[i] = config.mQualifiers[i]; + } + } + } + + /** + * Returns the first invalid qualifier, or <code>null<code> if they are all valid (or if none + * exists). + */ + public ResourceQualifier getInvalidQualifier() { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null && mQualifiers[i].isValid() == false) { + return mQualifiers[i]; + } + } + + // all allocated qualifiers are valid, we return null. + return null; + } + + /** + * Returns whether the Region qualifier is valid. Region qualifier can only be present if a + * Language qualifier is present as well. + * @return true if the Region qualifier is valid. + */ + public boolean checkRegion() { + if (mQualifiers[INDEX_LANGUAGE] == null && mQualifiers[INDEX_REGION] != null) { + return false; + } + + return true; + } + + /** + * Adds a qualifier to the {@link FolderConfiguration} + * @param qualifier the {@link ResourceQualifier} to add. + */ + public void addQualifier(ResourceQualifier qualifier) { + if (qualifier instanceof CountryCodeQualifier) { + mQualifiers[INDEX_COUNTRY_CODE] = qualifier; + + } else if (qualifier instanceof NetworkCodeQualifier) { + mQualifiers[INDEX_NETWORK_CODE] = qualifier; + + } else if (qualifier instanceof LanguageQualifier) { + mQualifiers[INDEX_LANGUAGE] = qualifier; + + } else if (qualifier instanceof RegionQualifier) { + mQualifiers[INDEX_REGION] = qualifier; + + } else if (qualifier instanceof SmallestScreenWidthQualifier) { + mQualifiers[INDEX_SMALLEST_SCREEN_WIDTH] = qualifier; + + } else if (qualifier instanceof ScreenWidthQualifier) { + mQualifiers[INDEX_SCREEN_WIDTH] = qualifier; + + } else if (qualifier instanceof ScreenHeightQualifier) { + mQualifiers[INDEX_SCREEN_HEIGHT] = qualifier; + + } else if (qualifier instanceof ScreenSizeQualifier) { + mQualifiers[INDEX_SCREEN_LAYOUT_SIZE] = qualifier; + + } else if (qualifier instanceof ScreenRatioQualifier) { + mQualifiers[INDEX_SCREEN_RATIO] = qualifier; + + } else if (qualifier instanceof ScreenOrientationQualifier) { + mQualifiers[INDEX_SCREEN_ORIENTATION] = qualifier; + + } else if (qualifier instanceof UiModeQualifier) { + mQualifiers[INDEX_UI_MODE] = qualifier; + + } else if (qualifier instanceof NightModeQualifier) { + mQualifiers[INDEX_NIGHT_MODE] = qualifier; + + } else if (qualifier instanceof DensityQualifier) { + mQualifiers[INDEX_PIXEL_DENSITY] = qualifier; + + } else if (qualifier instanceof TouchScreenQualifier) { + mQualifiers[INDEX_TOUCH_TYPE] = qualifier; + + } else if (qualifier instanceof KeyboardStateQualifier) { + mQualifiers[INDEX_KEYBOARD_STATE] = qualifier; + + } else if (qualifier instanceof TextInputMethodQualifier) { + mQualifiers[INDEX_TEXT_INPUT_METHOD] = qualifier; + + } else if (qualifier instanceof NavigationStateQualifier) { + mQualifiers[INDEX_NAVIGATION_STATE] = qualifier; + + } else if (qualifier instanceof NavigationMethodQualifier) { + mQualifiers[INDEX_NAVIGATION_METHOD] = qualifier; + + } else if (qualifier instanceof ScreenDimensionQualifier) { + mQualifiers[INDEX_SCREEN_DIMENSION] = qualifier; + + } else if (qualifier instanceof VersionQualifier) { + mQualifiers[INDEX_VERSION] = qualifier; + + } + } + + /** + * Removes a given qualifier from the {@link FolderConfiguration}. + * @param qualifier the {@link ResourceQualifier} to remove. + */ + public void removeQualifier(ResourceQualifier qualifier) { + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] == qualifier) { + mQualifiers[i] = null; + return; + } + } + } + + /** + * Returns a qualifier by its index. The total number of qualifiers can be accessed by + * {@link #getQualifierCount()}. + * @param index the index of the qualifier to return. + * @return the qualifier or null if there are none at the index. + */ + public ResourceQualifier getQualifier(int index) { + return mQualifiers[index]; + } + + public void setCountryCodeQualifier(CountryCodeQualifier qualifier) { + mQualifiers[INDEX_COUNTRY_CODE] = qualifier; + } + + public CountryCodeQualifier getCountryCodeQualifier() { + return (CountryCodeQualifier)mQualifiers[INDEX_COUNTRY_CODE]; + } + + public void setNetworkCodeQualifier(NetworkCodeQualifier qualifier) { + mQualifiers[INDEX_NETWORK_CODE] = qualifier; + } + + public NetworkCodeQualifier getNetworkCodeQualifier() { + return (NetworkCodeQualifier)mQualifiers[INDEX_NETWORK_CODE]; + } + + public void setLanguageQualifier(LanguageQualifier qualifier) { + mQualifiers[INDEX_LANGUAGE] = qualifier; + } + + public LanguageQualifier getLanguageQualifier() { + return (LanguageQualifier)mQualifiers[INDEX_LANGUAGE]; + } + + public void setRegionQualifier(RegionQualifier qualifier) { + mQualifiers[INDEX_REGION] = qualifier; + } + + public RegionQualifier getRegionQualifier() { + return (RegionQualifier)mQualifiers[INDEX_REGION]; + } + + public void setSmallestScreenWidthQualifier(SmallestScreenWidthQualifier qualifier) { + mQualifiers[INDEX_SMALLEST_SCREEN_WIDTH] = qualifier; + } + + public SmallestScreenWidthQualifier getSmallestScreenWidthQualifier() { + return (SmallestScreenWidthQualifier) mQualifiers[INDEX_SMALLEST_SCREEN_WIDTH]; + } + + public void setScreenWidthQualifier(ScreenWidthQualifier qualifier) { + mQualifiers[INDEX_SCREEN_WIDTH] = qualifier; + } + + public ScreenWidthQualifier getScreenWidthQualifier() { + return (ScreenWidthQualifier) mQualifiers[INDEX_SCREEN_WIDTH]; + } + + public void setScreenHeightQualifier(ScreenHeightQualifier qualifier) { + mQualifiers[INDEX_SCREEN_HEIGHT] = qualifier; + } + + public ScreenHeightQualifier getScreenHeightQualifier() { + return (ScreenHeightQualifier) mQualifiers[INDEX_SCREEN_HEIGHT]; + } + + public void setScreenSizeQualifier(ScreenSizeQualifier qualifier) { + mQualifiers[INDEX_SCREEN_LAYOUT_SIZE] = qualifier; + } + + public ScreenSizeQualifier getScreenSizeQualifier() { + return (ScreenSizeQualifier)mQualifiers[INDEX_SCREEN_LAYOUT_SIZE]; + } + + public void setScreenRatioQualifier(ScreenRatioQualifier qualifier) { + mQualifiers[INDEX_SCREEN_RATIO] = qualifier; + } + + public ScreenRatioQualifier getScreenRatioQualifier() { + return (ScreenRatioQualifier)mQualifiers[INDEX_SCREEN_RATIO]; + } + + public void setScreenOrientationQualifier(ScreenOrientationQualifier qualifier) { + mQualifiers[INDEX_SCREEN_ORIENTATION] = qualifier; + } + + public ScreenOrientationQualifier getScreenOrientationQualifier() { + return (ScreenOrientationQualifier)mQualifiers[INDEX_SCREEN_ORIENTATION]; + } + + public void setUiModeQualifier(UiModeQualifier qualifier) { + mQualifiers[INDEX_UI_MODE] = qualifier; + } + + public UiModeQualifier getUiModeQualifier() { + return (UiModeQualifier)mQualifiers[INDEX_UI_MODE]; + } + + public void setNightModeQualifier(NightModeQualifier qualifier) { + mQualifiers[INDEX_NIGHT_MODE] = qualifier; + } + + public NightModeQualifier getNightModeQualifier() { + return (NightModeQualifier)mQualifiers[INDEX_NIGHT_MODE]; + } + + public void setDensityQualifier(DensityQualifier qualifier) { + mQualifiers[INDEX_PIXEL_DENSITY] = qualifier; + } + + public DensityQualifier getDensityQualifier() { + return (DensityQualifier)mQualifiers[INDEX_PIXEL_DENSITY]; + } + + public void setTouchTypeQualifier(TouchScreenQualifier qualifier) { + mQualifiers[INDEX_TOUCH_TYPE] = qualifier; + } + + public TouchScreenQualifier getTouchTypeQualifier() { + return (TouchScreenQualifier)mQualifiers[INDEX_TOUCH_TYPE]; + } + + public void setKeyboardStateQualifier(KeyboardStateQualifier qualifier) { + mQualifiers[INDEX_KEYBOARD_STATE] = qualifier; + } + + public KeyboardStateQualifier getKeyboardStateQualifier() { + return (KeyboardStateQualifier)mQualifiers[INDEX_KEYBOARD_STATE]; + } + + public void setTextInputMethodQualifier(TextInputMethodQualifier qualifier) { + mQualifiers[INDEX_TEXT_INPUT_METHOD] = qualifier; + } + + public TextInputMethodQualifier getTextInputMethodQualifier() { + return (TextInputMethodQualifier)mQualifiers[INDEX_TEXT_INPUT_METHOD]; + } + + public void setNavigationStateQualifier(NavigationStateQualifier qualifier) { + mQualifiers[INDEX_NAVIGATION_STATE] = qualifier; + } + + public NavigationStateQualifier getNavigationStateQualifier() { + return (NavigationStateQualifier)mQualifiers[INDEX_NAVIGATION_STATE]; + } + + public void setNavigationMethodQualifier(NavigationMethodQualifier qualifier) { + mQualifiers[INDEX_NAVIGATION_METHOD] = qualifier; + } + + public NavigationMethodQualifier getNavigationMethodQualifier() { + return (NavigationMethodQualifier)mQualifiers[INDEX_NAVIGATION_METHOD]; + } + + public void setScreenDimensionQualifier(ScreenDimensionQualifier qualifier) { + mQualifiers[INDEX_SCREEN_DIMENSION] = qualifier; + } + + public ScreenDimensionQualifier getScreenDimensionQualifier() { + return (ScreenDimensionQualifier)mQualifiers[INDEX_SCREEN_DIMENSION]; + } + + public void setVersionQualifier(VersionQualifier qualifier) { + mQualifiers[INDEX_VERSION] = qualifier; + } + + public VersionQualifier getVersionQualifier() { + return (VersionQualifier)mQualifiers[INDEX_VERSION]; + } + + /** + * Updates the {@link SmallestScreenWidthQualifier}, {@link ScreenWidthQualifier}, and + * {@link ScreenHeightQualifier} based on the (required) values of + * {@link ScreenDimensionQualifier} {@link DensityQualifier}, and + * {@link ScreenOrientationQualifier}. + * + * Also the density cannot be {@link Density#NODPI} as it's not valid on a device. + */ + public void updateScreenWidthAndHeight() { + + ResourceQualifier sizeQ = mQualifiers[INDEX_SCREEN_DIMENSION]; + ResourceQualifier densityQ = mQualifiers[INDEX_PIXEL_DENSITY]; + ResourceQualifier orientQ = mQualifiers[INDEX_SCREEN_ORIENTATION]; + + if (sizeQ != null && densityQ != null && orientQ != null) { + Density density = ((DensityQualifier) densityQ).getValue(); + if (density == Density.NODPI) { + return; + } + + ScreenOrientation orientation = ((ScreenOrientationQualifier) orientQ).getValue(); + + int size1 = ((ScreenDimensionQualifier) sizeQ).getValue1(); + int size2 = ((ScreenDimensionQualifier) sizeQ).getValue2(); + + // make sure size1 is the biggest (should be the case, but make sure) + if (size1 < size2) { + int a = size1; + size1 = size2; + size2 = a; + } + + // compute the dp. round them up since we want -w480dp to match a 480.5dp screen + int dp1 = (int) Math.ceil(size1 * Density.DEFAULT_DENSITY / density.getDpiValue()); + int dp2 = (int) Math.ceil(size2 * Density.DEFAULT_DENSITY / density.getDpiValue()); + + setSmallestScreenWidthQualifier(new SmallestScreenWidthQualifier(dp2)); + + switch (orientation) { + case PORTRAIT: + setScreenWidthQualifier(new ScreenWidthQualifier(dp2)); + setScreenHeightQualifier(new ScreenHeightQualifier(dp1)); + break; + case LANDSCAPE: + setScreenWidthQualifier(new ScreenWidthQualifier(dp1)); + setScreenHeightQualifier(new ScreenHeightQualifier(dp2)); + break; + case SQUARE: + setScreenWidthQualifier(new ScreenWidthQualifier(dp2)); + setScreenHeightQualifier(new ScreenHeightQualifier(dp2)); + break; + } + } + } + + /** + * Returns whether an object is equals to the receiver. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof FolderConfiguration) { + FolderConfiguration fc = (FolderConfiguration)obj; + for (int i = 0 ; i < INDEX_COUNT ; i++) { + ResourceQualifier qualifier = mQualifiers[i]; + ResourceQualifier fcQualifier = fc.mQualifiers[i]; + if (qualifier != null) { + if (qualifier.equals(fcQualifier) == false) { + return false; + } + } else if (fcQualifier != null) { + return false; + } + } + + return true; + } + + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Returns whether the Configuration has only default values. + */ + public boolean isDefault() { + for (ResourceQualifier irq : mQualifiers) { + if (irq != null) { + return false; + } + } + + return true; + } + + /** + * Returns the name of a folder with the configuration. + */ + public String getFolderName(ResourceFolderType folder) { + StringBuilder result = new StringBuilder(folder.getName()); + + for (ResourceQualifier qualifier : mQualifiers) { + if (qualifier != null) { + String segment = qualifier.getFolderSegment(); + if (segment != null && segment.length() > 0) { + result.append(SdkConstants.RES_QUALIFIER_SEP); + result.append(segment); + } + } + } + + return result.toString(); + } + + /** + * Returns {@link #toDisplayString()}. + */ + @Override + public String toString() { + return toDisplayString(); + } + + /** + * Returns a string valid for display purpose. + */ + public String toDisplayString() { + if (isDefault()) { + return "default"; + } + + StringBuilder result = null; + int index = 0; + ResourceQualifier qualifier = null; + + // pre- language/region qualifiers + while (index < INDEX_LANGUAGE) { + qualifier = mQualifiers[index++]; + if (qualifier != null) { + if (result == null) { + result = new StringBuilder(); + } else { + result.append(", "); //$NON-NLS-1$ + } + result.append(qualifier.getLongDisplayValue()); + + } + } + + // process the language/region qualifier in a custom way, if there are both non null. + if (mQualifiers[INDEX_LANGUAGE] != null && mQualifiers[INDEX_REGION] != null) { + String language = mQualifiers[INDEX_LANGUAGE].getLongDisplayValue(); + String region = mQualifiers[INDEX_REGION].getLongDisplayValue(); + + if (result == null) { + result = new StringBuilder(); + } else { + result.append(", "); //$NON-NLS-1$ + } + result.append(String.format("Locale %s_%s", language, region)); //$NON-NLS-1$ + + index += 2; + } + + // post language/region qualifiers. + while (index < INDEX_COUNT) { + qualifier = mQualifiers[index++]; + if (qualifier != null) { + if (result == null) { + result = new StringBuilder(); + } else { + result.append(", "); //$NON-NLS-1$ + } + result.append(qualifier.getLongDisplayValue()); + + } + } + + return result == null ? null : result.toString(); + } + + @Override + public int compareTo(FolderConfiguration folderConfig) { + // default are always at the top. + if (isDefault()) { + if (folderConfig.isDefault()) { + return 0; + } + return -1; + } + + // now we compare the qualifiers + for (int i = 0 ; i < INDEX_COUNT; i++) { + ResourceQualifier qualifier1 = mQualifiers[i]; + ResourceQualifier qualifier2 = folderConfig.mQualifiers[i]; + + if (qualifier1 == null) { + if (qualifier2 == null) { + continue; + } else { + return -1; + } + } else { + if (qualifier2 == null) { + return 1; + } else { + int result = qualifier1.compareTo(qualifier2); + + if (result == 0) { + continue; + } + + return result; + } + } + } + + // if we arrive here, all the qualifier matches + return 0; + } + + /** + * Returns the best matching {@link Configurable} for this configuration. + * + * @param configurables the list of {@link Configurable} to choose from. + * + * @return an item from the given list of {@link Configurable} or null. + * + * @see http://d.android.com/guide/topics/resources/resources-i18n.html#best-match + */ + public Configurable findMatchingConfigurable(List<? extends Configurable> configurables) { + // + // 1: eliminate resources that contradict the reference configuration + // 2: pick next qualifier type + // 3: check if any resources use this qualifier, if no, back to 2, else move on to 4. + // 4: eliminate resources that don't use this qualifier. + // 5: if more than one resource left, go back to 2. + // + // The precedence of the qualifiers is more important than the number of qualifiers that + // exactly match the device. + + // 1: eliminate resources that contradict + ArrayList<Configurable> matchingConfigurables = new ArrayList<Configurable>(); + for (int i = 0 ; i < configurables.size(); i++) { + Configurable res = configurables.get(i); + + if (res.getConfiguration().isMatchFor(this)) { + matchingConfigurables.add(res); + } + } + + // if there is only one match, just take it + if (matchingConfigurables.size() == 1) { + return matchingConfigurables.get(0); + } else if (matchingConfigurables.size() == 0) { + return null; + } + + // 2. Loop on the qualifiers, and eliminate matches + final int count = FolderConfiguration.getQualifierCount(); + for (int q = 0 ; q < count ; q++) { + // look to see if one configurable has this qualifier. + // At the same time also record the best match value for the qualifier (if applicable). + + // The reference value, to find the best match. + // Note that this qualifier could be null. In which case any qualifier found in the + // possible match, will all be considered best match. + ResourceQualifier referenceQualifier = getQualifier(q); + + boolean found = false; + ResourceQualifier bestMatch = null; // this is to store the best match. + for (Configurable configurable : matchingConfigurables) { + ResourceQualifier qualifier = configurable.getConfiguration().getQualifier(q); + if (qualifier != null) { + // set the flag. + found = true; + + // Now check for a best match. If the reference qualifier is null , + // any qualifier is a "best" match (we don't need to record all of them. + // Instead the non compatible ones are removed below) + if (referenceQualifier != null) { + if (qualifier.isBetterMatchThan(bestMatch, referenceQualifier)) { + bestMatch = qualifier; + } + } + } + } + + // 4. If a configurable has a qualifier at the current index, remove all the ones that + // do not have one, or whose qualifier value does not equal the best match found above + // unless there's no reference qualifier, in which case they are all considered + // "best" match. + if (found) { + for (int i = 0 ; i < matchingConfigurables.size(); ) { + Configurable configurable = matchingConfigurables.get(i); + ResourceQualifier qualifier = configurable.getConfiguration().getQualifier(q); + + if (qualifier == null) { + // this resources has no qualifier of this type: rejected. + matchingConfigurables.remove(configurable); + } else if (referenceQualifier != null && bestMatch != null && + bestMatch.equals(qualifier) == false) { + // there's a reference qualifier and there is a better match for it than + // this resource, so we reject it. + matchingConfigurables.remove(configurable); + } else { + // looks like we keep this resource, move on to the next one. + i++; + } + } + + // at this point we may have run out of matching resources before going + // through all the qualifiers. + if (matchingConfigurables.size() < 2) { + break; + } + } + } + + // Because we accept resources whose configuration have qualifiers where the reference + // configuration doesn't, we can end up with more than one match. In this case, we just + // take the first one. + if (matchingConfigurables.size() == 0) { + return null; + } + return matchingConfigurables.get(0); + } + + + /** + * Returns whether the configuration is a match for the given reference config. + * <p/>A match means that, for each qualifier of this config + * <ul> + * <li>The reference config has no value set + * <li>or, the qualifier of the reference config is a match. Depending on the qualifier type + * this does not mean the same exact value.</li> + * </ul> + * @param referenceConfig The reference configuration to test against. + * @return true if the configuration matches. + */ + public boolean isMatchFor(FolderConfiguration referenceConfig) { + if (referenceConfig == null) { + return false; + } + + for (int i = 0 ; i < INDEX_COUNT ; i++) { + ResourceQualifier testQualifier = mQualifiers[i]; + ResourceQualifier referenceQualifier = referenceConfig.mQualifiers[i]; + + // it's only a non match if both qualifiers are non-null, and they don't match. + if (testQualifier != null && referenceQualifier != null && + testQualifier.isMatchFor(referenceQualifier) == false) { + return false; + } + } + + return true; + } + + /** + * Returns the index of the first non null {@link ResourceQualifier} starting at index + * <var>startIndex</var> + * @param startIndex + * @return -1 if no qualifier was found. + */ + public int getHighestPriorityQualifier(int startIndex) { + for (int i = startIndex ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null) { + return i; + } + } + + return -1; + } + + /** + * Create default qualifiers. + * <p/>This creates qualifiers with no values for all indices. + */ + public void createDefault() { + mQualifiers[INDEX_COUNTRY_CODE] = new CountryCodeQualifier(); + mQualifiers[INDEX_NETWORK_CODE] = new NetworkCodeQualifier(); + mQualifiers[INDEX_LANGUAGE] = new LanguageQualifier(); + mQualifiers[INDEX_REGION] = new RegionQualifier(); + mQualifiers[INDEX_SMALLEST_SCREEN_WIDTH] = new SmallestScreenWidthQualifier(); + mQualifiers[INDEX_SCREEN_WIDTH] = new ScreenWidthQualifier(); + mQualifiers[INDEX_SCREEN_HEIGHT] = new ScreenHeightQualifier(); + mQualifiers[INDEX_SCREEN_LAYOUT_SIZE] = new ScreenSizeQualifier(); + mQualifiers[INDEX_SCREEN_RATIO] = new ScreenRatioQualifier(); + mQualifiers[INDEX_SCREEN_ORIENTATION] = new ScreenOrientationQualifier(); + mQualifiers[INDEX_UI_MODE] = new UiModeQualifier(); + mQualifiers[INDEX_NIGHT_MODE] = new NightModeQualifier(); + mQualifiers[INDEX_PIXEL_DENSITY] = new DensityQualifier(); + mQualifiers[INDEX_TOUCH_TYPE] = new TouchScreenQualifier(); + mQualifiers[INDEX_KEYBOARD_STATE] = new KeyboardStateQualifier(); + mQualifiers[INDEX_TEXT_INPUT_METHOD] = new TextInputMethodQualifier(); + mQualifiers[INDEX_NAVIGATION_STATE] = new NavigationStateQualifier(); + mQualifiers[INDEX_NAVIGATION_METHOD] = new NavigationMethodQualifier(); + mQualifiers[INDEX_SCREEN_DIMENSION] = new ScreenDimensionQualifier(); + mQualifiers[INDEX_VERSION] = new VersionQualifier(); + } + + /** + * Returns an array of all the non null qualifiers. + */ + public ResourceQualifier[] getQualifiers() { + int count = 0; + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null) { + count++; + } + } + + ResourceQualifier[] array = new ResourceQualifier[count]; + int index = 0; + for (int i = 0 ; i < INDEX_COUNT ; i++) { + if (mQualifiers[i] != null) { + array[index++] = mQualifiers[i]; + } + } + + return array; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/KeyboardStateQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/KeyboardStateQualifier.java new file mode 100644 index 0000000..1397808 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/KeyboardStateQualifier.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2008 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.ide.common.resources.configuration; + +import com.android.resources.KeyboardState; +import com.android.resources.ResourceEnum; + +/** + * Resource Qualifier for keyboard state. + */ +public final class KeyboardStateQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Keyboard State"; + + private KeyboardState mValue = null; + + public KeyboardStateQualifier() { + // pass + } + + public KeyboardStateQualifier(KeyboardState value) { + mValue = value; + } + + public KeyboardState getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Keyboard"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + KeyboardState orientation = KeyboardState.getEnum(value); + if (orientation != null) { + KeyboardStateQualifier qualifier = new KeyboardStateQualifier(); + qualifier.mValue = orientation; + config.setKeyboardStateQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + if (qualifier instanceof KeyboardStateQualifier) { + KeyboardStateQualifier referenceQualifier = (KeyboardStateQualifier)qualifier; + + // special case where EXPOSED can be used for SOFT + if (referenceQualifier.mValue == KeyboardState.SOFT && + mValue == KeyboardState.EXPOSED) { + return true; + } + + return referenceQualifier.mValue == mValue; + } + + return false; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + KeyboardStateQualifier compareQualifier = (KeyboardStateQualifier)compareTo; + KeyboardStateQualifier referenceQualifier = (KeyboardStateQualifier)reference; + + if (referenceQualifier.mValue == KeyboardState.SOFT) { // only case where there could be a + // better qualifier + // only return true if it's a better value. + if (compareQualifier.mValue == KeyboardState.EXPOSED && mValue == KeyboardState.SOFT) { + return true; + } + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/LanguageQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/LanguageQualifier.java new file mode 100644 index 0000000..76514e2 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/LanguageQualifier.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Language. + */ +public final class LanguageQualifier extends ResourceQualifier { + private final static Pattern sLanguagePattern = Pattern.compile("^[a-z]{2}$"); //$NON-NLS-1$ + + public static final String FAKE_LANG_VALUE = "__"; //$NON-NLS-1$ + public static final String NAME = "Language"; + + private String mValue; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link LanguageQualifier} object or <code>null</code> + */ + public static LanguageQualifier getQualifier(String segment) { + if (sLanguagePattern.matcher(segment).matches()) { + LanguageQualifier qualifier = new LanguageQualifier(); + qualifier.mValue = segment; + + return qualifier; + } + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link LanguageQualifier} object. + * @param value the value of the qualifier, as returned by {@link #getValue()}. + */ + public static String getFolderSegment(String value) { + String segment = value.toLowerCase(Locale.US); + if (sLanguagePattern.matcher(segment).matches()) { + return segment; + } + + return null; + } + + public LanguageQualifier() { + + } + + public LanguageQualifier(String value) { + mValue = value; + } + + public String getValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean hasFakeValue() { + return FAKE_LANG_VALUE.equals(mValue); + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + LanguageQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setLanguageQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof LanguageQualifier) { + if (mValue == null) { + return ((LanguageQualifier)qualifier).mValue == null; + } + return mValue.equals(((LanguageQualifier)qualifier).mValue); + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String getFolderSegment() { + if (mValue != null) { + return getFolderSegment(mValue); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getShortDisplayValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (mValue != null) { + return String.format("Language %s", mValue); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/NavigationMethodQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/NavigationMethodQualifier.java new file mode 100644 index 0000000..f40bc6c --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/NavigationMethodQualifier.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import com.android.resources.Navigation; +import com.android.resources.ResourceEnum; + +/** + * Resource Qualifier for Navigation Method. + */ +public final class NavigationMethodQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Navigation Method"; + + private Navigation mValue; + + public NavigationMethodQualifier() { + // pass + } + + public NavigationMethodQualifier(Navigation value) { + mValue = value; + } + + public Navigation getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Navigation method = Navigation.getEnum(value); + if (method != null) { + NavigationMethodQualifier qualifier = new NavigationMethodQualifier(method); + config.setNavigationMethodQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/NavigationStateQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/NavigationStateQualifier.java new file mode 100644 index 0000000..91b81df --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/NavigationStateQualifier.java @@ -0,0 +1,75 @@ +/* + * 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.ide.common.resources.configuration; + +import com.android.resources.NavigationState; +import com.android.resources.ResourceEnum; + +/** + * Resource Qualifier for navigation state. + */ +public final class NavigationStateQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Navigation State"; + + private NavigationState mValue = null; + + public NavigationStateQualifier() { + // pass + } + + public NavigationStateQualifier(NavigationState value) { + mValue = value; + } + + public NavigationState getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + NavigationState state = NavigationState.getEnum(value); + if (state != null) { + NavigationStateQualifier qualifier = new NavigationStateQualifier(); + qualifier.mValue = state; + config.setNavigationStateQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/NetworkCodeQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/NetworkCodeQualifier.java new file mode 100644 index 0000000..1ef2015 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/NetworkCodeQualifier.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2008 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Mobile Network Code Pixel Density. + */ +public final class NetworkCodeQualifier extends ResourceQualifier { + /** Default pixel density value. This means the property is not set. */ + private final static int DEFAULT_CODE = -1; + + private final static Pattern sNetworkCodePattern = Pattern.compile("^mnc(\\d{1,3})$"); //$NON-NLS-1$ + + private final int mCode; + + public final static String NAME = "Mobile Network Code"; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link CountryCodeQualifier} object or <code>null</code> + */ + public static NetworkCodeQualifier getQualifier(String segment) { + Matcher m = sNetworkCodePattern.matcher(segment); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return null; + } + + NetworkCodeQualifier qualifier = new NetworkCodeQualifier(code); + return qualifier; + } + + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link NetworkCodeQualifier} object. + * @param code the value of the qualifier, as returned by {@link #getCode()}. + */ + public static String getFolderSegment(int code) { + if (code != DEFAULT_CODE && code >= 1 && code <= 999) { // code is 1-3 digit. + return String.format("mnc%1$d", code); //$NON-NLS-1$ + } + + return ""; //$NON-NLS-1$ + } + + public NetworkCodeQualifier() { + this(DEFAULT_CODE); + } + + public NetworkCodeQualifier(int code) { + mCode = code; + } + + public int getCode() { + return mCode; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Network Code"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean isValid() { + return mCode != DEFAULT_CODE; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sNetworkCodePattern.matcher(value); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return false; + } + + NetworkCodeQualifier qualifier = new NetworkCodeQualifier(code); + config.setNetworkCodeQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof NetworkCodeQualifier) { + return mCode == ((NetworkCodeQualifier)qualifier).mCode; + } + + return false; + } + + @Override + public int hashCode() { + return mCode; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String getFolderSegment() { + return getFolderSegment(mCode); + } + + @Override + public String getShortDisplayValue() { + if (mCode != DEFAULT_CODE) { + return String.format("MNC %1$d", mCode); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + return getShortDisplayValue(); + } + +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/NightModeQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/NightModeQualifier.java new file mode 100644 index 0000000..d3b6760 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/NightModeQualifier.java @@ -0,0 +1,74 @@ +/* + * 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.ide.common.resources.configuration; + +import com.android.resources.NightMode; +import com.android.resources.ResourceEnum; + +/** + * Resource Qualifier for Navigation Method. + */ +public final class NightModeQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Night Mode"; + + private NightMode mValue; + + public NightModeQualifier() { + // pass + } + + public NightModeQualifier(NightMode value) { + mValue = value; + } + + public NightMode getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Night Mode"; + } + + @Override + public int since() { + return 8; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + NightMode mode = NightMode.getEnum(value); + if (mode != null) { + NightModeQualifier qualifier = new NightModeQualifier(mode); + config.setNightModeQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/RegionQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/RegionQualifier.java new file mode 100644 index 0000000..bd033bd --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/RegionQualifier.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Region. + */ +public final class RegionQualifier extends ResourceQualifier { + private final static Pattern sRegionPattern = Pattern.compile("^r([A-Z]{2})$"); //$NON-NLS-1$ + + public static final String FAKE_REGION_VALUE = "__"; //$NON-NLS-1$ + public static final String NAME = "Region"; + + private String mValue; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link RegionQualifier} object or <code>null</code> + */ + public static RegionQualifier getQualifier(String segment) { + Matcher m = sRegionPattern.matcher(segment); + if (m.matches()) { + RegionQualifier qualifier = new RegionQualifier(); + qualifier.mValue = m.group(1); + + return qualifier; + } + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link RegionQualifier} object. + * @param value the value of the qualifier, as returned by {@link #getValue()}. + */ + public static String getFolderSegment(String value) { + if (value != null) { + // See http://developer.android.com/reference/java/util/Locale.html#default_locale + String segment = "r" + value.toUpperCase(Locale.US); //$NON-NLS-1$ + if (sRegionPattern.matcher(segment).matches()) { + return segment; + } + } + + return ""; //$NON-NLS-1$ + } + + public RegionQualifier() { + + } + + public RegionQualifier(String value) { + mValue = value; + } + + public String getValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean isValid() { + return mValue != null; + } + + @Override + public boolean hasFakeValue() { + return FAKE_REGION_VALUE.equals(mValue); + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + RegionQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setRegionQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof RegionQualifier) { + if (mValue == null) { + return ((RegionQualifier)qualifier).mValue == null; + } + return mValue.equals(((RegionQualifier)qualifier).mValue); + } + + return false; + } + + @Override + public int hashCode() { + if (mValue != null) { + return mValue.hashCode(); + } + + return 0; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String getFolderSegment() { + return getFolderSegment(mValue); + } + + @Override + public String getShortDisplayValue() { + if (mValue != null) { + return mValue; + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (mValue != null) { + return String.format("Region %s", mValue); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ResourceQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ResourceQualifier.java new file mode 100644 index 0000000..2997c8f --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ResourceQualifier.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + + +/** + * Base class for resource qualifiers. + * <p/>The resource qualifier classes are designed as immutable. + */ +public abstract class ResourceQualifier implements Comparable<ResourceQualifier> { + + /** + * Returns the human readable name of the qualifier. + */ + public abstract String getName(); + + /** + * Returns a shorter human readable name for the qualifier. + * @see #getName() + */ + public abstract String getShortName(); + + /** + * Returns when this qualifier was added to Android. + */ + public abstract int since(); + + /** + * Whether this qualifier is deprecated. + */ + public boolean deprecated() { + return false; + } + + /** + * Returns whether the qualifier has a valid filter value. + */ + public abstract boolean isValid(); + + /** + * Returns whether the qualifier has a fake value. + * <p/>Fake values are used internally and should not be used as real qualifier value. + */ + public abstract boolean hasFakeValue(); + + /** + * Check if the value is valid for this qualifier, and if so sets the value + * into a Folder Configuration. + * @param value The value to check and set. Must not be null. + * @param config The folder configuration to receive the value. Must not be null. + * @return true if the value was valid and was set. + */ + public abstract boolean checkAndSet(String value, FolderConfiguration config); + + /** + * Returns a string formated to be used in a folder name. + * <p/>This is declared as abstract to force children classes to implement it. + */ + public abstract String getFolderSegment(); + + /** + * Returns whether the given qualifier is a match for the receiver. + * <p/>The default implementation returns the result of {@link #equals(Object)}. + * <p/>Children class that re-implements this must implement + * {@link #isBetterMatchThan(ResourceQualifier, ResourceQualifier)} too. + * @param qualifier the reference qualifier + * @return true if the receiver is a match. + */ + public boolean isMatchFor(ResourceQualifier qualifier) { + return equals(qualifier); + } + + /** + * Returns true if the receiver is a better match for the given <var>reference</var> than + * the given <var>compareTo</var> comparable. + * @param compareTo The {@link ResourceQualifier} to compare to. Can be null, in which + * case the method must return <code>true</code>. + * @param reference The reference qualifier value for which the match is. + * @return true if the receiver is a better match. + */ + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + // the default is to always return false. This gives less overhead than always returning + // true, as it would only compare same values anyway. + return false; + } + + @Override + public String toString() { + return getFolderSegment(); + } + + /** + * Returns a string formatted for display purpose. + */ + public abstract String getShortDisplayValue(); + + /** + * Returns a string formatted for display purpose. + */ + public abstract String getLongDisplayValue(); + + /** + * Returns <code>true</code> if both objects are equal. + * <p/>This is declared as abstract to force children classes to implement it. + */ + @Override + public abstract boolean equals(Object object); + + /** + * Returns a hash code value for the object. + * <p/>This is declared as abstract to force children classes to implement it. + */ + @Override + public abstract int hashCode(); + + @Override + public final int compareTo(ResourceQualifier o) { + return toString().compareTo(o.toString()); + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ScreenDimensionQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenDimensionQualifier.java new file mode 100644 index 0000000..dce6c68 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenDimensionQualifier.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Dimension. + */ +public final class ScreenDimensionQualifier extends ResourceQualifier { + /** Default screen size value. This means the property is not set */ + final static int DEFAULT_SIZE = -1; + + private final static Pattern sDimensionPattern = Pattern.compile( + "^(\\d+)x(\\d+)$"); //$NON-NLS-1$ + + public static final String NAME = "Screen Dimension"; + + /** Screen size 1 value. This is not size X or Y because the folder name always + * contains the biggest size first. So if the qualifier is 400x200, size 1 will always be + * 400 but that'll be X in landscape and Y in portrait. + * Default value is <code>DEFAULT_SIZE</code> */ + private int mValue1 = DEFAULT_SIZE; + + /** Screen size 2 value. This is not size X or Y because the folder name always + * contains the biggest size first. So if the qualifier is 400x200, size 2 will always be + * 200 but that'll be Y in landscape and X in portrait. + * Default value is <code>DEFAULT_SIZE</code> */ + private int mValue2 = DEFAULT_SIZE; + + public ScreenDimensionQualifier() { + // pass + } + + public ScreenDimensionQualifier(int value1, int value2) { + mValue1 = value1; + mValue2 = value2; + } + + public int getValue1() { + return mValue1; + } + + public int getValue2() { + return mValue2; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Dimension"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean deprecated() { + return true; + } + + @Override + public boolean isValid() { + return mValue1 != DEFAULT_SIZE && mValue2 != DEFAULT_SIZE; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sDimensionPattern.matcher(value); + if (m.matches()) { + String d1 = m.group(1); + String d2 = m.group(2); + + ScreenDimensionQualifier qualifier = getQualifier(d1, d2); + if (qualifier != null) { + config.setScreenDimensionQualifier(qualifier); + return true; + } + } + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof ScreenDimensionQualifier) { + ScreenDimensionQualifier q = (ScreenDimensionQualifier)qualifier; + return (mValue1 == q.mValue1 && mValue2 == q.mValue2); + } + + return false; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + public static ScreenDimensionQualifier getQualifier(String size1, String size2) { + try { + int s1 = Integer.parseInt(size1); + int s2 = Integer.parseInt(size2); + + ScreenDimensionQualifier qualifier = new ScreenDimensionQualifier(); + + if (s1 > s2) { + qualifier.mValue1 = s1; + qualifier.mValue2 = s2; + } else { + qualifier.mValue1 = s2; + qualifier.mValue2 = s1; + } + + return qualifier; + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + } + + return null; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String getFolderSegment() { + return String.format("%1$dx%2$d", mValue1, mValue2); //$NON-NLS-1$ + } + + @Override + public String getShortDisplayValue() { + if (isValid()) { + return String.format("%1$dx%2$d", mValue1, mValue2); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (isValid()) { + return String.format("Screen resolution %1$dx%2$d", mValue1, mValue2); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ScreenHeightQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenHeightQualifier.java new file mode 100644 index 0000000..08bba61 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenHeightQualifier.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2011 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Pixel Density. + */ +public final class ScreenHeightQualifier extends ResourceQualifier { + /** Default screen size value. This means the property is not set */ + final static int DEFAULT_SIZE = -1; + + private final static Pattern sParsePattern = Pattern.compile("^h(\\d+)dp$");//$NON-NLS-1$ + private final static String sPrintPattern = "h%1$ddp"; + + public static final String NAME = "Screen Height"; + + private int mValue = DEFAULT_SIZE; + + public ScreenHeightQualifier() { + // pass + } + + public ScreenHeightQualifier(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 13; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean isValid() { + return mValue != DEFAULT_SIZE; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sParsePattern.matcher(value); + if (m.matches()) { + String v = m.group(1); + + ScreenHeightQualifier qualifier = getQualifier(v); + if (qualifier != null) { + config.setScreenHeightQualifier(qualifier); + return true; + } + } + + return false; + } + + public static ScreenHeightQualifier getQualifier(String value) { + try { + int dp = Integer.parseInt(value); + + ScreenHeightQualifier qualifier = new ScreenHeightQualifier(); + qualifier.mValue = dp; + return qualifier; + + } catch (NumberFormatException e) { + } + + return null; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + // this is the match only of the current dp value is lower or equal to the + if (qualifier instanceof ScreenHeightQualifier) { + return mValue <= ((ScreenHeightQualifier) qualifier).mValue; + } + + return false; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + ScreenHeightQualifier compareQ = (ScreenHeightQualifier)compareTo; + ScreenHeightQualifier referenceQ = (ScreenHeightQualifier)reference; + + if (compareQ.mValue == referenceQ.mValue) { + // what we have is already the best possible match (exact match) + return false; + } else if (mValue == referenceQ.mValue) { + // got new exact value, this is the best! + return true; + } else { + // get the qualifier that has the width that is the closest to the reference, but not + // above. (which is guaranteed when this is called as isMatchFor is called first. + return mValue > compareQ.mValue; + } + } + + @Override + public String getFolderSegment() { + return String.format(sPrintPattern, mValue); + } + + @Override + public String getShortDisplayValue() { + if (isValid()) { + return getFolderSegment(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (isValid()) { + return getFolderSegment(); + } + + return ""; //$NON-NLS-1$ + } + + + @Override + public int hashCode() { + return mValue; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ScreenHeightQualifier other = (ScreenHeightQualifier) obj; + if (mValue != other.mValue) { + return false; + } + return true; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ScreenOrientationQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenOrientationQualifier.java new file mode 100644 index 0000000..732a078 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenOrientationQualifier.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import com.android.resources.ResourceEnum; +import com.android.resources.ScreenOrientation; + +/** + * Resource Qualifier for Screen Orientation. + */ +public final class ScreenOrientationQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Screen Orientation"; + + private ScreenOrientation mValue = null; + + public ScreenOrientationQualifier() { + } + + public ScreenOrientationQualifier(ScreenOrientation value) { + mValue = value; + } + + public ScreenOrientation getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Orientation"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + ScreenOrientation orientation = ScreenOrientation.getEnum(value); + if (orientation != null) { + ScreenOrientationQualifier qualifier = new ScreenOrientationQualifier(orientation); + config.setScreenOrientationQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ScreenRatioQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenRatioQualifier.java new file mode 100644 index 0000000..b45946b --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenRatioQualifier.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2009 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.ide.common.resources.configuration; + +import com.android.resources.ResourceEnum; +import com.android.resources.ScreenRatio; + +public class ScreenRatioQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Screen Ratio"; + + private ScreenRatio mValue = null; + + public ScreenRatioQualifier() { + } + + public ScreenRatioQualifier(ScreenRatio value) { + mValue = value; + } + + public ScreenRatio getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Ratio"; + } + + @Override + public int since() { + return 4; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + ScreenRatio size = ScreenRatio.getEnum(value); + if (size != null) { + ScreenRatioQualifier qualifier = new ScreenRatioQualifier(size); + config.setScreenRatioQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ScreenSizeQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenSizeQualifier.java new file mode 100644 index 0000000..77193a2 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenSizeQualifier.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2009 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.ide.common.resources.configuration; + +import com.android.resources.ResourceEnum; +import com.android.resources.ScreenSize; + +/** + * Resource Qualifier for Screen Size. Size can be "small", "normal", "large" and "x-large" + */ +public class ScreenSizeQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Screen Size"; + + private ScreenSize mValue = null; + + + public ScreenSizeQualifier() { + } + + public ScreenSizeQualifier(ScreenSize value) { + mValue = value; + } + + public ScreenSize getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Size"; + } + + @Override + public int since() { + return 4; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + ScreenSize size = ScreenSize.getEnum(value); + if (size != null) { + ScreenSizeQualifier qualifier = new ScreenSizeQualifier(size); + config.setScreenSizeQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/ScreenWidthQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenWidthQualifier.java new file mode 100644 index 0000000..ab9134b --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/ScreenWidthQualifier.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2011 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Pixel Density. + */ +public final class ScreenWidthQualifier extends ResourceQualifier { + /** Default screen size value. This means the property is not set */ + final static int DEFAULT_SIZE = -1; + + private final static Pattern sParsePattern = Pattern.compile("^w(\\d+)dp$"); //$NON-NLS-1$ + private final static String sPrintPattern = "w%1$ddp"; //$NON-NLS-1$ + + public static final String NAME = "Screen Width"; + + private int mValue = DEFAULT_SIZE; + + public ScreenWidthQualifier() { + // pass + } + + public ScreenWidthQualifier(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 13; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean isValid() { + return mValue != DEFAULT_SIZE; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sParsePattern.matcher(value); + if (m.matches()) { + String v = m.group(1); + + ScreenWidthQualifier qualifier = getQualifier(v); + if (qualifier != null) { + config.setScreenWidthQualifier(qualifier); + return true; + } + } + + return false; + } + + public static ScreenWidthQualifier getQualifier(String value) { + try { + int dp = Integer.parseInt(value); + + ScreenWidthQualifier qualifier = new ScreenWidthQualifier(); + qualifier.mValue = dp; + return qualifier; + + } catch (NumberFormatException e) { + } + + return null; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + // this is the match only of the current dp value is lower or equal to the + if (qualifier instanceof ScreenWidthQualifier) { + return mValue <= ((ScreenWidthQualifier) qualifier).mValue; + } + + return false; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + ScreenWidthQualifier compareQ = (ScreenWidthQualifier)compareTo; + ScreenWidthQualifier referenceQ = (ScreenWidthQualifier)reference; + + if (compareQ.mValue == referenceQ.mValue) { + // what we have is already the best possible match (exact match) + return false; + } else if (mValue == referenceQ.mValue) { + // got new exact value, this is the best! + return true; + } else { + // get the qualifier that has the width that is the closest to the reference, but not + // above. (which is guaranteed when this is called as isMatchFor is called first. + return mValue > compareQ.mValue; + } + } + + @Override + public String getFolderSegment() { + return String.format(sPrintPattern, mValue); + } + + @Override + public String getShortDisplayValue() { + if (isValid()) { + return getFolderSegment(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (isValid()) { + return getFolderSegment(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public int hashCode() { + return mValue; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ScreenWidthQualifier other = (ScreenWidthQualifier) obj; + if (mValue != other.mValue) { + return false; + } + return true; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/SmallestScreenWidthQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/SmallestScreenWidthQualifier.java new file mode 100644 index 0000000..35d1ab1 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/SmallestScreenWidthQualifier.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2011 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Screen Pixel Density. + */ +public final class SmallestScreenWidthQualifier extends ResourceQualifier { + /** Default screen size value. This means the property is not set */ + final static int DEFAULT_SIZE = -1; + + private final static Pattern sParsePattern = Pattern.compile("^sw(\\d+)dp$"); //$NON-NLS-1$ + private final static String sPrintPattern = "sw%1$ddp"; //$NON-NLS-1$ + + public static final String NAME = "Smallest Screen Width"; + + private int mValue = DEFAULT_SIZE; + + public SmallestScreenWidthQualifier() { + // pass + } + + public SmallestScreenWidthQualifier(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 13; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean isValid() { + return mValue != DEFAULT_SIZE; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Matcher m = sParsePattern.matcher(value); + if (m.matches()) { + String v = m.group(1); + + SmallestScreenWidthQualifier qualifier = getQualifier(v); + if (qualifier != null) { + config.setSmallestScreenWidthQualifier(qualifier); + return true; + } + } + + return false; + } + + public static SmallestScreenWidthQualifier getQualifier(String value) { + try { + int dp = Integer.parseInt(value); + + SmallestScreenWidthQualifier qualifier = new SmallestScreenWidthQualifier(); + qualifier.mValue = dp; + return qualifier; + + } catch (NumberFormatException e) { + } + + return null; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + // this is the match only of the current dp value is lower or equal to the + if (qualifier instanceof SmallestScreenWidthQualifier) { + return mValue <= ((SmallestScreenWidthQualifier) qualifier).mValue; + } + + return false; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + SmallestScreenWidthQualifier compareQ = (SmallestScreenWidthQualifier)compareTo; + SmallestScreenWidthQualifier referenceQ = (SmallestScreenWidthQualifier)reference; + + if (compareQ.mValue == referenceQ.mValue) { + // what we have is already the best possible match (exact match) + return false; + } else if (mValue == referenceQ.mValue) { + // got new exact value, this is the best! + return true; + } else { + // get the qualifier that has the width that is the closest to the reference, but not + // above. (which is guaranteed when this is called as isMatchFor is called first. + return mValue > compareQ.mValue; + } + } + + @Override + public String getFolderSegment() { + return String.format(sPrintPattern, mValue); + } + + @Override + public String getShortDisplayValue() { + if (isValid()) { + return getFolderSegment(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (isValid()) { + return getFolderSegment(); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public int hashCode() { + return mValue; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SmallestScreenWidthQualifier other = (SmallestScreenWidthQualifier) obj; + if (mValue != other.mValue) { + return false; + } + return true; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/TextInputMethodQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/TextInputMethodQualifier.java new file mode 100644 index 0000000..784d43d --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/TextInputMethodQualifier.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import com.android.resources.Keyboard; +import com.android.resources.ResourceEnum; + +/** + * Resource Qualifier for Text Input Method. + */ +public final class TextInputMethodQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Text Input Method"; + + private Keyboard mValue; + + + public TextInputMethodQualifier() { + // pass + } + + public TextInputMethodQualifier(Keyboard value) { + mValue = value; + } + + public Keyboard getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Text Input"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + Keyboard method = Keyboard.getEnum(value); + if (method != null) { + TextInputMethodQualifier qualifier = new TextInputMethodQualifier(); + qualifier.mValue = method; + config.setTextInputMethodQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/TouchScreenQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/TouchScreenQualifier.java new file mode 100644 index 0000000..dce9f1d --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/TouchScreenQualifier.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2007 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.ide.common.resources.configuration; + +import com.android.resources.ResourceEnum; +import com.android.resources.TouchScreen; + + +/** + * Resource Qualifier for Touch Screen type. + */ +public final class TouchScreenQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "Touch Screen"; + + private TouchScreen mValue; + + public TouchScreenQualifier() { + // pass + } + + public TouchScreenQualifier(TouchScreen touchValue) { + mValue = touchValue; + } + + public TouchScreen getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + TouchScreen type = TouchScreen.getEnum(value); + if (type != null) { + TouchScreenQualifier qualifier = new TouchScreenQualifier(); + qualifier.mValue = type; + config.setTouchTypeQualifier(qualifier); + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/UiModeQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/UiModeQualifier.java new file mode 100644 index 0000000..1e302c5 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/UiModeQualifier.java @@ -0,0 +1,108 @@ +/* + * 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.ide.common.resources.configuration; + +import com.android.resources.ResourceEnum; +import com.android.resources.UiMode; + +/** + * Resource Qualifier for UI Mode. + */ +public final class UiModeQualifier extends EnumBasedResourceQualifier { + + public static final String NAME = "UI Mode"; + + private UiMode mValue; + + public UiModeQualifier() { + // pass + } + + public UiModeQualifier(UiMode value) { + mValue = value; + } + + public UiMode getValue() { + return mValue; + } + + @Override + ResourceEnum getEnumValue() { + return mValue; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return NAME; + } + + @Override + public int since() { + return 8; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + UiMode mode = UiMode.getEnum(value); + if (mode != null) { + UiModeQualifier qualifier = new UiModeQualifier(mode); + config.setUiModeQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + // only normal is a match for all UI mode, because it's not an actual mode. + if (mValue == UiMode.NORMAL) { + return true; + } + + // others must be an exact match + return ((UiModeQualifier)qualifier).mValue == mValue; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + UiModeQualifier compareQualifier = (UiModeQualifier)compareTo; + UiModeQualifier referenceQualifier = (UiModeQualifier)reference; + + if (compareQualifier.getValue() == referenceQualifier.getValue()) { + // what we have is already the best possible match (exact match) + return false; + } else if (mValue == referenceQualifier.mValue) { + // got new exact value, this is the best! + return true; + } else if (mValue == UiMode.NORMAL) { + // else "normal" can be a match in case there's no exact match + return true; + } + + return false; + } +} diff --git a/sdk_common/src/com/android/ide/common/resources/configuration/VersionQualifier.java b/sdk_common/src/com/android/ide/common/resources/configuration/VersionQualifier.java new file mode 100644 index 0000000..078d4af --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/configuration/VersionQualifier.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2009 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.ide.common.resources.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resource Qualifier for Platform Version. + */ +public final class VersionQualifier extends ResourceQualifier { + /** Default pixel density value. This means the property is not set. */ + private final static int DEFAULT_VERSION = -1; + + private final static Pattern sVersionPattern = Pattern.compile("^v(\\d+)$");//$NON-NLS-1$ + + private int mVersion = DEFAULT_VERSION; + + public static final String NAME = "Platform Version"; + + /** + * Creates and returns a qualifier from the given folder segment. If the segment is incorrect, + * <code>null</code> is returned. + * @param segment the folder segment from which to create a qualifier. + * @return a new {@link VersionQualifier} object or <code>null</code> + */ + public static VersionQualifier getQualifier(String segment) { + Matcher m = sVersionPattern.matcher(segment); + if (m.matches()) { + String v = m.group(1); + + int code = -1; + try { + code = Integer.parseInt(v); + } catch (NumberFormatException e) { + // looks like the string we extracted wasn't a valid number. + return null; + } + + VersionQualifier qualifier = new VersionQualifier(); + qualifier.mVersion = code; + return qualifier; + } + + return null; + } + + /** + * Returns the folder name segment for the given value. This is equivalent to calling + * {@link #toString()} on a {@link VersionQualifier} object. + * @param version the value of the qualifier, as returned by {@link #getVersion()}. + */ + public static String getFolderSegment(int version) { + if (version != DEFAULT_VERSION) { + return String.format("v%1$d", version); //$NON-NLS-1$ + } + + return ""; //$NON-NLS-1$ + } + + public VersionQualifier(int apiLevel) { + mVersion = apiLevel; + } + + public VersionQualifier() { + //pass + } + + public int getVersion() { + return mVersion; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getShortName() { + return "Version"; + } + + @Override + public int since() { + return 1; + } + + @Override + public boolean isValid() { + return mVersion != DEFAULT_VERSION; + } + + @Override + public boolean hasFakeValue() { + return false; + } + + @Override + public boolean checkAndSet(String value, FolderConfiguration config) { + VersionQualifier qualifier = getQualifier(value); + if (qualifier != null) { + config.setVersionQualifier(qualifier); + return true; + } + + return false; + } + + @Override + public boolean equals(Object qualifier) { + if (qualifier instanceof VersionQualifier) { + return mVersion == ((VersionQualifier)qualifier).mVersion; + } + + return false; + } + + @Override + public boolean isMatchFor(ResourceQualifier qualifier) { + if (qualifier instanceof VersionQualifier) { + // it is considered a match if the api level is equal or lower to the given qualifier + return mVersion <= ((VersionQualifier) qualifier).mVersion; + } + + return false; + } + + @Override + public boolean isBetterMatchThan(ResourceQualifier compareTo, ResourceQualifier reference) { + if (compareTo == null) { + return true; + } + + VersionQualifier compareQ = (VersionQualifier)compareTo; + VersionQualifier referenceQ = (VersionQualifier)reference; + + if (compareQ.mVersion == referenceQ.mVersion) { + // what we have is already the best possible match (exact match) + return false; + } else if (mVersion == referenceQ.mVersion) { + // got new exact value, this is the best! + return true; + } else { + // in all case we're going to prefer the higher version (since they have been filtered + // to not be too high + return mVersion > compareQ.mVersion; + } + } + + @Override + public int hashCode() { + return mVersion; + } + + /** + * Returns the string used to represent this qualifier in the folder name. + */ + @Override + public String getFolderSegment() { + return getFolderSegment(mVersion); + } + + @Override + public String getShortDisplayValue() { + if (mVersion != DEFAULT_VERSION) { + return String.format("API %1$d", mVersion); + } + + return ""; //$NON-NLS-1$ + } + + @Override + public String getLongDisplayValue() { + if (mVersion != DEFAULT_VERSION) { + return String.format("API Level %1$d", mVersion); + } + + return ""; //$NON-NLS-1$ + } +} diff --git a/sdk_common/src/com/android/ide/common/sdk/LoadStatus.java b/sdk_common/src/com/android/ide/common/sdk/LoadStatus.java new file mode 100644 index 0000000..babbd63 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/sdk/LoadStatus.java @@ -0,0 +1,24 @@ +/* + * 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.ide.common.sdk; + +/** + * Enum for loading status of various SDK parts. + */ +public enum LoadStatus { + LOADING, LOADED, FAILED; +} diff --git a/sdk_common/src/com/android/ide/common/xml/AndroidManifestParser.java b/sdk_common/src/com/android/ide/common/xml/AndroidManifestParser.java new file mode 100644 index 0000000..38dc1c4 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/xml/AndroidManifestParser.java @@ -0,0 +1,671 @@ +/* + * Copyright (C) 2007 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.ide.common.xml; + +import com.android.SdkConstants; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.common.xml.ManifestData.Instrumentation; +import com.android.ide.common.xml.ManifestData.SupportsScreens; +import com.android.ide.common.xml.ManifestData.UsesConfiguration; +import com.android.ide.common.xml.ManifestData.UsesFeature; +import com.android.ide.common.xml.ManifestData.UsesLibrary; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.io.StreamException; +import com.android.resources.Keyboard; +import com.android.resources.Navigation; +import com.android.resources.TouchScreen; +import com.android.xml.AndroidManifest; + +import org.xml.sax.Attributes; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +public class AndroidManifestParser { + + private final static int LEVEL_TOP = 0; + private final static int LEVEL_INSIDE_MANIFEST = 1; + private final static int LEVEL_INSIDE_APPLICATION = 2; + private final static int LEVEL_INSIDE_APP_COMPONENT = 3; + private final static int LEVEL_INSIDE_INTENT_FILTER = 4; + + private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$ + private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$ + + public interface ManifestErrorHandler extends ErrorHandler { + /** + * Handles a parsing error and an optional line number. + */ + void handleError(Exception exception, int lineNumber); + + /** + * Checks that a class is valid and can be used in the Android Manifest. + * <p/> + * Errors are put as {@code org.eclipse.core.resources.IMarker} on the manifest file. + * + * @param locator + * @param className the fully qualified name of the class to test. + * @param superClassName the fully qualified name of the class it is supposed to extend. + * @param testVisibility if <code>true</code>, the method will check the visibility of + * the class or of its constructors. + */ + void checkClass(Locator locator, String className, String superClassName, + boolean testVisibility); + } + + /** + * XML error & data handler used when parsing the AndroidManifest.xml file. + * <p/> + * During parsing this will fill up the {@link ManifestData} object given to the constructor + * and call out errors to the given {@link ManifestErrorHandler}. + */ + private static class ManifestHandler extends DefaultHandler { + + //--- temporary data/flags used during parsing + private final ManifestData mManifestData; + private final ManifestErrorHandler mErrorHandler; + private int mCurrentLevel = 0; + private int mValidLevel = 0; + private Activity mCurrentActivity = null; + private Locator mLocator; + + /** + * Creates a new {@link ManifestHandler}. + * + * @param manifestFile The manifest file being parsed. Can be null. + * @param manifestData Class containing the manifest info obtained during the parsing. + * @param errorHandler An optional error handler. + */ + ManifestHandler(IAbstractFile manifestFile, ManifestData manifestData, + ManifestErrorHandler errorHandler) { + super(); + mManifestData = manifestData; + mErrorHandler = errorHandler; + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator) + */ + @Override + public void setDocumentLocator(Locator locator) { + mLocator = locator; + super.setDocumentLocator(locator); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, + * java.lang.String, org.xml.sax.Attributes) + */ + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + try { + if (mManifestData == null) { + return; + } + + // if we're at a valid level + if (mValidLevel == mCurrentLevel) { + String value; + switch (mValidLevel) { + case LEVEL_TOP: + if (AndroidManifest.NODE_MANIFEST.equals(localName)) { + // lets get the package name. + mManifestData.mPackage = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_PACKAGE, + false /* hasNamespace */); + + // and the versionCode + String tmp = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_VERSIONCODE, true); + if (tmp != null) { + try { + mManifestData.mVersionCode = Integer.valueOf(tmp); + } catch (NumberFormatException e) { + // keep null in the field. + } + } + mValidLevel++; + } + break; + case LEVEL_INSIDE_MANIFEST: + if (AndroidManifest.NODE_APPLICATION.equals(localName)) { + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_PROCESS, + true /* hasNamespace */); + if (value != null) { + mManifestData.addProcessName(value); + } + + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_DEBUGGABLE, + true /* hasNamespace*/); + if (value != null) { + mManifestData.mDebuggable = Boolean.parseBoolean(value); + } + + mValidLevel++; + } else if (AndroidManifest.NODE_USES_SDK.equals(localName)) { + mManifestData.setMinSdkVersionString(getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, + true /* hasNamespace */)); + mManifestData.setTargetSdkVersionString(getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_TARGET_SDK_VERSION, + true /* hasNamespace */)); + } else if (AndroidManifest.NODE_INSTRUMENTATION.equals(localName)) { + processInstrumentationNode(attributes); + + } else if (AndroidManifest.NODE_SUPPORTS_SCREENS.equals(localName)) { + processSupportsScreensNode(attributes); + + } else if (AndroidManifest.NODE_USES_CONFIGURATION.equals(localName)) { + processUsesConfiguration(attributes); + + } else if (AndroidManifest.NODE_USES_FEATURE.equals(localName)) { + UsesFeature feature = new UsesFeature(); + + // get the name + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (value != null) { + feature.mName = value; + } + + // read the required attribute + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_REQUIRED, + true /*hasNamespace*/); + if (value != null) { + Boolean b = Boolean.valueOf(value); + if (b != null) { + feature.mRequired = b; + } + } + + // read the gl es attribute + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_GLESVERSION, + true /*hasNamespace*/); + if (value != null) { + try { + int version = Integer.decode(value); + feature.mGlEsVersion = version; + } catch (NumberFormatException e) { + // ignore + } + + } + + mManifestData.mFeatures.add(feature); + } + break; + case LEVEL_INSIDE_APPLICATION: + if (AndroidManifest.NODE_ACTIVITY.equals(localName)) { + processActivityNode(attributes); + mValidLevel++; + } else if (AndroidManifest.NODE_SERVICE.equals(localName)) { + processNode(attributes, SdkConstants.CLASS_SERVICE); + mValidLevel++; + } else if (AndroidManifest.NODE_RECEIVER.equals(localName)) { + processNode(attributes, SdkConstants.CLASS_BROADCASTRECEIVER); + mValidLevel++; + } else if (AndroidManifest.NODE_PROVIDER.equals(localName)) { + processNode(attributes, SdkConstants.CLASS_CONTENTPROVIDER); + mValidLevel++; + } else if (AndroidManifest.NODE_USES_LIBRARY.equals(localName)) { + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (value != null) { + UsesLibrary library = new UsesLibrary(); + library.mName = value; + + // read the required attribute + value = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_REQUIRED, + true /*hasNamespace*/); + if (value != null) { + Boolean b = Boolean.valueOf(value); + if (b != null) { + library.mRequired = b; + } + } + + mManifestData.mLibraries.add(library); + } + } + break; + case LEVEL_INSIDE_APP_COMPONENT: + // only process this level if we are in an activity + if (mCurrentActivity != null && + AndroidManifest.NODE_INTENT.equals(localName)) { + mCurrentActivity.resetIntentFilter(); + mValidLevel++; + } + break; + case LEVEL_INSIDE_INTENT_FILTER: + if (mCurrentActivity != null) { + if (AndroidManifest.NODE_ACTION.equals(localName)) { + // get the name attribute + String action = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (action != null) { + mCurrentActivity.setHasAction(true); + mCurrentActivity.setHasMainAction( + ACTION_MAIN.equals(action)); + } + } else if (AndroidManifest.NODE_CATEGORY.equals(localName)) { + String category = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (CATEGORY_LAUNCHER.equals(category)) { + mCurrentActivity.setHasLauncherCategory(true); + } + } + + // no need to increase mValidLevel as we don't process anything + // below this level. + } + break; + } + } + + mCurrentLevel++; + } finally { + super.startElement(uri, localName, name, attributes); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, + * java.lang.String) + */ + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + try { + if (mManifestData == null) { + return; + } + + // decrement the levels. + if (mValidLevel == mCurrentLevel) { + mValidLevel--; + } + mCurrentLevel--; + + // if we're at a valid level + // process the end of the element + if (mValidLevel == mCurrentLevel) { + switch (mValidLevel) { + case LEVEL_INSIDE_APPLICATION: + mCurrentActivity = null; + break; + case LEVEL_INSIDE_APP_COMPONENT: + // if we found both a main action and a launcher category, this is our + // launcher activity! + if (mManifestData.mLauncherActivity == null && + mCurrentActivity != null && + mCurrentActivity.isHomeActivity() && + mCurrentActivity.isExported()) { + mManifestData.mLauncherActivity = mCurrentActivity; + } + break; + default: + break; + } + + } + } finally { + super.endElement(uri, localName, name); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException) + */ + @Override + public void error(SAXParseException e) { + if (mErrorHandler != null) { + mErrorHandler.handleError(e, e.getLineNumber()); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException) + */ + @Override + public void fatalError(SAXParseException e) { + if (mErrorHandler != null) { + mErrorHandler.handleError(e, e.getLineNumber()); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException) + */ + @Override + public void warning(SAXParseException e) throws SAXException { + if (mErrorHandler != null) { + mErrorHandler.warning(e); + } + } + + /** + * Processes the activity node. + * @param attributes the attributes for the activity node. + */ + private void processActivityNode(Attributes attributes) { + // lets get the activity name, and add it to the list + String activityName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (activityName != null) { + activityName = AndroidManifest.combinePackageAndClassName(mManifestData.mPackage, + activityName); + + // get the exported flag. + String exportedStr = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_EXPORTED, true); + boolean exported = exportedStr == null || + exportedStr.toLowerCase(Locale.US).equals("true"); //$NON-NLS-1$ + mCurrentActivity = new Activity(activityName, exported); + mManifestData.mActivities.add(mCurrentActivity); + + if (mErrorHandler != null) { + mErrorHandler.checkClass(mLocator, activityName, SdkConstants.CLASS_ACTIVITY, + true /* testVisibility */); + } + } else { + // no activity found! Aapt will output an error, + // so we don't have to do anything + mCurrentActivity = null; + } + + String processName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_PROCESS, + true /* hasNamespace */); + if (processName != null) { + mManifestData.addProcessName(processName); + } + } + + /** + * Processes the service/receiver/provider nodes. + * @param attributes the attributes for the activity node. + * @param superClassName the fully qualified name of the super class that this + * node is representing + */ + private void processNode(Attributes attributes, String superClassName) { + // lets get the class name, and check it if required. + String serviceName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (serviceName != null) { + serviceName = AndroidManifest.combinePackageAndClassName(mManifestData.mPackage, + serviceName); + + if (mErrorHandler != null) { + mErrorHandler.checkClass(mLocator, serviceName, superClassName, + false /* testVisibility */); + } + } + + String processName = getAttributeValue(attributes, AndroidManifest.ATTRIBUTE_PROCESS, + true /* hasNamespace */); + if (processName != null) { + mManifestData.addProcessName(processName); + } + } + + /** + * Processes the instrumentation node. + * @param attributes the attributes for the instrumentation node. + */ + private void processInstrumentationNode(Attributes attributes) { + // lets get the class name, and check it if required. + String instrumentationName = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_NAME, + true /* hasNamespace */); + if (instrumentationName != null) { + String instrClassName = AndroidManifest.combinePackageAndClassName( + mManifestData.mPackage, instrumentationName); + String targetPackage = getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_TARGET_PACKAGE, + true /* hasNamespace */); + mManifestData.mInstrumentations.add( + new Instrumentation(instrClassName, targetPackage)); + if (mErrorHandler != null) { + mErrorHandler.checkClass(mLocator, instrClassName, + SdkConstants.CLASS_INSTRUMENTATION, true /* testVisibility */); + } + } + } + + /** + * Processes the supports-screens node. + * @param attributes the attributes for the supports-screens node. + */ + private void processSupportsScreensNode(Attributes attributes) { + mManifestData.mSupportsScreensFromManifest = new SupportsScreens(); + + mManifestData.mSupportsScreensFromManifest.setResizeable(getAttributeBooleanValue( + attributes, AndroidManifest.ATTRIBUTE_RESIZEABLE, true /*hasNamespace*/)); + + mManifestData.mSupportsScreensFromManifest.setAnyDensity(getAttributeBooleanValue( + attributes, AndroidManifest.ATTRIBUTE_ANYDENSITY, true /*hasNamespace*/)); + + mManifestData.mSupportsScreensFromManifest.setSmallScreens(getAttributeBooleanValue( + attributes, AndroidManifest.ATTRIBUTE_SMALLSCREENS, true /*hasNamespace*/)); + + mManifestData.mSupportsScreensFromManifest.setNormalScreens(getAttributeBooleanValue( + attributes, AndroidManifest.ATTRIBUTE_NORMALSCREENS, true /*hasNamespace*/)); + + mManifestData.mSupportsScreensFromManifest.setLargeScreens(getAttributeBooleanValue( + attributes, AndroidManifest.ATTRIBUTE_LARGESCREENS, true /*hasNamespace*/)); + } + + /** + * Processes the supports-screens node. + * @param attributes the attributes for the supports-screens node. + */ + private void processUsesConfiguration(Attributes attributes) { + mManifestData.mUsesConfiguration = new UsesConfiguration(); + + mManifestData.mUsesConfiguration.mReqFiveWayNav = getAttributeBooleanValue( + attributes, + AndroidManifest.ATTRIBUTE_REQ_5WAYNAV, true /*hasNamespace*/); + mManifestData.mUsesConfiguration.mReqNavigation = Navigation.getEnum( + getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_REQ_NAVIGATION, true /*hasNamespace*/)); + mManifestData.mUsesConfiguration.mReqHardKeyboard = getAttributeBooleanValue( + attributes, + AndroidManifest.ATTRIBUTE_REQ_HARDKEYBOARD, true /*hasNamespace*/); + mManifestData.mUsesConfiguration.mReqKeyboardType = Keyboard.getEnum( + getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_REQ_KEYBOARDTYPE, true /*hasNamespace*/)); + mManifestData.mUsesConfiguration.mReqTouchScreen = TouchScreen.getEnum( + getAttributeValue(attributes, + AndroidManifest.ATTRIBUTE_REQ_TOUCHSCREEN, true /*hasNamespace*/)); + } + + /** + * Searches through the attributes list for a particular one and returns its value. + * @param attributes the attribute list to search through + * @param attributeName the name of the attribute to look for. + * @param hasNamespace Indicates whether the attribute has an android namespace. + * @return a String with the value or null if the attribute was not found. + * @see SdkConstants#NS_RESOURCES + */ + private String getAttributeValue(Attributes attributes, String attributeName, + boolean hasNamespace) { + int count = attributes.getLength(); + for (int i = 0 ; i < count ; i++) { + if (attributeName.equals(attributes.getLocalName(i)) && + ((hasNamespace && + SdkConstants.NS_RESOURCES.equals(attributes.getURI(i))) || + (hasNamespace == false && attributes.getURI(i).length() == 0))) { + return attributes.getValue(i); + } + } + + return null; + } + + /** + * Searches through the attributes list for a particular one and returns its value as a + * Boolean. If the attribute is not present, this will return null. + * @param attributes the attribute list to search through + * @param attributeName the name of the attribute to look for. + * @param hasNamespace Indicates whether the attribute has an android namespace. + * @return a String with the value or null if the attribute was not found. + * @see SdkConstants#NS_RESOURCES + */ + private Boolean getAttributeBooleanValue(Attributes attributes, String attributeName, + boolean hasNamespace) { + int count = attributes.getLength(); + for (int i = 0 ; i < count ; i++) { + if (attributeName.equals(attributes.getLocalName(i)) && + ((hasNamespace && + SdkConstants.NS_RESOURCES.equals(attributes.getURI(i))) || + (hasNamespace == false && attributes.getURI(i).length() == 0))) { + String attr = attributes.getValue(i); + if (attr != null) { + return Boolean.valueOf(attr); + } else { + return null; + } + } + } + + return null; + } + + } + + private final static SAXParserFactory sParserFactory; + + static { + sParserFactory = SAXParserFactory.newInstance(); + sParserFactory.setNamespaceAware(true); + } + + /** + * Parses the Android Manifest, and returns a {@link ManifestData} object containing the + * result of the parsing. + * + * @param manifestFile the {@link IAbstractFile} representing the manifest file. + * @param gatherData indicates whether the parsing will extract data from the manifest. If false + * the method will always return null. + * @param errorHandler an optional errorHandler. + * @return A class containing the manifest info obtained during the parsing, or null on error. + * + * @throws StreamException + * @throws IOException + * @throws SAXException + * @throws ParserConfigurationException + */ + public static ManifestData parse( + IAbstractFile manifestFile, + boolean gatherData, + ManifestErrorHandler errorHandler) + throws SAXException, IOException, StreamException, ParserConfigurationException { + if (manifestFile != null) { + SAXParser parser = sParserFactory.newSAXParser(); + + ManifestData data = null; + if (gatherData) { + data = new ManifestData(); + } + + ManifestHandler manifestHandler = new ManifestHandler(manifestFile, + data, errorHandler); + parser.parse(new InputSource(manifestFile.getContents()), manifestHandler); + + return data; + } + + return null; + } + + /** + * Parses the Android Manifest, and returns an object containing the result of the parsing. + * + * <p/> + * This is the equivalent of calling <pre>parse(manifestFile, true, null)</pre> + * + * @param manifestFile the manifest file to parse. + * + * @throws ParserConfigurationException + * @throws StreamException + * @throws IOException + * @throws SAXException + */ + public static ManifestData parse(IAbstractFile manifestFile) + throws SAXException, IOException, StreamException, ParserConfigurationException { + return parse(manifestFile, true, null); + } + + public static ManifestData parse(IAbstractFolder projectFolder) + throws SAXException, IOException, StreamException, ParserConfigurationException { + IAbstractFile manifestFile = AndroidManifest.getManifest(projectFolder); + if (manifestFile == null) { + throw new FileNotFoundException(); + } + + return parse(manifestFile, true, null); + } + + /** + * Parses the Android Manifest from an {@link InputStream}, and returns a {@link ManifestData} + * object containing the result of the parsing. + * + * @param manifestFileStream the {@link InputStream} representing the manifest file. + * @return A class containing the manifest info obtained during the parsing or null on error. + * + * @throws StreamException + * @throws IOException + * @throws SAXException + * @throws ParserConfigurationException + */ + public static ManifestData parse(InputStream manifestFileStream) + throws SAXException, IOException, StreamException, ParserConfigurationException { + if (manifestFileStream != null) { + SAXParser parser = sParserFactory.newSAXParser(); + + ManifestData data = new ManifestData(); + + ManifestHandler manifestHandler = new ManifestHandler(null, data, null); + parser.parse(new InputSource(manifestFileStream), manifestHandler); + + return data; + } + + return null; + } +} diff --git a/sdk_common/src/com/android/ide/common/xml/ManifestData.java b/sdk_common/src/com/android/ide/common/xml/ManifestData.java new file mode 100644 index 0000000..9b68d60 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/xml/ManifestData.java @@ -0,0 +1,747 @@ +/* + * 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.ide.common.xml; + +import com.android.resources.Keyboard; +import com.android.resources.Navigation; +import com.android.resources.TouchScreen; + +import java.util.ArrayList; +import java.util.Set; +import java.util.TreeSet; + +/** + * Class containing the manifest info obtained during the parsing. + */ +public final class ManifestData { + + /** + * Value returned by {@link #getMinSdkVersion()} when the value of the minSdkVersion attribute + * in the manifest is a codename and not an integer value. + */ + public final static int MIN_SDK_CODENAME = 0; + + /** + * Value returned by {@link #getGlEsVersion()} when there are no <uses-feature> node with the + * attribute glEsVersion set. + */ + public final static int GL_ES_VERSION_NOT_SET = -1; + + /** Application package */ + String mPackage; + /** Application version Code, null if the attribute is not present. */ + Integer mVersionCode = null; + /** List of all activities */ + final ArrayList<Activity> mActivities = new ArrayList<Activity>(); + /** Launcher activity */ + Activity mLauncherActivity = null; + /** list of process names declared by the manifest */ + Set<String> mProcesses = null; + /** debuggable attribute value. If null, the attribute is not present. */ + Boolean mDebuggable = null; + /** API level requirement. if null the attribute was not present. */ + private String mMinSdkVersionString = null; + /** API level requirement. Default is 1 even if missing. If value is a codename, then it'll be + * 0 instead. */ + private int mMinSdkVersion = 1; + private int mTargetSdkVersion = 0; + /** List of all instrumentations declared by the manifest */ + final ArrayList<Instrumentation> mInstrumentations = + new ArrayList<Instrumentation>(); + /** List of all libraries in use declared by the manifest */ + final ArrayList<UsesLibrary> mLibraries = new ArrayList<UsesLibrary>(); + /** List of all feature in use declared by the manifest */ + final ArrayList<UsesFeature> mFeatures = new ArrayList<UsesFeature>(); + + SupportsScreens mSupportsScreensFromManifest; + SupportsScreens mSupportsScreensValues; + UsesConfiguration mUsesConfiguration; + + /** + * Instrumentation info obtained from manifest + */ + public final static class Instrumentation { + private final String mName; + private final String mTargetPackage; + + Instrumentation(String name, String targetPackage) { + mName = name; + mTargetPackage = targetPackage; + } + + /** + * Returns the fully qualified instrumentation class name + */ + public String getName() { + return mName; + } + + /** + * Returns the Android app package that is the target of this instrumentation + */ + public String getTargetPackage() { + return mTargetPackage; + } + } + + /** + * Activity info obtained from the manifest. + */ + public final static class Activity { + private final String mName; + private final boolean mIsExported; + private boolean mHasAction = false; + private boolean mHasMainAction = false; + private boolean mHasLauncherCategory = false; + + public Activity(String name, boolean exported) { + mName = name; + mIsExported = exported; + } + + public String getName() { + return mName; + } + + public boolean isExported() { + return mIsExported; + } + + public boolean hasAction() { + return mHasAction; + } + + public boolean isHomeActivity() { + return mHasMainAction && mHasLauncherCategory; + } + + void setHasAction(boolean hasAction) { + mHasAction = hasAction; + } + + /** If the activity doesn't yet have a filter set for the launcher, this resets both + * flags. This is to handle multiple intent-filters where one could have the valid + * action, and another one of the valid category. + */ + void resetIntentFilter() { + if (isHomeActivity() == false) { + mHasMainAction = mHasLauncherCategory = false; + } + } + + void setHasMainAction(boolean hasMainAction) { + mHasMainAction = hasMainAction; + } + + void setHasLauncherCategory(boolean hasLauncherCategory) { + mHasLauncherCategory = hasLauncherCategory; + } + } + + /** + * Class representing the <code>supports-screens</code> node in the manifest. + * By default, all the getters will return null if there was no value defined in the manifest. + * + * To get an instance with all the actual values, use {@link #resolveSupportsScreensValues(int)} + */ + public final static class SupportsScreens { + private Boolean mResizeable; + private Boolean mAnyDensity; + private Boolean mSmallScreens; + private Boolean mNormalScreens; + private Boolean mLargeScreens; + + public SupportsScreens() { + } + + /** + * Instantiate an instance from a string. The string must have been created with + * {@link #getEncodedValues()}. + * @param value the string. + */ + public SupportsScreens(String value) { + String[] values = value.split("\\|"); + + mAnyDensity = Boolean.valueOf(values[0]); + mResizeable = Boolean.valueOf(values[1]); + mSmallScreens = Boolean.valueOf(values[2]); + mNormalScreens = Boolean.valueOf(values[3]); + mLargeScreens = Boolean.valueOf(values[4]); + } + + /** + * Returns an instance of {@link SupportsScreens} initialized with the default values + * based on the given targetSdkVersion. + * @param targetSdkVersion + */ + public static SupportsScreens getDefaultValues(int targetSdkVersion) { + SupportsScreens result = new SupportsScreens(); + + result.mNormalScreens = Boolean.TRUE; + // Screen size and density became available in Android 1.5/API3, so before that + // non normal screens were not supported by default. After they are considered + // supported. + result.mResizeable = result.mAnyDensity = result.mSmallScreens = result.mLargeScreens = + targetSdkVersion <= 3 ? Boolean.FALSE : Boolean.TRUE; + + return result; + } + + /** + * Returns a version of the receiver for which all values have been set, even if they + * were not present in the manifest. + * @param targetSdkVersion the target api level of the app, since this has an effect + * on default values. + */ + public SupportsScreens resolveSupportsScreensValues(int targetSdkVersion) { + SupportsScreens result = getDefaultValues(targetSdkVersion); + + // Override the default with the existing values: + if (mResizeable != null) result.mResizeable = mResizeable; + if (mAnyDensity != null) result.mAnyDensity = mAnyDensity; + if (mSmallScreens != null) result.mSmallScreens = mSmallScreens; + if (mNormalScreens != null) result.mNormalScreens = mNormalScreens; + if (mLargeScreens != null) result.mLargeScreens = mLargeScreens; + + return result; + } + + /** + * returns the value of the <code>resizeable</code> attribute or null if not present. + */ + public Boolean getResizeable() { + return mResizeable; + } + + void setResizeable(Boolean resizeable) { + mResizeable = getConstantBoolean(resizeable); + } + + /** + * returns the value of the <code>anyDensity</code> attribute or null if not present. + */ + public Boolean getAnyDensity() { + return mAnyDensity; + } + + void setAnyDensity(Boolean anyDensity) { + mAnyDensity = getConstantBoolean(anyDensity); + } + + /** + * returns the value of the <code>smallScreens</code> attribute or null if not present. + */ + public Boolean getSmallScreens() { + return mSmallScreens; + } + + void setSmallScreens(Boolean smallScreens) { + mSmallScreens = getConstantBoolean(smallScreens); + } + + /** + * returns the value of the <code>normalScreens</code> attribute or null if not present. + */ + public Boolean getNormalScreens() { + return mNormalScreens; + } + + void setNormalScreens(Boolean normalScreens) { + mNormalScreens = getConstantBoolean(normalScreens); + } + + /** + * returns the value of the <code>largeScreens</code> attribute or null if not present. + */ + public Boolean getLargeScreens() { + return mLargeScreens; + } + + void setLargeScreens(Boolean largeScreens) { + mLargeScreens = getConstantBoolean(largeScreens); + } + + /** + * Returns either {@link Boolean#TRUE} or {@link Boolean#FALSE} based on the value of + * the given Boolean object. + */ + private Boolean getConstantBoolean(Boolean v) { + if (v != null) { + if (v.equals(Boolean.TRUE)) { + return Boolean.TRUE; + } else { + return Boolean.FALSE; + } + } + + return null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SupportsScreens) { + SupportsScreens support = (SupportsScreens) obj; + // since all the fields are guaranteed to be either Boolean.TRUE or Boolean.FALSE + // (or null), we can simply check they are identical and not bother with + // calling equals (which would require to check != null. + // see #getConstanntBoolean(Boolean) + return mResizeable == support.mResizeable && + mAnyDensity == support.mAnyDensity && + mSmallScreens == support.mSmallScreens && + mNormalScreens == support.mNormalScreens && + mLargeScreens == support.mLargeScreens; + } + + return false; + } + + /* Override hashCode, mostly to make Eclipse happy and not warn about it. + * And if you ever put this in a Map or Set, it will avoid surprises. */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mAnyDensity == null) ? 0 : mAnyDensity.hashCode()); + result = prime * result + ((mLargeScreens == null) ? 0 : mLargeScreens.hashCode()); + result = prime * result + ((mNormalScreens == null) ? 0 : mNormalScreens.hashCode()); + result = prime * result + ((mResizeable == null) ? 0 : mResizeable.hashCode()); + result = prime * result + ((mSmallScreens == null) ? 0 : mSmallScreens.hashCode()); + return result; + } + + /** + * Returns true if the two instances support the same screen sizes. + * This is similar to {@link #equals(Object)} except that it ignores the values of + * {@link #getAnyDensity()} and {@link #getResizeable()}. + * @param support the other instance to compare to. + * @return true if the two instances support the same screen sizes. + */ + public boolean hasSameScreenSupportAs(SupportsScreens support) { + // since all the fields are guaranteed to be either Boolean.TRUE or Boolean.FALSE + // (or null), we can simply check they are identical and not bother with + // calling equals (which would require to check != null. + // see #getConstanntBoolean(Boolean) + + // This only checks that matter here are the screen sizes. resizeable and anyDensity + // are not checked. + return mSmallScreens == support.mSmallScreens && + mNormalScreens == support.mNormalScreens && + mLargeScreens == support.mLargeScreens; + } + + /** + * Returns true if the two instances have strictly different screen size support. + * This means that there is no screen size that they both support. + * @param support the other instance to compare to. + * @return true if they are stricly different. + */ + public boolean hasStrictlyDifferentScreenSupportAs(SupportsScreens support) { + // since all the fields are guaranteed to be either Boolean.TRUE or Boolean.FALSE + // (or null), we can simply check they are identical and not bother with + // calling equals (which would require to check != null. + // see #getConstanntBoolean(Boolean) + + // This only checks that matter here are the screen sizes. resizeable and anyDensity + // are not checked. + return (mSmallScreens != Boolean.TRUE || support.mSmallScreens != Boolean.TRUE) && + (mNormalScreens != Boolean.TRUE || support.mNormalScreens != Boolean.TRUE) && + (mLargeScreens != Boolean.TRUE || support.mLargeScreens != Boolean.TRUE); + } + + /** + * Comparison of 2 Supports-screens. This only uses screen sizes (ignores resizeable and + * anyDensity), and considers that + * {@link #hasStrictlyDifferentScreenSupportAs(SupportsScreens)} returns true and + * {@link #overlapWith(SupportsScreens)} returns false. + * @throws IllegalArgumentException if the two instanced are not strictly different or + * overlap each other + * @see #hasStrictlyDifferentScreenSupportAs(SupportsScreens) + * @see #overlapWith(SupportsScreens) + */ + public int compareScreenSizesWith(SupportsScreens o) { + if (hasStrictlyDifferentScreenSupportAs(o) == false) { + throw new IllegalArgumentException("The two instances are not strictly different."); + } + if (overlapWith(o)) { + throw new IllegalArgumentException("The two instances overlap each other."); + } + + int comp = mLargeScreens.compareTo(o.mLargeScreens); + if (comp != 0) return comp; + + comp = mNormalScreens.compareTo(o.mNormalScreens); + if (comp != 0) return comp; + + comp = mSmallScreens.compareTo(o.mSmallScreens); + if (comp != 0) return comp; + + return 0; + } + + /** + * Returns a string encoding of the content of the instance. This string can be used to + * instantiate a {@link SupportsScreens} object through + * {@link #SupportsScreens(String)}. + */ + public String getEncodedValues() { + return String.format("%1$s|%2$s|%3$s|%4$s|%5$s", + mAnyDensity, mResizeable, mSmallScreens, mNormalScreens, mLargeScreens); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + boolean alreadyOutputSomething = false; + + if (Boolean.TRUE.equals(mSmallScreens)) { + alreadyOutputSomething = true; + sb.append("small"); + } + + if (Boolean.TRUE.equals(mNormalScreens)) { + if (alreadyOutputSomething) { + sb.append(", "); + } + alreadyOutputSomething = true; + sb.append("normal"); + } + + if (Boolean.TRUE.equals(mLargeScreens)) { + if (alreadyOutputSomething) { + sb.append(", "); + } + alreadyOutputSomething = true; + sb.append("large"); + } + + if (alreadyOutputSomething == false) { + sb.append("<none>"); + } + + return sb.toString(); + } + + /** + * Returns true if the two instance overlap with each other. + * This can happen if one instances supports a size, when the other instance doesn't while + * supporting a size above and a size below. + * @param otherSS the other supports-screens to compare to. + */ + public boolean overlapWith(SupportsScreens otherSS) { + if (mSmallScreens == null || mNormalScreens == null || mLargeScreens == null || + otherSS.mSmallScreens == null || otherSS.mNormalScreens == null || + otherSS.mLargeScreens == null) { + throw new IllegalArgumentException("Some screen sizes Boolean are not initialized"); + } + + if (mSmallScreens == Boolean.TRUE && mNormalScreens == Boolean.FALSE && + mLargeScreens == Boolean.TRUE) { + return otherSS.mNormalScreens == Boolean.TRUE; + } + + if (otherSS.mSmallScreens == Boolean.TRUE && otherSS.mNormalScreens == Boolean.FALSE && + otherSS.mLargeScreens == Boolean.TRUE) { + return mNormalScreens == Boolean.TRUE; + } + + return false; + } + } + + /** + * Class representing a <code>uses-library</code> node in the manifest. + */ + public final static class UsesLibrary { + String mName; + Boolean mRequired = Boolean.TRUE; // default is true even if missing + + public String getName() { + return mName; + } + + public Boolean getRequired() { + return mRequired; + } + } + + /** + * Class representing a <code>uses-feature</code> node in the manifest. + */ + public final static class UsesFeature { + String mName; + int mGlEsVersion = 0; + Boolean mRequired = Boolean.TRUE; // default is true even if missing + + public String getName() { + return mName; + } + + /** + * Returns the value of the glEsVersion attribute, or 0 if the attribute was not present. + */ + public int getGlEsVersion() { + return mGlEsVersion; + } + + public Boolean getRequired() { + return mRequired; + } + } + + /** + * Class representing the <code>uses-configuration</code> node in the manifest. + */ + public final static class UsesConfiguration { + Boolean mReqFiveWayNav; + Boolean mReqHardKeyboard; + Keyboard mReqKeyboardType; + TouchScreen mReqTouchScreen; + Navigation mReqNavigation; + + /** + * returns the value of the <code>reqFiveWayNav</code> attribute or null if not present. + */ + public Boolean getReqFiveWayNav() { + return mReqFiveWayNav; + } + + /** + * returns the value of the <code>reqNavigation</code> attribute or null if not present. + */ + public Navigation getReqNavigation() { + return mReqNavigation; + } + + /** + * returns the value of the <code>reqHardKeyboard</code> attribute or null if not present. + */ + public Boolean getReqHardKeyboard() { + return mReqHardKeyboard; + } + + /** + * returns the value of the <code>reqKeyboardType</code> attribute or null if not present. + */ + public Keyboard getReqKeyboardType() { + return mReqKeyboardType; + } + + /** + * returns the value of the <code>reqTouchScreen</code> attribute or null if not present. + */ + public TouchScreen getReqTouchScreen() { + return mReqTouchScreen; + } + } + + /** + * Returns the package defined in the manifest, if found. + * @return The package name or null if not found. + */ + public String getPackage() { + return mPackage; + } + + /** + * Returns the versionCode value defined in the manifest, if found, null otherwise. + * @return the versionCode or null if not found. + */ + public Integer getVersionCode() { + return mVersionCode; + } + + /** + * Returns the list of activities found in the manifest. + * @return An array of fully qualified class names, or empty if no activity were found. + */ + public Activity[] getActivities() { + return mActivities.toArray(new Activity[mActivities.size()]); + } + + /** + * Returns the name of one activity found in the manifest, that is configured to show + * up in the HOME screen. + * @return the fully qualified name of a HOME activity or null if none were found. + */ + public Activity getLauncherActivity() { + return mLauncherActivity; + } + + /** + * Returns the list of process names declared by the manifest. + */ + public String[] getProcesses() { + if (mProcesses != null) { + return mProcesses.toArray(new String[mProcesses.size()]); + } + + return new String[0]; + } + + /** + * Returns the <code>debuggable</code> attribute value or null if it is not set. + */ + public Boolean getDebuggable() { + return mDebuggable; + } + + /** + * Returns the <code>minSdkVersion</code> attribute, or null if it's not set. + */ + public String getMinSdkVersionString() { + return mMinSdkVersionString; + } + + /** + * Sets the value of the <code>minSdkVersion</code> attribute. + * @param minSdkVersion the string value of the attribute in the manifest. + */ + public void setMinSdkVersionString(String minSdkVersion) { + mMinSdkVersionString = minSdkVersion; + if (mMinSdkVersionString != null) { + try { + mMinSdkVersion = Integer.parseInt(mMinSdkVersionString); + } catch (NumberFormatException e) { + mMinSdkVersion = MIN_SDK_CODENAME; + } + } + } + + /** + * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set or is a codename. + * @see #getMinSdkVersionString() + */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + + /** + * Sets the value of the <code>minSdkVersion</code> attribute. + * @param targetSdkVersion the string value of the attribute in the manifest. + */ + public void setTargetSdkVersionString(String targetSdkVersion) { + if (targetSdkVersion != null) { + try { + mTargetSdkVersion = Integer.parseInt(targetSdkVersion); + } catch (NumberFormatException e) { + // keep the value at 0. + } + } + } + + /** + * Returns the <code>targetSdkVersion</code> attribute, or the same value as + * {@link #getMinSdkVersion()} if it was not set in the manifest. + */ + public int getTargetSdkVersion() { + if (mTargetSdkVersion == 0) { + return getMinSdkVersion(); + } + + return mTargetSdkVersion; + } + + /** + * Returns the list of instrumentations found in the manifest. + * @return An array of {@link Instrumentation}, or empty if no instrumentations were + * found. + */ + public Instrumentation[] getInstrumentations() { + return mInstrumentations.toArray(new Instrumentation[mInstrumentations.size()]); + } + + /** + * Returns the list of libraries in use found in the manifest. + * @return An array of {@link UsesLibrary} objects, or empty if no libraries were found. + */ + public UsesLibrary[] getUsesLibraries() { + return mLibraries.toArray(new UsesLibrary[mLibraries.size()]); + } + + /** + * Returns the list of features in use found in the manifest. + * @return An array of {@link UsesFeature} objects, or empty if no libraries were found. + */ + public UsesFeature[] getUsesFeatures() { + return mFeatures.toArray(new UsesFeature[mFeatures.size()]); + } + + /** + * Returns the glEsVersion from a <uses-feature> or {@link #GL_ES_VERSION_NOT_SET} if not set. + */ + public int getGlEsVersion() { + for (UsesFeature feature : mFeatures) { + if (feature.mGlEsVersion > 0) { + return feature.mGlEsVersion; + } + } + return GL_ES_VERSION_NOT_SET; + } + + /** + * Returns the {@link SupportsScreens} object representing the <code>supports-screens</code> + * node, or null if the node doesn't exist at all. + * Some values in the {@link SupportsScreens} instance maybe null, indicating that they + * were not present in the manifest. To get an instance that contains the values, as seen + * by the Android platform when the app is running, use {@link #getSupportsScreensValues()}. + */ + public SupportsScreens getSupportsScreensFromManifest() { + return mSupportsScreensFromManifest; + } + + /** + * Returns an always non-null instance of {@link SupportsScreens} that's been initialized with + * the default values, and the values from the manifest. + * The default values depends on the manifest values for minSdkVersion and targetSdkVersion. + */ + public synchronized SupportsScreens getSupportsScreensValues() { + if (mSupportsScreensValues == null) { + if (mSupportsScreensFromManifest == null) { + mSupportsScreensValues = SupportsScreens.getDefaultValues(getTargetSdkVersion()); + } else { + // get a SupportsScreen that replace the missing values with default values. + mSupportsScreensValues = mSupportsScreensFromManifest.resolveSupportsScreensValues( + getTargetSdkVersion()); + } + } + + return mSupportsScreensValues; + } + + /** + * Returns the {@link UsesConfiguration} object representing the <code>uses-configuration</code> + * node, or null if the node doesn't exist at all. + */ + public UsesConfiguration getUsesConfiguration() { + return mUsesConfiguration; + } + + void addProcessName(String processName) { + if (mProcesses == null) { + mProcesses = new TreeSet<String>(); + } + + if (processName.startsWith(":")) { + mProcesses.add(mPackage + processName); + } else { + mProcesses.add(processName); + } + } + +} |