1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.descriptors; 18 19 import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; 20 import static com.android.SdkConstants.ANDROID_URI; 21 import static com.android.SdkConstants.AUTO_URI; 22 import static com.android.SdkConstants.CLASS_VIEWGROUP; 23 import static com.android.SdkConstants.URI_PREFIX; 24 25 import com.android.annotations.NonNull; 26 import com.android.annotations.Nullable; 27 import com.android.ide.common.resources.ResourceFile; 28 import com.android.ide.common.resources.ResourceItem; 29 import com.android.ide.common.resources.platform.AttributeInfo; 30 import com.android.ide.common.resources.platform.AttrsXmlParser; 31 import com.android.ide.common.resources.platform.ViewClassInfo; 32 import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.AdtUtils; 35 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 36 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; 37 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 38 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 39 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 40 import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; 41 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 42 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; 43 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 44 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 45 import com.android.resources.ResourceType; 46 import com.android.sdklib.IAndroidTarget; 47 import com.google.common.collect.Maps; 48 import com.google.common.collect.ObjectArrays; 49 50 import org.eclipse.core.resources.IProject; 51 import org.eclipse.core.resources.IResource; 52 import org.eclipse.core.resources.IWorkspaceRoot; 53 import org.eclipse.core.resources.ResourcesPlugin; 54 import org.eclipse.core.runtime.CoreException; 55 import org.eclipse.core.runtime.IPath; 56 import org.eclipse.core.runtime.NullProgressMonitor; 57 import org.eclipse.jdt.core.IClassFile; 58 import org.eclipse.jdt.core.IJavaProject; 59 import org.eclipse.jdt.core.IType; 60 import org.eclipse.jdt.core.ITypeHierarchy; 61 import org.eclipse.jdt.core.JavaCore; 62 import org.eclipse.jdt.core.JavaModelException; 63 import org.eclipse.swt.graphics.Image; 64 65 import java.util.ArrayList; 66 import java.util.Collection; 67 import java.util.HashMap; 68 import java.util.HashSet; 69 import java.util.List; 70 import java.util.Map; 71 import java.util.Set; 72 73 /** 74 * Service responsible for creating/managing {@link ViewElementDescriptor} objects for custom 75 * View classes per project. 76 * <p/> 77 * The service provides an on-demand monitoring of custom classes to check for changes. Monitoring 78 * starts once a request for an {@link ViewElementDescriptor} object has been done for a specific 79 * class. 80 * <p/> 81 * The monitoring will notify a listener of any changes in the class triggering a change in its 82 * associated {@link ViewElementDescriptor} object. 83 * <p/> 84 * If the custom class does not exist, no monitoring is put in place to avoid having to listen 85 * to all class changes in the projects. 86 */ 87 public final class CustomViewDescriptorService { 88 89 private static CustomViewDescriptorService sThis = new CustomViewDescriptorService(); 90 91 /** 92 * Map where keys are the project, and values are another map containing all the known 93 * custom View class for this project. The custom View class are stored in a map 94 * where the keys are the fully qualified class name, and the values are their associated 95 * {@link ViewElementDescriptor}. 96 */ 97 private HashMap<IProject, HashMap<String, ViewElementDescriptor>> mCustomDescriptorMap = 98 new HashMap<IProject, HashMap<String, ViewElementDescriptor>>(); 99 100 /** 101 * TODO will be used to update the ViewElementDescriptor of the custom view when it 102 * is modified (either the class itself or its attributes.xml) 103 */ 104 @SuppressWarnings("unused") 105 private ICustomViewDescriptorListener mListener; 106 107 /** 108 * Classes which implements this interface provide a method that deal with modifications 109 * in custom View class triggering a change in its associated {@link ViewClassInfo} object. 110 */ 111 public interface ICustomViewDescriptorListener { 112 /** 113 * Sent when a custom View class has changed and 114 * its {@link ViewElementDescriptor} was modified. 115 * 116 * @param project the project containing the class. 117 * @param className the fully qualified class name. 118 * @param descriptor the updated ElementDescriptor. 119 */ updatedClassInfo(IProject project, String className, ViewElementDescriptor descriptor)120 public void updatedClassInfo(IProject project, 121 String className, 122 ViewElementDescriptor descriptor); 123 } 124 125 /** 126 * Returns the singleton instance of {@link CustomViewDescriptorService}. 127 */ getInstance()128 public static CustomViewDescriptorService getInstance() { 129 return sThis; 130 } 131 132 /** 133 * Sets the listener receiving custom View class modification notifications. 134 * @param listener the listener to receive the notifications. 135 * 136 * TODO will be used to update the ViewElementDescriptor of the custom view when it 137 * is modified (either the class itself or its attributes.xml) 138 */ setListener(ICustomViewDescriptorListener listener)139 public void setListener(ICustomViewDescriptorListener listener) { 140 mListener = listener; 141 } 142 143 /** 144 * Returns the {@link ViewElementDescriptor} for a particular project/class when the 145 * fully qualified class name actually matches a class from the given project. 146 * <p/> 147 * Custom descriptors are created as needed. 148 * <p/> 149 * If it is the first time the {@link ViewElementDescriptor} is requested, the method 150 * will check that the specified class is in fact a custom View class. Once this is 151 * established, a monitoring for that particular class is initiated. Any change will 152 * trigger a notification to the {@link ICustomViewDescriptorListener}. 153 * 154 * @param project the project containing the class. 155 * @param fqcn the fully qualified name of the class. 156 * @return a {@link ViewElementDescriptor} or <code>null</code> if the class was not 157 * a custom View class. 158 */ getDescriptor(IProject project, String fqcn)159 public ViewElementDescriptor getDescriptor(IProject project, String fqcn) { 160 // look in the map first 161 synchronized (mCustomDescriptorMap) { 162 HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project); 163 164 if (map != null) { 165 ViewElementDescriptor descriptor = map.get(fqcn); 166 if (descriptor != null) { 167 return descriptor; 168 } 169 } 170 171 // if we step here, it looks like we haven't created it yet. 172 // First lets check this is in fact a valid type in the project 173 174 try { 175 // We expect the project to be both opened and of java type (since it's an android 176 // project), so we can create a IJavaProject object from our IProject. 177 IJavaProject javaProject = JavaCore.create(project); 178 179 // replace $ by . in the class name 180 String javaClassName = fqcn.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$ 181 182 // look for the IType object for this class 183 IType type = javaProject.findType(javaClassName); 184 if (type != null && type.exists()) { 185 // the type exists. Let's get the parent class and its ViewClassInfo. 186 187 // get the type hierarchy 188 ITypeHierarchy hierarchy = type.newSupertypeHierarchy( 189 new NullProgressMonitor()); 190 191 ViewElementDescriptor parentDescriptor = createViewDescriptor( 192 hierarchy.getSuperclass(type), project, hierarchy); 193 194 if (parentDescriptor != null) { 195 // we have a valid parent, lets create a new ViewElementDescriptor. 196 List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>(); 197 List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>(); 198 Map<ResourceFile, Long> files = findCustomDescriptors(project, type, 199 attrList, paramList); 200 201 AttributeDescriptor[] attributes = 202 getAttributeDescriptor(type, parentDescriptor); 203 if (!attrList.isEmpty()) { 204 attributes = join(attrList, attributes); 205 } 206 AttributeDescriptor[] layoutAttributes = 207 getLayoutAttributeDescriptors(type, parentDescriptor); 208 if (!paramList.isEmpty()) { 209 layoutAttributes = join(paramList, layoutAttributes); 210 } 211 String name = DescriptorsUtils.getBasename(fqcn); 212 ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn, 213 attributes, 214 layoutAttributes, 215 parentDescriptor.getChildren(), 216 project, files); 217 descriptor.setSuperClass(parentDescriptor); 218 219 synchronized (mCustomDescriptorMap) { 220 map = mCustomDescriptorMap.get(project); 221 if (map == null) { 222 map = new HashMap<String, ViewElementDescriptor>(); 223 mCustomDescriptorMap.put(project, map); 224 } 225 226 map.put(fqcn, descriptor); 227 } 228 229 //TODO setup listener on this resource change. 230 231 return descriptor; 232 } 233 } 234 } catch (JavaModelException e) { 235 // there was an error accessing any of the IType, we'll just return null; 236 } 237 } 238 239 return null; 240 } 241 join( @onNull List<AttributeDescriptor> attributeList, @NonNull AttributeDescriptor[] attributes)242 private static AttributeDescriptor[] join( 243 @NonNull List<AttributeDescriptor> attributeList, 244 @NonNull AttributeDescriptor[] attributes) { 245 if (!attributeList.isEmpty()) { 246 return ObjectArrays.concat( 247 attributeList.toArray(new AttributeDescriptor[attributeList.size()]), 248 attributes, 249 AttributeDescriptor.class); 250 } else { 251 return attributes; 252 } 253 254 } 255 256 /** Cache used by {@link #getParser(ResourceFile)} */ 257 private Map<ResourceFile, AttrsXmlParser> mParserCache; 258 getParser(ResourceFile file)259 private AttrsXmlParser getParser(ResourceFile file) { 260 if (mParserCache == null) { 261 mParserCache = new HashMap<ResourceFile, AttrsXmlParser>(); 262 } 263 264 AttrsXmlParser parser = mParserCache.get(file); 265 if (parser == null) { 266 parser = new AttrsXmlParser( 267 file.getFile().getOsLocation(), 268 AdtPlugin.getDefault(), 20); 269 parser.preload(); 270 mParserCache.put(file, parser); 271 } 272 273 return parser; 274 } 275 276 /** Compute/find the styleable resources for the given type, if possible */ findCustomDescriptors( IProject project, IType type, List<AttributeDescriptor> customAttributes, List<AttributeDescriptor> customLayoutAttributes)277 private Map<ResourceFile, Long> findCustomDescriptors( 278 IProject project, 279 IType type, 280 List<AttributeDescriptor> customAttributes, 281 List<AttributeDescriptor> customLayoutAttributes) { 282 // Look up the project where the type is declared (could be a library project; 283 // we cannot use type.getJavaProject().getProject()) 284 IProject library = getProjectDeclaringType(type); 285 if (library == null) { 286 library = project; 287 } 288 289 String className = type.getElementName(); 290 Set<ResourceFile> resourceFiles = findAttrsFiles(library, className); 291 if (resourceFiles != null && resourceFiles.size() > 0) { 292 String appUri = getAppResUri(project); 293 Map<ResourceFile, Long> timestamps = 294 Maps.newHashMapWithExpectedSize(resourceFiles.size()); 295 for (ResourceFile file : resourceFiles) { 296 AttrsXmlParser attrsXmlParser = getParser(file); 297 String fqcn = type.getFullyQualifiedName(); 298 299 // Attributes 300 ViewClassInfo classInfo = new ViewClassInfo(true, fqcn, className); 301 attrsXmlParser.loadViewAttributes(classInfo); 302 appendAttributes(customAttributes, classInfo.getAttributes(), appUri); 303 304 // Layout params 305 LayoutParamsInfo layoutInfo = new ViewClassInfo.LayoutParamsInfo( 306 classInfo, "Layout", null /*superClassInfo*/); //$NON-NLS-1$ 307 attrsXmlParser.loadLayoutParamsAttributes(layoutInfo); 308 appendAttributes(customLayoutAttributes, layoutInfo.getAttributes(), appUri); 309 310 timestamps.put(file, file.getFile().getModificationStamp()); 311 } 312 313 return timestamps; 314 } 315 316 return null; 317 } 318 319 /** 320 * Finds the set of XML files (if any) in the given library declaring 321 * attributes for the given class name 322 */ 323 @Nullable findAttrsFiles(IProject library, String className)324 private static Set<ResourceFile> findAttrsFiles(IProject library, String className) { 325 Set<ResourceFile> resourceFiles = null; 326 ResourceManager manager = ResourceManager.getInstance(); 327 ProjectResources resources = manager.getProjectResources(library); 328 if (resources != null) { 329 Collection<ResourceItem> items = 330 resources.getResourceItemsOfType(ResourceType.DECLARE_STYLEABLE); 331 for (ResourceItem item : items) { 332 String viewName = item.getName(); 333 if (viewName.equals(className) 334 || (viewName.startsWith(className) 335 && viewName.equals(className + "_Layout"))) { //$NON-NLS-1$ 336 if (resourceFiles == null) { 337 resourceFiles = new HashSet<ResourceFile>(); 338 } 339 resourceFiles.addAll(item.getSourceFileList()); 340 } 341 } 342 } 343 return resourceFiles; 344 } 345 346 /** 347 * Find the project containing this type declaration. We cannot use 348 * {@link IType#getJavaProject()} since that will return the including 349 * project and we're after the library project such that we can find the 350 * attrs.xml file in the same project. 351 */ 352 @Nullable getProjectDeclaringType(IType type)353 private static IProject getProjectDeclaringType(IType type) { 354 IClassFile classFile = type.getClassFile(); 355 if (classFile != null) { 356 IPath path = classFile.getPath(); 357 IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot(); 358 IResource resource; 359 if (path.isAbsolute()) { 360 resource = AdtUtils.fileToResource(path.toFile()); 361 } else { 362 resource = workspace.findMember(path); 363 } 364 if (resource != null && resource.getProject() != null) { 365 return resource.getProject(); 366 } 367 } 368 369 return null; 370 } 371 372 /** Returns the name space to use for application attributes */ getAppResUri(IProject project)373 private static String getAppResUri(IProject project) { 374 String appResource; 375 ProjectState projectState = Sdk.getProjectState(project); 376 if (projectState != null && projectState.isLibrary()) { 377 appResource = AUTO_URI; 378 } else { 379 ManifestInfo manifestInfo = ManifestInfo.get(project); 380 appResource = URI_PREFIX + manifestInfo.getPackage(); 381 } 382 return appResource; 383 } 384 385 386 /** Append the {@link AttributeInfo} objects converted {@link AttributeDescriptor} 387 * objects into the given attribute list. 388 * <p> 389 * This is nearly identical to 390 * {@link DescriptorsUtils#appendAttribute(List, String, String, AttributeInfo, boolean, Map)} 391 * but it handles namespace declarations in the attrs.xml file where the android: 392 * namespace is included in the names. 393 */ appendAttributes(List<AttributeDescriptor> attributes, AttributeInfo[] attributeInfos, String appResource)394 private static void appendAttributes(List<AttributeDescriptor> attributes, 395 AttributeInfo[] attributeInfos, String appResource) { 396 // Custom attributes 397 for (AttributeInfo info : attributeInfos) { 398 String nsUri; 399 if (info.getName().startsWith(ANDROID_NS_NAME_PREFIX)) { 400 info.setName(info.getName().substring(ANDROID_NS_NAME_PREFIX.length())); 401 nsUri = ANDROID_URI; 402 } else { 403 nsUri = appResource; 404 } 405 406 DescriptorsUtils.appendAttribute(attributes, 407 null /*elementXmlName*/, nsUri, info, false /*required*/, 408 null /*overrides*/); 409 } 410 } 411 412 /** 413 * Computes (if needed) and returns the {@link ViewElementDescriptor} for the specified type. 414 * 415 * @return A {@link ViewElementDescriptor} or null if type or typeHierarchy is null. 416 */ createViewDescriptor(IType type, IProject project, ITypeHierarchy typeHierarchy)417 private ViewElementDescriptor createViewDescriptor(IType type, IProject project, 418 ITypeHierarchy typeHierarchy) { 419 // check if the type is a built-in View class. 420 List<ViewElementDescriptor> builtInList = null; 421 422 // give up if there's no type 423 if (type == null) { 424 return null; 425 } 426 427 String fqcn = type.getFullyQualifiedName(); 428 429 Sdk currentSdk = Sdk.getCurrent(); 430 if (currentSdk != null) { 431 IAndroidTarget target = currentSdk.getTarget(project); 432 if (target != null) { 433 AndroidTargetData data = currentSdk.getTargetData(target); 434 if (data != null) { 435 LayoutDescriptors descriptors = data.getLayoutDescriptors(); 436 ViewElementDescriptor d = descriptors.findDescriptorByClass(fqcn); 437 if (d != null) { 438 return d; 439 } 440 builtInList = descriptors.getViewDescriptors(); 441 } 442 } 443 } 444 445 // it's not a built-in class? Lets look if the superclass is built-in 446 // give up if there's no type 447 if (typeHierarchy == null) { 448 return null; 449 } 450 451 IType parentType = typeHierarchy.getSuperclass(type); 452 if (parentType != null) { 453 ViewElementDescriptor parentDescriptor = createViewDescriptor(parentType, project, 454 typeHierarchy); 455 456 if (parentDescriptor != null) { 457 // parent class is a valid View class with a descriptor, so we create one 458 // for this class. 459 String name = DescriptorsUtils.getBasename(fqcn); 460 // A custom view accepts children if its parent descriptor also does. 461 // The only exception to this is ViewGroup, which accepts children even though 462 // its parent does not. 463 boolean isViewGroup = fqcn.equals(CLASS_VIEWGROUP); 464 boolean hasChildren = isViewGroup || parentDescriptor.hasChildren(); 465 ViewElementDescriptor[] children = null; 466 if (hasChildren && builtInList != null) { 467 // We can't figure out what the allowable children are by just 468 // looking at the class, so assume any View is valid 469 children = builtInList.toArray(new ViewElementDescriptor[builtInList.size()]); 470 } 471 ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn, 472 getAttributeDescriptor(type, parentDescriptor), 473 getLayoutAttributeDescriptors(type, parentDescriptor), 474 children, project, null); 475 descriptor.setSuperClass(parentDescriptor); 476 477 // add it to the map 478 synchronized (mCustomDescriptorMap) { 479 HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project); 480 481 if (map == null) { 482 map = new HashMap<String, ViewElementDescriptor>(); 483 mCustomDescriptorMap.put(project, map); 484 } 485 486 map.put(fqcn, descriptor); 487 488 } 489 490 //TODO setup listener on this resource change. 491 492 return descriptor; 493 } 494 } 495 496 // class is neither a built-in view class, nor extend one. return null. 497 return null; 498 } 499 500 /** 501 * Returns the array of {@link AttributeDescriptor} for the specified {@link IType}. 502 * <p/> 503 * The array should contain the descriptor for this type and all its supertypes. 504 * 505 * @param type the type for which the {@link AttributeDescriptor} are returned. 506 * @param parentDescriptor the {@link ViewElementDescriptor} of the direct superclass. 507 */ getAttributeDescriptor(IType type, ViewElementDescriptor parentDescriptor)508 private static AttributeDescriptor[] getAttributeDescriptor(IType type, 509 ViewElementDescriptor parentDescriptor) { 510 // TODO add the class attribute descriptors to the parent descriptors. 511 return parentDescriptor.getAttributes(); 512 } 513 getLayoutAttributeDescriptors(IType type, ViewElementDescriptor parentDescriptor)514 private static AttributeDescriptor[] getLayoutAttributeDescriptors(IType type, 515 ViewElementDescriptor parentDescriptor) { 516 return parentDescriptor.getLayoutAttributes(); 517 } 518 519 private class CustomViewDescriptor extends ViewElementDescriptor { 520 private Map<ResourceFile, Long> mTimeStamps; 521 private IProject mProject; 522 CustomViewDescriptor(String name, String fqcn, AttributeDescriptor[] attributes, AttributeDescriptor[] layoutAttributes, ElementDescriptor[] children, IProject project, Map<ResourceFile, Long> timestamps)523 public CustomViewDescriptor(String name, String fqcn, AttributeDescriptor[] attributes, 524 AttributeDescriptor[] layoutAttributes, 525 ElementDescriptor[] children, IProject project, 526 Map<ResourceFile, Long> timestamps) { 527 super( 528 fqcn, // xml name 529 name, // ui name 530 fqcn, // full class name 531 fqcn, // tooltip 532 null, // sdk_url 533 attributes, 534 layoutAttributes, 535 children, 536 false // mandatory 537 ); 538 mTimeStamps = timestamps; 539 mProject = project; 540 } 541 542 @Override getGenericIcon()543 public Image getGenericIcon() { 544 IconFactory iconFactory = IconFactory.getInstance(); 545 546 int index = mXmlName.lastIndexOf('.'); 547 if (index != -1) { 548 return iconFactory.getIcon(mXmlName.substring(index + 1), 549 "customView"); //$NON-NLS-1$ 550 } 551 552 return iconFactory.getIcon("customView"); //$NON-NLS-1$ 553 } 554 555 @Override syncAttributes()556 public boolean syncAttributes() { 557 // Check if any of the descriptors 558 if (mTimeStamps != null) { 559 // Prevent checking actual file timestamps too frequently on rapid burst calls 560 long now = System.currentTimeMillis(); 561 if (now - sLastCheck < 1000) { 562 return true; 563 } 564 sLastCheck = now; 565 566 // Check whether the resource files (typically just one) which defined 567 // custom attributes for this custom view have changed, and if so, 568 // refresh the attribute descriptors. 569 // This doesn't work the cases where you add descriptors for a custom 570 // view after using it, or add attributes in a separate file, but those 571 // scenarios aren't quite as common (and would require a bit more expensive 572 // analysis.) 573 for (Map.Entry<ResourceFile, Long> entry : mTimeStamps.entrySet()) { 574 ResourceFile file = entry.getKey(); 575 Long timestamp = entry.getValue(); 576 boolean recompute = false; 577 if (file.getFile().getModificationStamp() > timestamp.longValue()) { 578 // One or more attributes changed: recompute 579 recompute = true; 580 mParserCache.remove(file); 581 } 582 583 if (recompute) { 584 IJavaProject javaProject = JavaCore.create(mProject); 585 String fqcn = getFullClassName(); 586 IType type = null; 587 try { 588 type = javaProject.findType(fqcn); 589 } catch (CoreException e) { 590 AdtPlugin.log(e, null); 591 } 592 if (type == null || !type.exists()) { 593 return true; 594 } 595 596 List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>(); 597 List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>(); 598 599 mTimeStamps = findCustomDescriptors(mProject, type, attrList, paramList); 600 601 ViewElementDescriptor parentDescriptor = getSuperClassDesc(); 602 AttributeDescriptor[] attributes = 603 getAttributeDescriptor(type, parentDescriptor); 604 if (!attrList.isEmpty()) { 605 attributes = join(attrList, attributes); 606 } 607 attributes = attrList.toArray(new AttributeDescriptor[attrList.size()]); 608 setAttributes(attributes); 609 610 return false; 611 } 612 } 613 } 614 615 return true; 616 } 617 } 618 619 /** Timestamp of the most recent {@link CustomViewDescriptor#syncAttributes} check */ 620 private static long sLastCheck; 621 } 622