.gitignore0100644 0000000 0000000 00000000176 12747325007 011610 0ustar000000000 0000000 *~ *.bak *.pyc Thumbs.db *.class *.DS_Store .gradle /build /out /repo .idea/workspace.xml .idea/dictionaries/tnorbye.xml bin .idea/0040755 0000000 0000000 00000000000 12747325007 010577 5ustar000000000 0000000 .idea/.name0100644 0000000 0000000 00000000003 12747325007 011506 0ustar000000000 0000000 swt.idea/codeStyleSettings.xml0100644 0000000 0000000 00000015101 12747325007 014770 0ustar000000000 0000000 .idea/compiler.xml0100644 0000000 0000000 00000001527 12747325007 013135 0ustar000000000 0000000 .idea/copyright/0040755 0000000 0000000 00000000000 12747325007 012607 5ustar000000000 0000000 .idea/copyright/aosp.xml0100644 0000000 0000000 00000001676 12747325007 014302 0ustar000000000 0000000 .idea/copyright/profiles_settings.xml0100644 0000000 0000000 00000000271 12747325007 017071 0ustar000000000 0000000 .idea/encodings.xml0100644 0000000 0000000 00000000334 12747325007 013267 0ustar000000000 0000000 .idea/inspectionProfiles/0040755 0000000 0000000 00000000000 12747325007 014456 5ustar000000000 0000000 .idea/inspectionProfiles/Project_Default.xml0100644 0000000 0000000 00000000771 12747325007 020254 0ustar000000000 0000000 .idea/inspectionProfiles/profiles_settings.xml0100644 0000000 0000000 00000000353 12747325007 020741 0ustar000000000 0000000 .idea/libraries/0040755 0000000 0000000 00000000000 12747325007 012553 5ustar000000000 0000000 .idea/libraries/JUnit3.xml0100644 0000000 0000000 00000000322 12747325007 014403 0ustar000000000 0000000 .idea/libraries/JUnit4.xml0100644 0000000 0000000 00000000621 12747325007 014406 0ustar000000000 0000000 .idea/libraries/Trove4j.xml0100644 0000000 0000000 00000001106 12747325007 014625 0ustar000000000 0000000 .idea/libraries/commons_compress.xml0100644 0000000 0000000 00000000735 12747325007 016665 0ustar000000000 0000000 .idea/libraries/easymock_tools.xml0100644 0000000 0000000 00000000657 12747325007 016335 0ustar000000000 0000000 .idea/libraries/gson.xml0100644 0000000 0000000 00000000645 12747325007 014245 0ustar000000000 0000000 .idea/libraries/guava_tools.xml0100644 0000000 0000000 00000000654 12747325007 015622 0ustar000000000 0000000 .idea/libraries/http_client.xml0100644 0000000 0000000 00000003162 12747325007 015611 0ustar000000000 0000000 .idea/libraries/intellij_annotations.xml0100644 0000000 0000000 00000000705 12747325007 017523 0ustar000000000 0000000 .idea/libraries/jcommon_1_0_12.xml0100644 0000000 0000000 00000000651 12747325007 015677 0ustar000000000 0000000 .idea/libraries/jfreechart_1_0_9.xml0100644 0000000 0000000 00000000663 12747325007 016303 0ustar000000000 0000000 .idea/libraries/jfreechart_swt_1_0_9.xml0100644 0000000 0000000 00000000707 12747325007 017177 0ustar000000000 0000000 .idea/libraries/kxml2.xml0100644 0000000 0000000 00000000640 12747325007 014327 0ustar000000000 0000000 .idea/libraries/org_eclipse_core_commands_3_6_0.xml0100644 0000000 0000000 00000000540 12747325007 021343 0ustar000000000 0000000 .idea/libraries/org_eclipse_equinox_common_3_6_0.xml0100644 0000000 0000000 00000000543 12747325007 021575 0ustar000000000 0000000 .idea/libraries/org_eclipse_jface_3_6_2.xml0100644 0000000 0000000 00000000510 12747325007 017601 0ustar000000000 0000000 .idea/libraries/swt.xml0100644 0000000 0000000 00000000351 12747325007 014106 0ustar000000000 0000000 .idea/libraries/truth.xml0100644 0000000 0000000 00000001011 12747325007 014431 0ustar000000000 0000000 .idea/misc.xml0100644 0000000 0000000 00000003134 12747325007 012252 0ustar000000000 0000000 .idea/modules.xml0100644 0000000 0000000 00000005133 12747325007 012770 0ustar000000000 0000000 .idea/runConfigurations/0040755 0000000 0000000 00000000000 12747325007 014316 5ustar000000000 0000000 .idea/runConfigurations/ddms.xml0100644 0000000 0000000 00000002303 12747325007 015762 0ustar000000000 0000000 .idea/runConfigurations/hierarchyviewer2.xml0100644 0000000 0000000 00000002333 12747325007 020320 0ustar000000000 0000000 .idea/runConfigurations/sdkmanager.xml0100644 0000000 0000000 00000002205 12747325007 017150 0ustar000000000 0000000 .idea/runConfigurations/traceview.xml0100644 0000000 0000000 00000001664 12747325007 017035 0ustar000000000 0000000 .idea/scopes/0040755 0000000 0000000 00000000000 12747325007 012073 5ustar000000000 0000000 .idea/scopes/scope_settings.xml0100644 0000000 0000000 00000000213 12747325007 015637 0ustar000000000 0000000 .idea/uiDesigner.xml0100644 0000000 0000000 00000021132 12747325007 013413 0ustar000000000 0000000 .idea/vcs.xml0100644 0000000 0000000 00000000431 12747325007 012107 0ustar000000000 0000000 build.gradle0100644 0000000 0000000 00000004403 12747325007 012074 0ustar000000000 0000000 // this applied only to swt/* projects subprojects { Project project -> // only configure leaf projects. if (!project.getSubprojects().isEmpty()) return apply from: "$rootDir/buildSrc/base/baseJava.gradle" apply plugin: 'sdk-java-lib' version = rootProject.ext.baseVersion // configuration for swt dependency since the packaged jar is platform dependent // but at compile time we don't care. Also we don't want the artifact in the // main repo, so use the provided configuration configurations { swt } dependencies{ swt "com.android.external.eclipse:swt:3.5.0" compile 'com.android.external.eclipse:org-eclipse-jface:3.6.2' testCompile "com.android.external.eclipse:swt:3.5.0" } // include swt for compilation sourceSets.main.compileClasspath += configurations.swt } apply plugin: 'sdk-files' sdk { linux { item("$rootProject.projectDir.parentFile/prebuilts/tools/linux-x86/swt/swt.jar") { into 'lib/x86' notice "$rootProject.projectDir.parentFile/prebuilts/tools/linux-x86/swt/NOTICE" } item("$rootProject.projectDir.parentFile/prebuilts/tools/linux-x86_64/swt/swt.jar") { into 'lib/x86_64' notice "$rootProject.projectDir.parentFile/prebuilts/tools/linux-x86_64/swt/NOTICE" } } mac { item("$rootProject.projectDir.parentFile/prebuilts/tools/darwin-x86/swt/swt.jar") { into 'lib/x86' notice "$rootProject.projectDir.parentFile/prebuilts/tools/darwin-x86/swt/NOTICE" } item("$rootProject.projectDir.parentFile/prebuilts/tools/darwin-x86_64/swt/swt.jar") { into 'lib/x86_64' notice "$rootProject.projectDir.parentFile/prebuilts/tools/darwin-x86_64/swt/NOTICE" } } windows { item("$rootProject.projectDir.parentFile/prebuilts/tools/windows/swt/swt.jar") { into 'lib/x86' notice "$rootProject.projectDir.parentFile/prebuilts/tools/windows/swt/NOTICE" } item("$rootProject.projectDir.parentFile/prebuilts/tools/windows-x86_64/swt/swt.jar") { into 'lib/x86_64' notice "$rootProject.projectDir.parentFile/prebuilts/tools/windows-x86_64/swt/NOTICE" } } } chimpchat/0040755 0000000 0000000 00000000000 12747325007 011557 5ustar000000000 0000000 chimpchat/.classpath0100644 0000000 0000000 00000002453 12747325007 013543 0ustar000000000 0000000 chimpchat/.project0100644 0000000 0000000 00000000560 12747325007 013224 0ustar000000000 0000000 chimpchat org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature chimpchat/.settings/0040755 0000000 0000000 00000000000 12747325007 013475 5ustar000000000 0000000 chimpchat/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 020462 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 chimpchat/MODULE_LICENSE_APACHE20100644 0000000 0000000 00000000000 12747325007 014677 0ustar000000000 0000000 chimpchat/NOTICE0100644 0000000 0000000 00000024707 12747325007 012472 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS chimpchat/build.gradle0100644 0000000 0000000 00000000420 12747325007 014027 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'chimpchat' version = rootProject.ext.baseVersion dependencies { compile project(':base:sdklib') compile project(':base:ddmlib') compile project(':swt:hierarchyviewer2lib') testCompile 'junit:junit:3.8.1' } chimpchat/src/0040755 0000000 0000000 00000000000 12747325007 012346 5ustar000000000 0000000 chimpchat/src/main/0040755 0000000 0000000 00000000000 12747325007 013272 5ustar000000000 0000000 chimpchat/src/main/java/0040755 0000000 0000000 00000000000 12747325007 014213 5ustar000000000 0000000 chimpchat/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 014771 5ustar000000000 0000000 chimpchat/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016411 5ustar000000000 0000000 chimpchat/src/main/java/com/android/chimpchat/0040755 0000000 0000000 00000000000 12747325007 020351 5ustar000000000 0000000 chimpchat/src/main/java/com/android/chimpchat/ChimpChat.java0100644 0000000 0000000 00000007060 12747325007 023054 0ustar000000000 0000000 /* * 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.chimpchat; import com.android.chimpchat.adb.AdbBackend; import com.android.chimpchat.core.IChimpBackend; import com.android.chimpchat.core.IChimpDevice; import java.util.Map; import java.util.TreeMap; /** * ChimpChat is a host-side library that provides an API for communication with * an instance of Monkey on a device. This class provides an entry point to * setting up communication with a device. Currently it only supports communciation * over ADB, however. */ public class ChimpChat { private final IChimpBackend mBackend; private static String sAdbLocation; private static boolean sNoInitAdb; private ChimpChat(IChimpBackend backend) { this.mBackend = backend; } /** * Generates a new instance of ChimpChat based on the options passed. * @param options a map of settings for the new ChimpChat instance * @return a new instance of ChimpChat or null if errors occur during creation */ public static ChimpChat getInstance(Map options) { sAdbLocation = options.get("adbLocation"); sNoInitAdb = Boolean.valueOf(options.get("noInitAdb")); IChimpBackend backend = createBackendByName(options.get("backend")); if (backend == null) { return null; } ChimpChat chimpchat = new ChimpChat(backend); return chimpchat; } /** Generates a new instance of ChimpChat using default settings * @return a new instance of ChimpChat or null if errors occur during creation */ public static ChimpChat getInstance() { Map options = new TreeMap(); options.put("backend", "adb"); return ChimpChat.getInstance(options); } /** * Creates a specific backend by name. * * @param backendName the name of the backend to create * @return the new backend, or null if none were found. */ private static IChimpBackend createBackendByName(String backendName) { if ("adb".equals(backendName)) { return new AdbBackend(sAdbLocation, sNoInitAdb); } else { return null; } } /** * Retrieves an instance of the device from the backend * @param timeoutMs length of time to wait before timing out * @param deviceId the id of the device you want to connect to * @return an instance of the device */ public IChimpDevice waitForConnection(long timeoutMs, String deviceId){ return mBackend.waitForConnection(timeoutMs, deviceId); } /** * Retrieves an instance of the device from the backend. * Defaults to the longest possible wait time and any available device. * @return an instance of the device */ public IChimpDevice waitForConnection(){ return mBackend.waitForConnection(Integer.MAX_VALUE, ".*"); } /** * Shutdown and clean up chimpchat. */ public void shutdown(){ mBackend.shutdown(); } } chimpchat/src/main/java/com/android/chimpchat/ChimpManager.java0100644 0000000 0000000 00000036700 12747325007 023552 0ustar000000000 0000000 /* * 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.chimpchat; import com.android.chimpchat.core.IChimpView; import com.android.chimpchat.core.PhysicalButton; import com.android.chimpchat.core.ChimpException; import com.android.chimpchat.core.ChimpRect; import com.android.chimpchat.core.ChimpView; import com.google.common.collect.Lists; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.SocketException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; /** * Provides a nicer interface to interacting with the low-level network access protocol for talking * to the monkey. * * This class is thread-safe and can handle being called from multiple threads. */ public class ChimpManager { private static Logger LOG = Logger.getLogger(ChimpManager.class.getName()); private Socket monkeySocket; private BufferedWriter monkeyWriter; private BufferedReader monkeyReader; /** * Create a new ChimpMananger to talk to the specified device. * * @param monkeySocket the already connected socket on which to send protocol messages. * @throws IOException if there is an issue setting up the sockets */ public ChimpManager(Socket monkeySocket) throws IOException { this.monkeySocket = monkeySocket; monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); } /* Ensure that everything gets shutdown properly */ @Override protected void finalize() throws Throwable { try { quit(); } finally { close(); super.finalize(); } } /** * Send a touch down event at the specified location. * * @param x the x coordinate of where to click * @param y the y coordinate of where to click * @return success or not * @throws IOException on error communicating with the device */ public boolean touchDown(int x, int y) throws IOException { return sendMonkeyEvent("touch down " + x + " " + y); } /** * Send a touch down event at the specified location. * * @param x the x coordinate of where to click * @param y the y coordinate of where to click * @return success or not * @throws IOException on error communicating with the device */ public boolean touchUp(int x, int y) throws IOException { return sendMonkeyEvent("touch up " + x + " " + y); } /** * Send a touch move event at the specified location. * * @param x the x coordinate of where to click * @param y the y coordinate of where to click * @return success or not * @throws IOException on error communicating with the device */ public boolean touchMove(int x, int y) throws IOException { return sendMonkeyEvent("touch move " + x + " " + y); } /** * Send a touch (down and then up) event at the specified location. * * @param x the x coordinate of where to click * @param y the y coordinate of where to click * @return success or not * @throws IOException on error communicating with the device */ public boolean touch(int x, int y) throws IOException { return sendMonkeyEvent("tap " + x + " " + y); } /** * Press a physical button on the device. * * @param name the name of the button (As specified in the protocol) * @return success or not * @throws IOException on error communicating with the device */ public boolean press(String name) throws IOException { return sendMonkeyEvent("press " + name); } /** * Send a Key Down event for the specified button. * * @param name the name of the button (As specified in the protocol) * @return success or not * @throws IOException on error communicating with the device */ public boolean keyDown(String name) throws IOException { return sendMonkeyEvent("key down " + name); } /** * Send a Key Up event for the specified button. * * @param name the name of the button (As specified in the protocol) * @return success or not * @throws IOException on error communicating with the device */ public boolean keyUp(String name) throws IOException { return sendMonkeyEvent("key up " + name); } /** * Press a physical button on the device. * * @param button the button to press * @return success or not * @throws IOException on error communicating with the device */ public boolean press(PhysicalButton button) throws IOException { return press(button.getKeyName()); } /** * This function allows the communication bridge between the host and the device * to be invisible to the script for internal needs. * It splits a command into monkey events and waits for responses for each over an adb tcp socket. * Returns on an error, else continues and sets up last response. * * @param command the monkey command to send to the device * @return the (unparsed) response returned from the monkey. * @throws IOException on error communicating with the device */ private String sendMonkeyEventAndGetResponse(String command) throws IOException { command = command.trim(); LOG.info("Monkey Command: " + command + "."); // send a single command and get the response monkeyWriter.write(command + "\n"); monkeyWriter.flush(); return monkeyReader.readLine(); } /** * Parse a monkey response string to see if the command succeeded or not. * * @param monkeyResponse the response * @return true if response code indicated success. */ private boolean parseResponseForSuccess(String monkeyResponse) { if (monkeyResponse == null) { return false; } // return on ok if(monkeyResponse.startsWith("OK")) { return true; } return false; } /** * Parse a monkey response string to get the extra data returned. * * @param monkeyResponse the response * @return any extra data that was returned, or empty string if there was nothing. */ private String parseResponseForExtra(String monkeyResponse) { int offset = monkeyResponse.indexOf(':'); if (offset < 0) { return ""; } return monkeyResponse.substring(offset + 1); } /** * This function allows the communication bridge between the host and the device * to be invisible to the script for internal needs. * It splits a command into monkey events and waits for responses for each over an * adb tcp socket. * * @param command the monkey command to send to the device * @return true on success. * @throws IOException on error communicating with the device */ private boolean sendMonkeyEvent(String command) throws IOException { synchronized (this) { String monkeyResponse = sendMonkeyEventAndGetResponse(command); return parseResponseForSuccess(monkeyResponse); } } /** * Close all open resources related to this device. */ public void close() { try { monkeySocket.close(); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to close monkeySocket", e); } try { monkeyReader.close(); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to close monkeyReader", e); } try { monkeyWriter.close(); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e); } } /** * Function to get a static variable from the device. * * @param name name of static variable to get * @return the value of the variable, or null if there was an error * @throws IOException on error communicating with the device */ public String getVariable(String name) throws IOException { synchronized (this) { String response = sendMonkeyEventAndGetResponse("getvar " + name); if (!parseResponseForSuccess(response)) { return null; } return parseResponseForExtra(response); } } /** * Function to get the list of variables from the device. * @return the list of variables as a collection of strings * @throws IOException on error communicating with the device */ public Collection listVariable() throws IOException { synchronized (this) { String response = sendMonkeyEventAndGetResponse("listvar"); if (!parseResponseForSuccess(response)) { Collections.emptyList(); } String extras = parseResponseForExtra(response); return Lists.newArrayList(extras.split(" ")); } } /** * Tells the monkey that we are done for this session. * @throws IOException on error communicating with the device */ public void done() throws IOException { // this command just drops the connection, so handle it here synchronized (this) { sendMonkeyEventAndGetResponse("done"); } } /** * Tells the monkey that we are done forever. * @throws IOException on error communicating with the device */ public void quit() throws IOException { // this command drops the connection, so handle it here synchronized (this) { try { sendMonkeyEventAndGetResponse("quit"); } catch (SocketException e) { // flush was called after the call had been written, so it tried flushing to a // broken pipe. } } } /** * Send a tap event at the specified location. * * @param x the x coordinate of where to click * @param y the y coordinate of where to click * @return success or not * @throws IOException on error communicating with the device */ public boolean tap(int x, int y) throws IOException { return sendMonkeyEvent("tap " + x + " " + y); } /** * Type the following string to the monkey. * * @param text the string to type * @return success * @throws IOException on error communicating with the device */ public boolean type(String text) throws IOException { // The network protocol can't handle embedded line breaks, so we have to handle it // here instead StringTokenizer tok = new StringTokenizer(text, "\n", true); while (tok.hasMoreTokens()) { String line = tok.nextToken(); if ("\n".equals(line)) { boolean success = press(PhysicalButton.ENTER); if (!success) { return false; } } else { boolean success = sendMonkeyEvent("type " + line); if (!success) { return false; } } } return true; } /** * Type the character to the monkey. * * @param keyChar the character to type. * @return success * @throws IOException on error communicating with the device */ public boolean type(char keyChar) throws IOException { return type(Character.toString(keyChar)); } /** * Wake the device up from sleep. * @throws IOException on error communicating with the device */ public void wake() throws IOException { sendMonkeyEvent("wake"); } /** * Retrieves the list of view ids from the current application. * @return the list of view ids as a collection of strings * @throws IOException on error communicating with the device */ public Collection listViewIds() throws IOException { synchronized (this) { String response = sendMonkeyEventAndGetResponse("listviews"); if (!parseResponseForSuccess(response)) { Collections.emptyList(); } String extras = parseResponseForExtra(response); return Lists.newArrayList(extras.split(" ")); } } /** * Queries the on-screen view with the given id and returns the response. * It's up to the calling method to parse the returned String. * @param idType The type of ID to query the view by * @param id The view id of the view * @param query the query * @return the response from the query * @throws IOException on error communicating with the device */ public String queryView(String idType, List ids, String query) throws IOException { StringBuilder monkeyCommand = new StringBuilder("queryview " + idType + " "); for(String id : ids) { monkeyCommand.append(id).append(" "); } monkeyCommand.append(query); synchronized (this) { String response = sendMonkeyEventAndGetResponse(monkeyCommand.toString()); if (!parseResponseForSuccess(response)) { throw new ChimpException(parseResponseForExtra(response)); } return parseResponseForExtra(response); } } /** * Returns the current root view of the device. * @return the root view of the device */ public IChimpView getRootView() throws IOException { synchronized(this) { String response = sendMonkeyEventAndGetResponse("getrootview"); String extra = parseResponseForExtra(response); List ids = Arrays.asList(extra.split(" ")); if (!parseResponseForSuccess(response) || ids.size() != 2) { throw new ChimpException(extra); } ChimpView root = new ChimpView(ChimpView.ACCESSIBILITY_IDS, ids); root.setManager(this); return root; } } /** * Queries the device for a list of views with the given * @return A string containing the accessibility ids of the views with the given text */ public String getViewsWithText(String text) throws IOException { synchronized(this) { // Monkey has trouble parsing a single word in quotes if (text.split(" ").length > 1) { text = "\"" + text + "\""; } String response = sendMonkeyEventAndGetResponse("getviewswithtext " + text); if (!parseResponseForSuccess(response)) { throw new ChimpException(parseResponseForExtra(response)); } return parseResponseForExtra(response); } } } chimpchat/src/main/java/com/android/chimpchat/adb/0040755 0000000 0000000 00000000000 12747325007 021077 5ustar000000000 0000000 chimpchat/src/main/java/com/android/chimpchat/adb/AdbBackend.java0100644 0000000 0000000 00000011704 12747325007 023700 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.google.common.collect.Lists; import com.android.SdkConstants; import com.android.chimpchat.core.IChimpBackend; import com.android.chimpchat.core.IChimpDevice; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; import java.io.File; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; /** * Backend implementation that works over ADB to talk to the device. */ public class AdbBackend implements IChimpBackend { private static Logger LOG = Logger.getLogger(AdbBackend.class.getCanonicalName()); // How long to wait each time we check for the device to be connected. private static final int CONNECTION_ITERATION_TIMEOUT_MS = 200; private final List devices = Lists.newArrayList(); private final AndroidDebugBridge bridge; private final boolean initAdb; /** * Constructs an AdbBackend with default options. */ public AdbBackend() { this(null, false); } /** * Constructs an AdbBackend. * * @param adbLocation The location of the adb binary. If null, AdbBackend will * attempt to find the binary by itself. * @param noInitAdb If true, AdbBackend will not initialize AndroidDebugBridge. */ public AdbBackend(String adbLocation, boolean noInitAdb) { this.initAdb = !noInitAdb; // [try to] ensure ADB is running if (adbLocation == null) { adbLocation = findAdb(); } if (initAdb) { AndroidDebugBridge.init(false /* debugger support */); } bridge = AndroidDebugBridge.createBridge( adbLocation, true /* forceNewBridge */); } private String findAdb() { String mrParentLocation = System.getProperty("com.android.monkeyrunner.bindir"); //$NON-NLS-1$ // in the new SDK, adb is in the platform-tools, but when run from the command line // in the Android source tree, then adb is next to monkeyrunner. if (mrParentLocation != null && mrParentLocation.length() != 0) { // check if there's a platform-tools folder File platformTools = new File(new File(mrParentLocation).getParent(), SdkConstants.FD_PLATFORM_TOOLS); if (platformTools.isDirectory()) { return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB; } return mrParentLocation + File.separator + SdkConstants.FN_ADB; } return SdkConstants.FN_ADB; } /** * Checks the attached devices looking for one whose device id matches the specified regex. * * @param deviceIdRegex the regular expression to match against * @return the Device (if found), or null (if not found). */ private IDevice findAttachedDevice(String deviceIdRegex) { Pattern pattern = Pattern.compile(deviceIdRegex); for (IDevice device : bridge.getDevices()) { String serialNumber = device.getSerialNumber(); if (pattern.matcher(serialNumber).matches()) { return device; } } return null; } @Override public IChimpDevice waitForConnection() { return waitForConnection(Integer.MAX_VALUE, ".*"); } @Override public IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex) { do { IDevice device = findAttachedDevice(deviceIdRegex); // Only return the device when it is online if (device != null && device.getState() == IDevice.DeviceState.ONLINE) { IChimpDevice chimpDevice = new AdbChimpDevice(device); devices.add(chimpDevice); return chimpDevice; } try { Thread.sleep(CONNECTION_ITERATION_TIMEOUT_MS); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Error sleeping", e); } timeoutMs -= CONNECTION_ITERATION_TIMEOUT_MS; } while (timeoutMs > 0); // Timeout. Give up. return null; } @Override public void shutdown() { for (IChimpDevice device : devices) { device.dispose(); } if (initAdb) { AndroidDebugBridge.terminate(); } } } chimpchat/src/main/java/com/android/chimpchat/adb/AdbChimpDevice.java0100644 0000000 0000000 00000051627 12747325007 024541 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.DdmPreferences; import com.android.ddmlib.IDevice; import com.android.ddmlib.InstallException; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.android.annotations.Nullable; import com.android.chimpchat.ChimpManager; import com.android.chimpchat.adb.LinearInterpolator.Point; import com.android.chimpchat.core.ChimpRect; import com.android.chimpchat.core.IChimpImage; import com.android.chimpchat.core.IChimpDevice; import com.android.chimpchat.core.IChimpView; import com.android.chimpchat.core.IMultiSelector; import com.android.chimpchat.core.ISelector; import com.android.chimpchat.core.PhysicalButton; import com.android.chimpchat.core.TouchPressType; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; public class AdbChimpDevice implements IChimpDevice { private static final Logger LOG = Logger.getLogger(AdbChimpDevice.class.getName()); private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0]; private static final long MANAGER_CREATE_TIMEOUT_MS = 30 * 1000; // 30 seconds private static final long MANAGER_CREATE_WAIT_TIME_MS = 1000; // wait 1 second private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final IDevice device; private ChimpManager manager; public AdbChimpDevice(IDevice device) { this.device = device; this.manager = createManager("127.0.0.1", 12345); Preconditions.checkNotNull(this.manager); } @Override public ChimpManager getManager() { return manager; } @Override public void dispose() { try { manager.quit(); } catch (IOException e) { LOG.log(Level.SEVERE, "Error getting the manager to quit", e); } manager.close(); executor.shutdown(); manager = null; } @Override public HierarchyViewer getHierarchyViewer() { return new HierarchyViewer(device); } private void executeAsyncCommand(final String command, final LoggingOutputReceiver logger) { executor.submit(new Runnable() { @Override public void run() { try { device.executeShellCommand(command, logger); } catch (TimeoutException e) { LOG.log(Level.SEVERE, "Error starting command: " + command, e); throw new RuntimeException(e); } catch (AdbCommandRejectedException e) { LOG.log(Level.SEVERE, "Error starting command: " + command, e); throw new RuntimeException(e); } catch (ShellCommandUnresponsiveException e) { // This happens a lot LOG.log(Level.INFO, "Error starting command: " + command, e); throw new RuntimeException(e); } catch (IOException e) { LOG.log(Level.SEVERE, "Error starting command: " + command, e); throw new RuntimeException(e); } } }); } private ChimpManager createManager(String address, int port) { try { device.createForward(port, port); } catch (TimeoutException e) { LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e); return null; } catch (AdbCommandRejectedException e) { LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e); return null; } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e); return null; } String command = "monkey --port " + port; executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE)); // Sleep for a second to give the command time to execute. try { Thread.sleep(1000); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Unable to sleep", e); } InetAddress addr; try { addr = InetAddress.getByName(address); } catch (UnknownHostException e) { LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e); return null; } // We have a tough problem to solve here. "monkey" on the device gives us no indication // when it has started up and is ready to serve traffic. If you try too soon, commands // will fail. To remedy this, we will keep trying until a single command (in this case, // wake) succeeds. boolean success = false; ChimpManager mm = null; long start = System.currentTimeMillis(); while (!success) { long now = System.currentTimeMillis(); long diff = now - start; if (diff > MANAGER_CREATE_TIMEOUT_MS) { LOG.severe("Timeout while trying to create chimp mananger"); return null; } try { Thread.sleep(MANAGER_CREATE_WAIT_TIME_MS); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Unable to sleep", e); } Socket monkeySocket; try { monkeySocket = new Socket(addr, port); } catch (IOException e) { LOG.log(Level.FINE, "Unable to connect socket", e); success = false; continue; } try { mm = new ChimpManager(monkeySocket); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to open writer and reader to socket"); continue; } try { mm.wake(); } catch (IOException e) { LOG.log(Level.FINE, "Unable to wake up device", e); success = false; continue; } success = true; } return mm; } @Override public IChimpImage takeSnapshot() { try { return new AdbChimpImage(device.getScreenshot()); } catch (TimeoutException e) { LOG.log(Level.SEVERE, "Unable to take snapshot", e); return null; } catch (AdbCommandRejectedException e) { LOG.log(Level.SEVERE, "Unable to take snapshot", e); return null; } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to take snapshot", e); return null; } } @Override public String getSystemProperty(String key) { return device.getProperty(key); } @Override public String getProperty(String key) { try { return manager.getVariable(key); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to get variable: " + key, e); return null; } } @Override public Collection getPropertyList() { try { return manager.listVariable(); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to get variable list", e); return null; } } @Override public void wake() { try { manager.wake(); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to wake device (too sleepy?)", e); } } private String shell(String... args) { StringBuilder cmd = new StringBuilder(); for (String arg : args) { cmd.append(arg).append(" "); } return shell(cmd.toString()); } @Override public String shell(String cmd) { return shell(cmd, DdmPreferences.getTimeOut()); } @Override public String shell(String cmd, int timeout) { CommandOutputCapture capture = new CommandOutputCapture(); try { device.executeShellCommand(cmd, capture, timeout); } catch (TimeoutException e) { LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); return null; } catch (ShellCommandUnresponsiveException e) { LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); return null; } catch (AdbCommandRejectedException e) { LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); return null; } catch (IOException e) { LOG.log(Level.SEVERE, "Error executing command: " + cmd, e); return null; } return capture.toString(); } @Override public boolean installPackage(String path) { try { device.installPackage(path, true); return true; } catch (InstallException e) { LOG.log(Level.SEVERE, "Error installing package: " + path, e); return false; } } @Override public boolean removePackage(String packageName) { try { String result = device.uninstallPackage(packageName); if (result != null) { LOG.log(Level.SEVERE, "Got error uninstalling package "+ packageName + ": " + result); return false; } return true; } catch (InstallException e) { LOG.log(Level.SEVERE, "Error installing package: " + packageName, e); return false; } } @Override public void press(String keyName, TouchPressType type) { try { switch (type) { case DOWN_AND_UP: manager.press(keyName); break; case DOWN: manager.keyDown(keyName); break; case UP: manager.keyUp(keyName); break; } } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e); } } @Override public void press(PhysicalButton key, TouchPressType type) { press(key.getKeyName(), type); } @Override public void type(String string) { try { manager.type(string); } catch (IOException e) { LOG.log(Level.SEVERE, "Error Typing: " + string, e); } } @Override public void touch(int x, int y, TouchPressType type) { try { switch (type) { case DOWN: manager.touchDown(x, y); break; case UP: manager.touchUp(x, y); break; case DOWN_AND_UP: manager.tap(x, y); break; case MOVE: manager.touchMove(x, y); break; } } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending touch event: " + x + " " + y + " " + type, e); } } @Override public void reboot(String into) { try { device.reboot(into); } catch (TimeoutException e) { LOG.log(Level.SEVERE, "Unable to reboot device", e); } catch (AdbCommandRejectedException e) { LOG.log(Level.SEVERE, "Unable to reboot device", e); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to reboot device", e); } } @Override public void startActivity(String uri, String action, String data, String mimetype, Collection categories, Map extras, String component, int flags) { List intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, extras, component, flags); shell(Lists.asList("am", "start", intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); } @Override public void broadcastIntent(String uri, String action, String data, String mimetype, Collection categories, Map extras, String component, int flags) { List intentArgs = buildIntentArgString(uri, action, data, mimetype, categories, extras, component, flags); shell(Lists.asList("am", "broadcast", intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY)); } private static boolean isNullOrEmpty(@Nullable String string) { return string == null || string.length() == 0; } private List buildIntentArgString(String uri, String action, String data, String mimetype, Collection categories, Map extras, String component, int flags) { List parts = Lists.newArrayList(); // from adb docs: // specifications include these flags: // [-a ] [-d ] [-t ] // [-c [-c ] ...] // [-e|--es ...] // [--esn ...] // [--ez ...] // [-e|--ei ...] // [-n ] [-f ] // [] if (!isNullOrEmpty(action)) { parts.add("-a"); parts.add(action); } if (!isNullOrEmpty(data)) { parts.add("-d"); parts.add(data); } if (!isNullOrEmpty(mimetype)) { parts.add("-t"); parts.add(mimetype); } // Handle categories for (String category : categories) { parts.add("-c"); parts.add(category); } // Handle extras for (Entry entry : extras.entrySet()) { // Extras are either boolean, string, or int. See which we have Object value = entry.getValue(); String valueString; String arg; if (value instanceof Integer) { valueString = Integer.toString((Integer) value); arg = "--ei"; } else if (value instanceof Boolean) { valueString = Boolean.toString((Boolean) value); arg = "--ez"; } else { // treat is as a string. valueString = value.toString(); arg = "--es"; } parts.add(arg); parts.add(entry.getKey()); parts.add(valueString); } if (!isNullOrEmpty(component)) { parts.add("-n"); parts.add(component); } if (flags != 0) { parts.add("-f"); parts.add(Integer.toString(flags)); } if (!isNullOrEmpty(uri)) { parts.add(uri); } return parts; } @Override public Map instrument(String packageName, Map args) { List shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r"); for (Entry entry: args.entrySet()) { final String key = entry.getKey(); final Object value = entry.getValue(); if (key != null && value != null) { shellCmd.add("-e"); shellCmd.add(key); shellCmd.add(value.toString()); } } shellCmd.add(packageName); String result = shell(shellCmd.toArray(ZERO_LENGTH_STRING_ARRAY)); return convertInstrumentResult(result); } /** * Convert the instrumentation result into it's Map representation. * * @param result the result string * @return the new map */ @VisibleForTesting /* package */ static Map convertInstrumentResult(String result) { Map map = Maps.newHashMap(); Pattern pattern = Pattern.compile("^INSTRUMENTATION_(\\w+): ", Pattern.MULTILINE); Matcher matcher = pattern.matcher(result); int previousEnd = 0; String previousWhich = null; while (matcher.find()) { if ("RESULT".equals(previousWhich)) { String resultLine = result.substring(previousEnd, matcher.start()).trim(); // Look for the = in the value, and split there int splitIndex = resultLine.indexOf("="); String key = resultLine.substring(0, splitIndex); String value = resultLine.substring(splitIndex + 1); map.put(key, value); } previousEnd = matcher.end(); previousWhich = matcher.group(1); } if ("RESULT".equals(previousWhich)) { String resultLine = result.substring(previousEnd, matcher.start()).trim(); // Look for the = in the value, and split there int splitIndex = resultLine.indexOf("="); String key = resultLine.substring(0, splitIndex); String value = resultLine.substring(splitIndex + 1); map.put(key, value); } return map; } @Override public void drag(int startx, int starty, int endx, int endy, int steps, long ms) { final long iterationTime = ms / steps; LinearInterpolator lerp = new LinearInterpolator(steps); LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty); LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy); lerp.interpolate(start, end, new LinearInterpolator.Callback() { @Override public void step(Point point) { try { manager.touchMove(point.getX(), point.getY()); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending drag start event", e); } try { Thread.sleep(iterationTime); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Error sleeping", e); } } @Override public void start(Point point) { try { manager.touchDown(point.getX(), point.getY()); manager.touchMove(point.getX(), point.getY()); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending drag start event", e); } try { Thread.sleep(iterationTime); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Error sleeping", e); } } @Override public void end(Point point) { try { manager.touchMove(point.getX(), point.getY()); manager.touchUp(point.getX(), point.getY()); } catch (IOException e) { LOG.log(Level.SEVERE, "Error sending drag end event", e); } } }); } @Override public Collection getViewIdList() { try { return manager.listViewIds(); } catch(IOException e) { LOG.log(Level.SEVERE, "Error retrieving view IDs", e); return new ArrayList(); } } @Override public IChimpView getView(ISelector selector) { return selector.getView(manager); } @Override public Collection getViews(IMultiSelector selector) { return selector.getViews(manager); } @Override public IChimpView getRootView() { try { return manager.getRootView(); } catch (IOException e) { LOG.log(Level.SEVERE, "Error retrieving root view"); return null; } } } chimpchat/src/main/java/com/android/chimpchat/adb/AdbChimpImage.java0100644 0000000 0000000 00000002457 12747325007 024361 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.android.ddmlib.RawImage; import com.android.chimpchat.adb.image.ImageUtils; import com.android.chimpchat.core.ChimpImageBase; import java.awt.image.BufferedImage; /** * ADB implementation of the ChimpImage class. */ public class AdbChimpImage extends ChimpImageBase { private final RawImage image; /** * Create a new AdbMonkeyImage. * * @param image the image from adb. */ AdbChimpImage(RawImage image) { this.image = image; } @Override public BufferedImage createBufferedImage() { return ImageUtils.convertImage(image); } public RawImage getRawImage() { return image; } } chimpchat/src/main/java/com/android/chimpchat/adb/CommandOutputCapture.java0100644 0000000 0000000 00000002452 12747325007 026065 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.android.ddmlib.IShellOutputReceiver; /** * Shell Output Receiver that captures shell output into a String for * later retrieval. */ public class CommandOutputCapture implements IShellOutputReceiver { private final StringBuilder builder = new StringBuilder(); @Override public void flush() { } @Override public boolean isCancelled() { return false; } @Override public void addOutput(byte[] data, int offset, int length) { String message = new String(data, offset, length); builder.append(message); } @Override public String toString() { return builder.toString(); } } chimpchat/src/main/java/com/android/chimpchat/adb/LinearInterpolator.java0100644 0000000 0000000 00000007014 12747325007 025556 0ustar000000000 0000000 /* * 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.chimpchat.adb; /** * Linear Interpolation class. */ public class LinearInterpolator { private final int steps; /** * Use our own Point class so we don't pull in java.awt.* just for this simple class. */ public static class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { return new StringBuilder(). append("("). append(x). append(","). append(y). append(")").toString(); } @Override public boolean equals(Object obj) { if (obj instanceof Point) { Point that = (Point) obj; return this.x == that.x && this.y == that.y; } return false; } @Override public int hashCode() { return 0x43125315 + x + y; } public int getX() { return x; } public int getY() { return y; } } /** * Callback interface to recieve interpolated points. */ public interface Callback { /** * Called once to inform of the start point. */ void start(Point point); /** * Called once to inform of the end point. */ void end(Point point); /** * Called at every step in-between start and end. */ void step(Point point); } /** * Create a new linear Interpolator. * * @param steps How many steps should be in a single run. This counts the intervals * in-between points, so the actual number of points generated will be steps + 1. */ public LinearInterpolator(int steps) { this.steps = steps; } // Copied from android.util.MathUtils since we couldn't link it in on the host. private static float lerp(float start, float stop, float amount) { return start + (stop - start) * amount; } /** * Calculate the interpolated points. * * @param start The starting point * @param end The ending point * @param callback the callback to call with each calculated points. */ public void interpolate(Point start, Point end, Callback callback) { int xDistance = Math.abs(end.getX() - start.getX()); int yDistance = Math.abs(end.getY() - start.getY()); float amount = (float) (1.0 / steps); callback.start(start); for (int i = 1; i < steps; i++) { float newX = lerp(start.getX(), end.getX(), amount * i); float newY = lerp(start.getY(), end.getY(), amount * i); callback.step(new Point(Math.round(newX), Math.round(newY))); } // Generate final point callback.end(end); } } chimpchat/src/main/java/com/android/chimpchat/adb/LoggingOutputReceiver.java0100644 0000000 0000000 00000002654 12747325007 026242 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.android.ddmlib.IShellOutputReceiver; import java.util.logging.Level; import java.util.logging.Logger; /** * Shell Output Receiver that sends shell output to a Logger. */ public class LoggingOutputReceiver implements IShellOutputReceiver { private final Logger log; private final Level level; public LoggingOutputReceiver(Logger log, Level level) { this.log = log; this.level = level; } @Override public void addOutput(byte[] data, int offset, int length) { String message = new String(data, offset, length); for (String line : message.split("\n")) { log.log(level, line); } } @Override public void flush() { } @Override public boolean isCancelled() { return false; } } chimpchat/src/main/java/com/android/chimpchat/adb/image/0040755 0000000 0000000 00000000000 12747325007 022161 5ustar000000000 0000000 chimpchat/src/main/java/com/android/chimpchat/adb/image/CaptureRawAndConvertedImage.java0100644 0000000 0000000 00000007567 12747325007 030355 0ustar000000000 0000000 /* * 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.chimpchat.adb.image; import com.android.ddmlib.RawImage; import com.android.chimpchat.adb.AdbBackend; import com.android.chimpchat.adb.AdbChimpImage; import com.android.chimpchat.core.IChimpBackend; import com.android.chimpchat.core.IChimpImage; import com.android.chimpchat.core.IChimpDevice; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; /** * Utility program to capture raw and converted images from a device and write them to a file. * This is used to generate the test data for ImageUtilsTest. */ public class CaptureRawAndConvertedImage { public interface IRawImager { public RawImage toRawImage(); } public static class ChimpRawImage implements Serializable, IRawImager { public int version; public int bpp; public int size; public int width; public int height; public int red_offset; public int red_length; public int blue_offset; public int blue_length; public int green_offset; public int green_length; public int alpha_offset; public int alpha_length; public byte[] data; public ChimpRawImage(RawImage rawImage) { version = rawImage.version; bpp = rawImage.bpp; size = rawImage.size; width = rawImage.width; height = rawImage.height; red_offset = rawImage.red_offset; red_length = rawImage.red_length; blue_offset = rawImage.blue_offset; blue_length = rawImage.blue_length; green_offset = rawImage.green_offset; green_length = rawImage.green_length; alpha_offset = rawImage.alpha_offset; alpha_length = rawImage.alpha_length; data = rawImage.data; } @Override public RawImage toRawImage() { RawImage rawImage = new RawImage(); rawImage.version = version; rawImage.bpp = bpp; rawImage.size = size; rawImage.width = width; rawImage.height = height; rawImage.red_offset = red_offset; rawImage.red_length = red_length; rawImage.blue_offset = blue_offset; rawImage.blue_length = blue_length; rawImage.green_offset = green_offset; rawImage.green_length = green_length; rawImage.alpha_offset = alpha_offset; rawImage.alpha_length = alpha_length; rawImage.data = data; return rawImage; } } private static void writeOutImage(RawImage screenshot, String name) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name)); out.writeObject(new ChimpRawImage(screenshot)); out.close(); } public static void main(String[] args) throws IOException { IChimpBackend backend = new AdbBackend(); IChimpDevice device = backend.waitForConnection(); IChimpImage snapshot = (IChimpImage) device.takeSnapshot(); // write out to a file snapshot.writeToFile("output.png", "png"); writeOutImage(((AdbChimpImage)snapshot).getRawImage(), "output.raw"); System.exit(0); } } chimpchat/src/main/java/com/android/chimpchat/adb/image/ImageUtils.java0100644 0000000 0000000 00000007302 12747325007 025066 0ustar000000000 0000000 /* * 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.chimpchat.adb.image; import com.android.ddmlib.RawImage; import java.awt.Point; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.PixelInterleavedSampleModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.util.Hashtable; /** * Useful image related functions. */ public class ImageUtils { // Utility class private ImageUtils() { } private static Hashtable EMPTY_HASH = new Hashtable(); private static int[] BAND_OFFSETS_32 = { 0, 1, 2, 3 }; private static int[] BAND_OFFSETS_16 = { 0, 1 }; /** * Convert a raw image into a buffered image. * * @param rawImage the raw image to convert * @param image the old image to (possibly) recycle * @return the converted image */ public static BufferedImage convertImage(RawImage rawImage, BufferedImage image) { switch (rawImage.bpp) { case 16: return rawImage16toARGB(image, rawImage); case 32: return rawImage32toARGB(rawImage); } return null; } /** * Convert a raw image into a buffered image. * * @param rawImage the image to convert. * @return the converted image. */ public static BufferedImage convertImage(RawImage rawImage) { return convertImage(rawImage, null); } static int getMask(int length) { int res = 0; for (int i = 0 ; i < length ; i++) { res = (res << 1) + 1; } return res; } private static BufferedImage rawImage32toARGB(RawImage rawImage) { // Do as much as we can to not make an extra copy of the data. This is just a bunch of // classes that wrap's the raw byte array of the image data. DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size); PixelInterleavedSampleModel sampleModel = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height, 4, rawImage.width * 4, BAND_OFFSETS_32); WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, new Point(0, 0)); return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster, false, EMPTY_HASH); } private static BufferedImage rawImage16toARGB(BufferedImage image, RawImage rawImage) { // Do as much as we can to not make an extra copy of the data. This is just a bunch of // classes that wrap's the raw byte array of the image data. DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size); PixelInterleavedSampleModel sampleModel = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height, 2, rawImage.width * 2, BAND_OFFSETS_16); WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer, new Point(0, 0)); return new BufferedImage(new SixteenBitColorModel(rawImage), raster, false, EMPTY_HASH); } } chimpchat/src/main/java/com/android/chimpchat/adb/image/SixteenBitColorModel.java0100644 0000000 0000000 00000004735 12747325007 027070 0ustar000000000 0000000 /* * 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.chimpchat.adb.image; import com.android.ddmlib.RawImage; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.Raster; /** * Internal color model used to do conversion of 16bpp RawImages. */ class SixteenBitColorModel extends ColorModel { private static final int[] BITS = { 8, 8, 8, 8 }; public SixteenBitColorModel(RawImage rawImage) { super(32 , BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB), true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); } @Override public boolean isCompatibleRaster(Raster raster) { return true; } private int getPixel(Object inData) { byte[] data = (byte[]) inData; int value = data[0] & 0x00FF; value |= (data[1] << 8) & 0x0FF00; return value; } @Override public int getAlpha(Object inData) { return 0xff; } @Override public int getBlue(Object inData) { int pixel = getPixel(inData); return ((pixel >> 0) & 0x01F) << 3; } @Override public int getGreen(Object inData) { int pixel = getPixel(inData); return ((pixel >> 5) & 0x03F) << 2; } @Override public int getRed(Object inData) { int pixel = getPixel(inData); return ((pixel >> 11) & 0x01F) << 3; } @Override public int getAlpha(int pixel) { throw new UnsupportedOperationException(); } @Override public int getBlue(int pixel) { throw new UnsupportedOperationException(); } @Override public int getGreen(int pixel) { throw new UnsupportedOperationException(); } @Override public int getRed(int pixel) { throw new UnsupportedOperationException(); } } chimpchat/src/main/java/com/android/chimpchat/adb/image/ThirtyTwoBitColorModel.java0100644 0000000 0000000 00000007326 12747325007 027425 0ustar000000000 0000000 /* * 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.chimpchat.adb.image; import com.android.ddmlib.RawImage; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.Raster; /** * Internal color model used to do conversion of 32bpp RawImages. */ class ThirtyTwoBitColorModel extends ColorModel { private static final int[] BITS = { 8, 8, 8, 8, }; private final int alphaLength; private final int alphaMask; private final int alphaOffset; private final int blueMask; private final int blueLength; private final int blueOffset; private final int greenMask; private final int greenLength; private final int greenOffset; private final int redMask; private final int redLength; private final int redOffset; public ThirtyTwoBitColorModel(RawImage rawImage) { super(32, BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB), true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); redOffset = rawImage.red_offset; redLength = rawImage.red_length; redMask = ImageUtils.getMask(redLength); greenOffset = rawImage.green_offset; greenLength = rawImage.green_length; greenMask = ImageUtils.getMask(greenLength); blueOffset = rawImage.blue_offset; blueLength = rawImage.blue_length; blueMask = ImageUtils.getMask(blueLength); alphaLength = rawImage.alpha_length; alphaOffset = rawImage.alpha_offset; alphaMask = ImageUtils.getMask(alphaLength); } @Override public boolean isCompatibleRaster(Raster raster) { return true; } private int getPixel(Object inData) { byte[] data = (byte[]) inData; int value = data[0] & 0x00FF; value |= (data[1] & 0x00FF) << 8; value |= (data[2] & 0x00FF) << 16; value |= (data[3] & 0x00FF) << 24; return value; } @Override public int getAlpha(Object inData) { int pixel = getPixel(inData); if(alphaLength == 0) { return 0xff; } return ((pixel >>> alphaOffset) & alphaMask) << (8 - alphaLength); } @Override public int getBlue(Object inData) { int pixel = getPixel(inData); return ((pixel >>> blueOffset) & blueMask) << (8 - blueLength); } @Override public int getGreen(Object inData) { int pixel = getPixel(inData); return ((pixel >>> greenOffset) & greenMask) << (8 - greenLength); } @Override public int getRed(Object inData) { int pixel = getPixel(inData); return ((pixel >>> redOffset) & redMask) << (8 - redLength); } @Override public int getAlpha(int pixel) { throw new UnsupportedOperationException(); } @Override public int getBlue(int pixel) { throw new UnsupportedOperationException(); } @Override public int getGreen(int pixel) { throw new UnsupportedOperationException(); } @Override public int getRed(int pixel) { throw new UnsupportedOperationException(); } } chimpchat/src/main/java/com/android/chimpchat/core/0040755 0000000 0000000 00000000000 12747325007 021301 5ustar000000000 0000000 chimpchat/src/main/java/com/android/chimpchat/core/By.java0100644 0000000 0000000 00000003217 12747325007 022516 0ustar000000000 0000000 /* * 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.chimpchat.core; /** * A class that lets you select objects based on different criteria. * It operates similar to WebDriver's By class. */ public class By { /** * A method to let you select items by id. * @param id The string id of the object you want * @return a selector that will select the appropriate item by id */ public static ISelector id(String id) { return new SelectorId(id); } /** * A method that lets you select items by accessibility ids. * @param windowId the windowId of the object you want to select. * @param accessibilityId the accessibility id of the object you want to select * @return a selector that will select the appropriate object by its accessibility ids. */ public static ISelector accessibilityIds(int windowId, long accessibilityId){ return new SelectorAccessibilityIds(windowId, accessibilityId); } public static IMultiSelector text(String searchText) { return new MultiSelectorText(searchText); } } chimpchat/src/main/java/com/android/chimpchat/core/ChimpException.java0100644 0000000 0000000 00000001410 12747325007 025054 0ustar000000000 0000000 /* * 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.chimpchat.core; public class ChimpException extends RuntimeException { public ChimpException(String s) { super(s); } } chimpchat/src/main/java/com/android/chimpchat/core/ChimpImageBase.java0100644 0000000 0000000 00000015515 12747325007 024746 0ustar000000000 0000000 /* * 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.chimpchat.core; import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; /** * Base class with basic functionality for ChimpImage implementations. */ public abstract class ChimpImageBase implements IChimpImage { private static Logger LOG = Logger.getLogger(ChimpImageBase.class.getCanonicalName()); /** * Convert the ChimpImage to a BufferedImage. * * @return a BufferedImage for this ChimpImage. */ @Override public abstract BufferedImage createBufferedImage(); // Cache the BufferedImage so we don't have to generate it every time. private WeakReference cachedBufferedImage = null; /** * Utility method to handle getting the BufferedImage and managing the cache. * * @return the BufferedImage for this image. */ @Override public BufferedImage getBufferedImage() { // Check the cache first if (cachedBufferedImage != null) { BufferedImage img = cachedBufferedImage.get(); if (img != null) { return img; } } // Not in the cache, so create it and cache it. BufferedImage img = createBufferedImage(); cachedBufferedImage = new WeakReference(img); return img; } @Override public byte[] convertToBytes(String format) { BufferedImage argb = convertSnapshot(); ByteArrayOutputStream os = new ByteArrayOutputStream(); try { ImageIO.write(argb, format, os); } catch (IOException e) { return new byte[0]; } return os.toByteArray(); } @Override public boolean writeToFile(String path, String format) { if (format != null) { return writeToFileHelper(path, format); } int offset = path.lastIndexOf('.'); if (offset < 0) { return writeToFileHelper(path, "png"); } String ext = path.substring(offset + 1); Iterator writers = ImageIO.getImageWritersBySuffix(ext); if (!writers.hasNext()) { return writeToFileHelper(path, "png"); } ImageWriter writer = writers.next(); BufferedImage image = convertSnapshot(); try { File f = new File(path); f.delete(); ImageOutputStream outputStream = ImageIO.createImageOutputStream(f); writer.setOutput(outputStream); try { writer.write(image); } finally { writer.dispose(); outputStream.flush(); } } catch (IOException e) { return false; } return true; } @Override public int getPixel(int x, int y) { BufferedImage image = getBufferedImage(); return image.getRGB(x, y); } private BufferedImage convertSnapshot() { BufferedImage image = getBufferedImage(); // Convert the image to ARGB so ImageIO writes it out nicely BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics g = argb.createGraphics(); g.drawImage(image, 0, 0, null); g.dispose(); return argb; } private boolean writeToFileHelper(String path, String format) { BufferedImage argb = convertSnapshot(); try { ImageIO.write(argb, format, new File(path)); } catch (IOException e) { return false; } return true; } @Override public boolean sameAs(IChimpImage other, double percent) { BufferedImage otherImage = other.getBufferedImage(); BufferedImage myImage = getBufferedImage(); // Easy size check if (otherImage.getWidth() != myImage.getWidth()) { return false; } if (otherImage.getHeight() != myImage.getHeight()) { return false; } int[] otherPixel = new int[1]; int[] myPixel = new int[1]; int width = myImage.getWidth(); int height = myImage.getHeight(); int numDiffPixels = 0; // Now, go through pixel-by-pixel and check that the images are the same; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) { numDiffPixels++; } } } double numberPixels = (height * width); double diffPercent = numDiffPixels / numberPixels; return percent <= 1.0 - diffPercent; } // TODO: figure out the location of this class and is superclasses private static class BufferedImageChimpImage extends ChimpImageBase { private final BufferedImage image; public BufferedImageChimpImage(BufferedImage image) { this.image = image; } @Override public BufferedImage createBufferedImage() { return image; } } public static IChimpImage loadImageFromFile(String path) { File f = new File(path); if (f.exists() && f.canRead()) { try { BufferedImage bufferedImage = ImageIO.read(new File(path)); if (bufferedImage == null) { LOG.log(Level.WARNING, "Cannot decode file %s", path); return null; } return new BufferedImageChimpImage(bufferedImage); } catch (IOException e) { LOG.log(Level.WARNING, "Exception trying to decode image", e); return null; } } else { LOG.log(Level.WARNING, "Cannot read file %s", path); return null; } } @Override public IChimpImage getSubImage(int x, int y, int w, int h) { BufferedImage image = getBufferedImage(); return new BufferedImageChimpImage(image.getSubimage(x, y, w, h)); } } chimpchat/src/main/java/com/android/chimpchat/core/ChimpRect.java0100644 0000000 0000000 00000006367 12747325007 024033 0ustar000000000 0000000 /* * 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.chimpchat.core; /** * A class for holding information about view locations */ public class ChimpRect { public int left; public int top; public int right; public int bottom; /** * Creates an empty ChimpRect object. All coordinates are initialized to 0. */ public ChimpRect() {} /** * Create a new ChimpRect with the given coordinates. * @param left The X coordinate of the left side of the rectangle * @param top The Y coordinate of the top of the rectangle * @param right The X coordinate of the right side of the rectangle * @param bottom The Y coordinate of the bottom of the rectangle */ public ChimpRect(int left, int top, int right, int bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } /** * A comparison method to determine if the object is equivalent to other ChimpRects. * @param obj The object to compare it to * @return True if the object is an equivalent rectangle, false otherwise. */ @Override public boolean equals(Object obj) { if(obj instanceof ChimpRect){ ChimpRect r = (ChimpRect) obj; if (r != null) { return left == r.left && top == r.top && right == r.right && bottom == r.bottom; } } return false; } /** * The width of the ChimpRect * @return the width of the rectangle */ public int getWidth() { return right-left; } /** * The height of the ChimpRect * @return the height of the rectangle */ public int getHeight() { return bottom-top; } /** * Returns a 2 item int array with the x, y coordinates of the center of the ChimpRect. * @return a 2 item int array. The first item is the x value of the center of the ChimpRect and * the second item is the y value. */ public int[] getCenter() { int[] center = new int[2]; center[0] = left + getWidth() / 2; center[1] = top + getHeight() / 2; return center; } /** * Returns a representation of the rectangle in string form * @return a string representation of the rectangle */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("ChimpRect "); sb.append("top: ").append(top).append(" "); sb.append("right: ").append(right).append(" "); sb.append("bottom: ").append(bottom).append(" "); sb.append("left: ").append(left).append(" "); return sb.toString(); } } chimpchat/src/main/java/com/android/chimpchat/core/ChimpView.java0100644 0000000 0000000 00000015142 12747325007 024037 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.core.IChimpView.AccessibilityIds ; import com.android.chimpchat.ChimpManager; import com.google.common.collect.Lists; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /* A class for querying a view object by its id */ public class ChimpView implements IChimpView { private static final Logger LOG = Logger.getLogger(ChimpView.class.getName()); public static final String ACCESSIBILITY_IDS = "accessibilityids"; public static final String VIEW_ID = "viewid"; private String viewType; private List ids; private ChimpManager manager; public ChimpView(String viewType, List ids) { this.viewType = viewType; this.ids = ids; } @Override public void setManager(ChimpManager manager) { this.manager = manager; } private String queryView(String query) { try { return manager.queryView(viewType, ids, query); } catch(IOException e) { LOG.log(Level.SEVERE, "Error querying view: " + e.getMessage()); return ""; } } /** * Get the coordinates for the view with the given id. * @return a ChimpRect object with the coordinates for the corners of the view */ @Override public ChimpRect getLocation() { List result = Lists.newArrayList(queryView("getlocation").split(" ")); if (result.size() == 4) { try { int left = Integer.parseInt(result.get(0)); int top = Integer.parseInt(result.get(1)); int width = Integer.parseInt(result.get(2)); int height = Integer.parseInt(result.get(3)); return new ChimpRect(left, top, left+width, top+height); } catch (NumberFormatException e) { return new ChimpRect(); } } return new ChimpRect(); } /** * Retrieve the text contained by the view * @return the text contained by the view */ @Override public String getText() { return queryView("gettext"); } /** * Get the class of the view * @return the class name of the view */ @Override public String getViewClass(){ return queryView("getclass"); } /** * Get the checked status of the view. * @return true if the view is checked, false otherwise */ @Override public boolean getChecked(){ return Boolean.valueOf(queryView("getchecked").trim()); } /** * Get whether the view is enabled or not. * @return true if the view is enabled, false otherwise */ @Override public boolean getEnabled(){ return Boolean.valueOf(queryView("getenabled").trim()); } /** * Get the selected status of the view. * @return true if the view is selected, false otherwise */ @Override public boolean getSelected(){ return Boolean.valueOf(queryView("getselected").trim()); } /** * Set the selected status of the view. * @param selected the select status to set for the view */ @Override public void setSelected(boolean selected) { queryView("setselected " + selected); } /** * Get the focused status of the view. * @return true if the view is focused, false otherwise */ @Override public boolean getFocused(){ return Boolean.valueOf(queryView("getselected").trim()); } /** * Set the focused status of the view. * @param focused the focus status to set for the view */ @Override public void setFocused(boolean focused) { queryView("setfocused " + focused); } /** * Get the parent of the view. * @return the parent of the view */ @Override public IChimpView getParent() { List results = Lists.newArrayList(queryView("getparent").split(" ")); if (results.size() == 2) { ChimpView parent = new ChimpView(ChimpView.ACCESSIBILITY_IDS, results); parent.setManager(manager); return parent; } return null; } /** * Gets the children of the view. * @return the children of the view as a List of IChimpViews */ @Override public List getChildren() { List results = Lists.newArrayList(queryView("getchildren").split(" ")); /* We make sure this has an even number of results because we don't necessarily know how * many children there are, but we know all children will return a pair of accessibility ids */ if (results.size() % 2 == 0) { List children = new ArrayList(); for (int i = 0; i < results.size()/2; i++) { List ids = Lists.newArrayList(results.get(2 * i), results.get(2 * i + 1)); ChimpView child = new ChimpView(ChimpView.ACCESSIBILITY_IDS, ids); child.setManager(manager); children.add(child); } return children; } return new ArrayList(); } /** * Gets the accessibility ids of the current view * @return an AccessibilityIds object which contains the window id as an int * and the accessibility node id as a long. */ @Override public AccessibilityIds getAccessibilityIds() { List results = Lists.newArrayList(queryView("getaccessibilityids").split(" ")); if (results.size() == 2) { try { return new AccessibilityIds(Integer.parseInt(results.get(0)), Long.parseLong(results.get(1))); } catch (NumberFormatException e) { LOG.log(Level.SEVERE, "Error retrieving accesibility ids: " + e.getMessage()); } } return new AccessibilityIds(0, 0); } } chimpchat/src/main/java/com/android/chimpchat/core/IChimpBackend.java0100644 0000000 0000000 00000002706 12747325007 024567 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.core.IChimpDevice; /** * Interface between the ChimpChat API and the ChimpChat backend that communicates * with Monkey. */ public interface IChimpBackend { /** * Wait for a default device to connect to the backend. * * @return the connected device (or null if timeout); */ IChimpDevice waitForConnection(); /** * Wait for a device to connect to the backend. * * @param timeoutMs how long (in ms) to wait * @param deviceIdRegex the regular expression to specify which device to wait for. * @return the connected device (or null if timeout); */ IChimpDevice waitForConnection(long timeoutMs, String deviceIdRegex); /** * Shutdown the backend and cleanup any resources it was using. */ void shutdown(); } chimpchat/src/main/java/com/android/chimpchat/core/IChimpDevice.java0100644 0000000 0000000 00000016442 12747325007 024441 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.annotations.Nullable; import com.android.chimpchat.ChimpManager; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import java.util.Collection; import java.util.List; import java.util.Map; /** * ChimpDevice interface. */ public interface IChimpDevice { /** * Create a ChimpManager for talking to this device. * * @return the ChimpManager */ ChimpManager getManager(); /** * Dispose of any native resources this device may have taken hold of. */ void dispose(); /** * @return hierarchy viewer implementation for querying state of the view * hierarchy. */ HierarchyViewer getHierarchyViewer(); /** * Take the current screen's snapshot. * @return the snapshot image */ IChimpImage takeSnapshot(); /** * Reboot the device. * * @param into which bootloader to boot into. Null means default reboot. */ void reboot(@Nullable String into); /** * List properties of the device that we can inspect * * @return the list of property keys */ Collection getPropertyList(); /** * Get device's property. * * @param key the property name * @return the property value */ String getProperty(String key); /** * Get system property. * * @param key the name of the system property * @return the property value */ String getSystemProperty(String key); /** * Perform a touch of the given type at (x,y). * * @param x the x coordinate * @param y the y coordinate * @param type the touch type */ void touch(int x, int y, TouchPressType type); /** * Perform a press of a given type using a given key. * * @param keyName the name of the key to use * @param type the type of press to perform */ void press(String keyName, TouchPressType type); /** * Perform a press of a given type using a given key. * * @param key the key to press * @param type the type of press to perform */ void press(PhysicalButton key, TouchPressType type); /** * Perform a drag from one one location to another * * @param startx the x coordinate of the drag's starting point * @param starty the y coordinate of the drag's starting point * @param endx the x coordinate of the drag's end point * @param endy the y coordinate of the drag's end point * @param steps the number of steps to take when interpolating points * @param ms the duration of the drag */ void drag(int startx, int starty, int endx, int endy, int steps, long ms); /** * Type a given string. * * @param string the string to type */ void type(String string); /** * Execute a shell command. * * Will timeout if there is no ouput for 5 secounds. * * @param cmd the command to execute * @return the output of the command */ String shell(String cmd); /** * Execute a shell command. * * @param cmd the command to execute * @param timeout maximum time to output response * @return the output of the command */ String shell(String cmd, int timeout); /** * Install a given package. * * @param path the path to the installation package * @return true if success */ boolean installPackage(String path); /** * Uninstall a given package. * * @param packageName the name of the package * @return true if success */ boolean removePackage(String packageName); /** * Start an activity. * * @param uri the URI for the Intent * @param action the action for the Intent * @param data the data URI for the Intent * @param mimeType the mime type for the Intent * @param categories the category names for the Intent * @param extras the extras to add to the Intent * @param component the component of the Intent * @param flags the flags for the Intent */ void startActivity(@Nullable String uri, @Nullable String action, @Nullable String data, @Nullable String mimeType, Collection categories, Map extras, @Nullable String component, int flags); /** * Send a broadcast intent to the device. * * @param uri the URI for the Intent * @param action the action for the Intent * @param data the data URI for the Intent * @param mimeType the mime type for the Intent * @param categories the category names for the Intent * @param extras the extras to add to the Intent * @param component the component of the Intent * @param flags the flags for the Intent */ void broadcastIntent(@Nullable String uri, @Nullable String action, @Nullable String data, @Nullable String mimeType, Collection categories, Map extras, @Nullable String component, int flags); /** * Run the specified package with instrumentation and return the output it * generates. * * Use this to run a test package using InstrumentationTestRunner. * * @param packageName The class to run with instrumentation. The format is * packageName/className. Use packageName to specify the Android package to * run, and className to specify the class to run within that package. For * test packages, this is usually testPackageName/InstrumentationTestRunner * @param args a map of strings to objects containing the arguments to pass * to this instrumentation. * @return A map of strings to objects for the output from the package. * For a test package, contains a single key-value pair: the key is 'stream' * and the value is a string containing the test output. */ Map instrument(String packageName, Map args); /** * Wake up the screen on the device. */ void wake(); /** * List the possible view ID strings from the current applications resource file * @return the list of view id strings */ Collection getViewIdList(); /** * Retrieve the view object for the view with the given id. * @return a view object for the view with the given id */ IChimpView getView(ISelector selector); /** * Retrive the root view object. * @return the root view object. */ IChimpView getRootView(); /** * Retrieves the view objects that match the given selector * @return A list of views that match the given selector */ Collection getViews(IMultiSelector selector); } chimpchat/src/main/java/com/android/chimpchat/core/IChimpImage.java0100644 0000000 0000000 00000002232 12747325007 024254 0ustar000000000 0000000 /* * 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.chimpchat.core; import java.awt.image.BufferedImage; /** * ChimpImage interface. * * This interface defines an image representing a screen snapshot. */ public interface IChimpImage { // TODO: add java docs BufferedImage createBufferedImage(); BufferedImage getBufferedImage(); IChimpImage getSubImage(int x, int y, int w, int h); byte[] convertToBytes(String format); boolean writeToFile(String path, String format); int getPixel(int x, int y); boolean sameAs(IChimpImage other, double percent); } chimpchat/src/main/java/com/android/chimpchat/core/IChimpView.java0100644 0000000 0000000 00000005365 12747325007 024156 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.ChimpManager; import java.util.List; /** * An interface for view introspection. */ public interface IChimpView { /** * A representation of accessibility ids containing * the window id of the accessibility node, and the * id of the node itself. */ public static class AccessibilityIds { private final int windowId; private final long nodeId; public AccessibilityIds() { this.windowId = 0; this.nodeId = 0; } public AccessibilityIds(int windowId, long nodeId) { this.windowId = windowId; this.nodeId = nodeId; } public int getWindowId() { return windowId; } public long getNodeId() { return nodeId; } } /** * Set the manager for this view to communicate through. */ void setManager(ChimpManager manager); /** * Obtain the class of the view as a string */ String getViewClass(); /** * Obtain the text contained in the view */ String getText(); /** * Obtain the location of the view on the device screen */ ChimpRect getLocation(); /** * Obtain the checked status of this view. */ boolean getChecked(); /** * Obtain the enabled status of this view. */ boolean getEnabled(); /** * Obtain the selected status of this view. */ boolean getSelected(); /** * Set the selected status of the this view */ void setSelected(boolean selected); /** * Obtain the focused status of this view. */ boolean getFocused(); /** * Set the focused status of this view. */ void setFocused(boolean focused); /** * Retrieve the parent of this view if it has one. */ IChimpView getParent(); /** * Get the children of this view as a list of IChimpViews. */ List getChildren(); /** * Get the accessibility ids of this view. */ AccessibilityIds getAccessibilityIds(); } chimpchat/src/main/java/com/android/chimpchat/core/IMultiSelector.java0100644 0000000 0000000 00000001745 12747325007 025054 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.ChimpManager; import java.util.Collection; /** An interface for selectors that select more than one item */ public interface IMultiSelector { /** * A method that allows you to get a list of views based on the given selector type */ Collection getViews(ChimpManager manager); } chimpchat/src/main/java/com/android/chimpchat/core/ISelector.java0100644 0000000 0000000 00000001617 12747325007 024037 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.ChimpManager; /** * An interface for selectors */ public interface ISelector { /** * A method that allows you to get a view based on the give selector type */ IChimpView getView(ChimpManager manager); } chimpchat/src/main/java/com/android/chimpchat/core/MultiSelectorText.java0100644 0000000 0000000 00000005316 12747325007 025606 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.ChimpManager; import com.google.common.collect.Lists; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** A class for selecting objects by their text */ public class MultiSelectorText implements IMultiSelector { private static final Logger LOG = Logger.getLogger(ChimpView.class.getName()); private String text; /** * @param text the text which to select objects by */ public MultiSelectorText(String text) { this.text = text; } /** * A method for selecting views by the given text. * @return The collection of views that contain the given text */ @Override public Collection getViews(ChimpManager manager) { String response; List ids; try { response = manager.getViewsWithText(text); ids = Arrays.asList(response.split(" ")); } catch (IOException e) { LOG.log(Level.SEVERE, "Error communicating with device: " + e.getMessage()); return new ArrayList(); } /* We make sure this has an even number of results because we don't necessarily know how * many views with the given textthere are, but we know all of the views will return a pair * of accessibility ids */ if (ids.size() % 2 == 0) { List views = new ArrayList(); for (int i = 0; i < ids.size()/2; i++) { List accessibilityIds = Lists.newArrayList(ids.get(2 * i ), ids.get(2 * i + 1)); ChimpView view = new ChimpView(ChimpView.ACCESSIBILITY_IDS, accessibilityIds); view.setManager(manager); views.add(view); } return views; } LOG.log(Level.SEVERE, "Error retrieving views: " + response); return Collections.emptyList(); } } chimpchat/src/main/java/com/android/chimpchat/core/PhysicalButton.java0100644 0000000 0000000 00000002157 12747325007 025116 0ustar000000000 0000000 /* * 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.chimpchat.core; public enum PhysicalButton { HOME("KEYCODE_HOME"), SEARCH("KEYCODE_SEARCH"), MENU("KEYCODE_MENU"), BACK("KEYCODE_BACK"), DPAD_UP("DPAD_UP"), DPAD_DOWN("DPAD_DOWN"), DPAD_LEFT("DPAD_LEFT"), DPAD_RIGHT("DPAD_RIGHT"), DPAD_CENTER("DPAD_CENTER"), ENTER("enter"); private String keyName; private PhysicalButton(String keyName) { this.keyName = keyName; } public String getKeyName() { return keyName; } } chimpchat/src/main/java/com/android/chimpchat/core/SelectorAccessibilityIds.java0100644 0000000 0000000 00000003357 12747325007 027101 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.ChimpManager; import com.google.common.collect.Lists; /* A class for selecting objects by their accessibility ids */ public class SelectorAccessibilityIds implements ISelector { private int windowId; private long accessibilityId; /** * @param windowId the window id of the node you want to select * @param accessibilityId the accessibility id of the node you want to select */ public SelectorAccessibilityIds(int windowId, long accessibilityId) { this.windowId = windowId; this.accessibilityId = accessibilityId; } /** * A method for selecting a view by the given accessibility ids. * @param manager The manager object used for interacting with the device. * @return The view with the given accessibility ids. */ @Override public IChimpView getView(ChimpManager manager) { ChimpView view = new ChimpView(ChimpView.ACCESSIBILITY_IDS, Lists.newArrayList(Integer.toString(windowId), Long.toString(accessibilityId))); view.setManager(manager); return view; } } chimpchat/src/main/java/com/android/chimpchat/core/SelectorId.java0100644 0000000 0000000 00000002437 12747325007 024204 0ustar000000000 0000000 /* * 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.chimpchat.core; import com.android.chimpchat.ChimpManager; import com.google.common.collect.Lists; /* A class for selecting objects by their id */ public class SelectorId implements ISelector { private String id; /** * @param id the id to select objects by */ public SelectorId(String id){ this.id = id; } /** * A method for selecting a view by the given id. * @return The view with the given id */ @Override public IChimpView getView(ChimpManager manager) { ChimpView view = new ChimpView(ChimpView.VIEW_ID, Lists.newArrayList(id)); view.setManager(manager); return view; } } chimpchat/src/main/java/com/android/chimpchat/core/TouchPressType.java0100644 0000000 0000000 00000002734 12747325007 025110 0ustar000000000 0000000 /* * 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.chimpchat.core; import java.util.HashMap; import java.util.Map; /** * TouchPressType enum contains valid input for the "touch" Monkey command. * When passed as a string, the "identifier" value is used. */ public enum TouchPressType { DOWN("down"), UP("up"), DOWN_AND_UP("downAndUp"), MOVE("move"); private static final Map identifierToEnum = new HashMap(); static { for (TouchPressType type : values()) { identifierToEnum.put(type.identifier, type); } } private String identifier; TouchPressType(String identifier) { this.identifier = identifier; } public String getIdentifier() { return identifier; } public static TouchPressType fromIdentifier(String name) { return identifierToEnum.get(name); } } chimpchat/src/main/java/com/android/chimpchat/hierarchyviewer/0040755 0000000 0000000 00000000000 12747325007 023551 5ustar000000000 0000000 chimpchat/src/main/java/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java0100644 0000000 0000000 00000013442 12747325007 027515 0ustar000000000 0000000 /* * 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.chimpchat.hierarchyviewer; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.device.DeviceBridge; import com.android.hierarchyviewerlib.device.ViewServerDevice; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import org.eclipse.swt.graphics.Point; /** * Class for querying the view hierarchy of the device. */ public class HierarchyViewer { public static final String TAG = "hierarchyviewer"; private IDevice mDevice; /** * Constructs the hierarchy viewer for the specified device. * * @param device The Android device to connect to. */ public HierarchyViewer(IDevice device) { this.mDevice = device; setupViewServer(); } private void setupViewServer() { DeviceBridge.setupDeviceForward(mDevice); if (!DeviceBridge.isViewServerRunning(mDevice)) { if (!DeviceBridge.startViewServer(mDevice)) { // TODO: Get rid of this delay. try { Thread.sleep(2000); } catch (InterruptedException e) { } if (!DeviceBridge.startViewServer(mDevice)) { Log.e(TAG, "Unable to debug device " + mDevice); throw new RuntimeException("Could not connect to the view server"); } return; } } DeviceBridge.loadViewServerInfo(mDevice); } /** * Find a view by id. * * @param id id for the view. * @return view with the specified ID, or {@code null} if no view found. */ public ViewNode findViewById(String id) { ViewNode rootNode = DeviceBridge.loadWindowData( new Window(new ViewServerDevice(mDevice), "", 0xffffffff)); if (rootNode == null) { throw new RuntimeException("Could not dump view"); } return findViewById(id, rootNode); } /** * Find a view by ID, starting from the given root node * @param id ID of the view you're looking for * @param rootNode the ViewNode at which to begin the traversal * @return view with the specified ID, or {@code null} if no view found. */ public ViewNode findViewById(String id, ViewNode rootNode) { if (rootNode.id.equals(id)) { return rootNode; } for (ViewNode child : rootNode.children) { ViewNode found = findViewById(id,child); if (found != null) { return found; } } return null; } /** * Gets the window that currently receives the focus. * * @return name of the window that currently receives the focus. */ public String getFocusedWindowName() { int id = DeviceBridge.getFocusedWindow(mDevice); Window[] windows = DeviceBridge.loadWindows(new ViewServerDevice(mDevice), mDevice); for (Window w : windows) { if (w.getHashCode() == id) return w.getTitle(); } return null; } /** * Gets the absolute x/y position of the view node. * * @param node view node to find position of. * @return point specifying the x/y position of the node. */ public static Point getAbsolutePositionOfView(ViewNode node) { int x = node.left; int y = node.top; ViewNode p = node.parent; while (p != null) { x += p.left - p.scrollX; y += p.top - p.scrollY; p = p.parent; } return new Point(x, y); } /** * Gets the absolute x/y center of the specified view node. * * @param node view node to find position of. * @return absolute x/y center of the specified view node. */ public static Point getAbsoluteCenterOfView(ViewNode node) { Point point = getAbsolutePositionOfView(node); return new Point( point.x + (node.width / 2), point.y + (node.height / 2)); } /** * Gets the visibility of a given element. * * @param selector selector for the view. * @return True if the element is visible. */ public boolean visible(ViewNode node) { boolean ret = (node != null) && node.namedProperties.containsKey("getVisibility()") && "VISIBLE".equalsIgnoreCase( node.namedProperties.get("getVisibility()").value); return ret; } /** * Gets the text of a given element. * * @param selector selector for the view. * @return the text of the given element. */ public String getText(ViewNode node) { if (node == null) { throw new RuntimeException("Node not found"); } ViewNode.Property textProperty = node.namedProperties.get("text:mText"); if (textProperty == null) { // give it another chance, ICS ViewServer returns mText textProperty = node.namedProperties.get("mText"); if (textProperty == null) { throw new RuntimeException("No text property on node"); } } return textProperty.value; } } chimpchat/src/test/0040755 0000000 0000000 00000000000 12747325007 013325 5ustar000000000 0000000 chimpchat/src/test/java/0040755 0000000 0000000 00000000000 12747325007 014246 5ustar000000000 0000000 chimpchat/src/test/java/com/0040755 0000000 0000000 00000000000 12747325007 015024 5ustar000000000 0000000 chimpchat/src/test/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016444 5ustar000000000 0000000 chimpchat/src/test/java/com/android/chimpchat/0040755 0000000 0000000 00000000000 12747325007 020404 5ustar000000000 0000000 chimpchat/src/test/java/com/android/chimpchat/AllTests.java0100644 0000000 0000000 00000003124 12747325007 022777 0ustar000000000 0000000 /* * 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.chimpchat; import com.android.chimpchat.adb.AdbChimpDeviceTest; import com.android.chimpchat.adb.LinearInterpolatorTest; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestResult; import junit.framework.TestSuite; import junit.textui.TestRunner; /** * Test suite to run all the tests for MonkeyRunner. */ public class AllTests { public static Test suite(Class... classes) { TestSuite suite = new TestSuite(); for (Class clz : classes) { suite.addTestSuite(clz); } return suite; } public static void main(String args[]) { TestRunner tr = new TestRunner(); TestResult result = tr.doRun(AllTests.suite(ImageUtilsTest.class, LinearInterpolatorTest.class, AdbChimpDeviceTest.class)); if (result.wasSuccessful()) { System.exit(0); } else { System.exit(1); } } } chimpchat/src/test/java/com/android/chimpchat/ImageUtilsTest.java0100644 0000000 0000000 00000007171 12747325007 024155 0ustar000000000 0000000 /* * 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.chimpchat; import com.android.ddmlib.RawImage; import com.android.chimpchat.adb.image.CaptureRawAndConvertedImage; import com.android.chimpchat.adb.image.ImageUtils; import com.android.chimpchat.adb.image.CaptureRawAndConvertedImage.ChimpRawImage; import com.android.chimpchat.adb.image.CaptureRawAndConvertedImage.IRawImager; import junit.framework.TestCase; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import javax.imageio.ImageIO; public class ImageUtilsTest extends TestCase { private static BufferedImage createBufferedImage(String name) throws IOException { InputStream is = ImageUtilsTest.class.getResourceAsStream(name); BufferedImage img = ImageIO.read(is); is.close(); return img; } private static RawImage createRawImage(String name) throws IOException, ClassNotFoundException { ObjectInputStream is = new ObjectInputStream(ImageUtilsTest.class.getResourceAsStream(name)); Object obj = is.readObject(); is.close(); return ((IRawImager) obj).toRawImage(); } /** * Check that the two images will draw the same (ie. have the same pixels). This is different * that BufferedImage.equals(), which also wants to check that they have the same ColorModel * and other parameters. * * @param i1 the first image * @param i2 the second image * @return true if both images will draw the same (ie. have same pixels). */ private static boolean checkImagesHaveSamePixels(BufferedImage i1, BufferedImage i2) { if (i1.getWidth() != i2.getWidth()) { return false; } if (i1.getHeight() != i2.getHeight()) { return false; } for (int y = 0; y < i1.getHeight(); y++) { for (int x = 0; x < i1.getWidth(); x++) { int p1 = i1.getRGB(x, y); int p2 = i2.getRGB(x, y); if (p1 != p2) { WritableRaster r1 = i1.getRaster(); WritableRaster r2 = i2.getRaster(); return false; } } } return true; } public void testImageConversionOld() throws IOException, ClassNotFoundException { RawImage rawImage = createRawImage("image1.raw"); BufferedImage convertedImage = ImageUtils.convertImage(rawImage); BufferedImage correctConvertedImage = createBufferedImage("image1.png"); assertTrue(checkImagesHaveSamePixels(convertedImage, correctConvertedImage)); } public void testImageConversionNew() throws IOException, ClassNotFoundException { RawImage rawImage = createRawImage("image2.raw"); BufferedImage convertedImage = ImageUtils.convertImage(rawImage); BufferedImage correctConvertedImage = createBufferedImage("image2.png"); assertTrue(checkImagesHaveSamePixels(convertedImage, correctConvertedImage)); } } chimpchat/src/test/java/com/android/chimpchat/adb/0040755 0000000 0000000 00000000000 12747325007 021132 5ustar000000000 0000000 chimpchat/src/test/java/com/android/chimpchat/adb/AdbChimpDeviceTest.java0100644 0000000 0000000 00000004075 12747325007 025427 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.google.common.base.Joiner; import com.google.common.io.Resources; import junit.framework.TestCase; import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; import java.util.List; import java.util.Map; /** * Unit Tests for AdbChimpDevice. */ public class AdbChimpDeviceTest extends TestCase { private static String MULTILINE_RESULT = "\r\n" + "Test results for InstrumentationTestRunner=.\r\n" + "Time: 2.242\r\n" + "\r\n" + "OK (1 test)"; private static String getResource(String resName) throws IOException { URL resource = Resources.getResource(AdbChimpDeviceTest.class, resName); List lines = Resources.readLines(resource, Charset.defaultCharset()); return Joiner.on("\r\n").join(lines); } public void testSimpleResultParse() throws IOException { String result = getResource("instrument_result.txt"); Map convertedResult = AdbChimpDevice.convertInstrumentResult(result); assertEquals("one", convertedResult.get("result1")); assertEquals("two", convertedResult.get("result2")); } public void testMultilineResultParse() throws IOException { String result = getResource("multiline_instrument_result.txt"); Map convertedResult = AdbChimpDevice.convertInstrumentResult(result); assertEquals(MULTILINE_RESULT, convertedResult.get("stream")); } } chimpchat/src/test/java/com/android/chimpchat/adb/LinearInterpolatorTest.java0100644 0000000 0000000 00000011722 12747325007 026452 0ustar000000000 0000000 /* * 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.chimpchat.adb; import com.google.common.collect.Lists; import com.android.chimpchat.adb.LinearInterpolator.Point; import junit.framework.TestCase; import java.util.List; /** * Unit tests for the LinerInterpolator class.S */ public class LinearInterpolatorTest extends TestCase { private static class Collector implements LinearInterpolator.Callback { private final List points = Lists.newArrayList(); public List getPoints() { return points; } @Override public void end(Point input) { points.add(input); } @Override public void start(Point input) { points.add(input); } @Override public void step(Point input) { points.add(input); } } List STEP_POINTS = Lists.newArrayList(0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000); List REVERSE_STEP_POINTS = Lists.newArrayList(1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 0); public void testLerpRight() { LinearInterpolator lerp = new LinearInterpolator(10); Collector collector = new Collector(); lerp.interpolate(new LinearInterpolator.Point(0, 100), new LinearInterpolator.Point(1000, 100), collector); List points = collector.getPoints(); assertEquals(11, points.size()); for (int x = 0; x < points.size(); x++) { assertEquals(new Point(STEP_POINTS.get(x), 100), points.get(x)); } } public void testLerpLeft() { LinearInterpolator lerp = new LinearInterpolator(10); Collector collector = new Collector(); lerp.interpolate(new LinearInterpolator.Point(1000, 100), new LinearInterpolator.Point(0, 100), collector); List points = collector.getPoints(); assertEquals(11, points.size()); for (int x = 0; x < points.size(); x++) { assertEquals(new Point(REVERSE_STEP_POINTS.get(x), 100), points.get(x)); } } public void testLerpUp() { LinearInterpolator lerp = new LinearInterpolator(10); Collector collector = new Collector(); lerp.interpolate(new LinearInterpolator.Point(100, 1000), new LinearInterpolator.Point(100, 0), collector); List points = collector.getPoints(); assertEquals(11, points.size()); for (int x = 0; x < points.size(); x++) { assertEquals(new Point(100, REVERSE_STEP_POINTS.get(x)), points.get(x)); } } public void testLerpDown() { LinearInterpolator lerp = new LinearInterpolator(10); Collector collector = new Collector(); lerp.interpolate(new LinearInterpolator.Point(100, 0), new LinearInterpolator.Point(100, 1000), collector); List points = collector.getPoints(); assertEquals(11, points.size()); for (int x = 0; x < points.size(); x++) { assertEquals(new Point(100, STEP_POINTS.get(x)), points.get(x)); } } public void testLerpNW() { LinearInterpolator lerp = new LinearInterpolator(10); Collector collector = new Collector(); lerp.interpolate(new LinearInterpolator.Point(0, 0), new LinearInterpolator.Point(1000, 1000), collector); List points = collector.getPoints(); assertEquals(11, points.size()); for (int x = 0; x < points.size(); x++) { assertEquals(new Point(STEP_POINTS.get(x), STEP_POINTS.get(x)), points.get(x)); } } public void testLerpNE() { LinearInterpolator lerp = new LinearInterpolator(10); Collector collector = new Collector(); lerp.interpolate(new LinearInterpolator.Point(1000, 1000), new LinearInterpolator.Point(0, 0), collector); List points = collector.getPoints(); assertEquals(11, points.size()); for (int x = 0; x < points.size(); x++) { assertEquals(new Point(REVERSE_STEP_POINTS.get(x), REVERSE_STEP_POINTS.get(x)), points.get(x)); } } } chimpchat/src/test/java/com/android/monkeyrunner/0040755 0000000 0000000 00000000000 12747325007 021200 5ustar000000000 0000000 chimpchat/src/test/java/com/android/monkeyrunner/adb/0040755 0000000 0000000 00000000000 12747325007 021726 5ustar000000000 0000000 chimpchat/src/test/java/com/android/monkeyrunner/adb/image/0040755 0000000 0000000 00000000000 12747325007 023010 5ustar000000000 0000000 chimpchat/src/test/java/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java0100755 0000000 0000000 00000006421 12747325007 031173 0ustar000000000 0000000 /* * Copyright (C) 2013 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.monkeyrunner.adb.image; import com.android.chimpchat.adb.image.CaptureRawAndConvertedImage.IRawImager; import com.android.ddmlib.RawImage; import java.io.Serializable; @Deprecated public class CaptureRawAndConvertedImage { /** * When com.android.chimpchat.ImageUtilsTest.createRawImage(String) deserializes * an image from a file, it might try to recreate this older class -- the class itself * is obsolete and replaced by the {@code ChipRawImage} but this exact implementation * is still needed for deserialization compatibility purposes. *

* This implementation is restored from sdk.git @ 8c09f35fe02c38c18f8f7b9e0a531d6ac158476e. */ @Deprecated public static class MonkeyRunnerRawImage implements Serializable, IRawImager { private static final long serialVersionUID = -1137219979977761746L; public int version; public int bpp; public int size; public int width; public int height; public int red_offset; public int red_length; public int blue_offset; public int blue_length; public int green_offset; public int green_length; public int alpha_offset; public int alpha_length; public byte[] data; public MonkeyRunnerRawImage(RawImage rawImage) { version = rawImage.version; bpp = rawImage.bpp; size = rawImage.size; width = rawImage.width; height = rawImage.height; red_offset = rawImage.red_offset; red_length = rawImage.red_length; blue_offset = rawImage.blue_offset; blue_length = rawImage.blue_length; green_offset = rawImage.green_offset; green_length = rawImage.green_length; alpha_offset = rawImage.alpha_offset; alpha_length = rawImage.alpha_length; data = rawImage.data; } @Override public RawImage toRawImage() { RawImage rawImage = new RawImage(); rawImage.version = version; rawImage.bpp = bpp; rawImage.size = size; rawImage.width = width; rawImage.height = height; rawImage.red_offset = red_offset; rawImage.red_length = red_length; rawImage.blue_offset = blue_offset; rawImage.blue_length = blue_length; rawImage.green_offset = green_offset; rawImage.green_length = green_length; rawImage.alpha_offset = alpha_offset; rawImage.alpha_length = alpha_length; rawImage.data = data; return rawImage; } } } chimpchat/src/test/resources/0040755 0000000 0000000 00000000000 12747325007 015337 5ustar000000000 0000000 chimpchat/src/test/resources/com/0040755 0000000 0000000 00000000000 12747325007 016115 5ustar000000000 0000000 chimpchat/src/test/resources/com/android/0040755 0000000 0000000 00000000000 12747325007 017535 5ustar000000000 0000000 chimpchat/src/test/resources/com/android/chimpchat/0040755 0000000 0000000 00000000000 12747325007 021475 5ustar000000000 0000000 chimpchat/src/test/resources/com/android/chimpchat/adb/0040755 0000000 0000000 00000000000 12747325007 022223 5ustar000000000 0000000 chimpchat/src/test/resources/com/android/chimpchat/adb/instrument_result.txt0100644 0000000 0000000 00000000640 12747325007 026567 0ustar000000000 0000000 INSTRUMENTATION_STATUS: id=InstrumentationTestRunner INSTRUMENTATION_STATUS: current=1 INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest INSTRUMENTATION_STATUS: stream=. INSTRUMENTATION_STATUS: numtests=1 INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly INSTRUMENTATION_STATUS_CODE: 0 INSTRUMENTATION_RESULT: result1=one INSTRUMENTATION_RESULT: result2=two INSTRUMENTATION_CODE: -1 chimpchat/src/test/resources/com/android/chimpchat/adb/multiline_instrument_result.txt0100644 0000000 0000000 00000000700 12747325007 030646 0ustar000000000 0000000 INSTRUMENTATION_STATUS: id=InstrumentationTestRunner INSTRUMENTATION_STATUS: current=1 INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest INSTRUMENTATION_STATUS: stream=. INSTRUMENTATION_STATUS: numtests=1 INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly INSTRUMENTATION_STATUS_CODE: 0 INSTRUMENTATION_RESULT: stream= Test results for InstrumentationTestRunner=. Time: 2.242 OK (1 test) INSTRUMENTATION_CODE: -1 chimpchat/src/test/resources/com/android/chimpchat/image1.png0100644 0000000 0000000 00000547741 12747325007 023365 0ustar000000000 0000000 PNG  IHDR@ԌDIDATx}+F 0`0`  0`A 4h``pAAA>{-ܛ`Riig9o/yK^QMxK^@fGQng[}nqpϟߞ3cχ{>g~u_:n>cY[Suo=پs}_?ۯv<ϏYx,}/0,o"$I-XNI<;f-<\s|4>m%`f *uD |:w/;7GXT]y`h{~&n^ܩ^ 0у~M> }c 'rcq7VLyoϟ1` '^BUe#Ȳ̾%.X It~xs90g\heйho}r_ruxʘ|o1{b~/%N``7CT]n8@=>6X3YRu9߽7G һ߽Y|ߎg?8(z`yh:(vuXil}t}{a_2`&5aXƾ9?T?mgKv_nwCϤqǎq|1{;}z&w(̢ )"_@/tm->o89x}>sm=O4^O[?b3>Cv18O4?0ط7V}xóLm8>Ǥޮlvtò%/{~t&$,N$҇mMq.6G=ޡQǹ\.5H/XqN +vـ|AMՄm1E0*yl4^"?`2UuVe^߁9ZF}߶~^}Y~|,ͺZg7m\˳\/ |@XkmXT%rYv<)ųCW]s_pNsl32Zk"xp6@{@Ǔ/DFg6_sQMڃYT ;k}nZ/zlElanow؞]ygnܷot[#fm ܪOuS/G7{E\ZYγe {]>4s?B Dȳ7mm1$% uMlX/h-*vM.N7Z 0\ dz@ _q d| ڇ FO%xqaa[|Yxki_{8:|wIc[-pqMƇ{S겨V% җɱoI|V\}a i9?bc?0>o7ugݦmg;ctq?^vEx5Zalo;t=mXjCR-kGG-؜[dlKg{}W$)uveoMȪf.αem 6zf8@qnI .6l8Wu]u Nq0pc?9׾c0O?' |(}6YCߋ*^`K웞e7[\+k7)9Fזw ̱3Tv_m`.`DZJܟf å_ |ʱMLi6=zX>]s4΁k9pg|ikTۚ7?6yOm>5S>MKxn%Ҷ]5`k &W-3p겇I)vXSoB_S}=cbՒ+v}`A0 MMq^1 J m_ӧfV lM56|c5^<3}1]q~KxaZiy})ebfۮkl~Ș|0uG,=s#u@-r=F o.+*=% 6c$T7hxXxPz~l@1fp05 €nAby?ur<VpE Pڸ,M2j^Z%čo;kS&fS6%tI( voyf>==3v;M rOcBu,ڞ^f3AT5;NS͆$j&b{apӉ:LZczaPki3@/_GU wC!g8*#&ޱq;--惕ŶT5͗}lP~5X܊1|)AO{io\}mv19~L1 Havm3?|Иhkv+ЪC-sö9acպO#ɟOmcSӣf7}X瘱ff, Aɢ t.}Nk Cի\f;.cHPa^nqZ+6ov6X2~ /,Zqr/1ӊS{f?guQX}Ywlпw Ž/*Xt u__R`{-xF;mwm?l?WJp<[|z:뫫u|XmѲάQh;o͸sG-X7^sٮ}|~]cVmdZ~1vɼxc-f/m.>4֒uݽonfܳS놮UY-Ͻ]>s;*:9VƖ|ﻗcw?{x~k<6;sxơ?0բFT&W4&(xޜT闎 Vֳg?sAk2_][u~gh3e3t<܇ahmL]G ߿frpTE%*͗ZLd@(4}?T 0'%/݈ .>qß<2?SS1@|I%/,ª)03{> l8as#E&t:<׸%Ta0m lK/yK )>Rւex9ϯ?,,x`#Kb_gG~/yɇ9|  Ԝe9I=~81{Z5nK^]4hzn $- s j%s%P˿4nf?k/yɻTJ$@hL.?l3N`y]V^rZLnzr1T_%*]]+366Йۙ1CZxoJ}ՙgGTv=MP;8@@_˧s5 a1MB(m~ LV,Ym*Ԍ+lΗdHyyIX"]Zѯ8& >X=1eN6qOao}%gz0ضbDc!3#sH܏׼/yFě+`;vU>g@9(V`C*&r%N9]Ϝ`@x"H1RM{CMP@umSU".Pcolsx`u>~c i;s˒3,KP.3 y># ?'SiO *sNϋ#@Gbag(jƾoKu\,*v{$CM~/H"AB O4ɶcj䉑>ob7 We0I̟ ~C}S\4 L1\9A>L>8Xa`պ}~Rh!%/ ;0ު@Vye^$ޱѪg@ &@)8t3!zC\568.Ǎm[,P[[p`Z%SAʗ7&/D/y#O@ “<[8}xp=Pmh(z.\ p0IM_gmP":lA`3^ċi]V]A xk0٣bgY9^g#¥Uy mƌ% muPyBө*,QxV&;O >"D8}֜^L#Zvɮsp^%>cyp(^@&LPa]ClGnU 0 ovۓ{=/yotLe~!Z@H,^b u*Ѹ @/m,i{K~ #)pϫ.ښ(]q>6K^ߕ< ?~S"558 f|2m߃~NZC74)z|L%{_NĘﲤd> ro}G/qS'FK^91Nʧk5!MRvn) kn`uտ/yKxoW5U,̴.jٯ)^MFb5g}/?3aV`apO)Z2坯47cd)-ahK^Eh*0SRL_7%/y?`YV( /yKIKSͰi1,ݽ_$ /yK^{(>'IJ~;U /)3A6PPh|Is~3X>R-7skm?Қ׎vh#AOkJmnrn/߶roD\g _I*B e_SKGMHbZ@_\zhg Ψ;, \O/Q^*䡟zJݢ߷1`ȠO;w'2exݱ}͊@ ݚY U|36A#>mmDK!_U+,kպ_7Uc~sotF7o^K[߶ڿ5zaLwF;Պ8}G}~Wbi$ 17~c?Rޗ9gޗ'kvX^sL`G"-1K׌$wʷ-og_nol㻋r6wI3f-W Kzˌ-I3pew?7_h[IZ9c " {@0PcYdgP.>˒ι!qnt!1ovt,Zkj5Sk_(' W0 =u@A৷[-~T&U n Lk#mC[Lh K{~G{n$\28vGL@@/ٹG{^wisDwM\@?;߽Gvqx;6U W ]j[.m1GmXz RWS>k{AIW%<eAj匹 p4Vsh;@aퟬu[Lor6#`h2Qmk/a( p,lbj5P_ zN pT뻙:@6sOr/J ]3$`H] K,me[h@P%#Gv& !)f8ɍevλmm/ppxXlm mѴ!2Ǿ n}L0IQN[v@H=3A+tG([#5p+۟hS6*gP'풘~rZ30!%D3=Qw LLl3=x #D6AAyrITu6Mj:P\3'maV6Bm3@t-q˛AаFmwENj 6 6&x 6d][YFvړHg1]!\Tv@c_A 7+DC1@nZ`FͨQdAHට*x(w-I=b /E@xF{ `!Nb~LGxZ1Aw~Q\o3>syf- & {_ܲ{^_Tk2ٿ$NS8g記3W:ޏ^(`V2< C1^K:g'ɉ`TY j=>>2=`3-`8krۖiFx᩾QN3@]d+{s9]TZOS2j0 &Uad$: &azֹ?ge#ܞھk7` }.;̒._oh36fP`(FضJ;&y{/cP󴼞ίq類տXelomf=˝` 6aO `h 3!0| 0!QNpBT7h>{& ?) A YyB0~;7Vo=Dϭ?\R\/%Ol}|Q lCT΅ft*)pUl̈E?bZUw`P*<pX @0#X >`fK\$@љ ~*gl\&hlNu\ 5 aP4l.fَ/a~^1g0=kNkbWa6.${g3%&k:9/Q!gfp.Afxtoސxٍ. 旫L(boѴڻZ7Z&I“\8׷]u]9?f|Z1Z H{~"_ H“_i B* V2C(&x1j'H6pB~c[7Ǔv[a ۷mm<]ۛ oVPs*[~֗QV[3|BuWIqEFV_3Tey쵅Ae*>~w˻Tu8?#xKA Ig8ܲnu ~:9:)[`rr+~4<LtoPUmQgҭR ա.K ~`a@sU_M}.Jep^cT;B&C @^!1{LV R'nfws ?s'N{8vapP8*Q/Þ jkX XCcv̠ 퇘X8[3%qtZj4 `؟x P\ 5+D V}Aa&[.g./f /A~(|/cT_@Qwz s1|V^"pCSJvAO0 LR"Z@0;_kjtn vMnONaT^5^*U gCcPY3DrK#t+W/!z8ddЁb~]u& -כӚ^ܚJ!H_νS뱚|a@N^?%Hq~:Cbtڰ Ď#? ] {L0~QXFЌO" iٱ uPb( daqV9xLl$V%eQA9Q)5w%eee&ʔ008Dn`Rpr{Wy-ة`rucZaܟ6W/gcNpLFJFh(Ǹü7pB[W͸E ??g:ϰL0 x+mt-;c0#d_P0d@l .m|pıUbNw*Eo; ~)o/V\3j[@ tZJeU, r+&g&Tέ[LpjU`"4O7?K, 64^ 9ńZyLdK{ֶ= Z/0qd[a01{{Fz eb=ܭ.N"#SUɫ. n~;g r+.q` q0˞+ cЩѿ4cF\5fl1WoOadG1"le ͊ca|&xB)C.(?Bln<|5.@ip*0*Waj̤m# fwn[}#r@.[lC=6!0ۚfM^6\aaґ{ x^`U K Ps2 S%6+ 8k*wY !G v5V=/`)۟^T$|m0>f| aƝ |d8s<`W9P /|w78>˼e~3̯>6R -= P.^n`@<a9a2`W% 6؝U%{7 ļӝ.&8OUA\-n >S:&8F<&dx[eXCᜀRlv3z=S\Bp k94QƊ7_rU͏ |6:_Hp:_rQjK{S=_ Waw޷̐ua~m>7v@0V\`.O-~o b۠"$tWlMm(9B aЇC cvu>X6N7F(0oVilcmQlƜ#L1]v /}d~N+Ziy%zÆ XWhwkq텨e*&`^Ly [/  W{y ~J f{)oĸ'ؠ^ r86myQe`yBA^gH!0Xי!J * m∹xF4+m>cGif]Q状rMoFj.r]聡.Y Zɇs[’6M,USȴ+IOP; Di\Kb.O@ॕB-KVAE2ea x>xwWB;,yٖt? EB#_8j _XV}QwP=1?"^^j&X?krz<{LHj5Xpl_{װ5 ^ T3=ߌ#ǔXw8TY[ܕ!2фJ\9ZMÁ !}yfz#w^~o0ybxLpJ[E'fge@SAW3A ZFiT^0a>0@/P hr7R3>q UR\Uڌ0M^9?t8TQ ~CcΜ%{;g<02@7֟|؉p: pW.!.W| "D#Xr q"6jC$5[ל].`u t v(q#dw9 NrFFтސ Ɨ;\0 ]!Rfi_vHbg>c0D0ERX`;pl\ĸv֏?N\ r v۟IEϧ@ Jb Omw=io6_rxځat|w *Iw L>b⼧u(#\y:8~zܟet-c57ehHk{/RA@'386p@:&SaG gaKb  uW_*c qH{9f5ߍDT%}e JbiɅ|\'Bbh7|u A/h4+~##5vYڳ?RjXNTtʲxwLig Sr>dTjXyf>GbgtuY-i;?$R?f\&Ǡk+ З"C\:bu/EHT moZyv$`BLPx\ !5R7 7Und~j2R3\`O܇.>g\:M&O.Ä l~jA!Kw uxLpZb@aUpd^F_!Oמ|=ɺܸ5HtwxҴ[j6L۾aO:3ҋ<*ߦ 3Wx+䢨H]mt4mma-SjpNyGbI']@ Nko.8+@S,`S1/ 8ͰcI ;2H:gQxu[O ~&hCZ We0 q|`!^@0KB=`4uV2=niT2moZag:Dy $Lb>om|*P9@T5h=v[1@3[M:3o,H Uݏ "dܕ+TېDWDrC '9$ZcX9/*G7N #V5o{Z86Z &oa ~R|V 5+]*8` 8"-75埑)qh0-k"#t&E`P ; D20#r^rM@$c6.t|y8l{"[.e5YoW1`XdO-ss-s6a#;܈ lUzl61"7>3H/8J|j\so^jvPK"c N#U! b`ɂ!0I0bH, ~p,/IVFG&8ϯ!ƌPg 4dIl6@8?R.AV zfe!y30( iV6?Q׬,"2 G,n.G5!*1;>v\X`QZ+k2f_@;u rrº1jf;癶;ikfޣ럝*)]r.Q^q^;%ahUF'x^N6Y: @H,V4hc3*&& Hښb\yz8*Ii{:[ى=p?6!"eNU^Bl~W~A4CTd)As"'"ru*7d|]퐍Lou?Ǧ1"ؑV9ЇhGl|s "!J>`;ٽtfD=%nn6⫰Ś@xS|~jغCD4lሌhwxuUy9G'n}8q!9ժjҞW(lC<.O5:/=e{1>q-}Uf6k+Ѳ UvVꢐ>&)>\yޢ"X9=y,>JWDk*-.I]T-`@)rZ0F!&{b<Rԇikԥ\ rn;\ҝ-qCoJP ݱLk'fixqPtx 0jqj%4=Rc<*&&V{l`c'I5S#jf[  J9L  ~ox* 7$m1~!{ PΛAb;발 EƔ7@N1?~ yP֤uCM 2EǬsYEtOXչ n$"Flp=и$ -N+ F]T|=T\ho0pêpI_b9;į*6PV^jv'mAm1{̯'hwn_Wn_w$70éu^\zUZV:' IU9ZäG2#$ P1>"pbDJ4"ьLRLԗaѪ6.Rsgx*SÈg9ʩ:7H̶@ @3M*6̛q;<7 wt=.u}==]X_q)B f_n[˜ [@`Vr0nNlv'E8?ׯS@1DR Ԃ*/x5bB\ QK['I-B CH \0`O{\76NpԼgWxlF9D1pMfXFfN&` NH9B8F3X.Q5`gbb K+[aP<ҌR;7T -c%/RjtMB)B^ [ ON'M 9@Z`'Y`2 >!LpKj.l}G>E]RHyVe.LT#ALjS𴣣 FG_Ӂ<.A##d1W+3@Ռ=bji@̃X)1P2F [3`tZeC6Hc}6 "aﰙ+XlF(̣ L{y[n!Ƭq(lG_g``}Q [L0u |. idOpŠ.jg ZvCY 8g1!${ITXc%Sjm.ȚPq-`-3>f3-%ih\r SsCu0mU lywWU "T>Lcpnɱ/+蝇 .H(:P?'4u|%Ӡ`Ϲ  [PRfOlՂ'>a~ t{y✞2Ngsƨ]6| 016>9ȘGyGz]fp[8>b<'[P Ấ0A\dj2Oh^y )D#Ä#lS Om 0ؗ< ~P~\F๚S1b O ϛyn❣ EWTdžLbUMApVK1RqaJC۩\mV=ciViPPs4̎NH&H<\EBਊK=1ARWs@e|]@쌲Y~gu`;n2D{zqsX*QۘCHL+Q:P٦_kߓh tCU2D'@ˋd`5lvD!Q ]PQ%X;KGXlfcU #܅T3@)xP:m$?e.UcD*l@,TP< 7~CrMa>ޖ'f;}`&LP GO>sЩG31?U־,0.g.byX<9jI'F8\y~KB_Kfjufg(-~3̯ nӼ®@ h^Vb (V8ih_ҌQxߤ'fah[`f}Xi2% c TR0J=é**::@RAr\Y& #D{EcT2!_ !.}\þ"{/9?GG2Io/ gmrߞ -`[arv}`1#'Gx`Un /+;aO $ S pfVI ;:Fhjp}N5byYi+-3J*[a>[ vu `#д 3V;&t v_s\P*8ZǴ3B 0#N'!dO gd TGG V8E\, ItUU@MV2 $ u梫pD ׹ aQƙVkPƨoVl\,yi x |wð[L_Rٷn;fx+AngA.pҢ8aUbSݩB6Cc~T5aWTn1Ry|9v?N^jMȖZ=p,s-sv`r-`fcf3y}S_fOMf~ce[5E 2Y^`"AҞbђZ8jH`R ~Z̐H:LHdIy-{ Hc$` vsWR~Rs<3O0@L]p~_z8jfe].T;o)zI]nO ~^[v>#T8h8+#,g>>%Aa^*L%-[MˉQ*?8rq g ^Y/we 9w$`| 9) fփz@t Z+, Wk`6gH$UdċA>qpPIJ" +& 1`l``X``(P2ELX9K190in7=*\E=V ib)3S뙎-Jb!SOpؠ@!4۠QeE;_ 鹜{=;<`C͚ 9p S\>{n5`; GL"@I1926' I2()xƧXsYs<<> 70qG_Úۈ `1`79t  ɚvN`<<@X_V2uq`` $#w ֻS&lw 8#؆*C0"^U۰1ipZn3X9=Z-CZièbi%Ɨ(X:%W (\W=9W@xT6_S fF*lNr~g:Y Vx<HUV6@c^c?Wrؘl#ݟzBQ٣G >¬&ۭ #,_2+<1;l 2eٱQ90݁,s ٖWj顼eS[˷KXɑ3fފ# =#=9H::hpPVyn#TF193 TApd p`*%mD}#*^06X:Kd(ǜ8Ik`3m,k~ʱQB_ x@=BB%#*1s  3t_2W+]1AZ̠mpr .w[AQ<"'L"L˙{%^A`v!W˻d> _+/ɦ;  6\a1K.=g5 =pR:a017zKA_ =j7tM;ĄUt!Ry| =%n鋽c8>P2ËioJ*@'ż:t92 >Ni`^yv)W->#&91L\b2 1A`pv]0 X [q lfIfv8 I8(a*3Zmf/Ҡ(`vTqa0(Gn$ 0MtiP@P4yZ6S6~8e (w&+\ʤQjB^*afx g\)%u'وᝊA=ұ`\֜2Y (̙X-3{;[ #z 풾2Xam7} D2=ODf>+qj72<"7ocp*A6,gb8Gmfydt^p{$G, 09$@i pB_ ([ -k12mGT?j5DU0<m6`;[?= -s( (5e:XMt-WѸ /q΀W;Zx GOtkF@'@*F[bY"OjכmkV52 U\xA Y0AF%ÑZj=nQ%:   5@P5Z3<ߗ<_G$&`CLR~ OZPqn ɻZ*͚~' ՗+m 3>ζGwb\$AJ;t m2=S&JT/>bo.= x`df{/<1Pe ~n >r<0bglr 1KNtGЄT vjv|`{%,gbt[K=4v%AN^c3JB5#$$PMi-}ʊa5+jlWo 2*LpH64);/xeFFOU is?qe}Ж<'g  ()u@#SV8NM(#`3S"-q(0 ueUA9FRIb f*+CaHP!h@)^D<1;;$VϚ ^:~&IKC33 qs?)mt63L@ɵ旋n}RR<~%^l~1p|=.>IM5!<%6!i^]0t x~b GLp3M)13&ʕp_xZ%cJJg7#y- Xz"s lOKk,Ïޟ0ܗh+Pa\ {ُ`Iycc~~9*0C}! }2]/ܸ@R%/..=ǻJN =Xi[9 #h:\x;UgDU `oƮ!1*yTͳ?0?pu LULsLql$rZ1í`l@xL\xY:'POAsG}C.#I;d|^=^~U9l@x/x20'ar/OMp~g/v7?~7'&::Ft+m1Bp u\ؠzKyܿXޟ6y&i0A3F o }o`E>Uz{#% t;0j=N,e;\>/s0@[VHu~%1;r/uҽއXcvU Q:{cBIև:*wVi01qFd.޶::`?{ch@#F߇&K%[y%*wFay\F$I23z}` b()2Hod9e)LzSncx/{;y6zˡUz5@P2!L!qtPdbݳj{kl7De/.afx`~yGI:Yx{po.3?Ҿ,@x?.Ǥͧ[k!SVN`h1LA C$y;4e" |;/f5mJo2tgFZ {=!C[ޠ v}ĭ7"2Mos3kav a"%( H$1BIwhyQUBS[M.u{m3혌a1`|2ҝ H/lk{Ra`}e{x30Ʒ̨nVUH\Is[b@ϱb 0LOdɵł[`v w1vw=%so$h%'"d0UV8bdϏɥ+6 FeU>9r/=n?agx\goT$ ;xFN]vb]>`w7dpILûc;idI?[7>\9 ;mf6Q׵FvSFyT6 s|m}1\ח ԢDHVRLv܈J䚒.uWWvapNmF+, eK_ VbvXV@f6ٱ~7Xӝ`R5UR"sFp&!ܑ!g:. u}麰>I6U ; "BOp{y1{߿z!sc+5*g}si_kj}͕]VŝY,SIs>Jev_|L1r|cd/s1‡]A>1S5 `POpٴD耸c`涩۬W[w2 !cƇ"MX~QOAlF8^K0w/Jmmr(S(ՐczVa>Ax ۋRS17h"h[m 8Vetr32B6h31va}HPJzM7vGjt, 6+28gv}J$zHfg*,Kf2OA:3 d cm +~SiPmfXO=d>LtS_@P=?{jQ{f:@d5Xb`- pz&R$frKfgDJe9=`}Ag=w*cx>|7mǝ]ߵΘ}9Pà4?/#zڙ8ڦr7e[g@{ H`?&zru`~R~[cFmkGd, %W+/e ax4m#ƈp:7Xv0 .q X y2tdf l :`q*/ϳ1ELci2IV\Ȥ#l:烔 9㼷/z3x 0HYnIJ) NeQO>nfc {7Z`t.<5<BAHReG-4A3NPΣWPӎ.o&@ hd1-I/[(ͽw7Jp{i}4%|.)C&=F+23pڛ<| j[l]Mk X[ pXyb p)frBY: $0bhcV&W_n%vfG^f UIqYF.3;#|cv-vJMxj٘1 66;ׄXY;g/{)gz'i;lt9C[ɰA@S1kMCY1jL~dALpz?zMp:^#|'LDbI qnj~prr˴TP>c^ W=QW^mmadR\Kَ@./s\wȀ.bӐp쵶尲~k[r4ƘY)ꛯC/#2߷5žހgE0S^a]|`D/k>d[YH7'd]v";ܰfG,Uyty<~`aZ{8Ġ: O oa `^ۄRx#|,:,Nw`~g)4:F?`lUh?`msF$/c|ft+czsM =Q-B1la-?ö)}L#[iz3Sqű7H  @q}.0^O6H =՗#n=/>{_M?33 j_%,ae6PR0Y@'sڑbc?r{v?>.]Cxfj'D\ ><0[<^/ai nIIۇ` Kd pl<IDAT4بŠAngס30lfF*~rNrMa+& +`r#GݎoL9Āإ^.t[Jtzƒ0d1f`$$KIl t}1iݭI#{.lu}YY9 :lt3yT>3O=G b=^ M$;0Jm?Pfb}ؾb9 83QqĖhQ"s%=|ڞ뭱a|;4wrb9L⮑jpd Uc!c})5ѥGG/A] TVU^k3 0V%xcoT])R֥ e( LPT` V>.ǃKe1Se.&>IҠ~ 7?^u:GO>lsjowUѣ#܎>1N0%t~oqS)Weg`9 IG+&x8Xx=NAz .?N#]waLw؛Y)Bh S0bŽC_6HOc`]rbc!bw8cޤ^f,Ց79 n/.}9J@&\}T| ]0D0&KGI"X"1?8HZ+wo7kdK p]foڇ]fX/}"C#uJ2 i8NW2)J1byCMzJwoYJ3ʨV܊–i%̝QPGj+~!5DY}f?2@y=CQϦJoz!~r^C .׻3 p7 y&Ez r-M1`asZcnMӋ1mҗK.='@pݦqTǺO@^ ?5Hcz}CT4w] `)mIc;> 1/_0/L3F)3<@/9\nXv;e`p>1U 3 }:@/bd/8}j0HM|lrJya'7avW@)> ;/|/DŮM6˭.@ITݬ%7V le9vK!P/* %)cld`}R4@=X' ]6S{z]Ҹ{G %F#asW8e@B 3`b ˊ̭ɼ (ƓWZ~w0)#mLѽ7K&f?۞/uϛ$ >N(sק'P|b)?2G%\(CoWӪ*$ʘ_y]6?8`r Fp^ޯ zAz05E(= 4$>` 0fĈ^BkوMk EcutlEbz @^0*'H&HztR%٭eEj8PcQGG[-V =o^ìߑ]&%,e+f  _qa}mE>#kwCRn<$򾯗8IG?g2v7pU,kAOƳA0@v .n-oK\aOoz_ן͟9 񧟿g/A(vWoch"AȝK%fN/\܌,alV"kY QSmeNv][npj@@uM@U ܕaK 2K0k \"JƝ_v{=7}z}M;MGY~J/M匱-vz6L.( p`=PsV ڽ#17 l3/\!wky)|YW qpƟW';?NVٞW\_o]e-ڐ7kz8XYǷ +VB,:{ br B9zOG\Ը0EN)˷r͊0"lp`fȾcLw*nɚY]-J~XX;?08V5ouhGyZ,R˘A e+cX߿hOg8er0o OxYݦ#4&rxY3̠'Hz ruCMC4ǚaٞAR̓}^B.i`{ mb":ўekwn%aO䃐%s( y:}?~2-L'NO6_c/@o/&1.UtS74@W׵{#+CƖd#^"JၱƝ} `nv bm6Ɂ@d~S/qa*/ ?Ú"$=cНAy] @z[wEB5b'4jIUv:]t3" !ak,pFz^U$9Pv;cu_}B/XRAKcV7/r)2n>,z)=߀a"۰I <yH_"BIZAխb%~aDx08%ոTR !Fy0Mf#vk,wqboE\6S4͢tEW'PlRS * dU(ݔRZ)XQ)GCg0$nm=HєX}8\m֪B.MƠ_tO;B@" ހ r XZ7U:ۓ*Vw:5>>Xaō<&ؑ=!iz&S׃$5Q^q޷by}y3vŝʂ%OY$=R–g;țE06Nu/5-(Y}3cf/ΟM%=_eWmnC\=<mL׃%(QZ1Cؠ2>}F?!oEfT0TYnIdK/'X J#߽P"|/N X j#08̒4L`K]UdK9ǔڎb{ 51̩݇PEVF~>usv^9;ypf%0 {Y.2`i}փ9d/ښ*2BT z9'&ܦ'E&!9YyD{bPUSf=O_n| WV8Ƿ)pbGt@Z =շƒץS Ӟ/hxc3h5FJ냽[+c@W&?3,5X9ChEO[!\i2FrHInٞȍo1\ȴ&+(i+޲5Oi(+R<C@ǦÈT,/()V"Do I=%cHN B[Cc@9^+mek>`g:#X#;,v.|J_,<ɀ@y}9I;!4S*sy +n=kh)1[&0 @8@Pj6f8+#LvWJyom4Dh]*'^"A-,Dk/LL!J-8V<a$:޲J7H>mP_xa#qKIOU6:s#۞ᇟ/E^[t}Hb:\o),b,CbtVTbMsmۦ5X/bd`006: 1BڲeZ.UJ' SkHlg!Kd5dso|X԰D7扥D̘K}^n QI)`X$R&@jR_MC6cPH+#&Q>ci$S/ǥj$R咿1dW С/9JwA0[_A}RCDlҜD,0Wܓiim֔>;,`.HF[9.0OtyOh(v_U/ab13c_Yz텲Rރ V oWkx':Ëb+[K&_+Fߧśq^ vW`Y8 gMMx$-nN :e{+bmhhۣloW{~HTaJF -VVŽZ )m w*07"`|^q; {G/dboڙ'^X=C.`(͟Ӎ4|& dy|3(Di %(#-#W8 ep E?G++cMVkb_f`9dy C$wan ԦEG)4}q/C?C6@2Fx12~ܤR σVY G@wE3 `{in>N#krOZ]d;SLs3b '9ΐc, $ϘbMb=}ng'\}g%~3.C~W-L? ?μ| \Dq`lbLޣY|d-3PGaZ{a mrbSXݚ\[^'Yt|zk{%΋#4200dnr<T9r?\9H`AkA_S[>FN}anxM9:@`*(+G'ufFo6aQHK՛rl#氹Tf{,/`ypVrd:,jCpo+yl P\)3dI*~ c1aFvFHI|G|(y}qǚQރ8T+<߮\x} ćLKw $1ïb?}t~^*;~{F!O: _f4/zOC=@?1MHٳM|qD$Vf(6vtSkmX|-Miv{) tX;7HpLLJ]W?RX]IR>9媍&J+cX܃4Q!H%Kʒ]ΧZ  1̽Jv8?9qpw*%:3;òooY& rxrىDavty*VKNR}sw yҶT/Mpoq<Fb^>n4hrd p+_BnԞGf`ʠ,@v3eҋAbBOEsQ r:Y [ax;n78r AX7Lu{Wzuˏx=}8;ݡ?4~R#oFϷR&óOSb~}^F\ e2`l0Z.2|.w:cW\6hx~ PV|{{-2>NVż(XR3`S e%룗64ZRAj!BgUh"DFv*ǬȵWH p/"zb$b1 N Ir}ot:v0;_TDП;ovt3w^N~vsmZ+?l(yK"GLc6 ~5x2c9(L)K6]e(jj~|rWcW^'Y u! P{+r هWoa~gL<#|[?2 xv| 2Op7=ćd]61qCo)xc9+|, SX$+ tv,``¡M]Aj5*B pS['zhw1M=^5ŕ-w;߱#3e8,s:_G׾I="r#א#: 2*״הij֚l&/Pat[nkwkodsy&\c{n6ƾ5p<@_KUcybdЀ6w`;NF#v `:lTj 7Gg`wL?ud9&:d9+aF+N{AT={lIe=r$ a S'6״(x-&hf/0g u=C~~d^gz_0/Q0ן>R{I~cRj2ˬOO՗L{:>?o? ]o)fep7% ҼpPFa- % Wn, VQ,nu+h< d7p{yN[cg_c!C&фS x=63q\QJXC]h^PD2k@> N+S uM6S ܉u~G²GD*} Pf'됬 _؃o$pTHUh_غt[,ZI~ɍ$2*+OM!T47Z6sH'`%Bz&g`ꦯ=8a=)ZS e)aO0OdIsǜܩZS_ 6iR-`P_/aZr8y|sn;hpڍuA5= 1B >2< 9?LѵɿH*W_2V>e/W! tvTk7#p_ߎgp\mȮ‹Ϋϙ],<$1 [V |#WerxK@`6nEOIJϮ1p*Z~]B¾pڀ`F</ Eo7zM %,3=v<p\ޮ6a'f'DS!mܙ:p-fқ],u^q@or(ɷ*?1%(**a|N& ?t#;XV`[r)ÃTX nv}w;_ƥ:9+/{n5^!gb{)5 H?r̒!f9;O&wp:IL_ ;'Z(])2 yW"E}BQ rjrAcGi4`!ekVACnta уL).&ȣd^#nށG{&. ~N_F ׇs1}?}-ޟnUͷpԇa%s#Л}mwBMlq5M`}3cܝa+17"f0𪍐:V_}}峃Yj`xT* 6 KCdRX[(v\WL KTiE$@ÑȬ9*)wrbI7v*`Nx5W%d5Ektlk>b5M{ l_O9^oGc=Vm'oc" f߸eB_ pomZ=gF _0{uAr4e^ .lo p&<36_N?[ix?݅BwPbl#Bf/7|=*0E@ KYore5ɧ8=6ܡ92PMF`oO`7 8ޝ,1&bqÔqBaI/X8šn gX5wFNm!jz3$P\$@@I3ib1C &_!+QWN 0ZV> қuF ;G4 jy8S'=1\e=v-h2~_cy-zsJ]J2CJ}M=%T+r4>beb-knHtO)>n̯Pr~y68<^/`m7lo"Ja*m>ZVmÄ[z`@|a~]wiNI  |F$?0u2y=3\,~#,9y,Gg_c ?7p"BCꫥG/ y(bp`>(lӴgf649-_o7ءlϰ3 ܶ^/hT*GB=`:1nk_Hr^./ձ4Qy\Fuɷ~Vw+q/fMzm{OЀ]`2 C23g[&ة7gpi0G;ޚ!BcCjWL.`F^lt<]>0\HRTF Y6hu eVG2P4%vwl"p *iaer$U,2l92`*BBZ[M٤B\o9&~S-͢ӇY t3l)|tgl o6u׫O؇( w);^4ވ|@pʀpk/NoN5Sοvp `ؽ1Aǫza_ryx^ zwx{^3].03eRܗ/0&aG6\!.4'*Iao0:+7Fhp 6 !=lx8)@uHgdis5$AW\Hez+ LPRF %5kuaz~?1<~_c$/p_?9Cē3>zerB.Ba Sj4 RL}@e-#gk. E@l,BLcY{xU~ao׀xJ)l#.Jh Wfˎot\i0%y&i@uЅQNlk |zzdǧ 9 l-dV"4^r(&> lgMgDŽuZ2QNwCGȤWvXalbB939C <Mj*Fk Av2'a-Ÿ.MmR| s_C## fGw{6J(߾|}'Ց2wa+wVV]+lU|] R ̦mol^aOgXi|}?J>}iJ4ҏ2-$-0(57rtk p47A }R§Wx.?3ç=謞nzZ?1S?L/.?2ƋrƓ 04ZMǣld#wwrv1%m {4Wf(-)I9˫VekiKH*ZV&-`ܚ*2\=*0K_>Y.$ v?@jV1up(&XDk| d/IéIBp>"p!@e; )&Vo4h1GbAk%``ĄXJBp4t @*dJ{ȾIݏ:@SOp ^z9ߙ;̇^ᛞg [# q~e% icj0M*+azȒ; BRRr&`>ӛĦrfx>1mN`tJm=2D;X}}$szP(XXQnwZzQfԘelvl[ /RmSJ`r @rXkFKܝi0S^[ju*0H%n7ܸɁ,.7؏&㑀pնG!NSC$66מQ wÎ>&`H{{Tpi0`/ӃFC =?PB`*~+CJ0P1 iKkA׉U־0@t+ҁvZJROĉNѺNGF=jgnȽ:Q\u8pr;}UX"51S@"E=RFecC2b`[ ̷!zRY#RV9 +c5Sⶎ `IZ*hb+3 )ye=z%k SYzޛa<JazqRc:#V({u*:@@Jn}m L-].WFLDލv5Iq-] G*E6c02r)/5XX9j{ t2Ur^+{O 76{ 5qp2 ؁= Xetvȃȋjd5l* [۷:]v^RʲI2־Krv=Ǿ`%WYVXXbs @X A+I@5񝵇I-/7 |b{9lЉ2mB`aPG=7?KCHOpv.w'F0 &Sf5_2ǞR>2n~~߂ߣ~QP> +})u(i@mqS b B"y<8&Wk # 5 ik)"wKrN2Lꢓp|=Y愔]5xm5VXL߮yήYk+qybf+3~J+r,{sjmC28RFЦ@Tl$4v=ǥObH@[+xch6wCCo !U6W>rzR;Q ۛxq\4%= Y2Mrwa8Z ]WC0D°"2 EHs2FF%@ku]V:nVj{7T^#&Jebzzuv,d\y1VtKIg`R6Fv{8a~C "<ߤrfhzh6bmc|Í3;|8sPi {zVn|;/|}}$ҎZ+^l6O 'NLn20K91`J.;:_~.$10bm`6PσO?VP"9\.A[o#\ÿF~oG2orwګEa}Xh%gGD09)˔cؽdRxu{v9M$A}Uh{Lvb^\ ![{sdEf0`czZ\חL.7.zΕ?PJ^KGvrZ߫]JZ!Kf@6?HwnArTzXo `q}ǖi/C$-}A]zdeH(T+GLT/d ry: &Ñ`q4(^G+e d6zLmbF@G /]CJ® iPopep}?11%ks=n$$xtAp2[ p\ L,JcǖNuLP&g`F룇ׄ 2er[#m@7 AC{^bha $X^Qcft1a}Z`ΐLV"7OL #6?*+ Oz[ z~~{;un!|d_,Qw7+և&e#T +H\}""u J}6]+|tL,[I:dR0 '\(3!CNl}¸)l,ʊl4hc 4;ӭ\A簔wИ]dê M7p}(}Cs\q8JKZ# |F[cg]l'*6GMQU}3%PPJ?.0=v CIГAyƔ!q<viVY0T#gS߯@Q+@*jM;{38 xEh}4Ĥ\Ja4LF^6+t 4:1@Cc2eq[)( Ѐd[9 e߄Wep af S|n{Loz8TuUY߷ߕԴ 0XV$pN4ux1Dt 1>`I@0. z MMy hU<PfRYH~wKdeLRF5?\pbPƥ/ynhװVU,q#qw 3U|[]CaX7ekd28Rکoב}(0LIR7.e<$k4AXt*swjAG gL&U \$!fp[iW`Pdald[RCT6 x%bs9{$LŠv?DS .( i0$vm%CDxE&*c9p ˃(9#RN b>c0aH FV]h5 `Ӵ3@9q[Q ^ZuۿMfؾg\yz顧3؉jC _oF Lp+fZ2 22dˠGy~[d rrq#NwJH;s)@W$wƷ3x>p{*q$N$G&IDmq~4b+Q? 3+R~zApV^?֚޲o\ # NY$4k4}uj ?W_/!}1VdKEFrߺl%&iRXlE:`oL#xPﯧ, I4/a:{s '~";Ln4F>Y<I @=c8U>WՇ3i/0@@ֻ >H(+Ƿ &|;׾]7<`R," x+|ōE7j}f vK?LYU_/WKpl?;2 0̱u?cجha$4͔!F#}^d$f#$~0Ǿ0b1I0H&MƳ9R{=BArQBAg: &d@šJCEb8TZrt+ 6Xr)}xR瘸v{-%btV15` Jx~ .n{ `4C^$u`c4;̠ ;  N-#ip5pR^U4b?盃Ly4^@ph>T<1>20U^?~<_kX=k߂ӹpݶ ,hX6L;^YHddP;Kj[gzv7%vaj@ܘj`AD}X1˕1B"iJTR-0[ I(].qv0d >v2T/Qb/a%N߆i/#@i3Xܧ1_>䋸8fT-W*P|vQtW.[w]{m=Rri0=b{z+]GgnHZN}@}dGH@$vھ5{vM#,g.\&€#QrY8R*c#Im*FVfCtWJVvnӕEP!Qi %~1;RL«vJ ҷ\Moh;#ya 7-&݆r:T,qmZNc80K㡽UV'Hk Yߧd?WL 糀]³< ޟ;<7@H۶K*Јv>F;U-y(HTiNRjNq5Ey%p#A7UfX>pqy>xyyg08B[w#QG r7݇h< :;\NfJ&FڽXkIOvV\_ ,0Z>}h-p_CGz (D9p 72(`"u:<ĠMZ w^+ŀ+ MRE{ Y~w^9 IK47YYcNiRz3'4gw>ո1M2.cߨ};XIla 4Ƀil9ALUPۡszbpgzb ΰ6O l =kLBoU?7A¡pYn}eOj6 A€Czf4I` .hanu‡8j8}a(%(`2zeln)y9Xa9_$Q6P|&;7t & عRڛbX)L~?.}f`wzc A~920(wnAw!dhiig@zsrY :se]ۋ{MH>q]>\ xtn8"̓DOLu(sɮ<㝀v1OZ@δs0tr ,aFnX~dwnoxIne%u`Zߘƅ= 0I ceqlӄA;]0Sh4ДΣJZ$0g^oo0Slo i.%>0DZa=[z(iL59%aE\5r%Yh t3ާ~>ɛOߧL/\Yd,O 0?oq}-hAǦGdu ,/}[?0/, &ljhv $wH9L땙 Q v5Pzz^B|M)Czo t=@qv .@,X`P`pAAd'O8NI33gff^6`ZA2oن3ܭ麿"5tL/󓕖 ~rtIX`4L NH*qA f/K֪L?@ieyȹŇGt++JB`8Gy/;_}LXtO6B*c TسJO`0qҪ .2C`VlPx0 ^y'M#+E]$='6Yx6EQ8)QbW[I7bn`+*貜*q7Yt9r^=w-#9# ډ^qL\`ѼZt\2.d-Bh)s*f OAn徜4 >;= ,~Lo== DZֳQ ;|}Jڑ2E-bWzgf8[eHzL-vw]S}Q~Ij`5 &gme~+}@QI6V ;pWBBav7|ݎ tR8{JE|mo|߫u/\]_R_ᷞ?=077e&)q MVoҕj\fdgDCCyv{*xPa: fY=* ߾{`!!ڽN6iDD엝Jalۇjc1Uec&`ɚ2aER;L;ϜѕS+crs54Ok*6d3`v5Mx%{wm1[4pھQ옫hT339v>-ݔTItb *iO۫t'pf9S5XkTbAj%6;ߘy \jP!߿}04?KjR R|^(ıD:ʰ HF|ʗe}!FƔ 43O c!V;0P*V>9.<8!QbWT,* %W8ZIXI.j MՋsVy/`0W8JG !5tryt;5l BRQ_OSe`g7zlgM@OVYj' I2qVV>ϽLn JϺVB܆$k7J*ZYpk_FOe({1%ᬒ,\;Ǥ5QΎګuY$6sֽ#ŠiJxdRL *o za`;\ &^0] t2syIm-H'z xRw @&KzFR*" tǭ] wOy룗跱Iqxs#Y2w:.G80 <4e;N l6zh}fse>$;p/f7}~~O+αF>\U-WAz\g#~->&Hcƙ&H|gվTsA7($D%>~5u`Nӛؘߋ|7@]"2]G0%CD @~Qq|"y d*Lʧx9^e~"3ŗAwpZz~b~ hs 0.cmo7,XGx~LPNa!Ŕ=# y*?z2؅ί HenYPe4=7e ^=7U(9@d~JL@%!q1 bK)@)<Gp}7cNܧ9(jve FX1k e3vhO G|,=!Ν+t<~ f E]<dn|Dp=)_$D8t7{ P. y0;2sK6 E/" &x}>;2g_2uESo2|`LF1Mr&Xݳߪ b?5$i| ,&yMV2ر OfAD5R밬b@f\jo*~{ޯ|(H[վFOOkw,Y V\Dp:{wfwl"h]:Az~ A][b}d^3b鶌r1 ̦¬鋀nw841\(WaΠm0t?#W'fenaPrzwޓq].2![*콬]h֥.*cu76=ARՍRϞ-3fo2x2y) !ݷCj6 #9]V|t4F0kZŕohi fI2+/R#uL|e`+kQfi1airR#hH{ @*)1 60c.`~a ǞyoKTi֟;"C!$1oHcvzPzP;#b|5P==Zz~7{mm$\ M_r^Y: d |i 4)!td5_**2 ;woLoA ,mwb!cse,8 M)-K_?`"l2XD6OIDL Ԥ;fLV]wZ蓕vc>Z LP1?$جhI@oM5 Xy_6uHg7bb9wr|م~^Hߢbϱ\_L[^e2mWZ= '^(a&Y)vz'7wU ;*o[zUT)Bħ%7$"ɀAl8[c탖]g(~joUi'\%iԟ<."YbN@X!e/;.L.󶭤4~]V^ ^E\o.yaw~VViq)?oWp9l}#K0kx?2 96[T~az]Uz%/Zr<@rf2/-6?|aM |bHؙdYnXۜeBq,*d-S.AgbQ0G&Ye2H .グ)ҫ^Qʩ揵l5X{E#qQHwnTQPƒLiޢ v`+[V(36# 0d7u~A$Zkyؖ kvi(T2ԸeVe/e-*9@5& JgmC^ar'dLvAO}ɥBB;MDg5yO:%rG3K Be̘A0is?0P*򘊯UdYF\z$wb=Q֟$H [=Jއ%Xg_a!Ğ1r&bϋ6Kx?0JfMrnۅA-r 2MZ$PQaJ>~r|Հxolc?1dϷk @0UgPdh@$ ,i!\Gz|X=(c\>Ч0&zh5/y›U: 3z~~d.{xNnik]l%w-JXOQ:0BH%l xUmz^R"EwzuP;m0gΪȭQ"HE8Fy+i.Y2(pu8F'Cp*F+&S9(}c@)&؅3k,|2%_G6" LԸ۱"@b28s&Џrr`JV/{%LQ*\ ӥST#v@SpBqvraKD\2$:Vŗ#vnp?hRKGN/%\u8]ߖ!Iѓy%U"mǴ\H(Xɻ(mV7$nɽoHPa@J^*C; PYK&LfuTΑO&tok0i#F"2P|hˣɡrX*ot[.s7x*ݬ,Vvߵb߉d/a\dMGj|j)Il 'Sad2;V7Q8U+lŖ̓N42f\ ^5z?zlϘf,j x<`B,, [z}k=[Wy/[Ȕ^ުIrC@Yʷ7gXX) -!vq}kʅ9d! avElbÞLL{HfT!pD7hd@\#G3xbϽX%#*;ԷG^3H*^7={W|emZVWq"=haWڲ@5ޛokOS)!sHMx( |r'fpmLQ: >4yq3nπL֖;Ǥ u\,6b(D%|A{>Z7lGcw6YuqCmZcëlpŕߔU7bmK߇F.G %Y m^Jn ؇n}iM{] fdDoqݯ}UZtyH mڰ~SNj~г1t&Zt: oQ2{L)B ¿rljxo`5J_﻾I ί5kٔ@f8y^Q Ę&txs3dL2Lg05mϝuIZ9jq3Ec$4xD61 B"RRV;9Xt{h:F'+$:yJ6M6W@fXz9 N^r~lRO+&usS;­p ݟݠ=G={xHXID<[;ˡV q2Nݷ1s-+|3C@0u6Xd2^o7?qZL@\W5ҳO?C`Rr`cr({YZZs(J?@YHQx^&ϗcVV {c!cɖhuw$T5qwlR7.H*}O `WPQ|5g mgv#·v\ϻY=GZ.6(Z43=mmpHO6_٘ݥLA/VѴ1Eb %larxU||HBY 룿ws{lfAo~t:]kY"]ʾ?\C њ&z"fW M9F}_H`oO_6K 340'n!yQFޢK7IJhGj `ݼ&{<.o4O0zjr}n zߋ`E_=D}OV9?ԫ$pdYxYÑZ'_jyH6 o̧9OR&c<=^A mO٧E`n %%v_g2daQptLlPfLQ1AeM^1jzWFOgh 5"4P٥>)PW9v.yZx>@"?1op]_pCh1 PNwn}3b\XdM}7fkC9_,wT (WӪ\GLpl_2Nr&7ɶzk8K49.LPp!eghDk/eGV=n @"W߃[T X…?I*60ކ)ҟvVNO;Jy{/|u1[(D9H h-SAΒǴ&,+\~g= MJMHk1nj%Včw{SklpVh0Y$ ^vi_Y.,2kp:_i cerĻMlz( 0v+M6$1`J*M !^^wJMFiDаpHsr  k<|=xr^()I]Cm`j[s]yoXm*e{U[%*c8H.@#^v50t?1Bgw8okF`zsF:ّ`YwlH< >mXZ *]K/+%XA;.m/IeX65 {CAl.-| V|0ָ 0=E5PR3Ci**%s̓dZbh&1%7&doz/1qF㽭Tީ+70SYk ?NB; Æ oCĠɛŒu0z@OWFd<aM XGyU`W6`w[1R_O ]gU9XƿߤZ&,Y=~gXi+dD  ?TtrD. vBV]I}P/7AV@0ڨ*TuZG# Gw1縠(tRaBllA.6i| ;Cw<(yLp/LHUr!Z/rz{p ~,,0]ztmUFe' bP;qkG~ v?;~Gg@;_r\(\oI[Y4Er?T9udjcBܯ*! 'iyUc3Jg[:6c%Q#Xd~} IeOFzՉ^>$p,e\_Ϝ5yB}>޲,uz5=`d E)\?p)M05rOgLր`PqAx./cMyY[!gۭJ嘈B~w#_#'4 | .yj72FfOܧ^=̎ߌ_"-YbU_t{u 1Op=ib c8Z),Tev!<&- PaFo0f)U@UN )p.#$] 8*Gtoi1U [YlôO8%VÛ0k){a JX++_ -U2L=/=Hw~V zކH(ʚMk^qߊyn}]AEBnc_lx-h~?_EiP;1 Itd1do{i)|Se]bH[é B9y}ъ& z{bΜ4u}={e5¬t@€I@3sƘ>ͦ7,wʘb'vAW e% (v_C ;d'6bITϭT `9$ [ [#5qQu)8~Rv G rһkФs&q^0RJJ]-re&J2]Ŝ6_ֱ[nZǓb%o#+C Iil 3c3 ŀa}PNq ?08\d26jk/՛   9r" (h2z8`_"J߆Ł8R <};3mRL^;*=-v~N^wSn$lfIHj$ף]uF@:uzwNJWty4Sr  `x\eYĕp,b|e|O'n2#tKa]VLQLR^O{+wf`g1C&oV֩X'Q2:(¢< I| v`*l> Y:K,[bx^#7Uk=֔+ȽEsU RYjO[0&j_D?,&M#^r^ 51>(M@*h<\g=3'"ohǬNodC=8:[ ~$Eatn<___Kf;ɢ#+M[@bi]4y0MzE˽=GY\YC2}}ejR $Ǵ!_; MgL< ~b/ǧtWk:}Wj6yܝ ލ]+ onй}O+.hyyGN0BeR%p1G(%ėvMzaNmFaE^,Atx#!*/&dburL^\㼜-0T:{!j$ yIbRDD {䢐zFU 77-TV[.kUϯԄZ/@!r=|,1}:ʜѺ4tSp#u2 o{ | 0~G! Jȍd%lݍ "+ MJeUVk@0+`-]ŜB˷Rٗ"ļ ,d101Hk cz\tF1466&=rqaE'@:bvz}G%7F}? g)vmFdU/w&G)]ek;77!jCm?Ĺjxs0ұUo`?1ߙL;ZjdQDbsE$.FGfWG솧.9eڸ-uH h.;ljRJ'z j+;L:lTչ2n%p%s+} KQ0ߎV1=}'!xx̾"&u{i!gfE<(J1 f{e?#˝!rq2,..%YńAG? S`)W/jՊA39=-"&?"m~5 _w[1#{2J;5"Yձw-Bz}MQњN)8U@ttvc+Sk`4/nEh} ڟaW}n vG6==y LpOcz|u'&7V{AzMILiW2Q^~w6]|˧I`іl F)!he}µ>YZxǼ\mlN`x ?1럙E/oB;'FմMaPRJ1!dz%05Lm:^;h Orx:t^汄޻4f:Y*1UfLϹ>s_w?0v1ib߹IzmrA,W[&}ɣ1 ]|aɉfT4,ĴipAu}J`m pb{:v_`NrLrFu2αWX@^L}) Vo1H"h4~&//y!t%gg }@1$Noy~y8h؟_Zi)M~篠<ۿ/ O)!'pqqh-fgc(M ( dGnj6T$27@fe/ʭpcVDze[a8$,J /9iOlLc8ĀA$$Y_4w.|zsZut$:~?_1!aIY7`̣${]@Ӕ01\w !٠|&uHR!zlsI6h{XERk)ò૆#lp!vt 'A`@2 Zc#岏oS:{ ,-?ݣ.)/<(, ?c^^\2 r)2qq(F 01S֚(| O 7/ґ3 @ȻOeFD{< e_~ /'pxNbeu)}Ŭt0Ri9a74z'NIP44V)3/k* ^UZ6bԸT~ºT!|:׵p<~ П5Ap,!<*7$6&] ˦GμGmy.m89ϩ㹼wXly-aф@iz4~r}Ʉ:>Lĭzc;_"}O+~Yn']OcHKJ~fpl|6?hSW|&! >h'wDuusA}Rʔa%+mܖc΍%Vx .6 igVzvej7d|DQVGM4%?%At*}mUJPd`S;\}*$wph<5zQ+y|}Yw!em[/@OLC99bhz cf_Yt2۽aiPgrQb&O?cg ~7esɊX('+Rŀ1pWhD9 %"iR a<5LjW+0,Y//vBЋfڻpJ4q~jK%muezJKzƧd*n/F2Pn}cJmtwӴWٌ֎ ԰hgM6Le}HbOFg6:254lv@fKyjׅ@Й{&wʽrI*Z'[o xu@0\PгBZ1P\n5j>keʼ`0 򲴊u6cj)|N>Cy e$}ݷD ML+49EQێGD_թv) oAn8#30[R}[6X39,1pu~;ks~\-,5bkbꨟ+mr }a[+|RE sϓ%#.:0>Ru5>7'.B`d&ۣ``QlNlaNcrL DslϾƇ$-Uw0td|_1~rx| 9$0\vgǰɝam|%m*mJ}g`}DLz' !iJǐpN[dQ]o"X(auL03_彲* 0ԄiEʯMd x/ :(Hʓm1cb9)0@pGS?0WY  e*d tzf2ܘ_Xrqc4vsj-\t#Ymni 6BH-V\(x=\Lr8Ƣ벐z? X %>fǛ__7>F`$s}'n;+/L”^3YjZB kTraI$r=ZoSYKc_qn]x  G.~}N;DW2 x"c5 ]" sU{b"O/W>Muֶ ǝ=6ˆ?6;`h A*$UC%f(\u)SW;݋'x ~[9|! +]E)m~ `IJkfS Xq >WyOo|e _-%F2/U r0ĦlȺ: vɥ4g h c6Dx}7@S*ǤsyȌ8uocB\q_xƄ(Y߶ |I!U^`]%g!d.ԛK%*.lb@EQ,4,1Rno;&s\|,\dez FG+rJ$J .o&iQvJ =3UfŪgMg,J).{o7So,`M?:Ϗ}ZߣE AxF*;vV"Be2>(b6A iot.5rr+Hm*$ʭb)퐤r!qr=HB8GIs!{A8!1l La3M0^KL5_L5C(%<%aS۶[}:L ܘ¾IöIC.r+v]W{ܫnO2_ 12z1vxFS4y1R~XRm ;9becdw C@Ic%Fmj5af7 ^4Qν' ‚@)zyí{+:!u#$/Zb3\pqM̖kn<:Bڃ=/-hC"yaj 25>}.b0<-!&lҖfrv u=X ++\\=\d_b|o9C[NO=n $ư#H(wVQ@܅q/iח c:lS@oȒ^Pjz@ć(s+zPJ]Tb{.%xhC&Ж+D)Kq754y9ȫM$]@< Jm`Rf p㾪3|0|U3p}G[z6LIBcc[EocFE<b%v ]=E)qYk P)%j'񻱚w+opnjyML =FC@蕞Y;ޔ3ZLLL/Xֳ{&D4P;s)3ՐAq8Z;3֋t,{ٛgrRlM 48 )|:b9-n\0 ǓW;Ž>zyGOɥLO)j`[KEt<;~83{6 %inI6SktV0&&[P ߯#_IU\rYնZ>6?J1G<6#mL6e2K֍J5CdA ;L_{xg e%@|+=}!aY=6^EI\IUNe9I$K4puyr~Q[z*[4(Fϱ-'FNN(I-Y\9 +`LX:XXNχvfǦߌ80]VJۛIֿnM)Wo()zv܏vb-҆0M C\?#+ul2KQX@QڳBCN܏ۻ^;;sc贈5вϟQnraqa;d@ laj wRmhZR$@/xNіɯrUasjni|=WPS%Svo8>lfQx5,)4~{ / oJP,wa$)2JVFnp@mZ NhE{>vfX~sTiPko# c o.h^gǖ^O^t׃e3e&2XG2y0G<|b[,ȠfO#!过n_+^_04Iߔ\m i?W[+zqdn@IXgJ@*Ӊeq,R>@@ۉkwƊv22#Qaob'p|py= OwBNir8~?NzIf p*Z fRahZ=aYy]xLc&˄wx̲)Q,W^vU3D +:Rb=[\u ˫-= T^+޲C^yz egb݅E\ ZKӴcJtǮm6ǥs~V}/LK5`ٷr9#3ylx.XAu'(CXA/Ĵ$/hNDJ|pQ9(rbS&* ew")H@ yR&KzXy0AYi# R`` -/p`n7Fo*Qt~$iǸ Kwuh:1qQ&a~'jaDإ3sxa򀫎T wMfn <ۛ8,`]^+m Iu\ C P~x ,_*י|^RQ8} ^珝4pz_N091]y!WcXv9858lZzq򽱎-V+ij1]8ݡ9,*g|<+$A0 =k"= z~h;W ``='b4&O:'+w1E4Quf㒕 Ԓyv{%gˋ+ko/#z\0wsƧ*1&bj6Vm@T6Y] \*e4>F8" fZ pM;n0unpU?҈bekxc/6qAkݤ/!f{lהږE[kem=<4}tgA6\[sg-6=.t[؉yi`fMaKZo;XbuA~]]Sxψ?I>]~#@U/ǟ9 ~iMhڰU&m1a/x"hrry%]ou{N>LG@u2zz:ne21F`-/Yv\^ lnOҋ=F^["qD냑d s/pNsNOor/O뺎RH_6]7Wl3@gR D@GL7p_= 8pÏs/Gu#I9Kzku 4 mlt,rUFn:TTf2z xzg0-J徣|* lK7lݽFUE,mUӏ7{IW=59T%Hޞ7auD*3y=^I"DdR^bwSvP^ >79<#e{Q)%_˗q{^@ ~\/&_@z4p}|^^4LyIɶ h]NRh찞"C^ jj{Ht^\MZ[Y콽:R< ל3++mz,r+ #ξp1u2n`:յ;K4qy'5_cѠv1;םv~oL Q'-`Dzv?rLh^Qi{rM-ͶV=i7oiS.v<*|J&O24'B`:2Fh+THO4_(s$.-c.HERXyop{f[^p_8:`e7VѶ$#hR*Y3|lϛ%qbeo.μHύ [|noe9EOPҫ74Bލav*&7p8X\݌}ZC/Verb?N~;S4ܮ닁링/o,1'ufeY |{}l.)) t5EmF^EOkjoCE-)H;yf_nw ]zOOҙ/4%[\6wN=+ M!iGɥylL1-i Su 1$ MLݖޙą:-;Depo\e=OvFeI4ljbzU5Á/_4y5׆sfY%jV`XM?uybɉ~}.s1h \ܧG(ȋ}:7.gwE0_j?@y9ȑd/cw Fw;eR)qwJdcshЁ͘3{|c{6PXnXn`| PDۉh pVΎmߌ+DyM]g5޺p`vUCZrE`};&Pk!Eu( EBCW>ȱ題ވV%wİcQhg{0zwc'~yd~`mUV!I`n $tlLFL6~I_Ҵ7>vf\+][#召twJ.m:o0i4UL$+`JbiBmS0# Tgq|_㬳5"&&"XCT1Q'RyRE)#q㹒h9=q9XƔ[5hϞ +UjV&z_nkىn;+ߗ(Q,x500d9 W;{e}Q4~GS/mc 3N3Pz5}=|+ә￐̊ģ~''$bQ_o'+ 7Dz? T0O:/3Id %?W;/۲Ӗkm "dc6|tHU˥t20)R?ae:ỹ64a/Cl">ZzR]تdjv c UK/pY3| +3+X554ƴ1,݃52󧲸VXH""g|XHvv[C}41ޔq^Q[UR:m+ 0\h1Hp΃ j3d8KT*W뢓=> 0po<ڢzN1FңΦIcWXSbJY9"ڇj؇'HG^It M`n}@岳;,LE~!}WcxG0\u> Ԯ՘l}׳lC񥿬U7c#҂`_h̕վ=H˸o0J{NE 6Ƈ'm삁W)b!3Dg=vc@<}p(-Y)5|:|gHh>5}ܯE;Q2N\ƴ͓CG`v*p$ "NTgRMQdÔ aPLV'm42DdM&{Kh.8;v d $qmgmZu?ob 7A=CJZV?0@Hi@Ix4py`<@FQx$!0*JqԂI +2|f77vx|Rsg}0 ;v6P|3\4_JÙE c^^ 5H~dVJjo/8>Qk0C}5׋Idn htXd*yh5>?GO>;u6H/G[ijy/T#\Y&VZL ({l[;vTinxMǞa>@Ӯ?!'$`} 0Lez{ywinҐ?\њ](+t߹LMWNLPKƖ螄! O X{v=>sPUe2a^%pb*\GoܔLg;.8fƒ'z7?9 0 vy'qA*,lJME @$oƨIk9!i -"֑ Z=?Ɯ^S`_qaPDٺ7 }Riͯ_N=EoVq^+21G;K"fja:?7K0%ascGcgb`O z^?8r2Ġb d'F-'ƾn U0FKuY{ mF{JVLs̚&~XlJ+mnXo$e>Ō?Jaoÿ{r p,ZN1 ~ld_t?0SM ~̮uWO-σ*E//%VVW/pnK/Ó&x̜g'’HNsW0g#ZNՀ!,`p8.rsN"~<3|6~xo\J)/x-~ꉢU|v-}?w7P `1~ W OdgcL+1)JXOὸ^E=8]bb0.Ʀ><={NV4yQbq\ez>Ibe#Í_o˂bd V~ D ܖ~"Y%iʬܵb{9wav$ev( ۇz0Y@iL魃}b:TNp >4F`5-ÁM">}[JZҟNy?Ҏt { MC".Gub2,Ai6N§ulD؅'SM.S99 f 6.1yLswT~It~[vnzgb!y7 n_ys (>1ƏR:1KZBt~InߞN9O$˷zkeL ȀFǫ9{\8r=ޭhE`f@8hKHؿKr?(կ 1% Sј71>-vј**,@p]i^*04aÒ$|R%'=kj{I47jʭc ӛE -*6/r0x/ w6B0 wwnM>zcZM<Ã׺ҮאTrjW?X.V~C@׮?*s_"ƪ-, |nc&0}-` =Hm1gi~hK:4s4)x3v{ɖmYGk[[>uZi"ǃHgB_׬F~Ɂ yKZc>_$[D<]}[FUF_U^(e`hY_51r7µ:0!`CLw܎&r뽚 #Fr+@ۯ@ңׯƌ&g>;mS`hL)@Ђ~5]1@:ZL<@7ڟ 7愝ȍ [Nhb&GgLf \^HV*C ,3-uCH,,fxfoFl\e9ꒈ/Y'Ls+- VS#d i M8#Ax nL@14ŝ`>4t&nl !Q !e%sr>0mp|Tb+`E-/j a2ǃMLS91u܀M!`hhb6Ĕ}_3W0BL|-r|63Th }S V]AEf`nɌ` :`Z# H<<[YG*V%!,Uu/(`5"kjG-r~\  -٭u,pq<+Fx(KYwZZ7^!c8M80<|#sb)A2?ꋦh*LmM܈;@7C9B7WdL^.Y:WO30TOU_ʄHS@V84s6jfLPKފ)=GV9*:).7=jA~`O;=(b%0n>y3ځywdcGd}5hpf 0ÒE&@%k[>bnAߒ)/4 ~D?#s ?xV0@\ PA Q\( ɳHr#8Y(eRKY}l|%}WpFB{V᷍0A$RZME??( ^R;;Qnwb A@y+ٸe$d ,MSs%cCuK}}hm@}>\!j=vv;e|9q`9-StsbӾ/׻>1a !ӛݔ~B0-ZkK(B8s>?3x}qa5Vrկ v?_4(a._ĸ |Zrl^:pO]r%JTպձ7o.7ĺBAjU4a^" StbuuCFsa 0F~4(xsEvxÌ81QYSuY9bfJ3TV;Z0E41U6f5i6T{^fz<|8. JP2LmJD `.MtSR_(BoBD fUHC? =Wn:BPzw?9_0]gZ]I.0/wګBُU{\EtX͏(?m' ndz% [QGJiO#^>bflVi5}Z܍l.g/ꕘ2zHyOT 1VY*j&[YT/_' ]P?|(#\!a]Y4\`w 8195g.sP_J5J.iG1&%_};?4wr{x*xgF /R3+gQkcoʛ-nN@.GX7O}EQ BX] DZ9w*>ڛbi2@r)fKxLi .2? Wk(3TfVtsYxQgDe!xYngWO(aFG[-N5[1Y+>*m fNƇU~/YC ״,g,`>|rlH}2({ǻ-6>A\7I`$u FA<kqjރE^h8/=\LqLQ2@asKiq5S_<@4fz0R툭->aw deXzG!e//%kRxR6+B<"]X ;_`U, ? /2s XY`l%qV1⒣˹+PC[ Z|m ); .0M8w2Dp&+ `\|ۑiJ\9j Ɖi.ZߣKt] -C;miG_k:cn1ԚhzЦFv/շ$h g7OXzO!^9>YD+ЉKM)LxzsEk0?u*Ƹ*DǨxTdJ(1yIlsK-  _]v43kʌ 7*(NMpM}7#3:pn; KQXE=MVeH09դ;ue&JPE¬P-frEmLsj%a`ErPOAa%me{`~z3 aPA7`B -tЌqnGO 4;R'o)fq'&&0A"l[c7lХ|.m7l| 1doy~=F(8b!Y#Y#n~ȿF{{jRb8 {LYkRsR](ilV19K@.TIZVBpRd`럏㎂|;/Ou .@U x2-7-2S/TSHjP6SdiퟜX'=B\`f>`FL.JgzMf. *a) Va ol5RG_MIw _ar1 0QWh>#A2RF 5L5*\&(ə#?ǥfHF>!okDH&LQ@Ũ3oٓE) "*:C4]0u d}-@Q7F.CaPݞhެi )< 5m\ VFw jFH+|22'a96ɵŮ봩7|7X5<ԛl Ω|) w0qt딟5`c //!ʨVV%B1'b+S:ɟ,'?'@@0w'ӍnqHu.5fX*=S+PA`]nKKq:JHGVJ0'a~" {fR!V4a2QrÄc8PƮM=#U7"WvT3$T h2w>WehՉZ} yMf 0%/o[0Ü^JK kWY E+sX,Wn/oZw|K! Vea!"9xxx;֑i2#ެv=;^JDkCuKn6`~/+?c.>j̟-X<1h:Dgc_0G1k{EgW=Cn!$@.qATآ^@،oX1} G03P\Տ~-2?˟,lq˩*o(݀qBכ"-=d*Z\3vc[#B@0Dm~A @F8B02u SKoC[#X[Detߣu~1 9柙Ѳ2gP65)_o)J  8KAT<5>Caٝ@h3 l+:<" F1]]`/^ %0| FW\K "[z2gOq gHï*wVj7LRg/L.;Ň#;AHZgke rI+[XbZY15Gxqioۣk80'1t>9@Őu_w+o joӠ|Zv0DN̑[xo  'ո`t(ϒɷɲI+دWp Y .ҼM@A5_a#O}& 6yb Kmq!&24 YoTlK48jbj>< ‰aiL?`cͭ-z@' (-ӆ\)K 0 ̼\GLlQ,0ao2S Av* k.L="^`[vw|~ee=M5)0W9  te>yg}|H8 `|k)Xw?ʹ~4FѢݍHJkD\ ËZ@dA:+dʌ M(S #ȗpL8BJ\Aqƕ71!uYx &0+ak "%o|fC> `Uiz ^G>jXQk~n8j+ZXhgP l-*UE!*jw?E6^!u$7ܔ)3o1 v$<"L`a8˘6T9v'ᣰBU%;3&\VS UU4)zڧ dWٟc lhuNpf ' ο[y}HWCenIX>+fhB뺴0WӇKm}?ۻIQ/M<0ͅl% ]+Ib6tf7͍Z .,0,*L/Х0? k0sK!EL)f8:Z(3 0^j=H}'L cu`cy%&>13ֱE4?l|#4"T HUn`AH|#vs V" !aҜe7aLФ4 ]P4OU<6 y=dT`UDMs/TI*GY08O Ͽ-to-.(䐅= `{=X:ˎ@ܵFpNbV<k7 ? ,Χ#-` s0"78Z^"mf ~OO||jٱ{ݹd@xRy[m֏Tkt с)7$i4,rGD3- v5INq-n@ <{5L?؟p!9}"8 Ҫ3BCs# Y5nŒʳ?-([%0aSO[`qf1W!pS q30\4A [>QIXY?_֒%1<J֪Lz_k+f U0fXkKZ#3`cQ&yh.Uv䕩JOu.eI:LQ~/Ǒ]oԄiB | jBܑX^rM_i s#Ť # t.&R(Ejm CTUP ͋Z.v)MYEI ۊ2|`0kOZJ3Eps|>BKXmFF;Yr \_n%?2Ouc1^X +srvy/C dϯA8/Dz`G/;pNlxfe}ߗ(1sjY`L=p#)2Aɏ0oP.XX_% R?FXd \7YkNj1Y^b2OBE#)uV|@/UB*1*^8 րHf(+SIg%SV!z\J>qr7bfWN0 (n~;ùoBux?j+ !$`Bcrև HjAWsfӏg*=?#{@!b0E$}<MI/EB@~FPF_Zlz_zȲLt8o 3+e_ȚMq80@k94d "X}UD?ŖUn:s[9Ƶ?0}Y5M</;WX'a~Y8kF>FwCs"WM!{R\'ȴR$sc`j)l,΁` _jEIU_}(ـ^9PVA[(kPQ1'ը!J <4S Al:OXzx iC?Dؑ努OH/A0RʱZXbqkCIZ*af)!a?0?z(壀X'k;\  .[_]!Z[Et.@EcqD(.4j\7 xi0 zlncᤴQ@=l^Lؼ kdty|̅e* G"RcN[B|[j`fթ/&f*]eNDkSWe vMdFX"T&~q{ݭ~39aqCA\=LX,L ~1>S) `GKNf23}i RR9S{4xe 1ϒ~-)4r:Kf`8ا(o}fDm_\R Jඔd_A82)Y)n͠c !~#O?6 {P02(/6W7olS ?#{9w9x؁%kA#`akm92%$ `YOX!@Mb9t 478G7g{4fZOH 3?vmys9-bH{J[ʋ;D#RSZa*VhH\p4sZD<$~"I8-~> +oO2 Ϛk _Ti;kGv  Ludd.i䆟K{ci!"\LPCAiz4 iE{|_twb`)0mtWh0A~zE _r~vQwl9AvbceM;fDXHޓ_]$F:̤`Qc9ZL'M9=\Lp8ad)70KH6>'0Tp pc:+ [BF, K4%\#ƃ 'V,l 5kT9a'@cE~abo9 |4gKmڼM*0DPͼoPs+K]2 8( YH1gP9*-hn~DE}W_s&loʾ^ r7&iy/ϯ[m¿-#PgZ'hߤ1t7ɇ @p4R6@ۿF~>C@췤o <4!|o.}$cJ;fX86hE@0f4@NUL$;]YzKVCi"&zf{s(iDgccهYSc*9`:s4GD= Z@KcLFT! S+a2wKd!?qoEB:0@L N߉ՄeR1cgCX(0  WܠgLV! NEײod;lB>G  MG O)H]|܌^#??G^ۊI(.~cS w=V-VwS5 6s4(4T"WX-yh_<٢@R n^!=GVG1.~_pۺ_W7W5onT9S3{#L؛M}$BCUzaJRb4`!% Lv_ٮW U}!cTc?50+b<FQ"&1q>=Q၈?>ҵ#p)6 ۦ.naZ{1C ~E01gF!ja3ѿe\[XWmG*!;><_ނ`-*Amhf?ߵMC0|B>#j[dIwCtt`2q_p/_g} ~5ɶC/aO‰' 'C$& 毰V>ߑuw~OO  "l!:&|]o}Z9] &% /gVС*` TPKݦodҽ-s]>(/ 19wecF,|]\3ϿYyaHp pc7_r_8 `dž]L=K1w_'SDAK}li XZmoe& `,"L<c[M1Lm,͐b~Y> +@8H8ƾ,GHӋyxi帷q? &֖\ ̓[yS. aݡ7K 0sn(4*6Y/kJ@l{krخY&I5|)sZ9*1*;F>FowYǧ0oe4?>-?>H<@ע%^Iegy#:|CYZ KnhQd0_߰H>(p e5o*2Żrڇ |iʍ}c<̒c(@J5ƅIgZKM"H1 'G\[1o\|ca|>To^[U!ǾȺAI[!@s(\ ? b.uV T (h6ps ߏƌ3*sӷDZjv;<CI`(f u iW#("r}e޵L>糈]?k) P-#ؖjwYܿ3Ԍ}"bʆIHߢJqUYmWT!L 4 G0䏲o.ď{/Jp{Ď__pJ9ߏ}oᯯb8fu!dx̶^}~_˜~9W>^MIEJ Iˊj1N-> ֯KR-sj/I(`:Dt EP.1z~y1 #W0F#ւJOCu;Ay|P`Bc!|#*bRr΄hkQXf%@ 5P!e=_GK@mˡvU9D`< J> s A:c'Ё^,hAa\.xNf\FP,Q[jS_0:F+ SrQvQY ce&J8NV /5'YFH!Qpts06hbB) P}qbZ56h.e"n/"K #E5c/0@4Z_Ηrh}0U-B[T@^Bl9Km8V77}^C{B&]Кy~d, /'Ckߥ|ٽ0i:VJo(T]\Fw~󞦁ܺ!/PF g\#_AdXebJ/yI䜫S `;KN v $6h^8ba:kõAp~T ~'>vl&G 1O-A>ϖh#ll/?]ݻ9y-@-јoa۔\C.%+LU¾L}uK2!r+@l0B;yXWV773XhUkp3Oq 63@ 9mJa @A_] ڢ)jK5 $"3D$0wndkofDTM#{).dDM'@~+YZxg돾i/k`8=9ѱs8r7%v–X"Qf攭aoo `g+C;F2|{uobKCۧE\+72Η@A$fMn ׆^QUS&"+ɜl2uئ 㣻A\KhA0)25_[iNJ{xk`(Lh2#xyTRuUS6hc؜‹죘)X1PBfsc [f4.| !/b +" ͷv3\BO"籶ׄ9ܡvӼƱ"el)gB~ǫumӓ, Ժe?J |\s#Lg5-&cN,^gV}5Ӄ22DDk[SsbB$ѳDfVm(~$LlF] Pmeg;?mv~q,w-Ý]ߌbfvJL,ꫵU$bS ё1z]P;10 Fc6/?A ~v5VD|,hycĘ) * H$Hr^g!atJiW.M-}lyE;Ͱp= {/"|h?9`ZF^33-Y%2w3-@>RLeݸJ$zhˀ5)}|0azb `87 f3~g|Y.j%A4-[ &7$+I!Ъ>H˦`c{&;Ebsy6&XRc+M -A?ax]hh*Eڲu)H KBP1`PA% b ̛3S;fa';q$?˽"קlČ\ (GndS06>cuYٟHq37|G8~j0*>77Y Sy@Yr'w,s\[m{C$9Y1PlIKnH%D%\^/1dy1TyM}{A#nkǨCf5Js4PtZdS&`X9ꃫlx?}]#Lqm|?2@t>~QV e(-tGmD&kS"X"(uLm|x̱?WZs!hDRZx)p;`@TU)19^Pd{s,yqEqE^1 KE8ldLIo9Ns)Vy!)`nK*Lǂu(+51("G2hhRM|"#0:.BFa^v%n }kTw(2=/~t`c=M0~ cZw Ru#7}M_ :4]˵`vK"( 3Kͮo٠o(:arcokDFg%rx &ڎl,J0% a65wl9h^Ap)7oTUdl/gt5n;1lm1`Jqn8WF(u Yc|-P t:u=Hb.S'GO 1Z?V4EO/ o [&W9pc dZ@̑[Ce/]c%'n|Θ, &=\27ًmCA ؀d!lif;|`&²_ L Yi#% s,VAt5 QI!;)"G:4`؟`hT y81u[KL~$BDӁk:Gh,Q7#m(+l݈ŀGJ .(j4uCN 14G(ރ!`jƪOyyl{!M"m w X*꺚zL+Pf9YAps͛ ݥMjCiy"`J3kd)l(s,2HhV;fSduà"`Hw0UfQ,`^G8 &ɶsLMAMK!@^O3N޶;r%-B9Y@ c'nS2:_ EDRXS6p` F@d'KU%^lO} `}(gf{fwԪY V1|3!ud6~0}mw9xQcÙ9$E'@NlQksgX8g21xn>ɐ@~v fS$;C*QDpci4`gHOĈ"KzZF d6n2ꋦmbDam,д$E[w- 㣲ge ub6= eqNxyFȞӑ~ghAh]ĎYZ PLfhj-Bkꘇ>PgߛTyz|:q><`fH E.J e4smxzd#L,F7616m-GT浓#(6w7"jN{#nL$^m5p-.Nkfۼ\I\2dz#_kpˑn>ֹqLc:c?4)8>D#cDBs%*}U RvjIZAdΈ@hi`RglMH0?1"cop%A%?8([dH!xہ>5fIfOLt?j\L?%֬l}Çf" ׮|Ufd^3)\}{0wo.sw4:N] oOp3|y$ N\5ۻbGa|K1fdT+Ʌ0 HȾ|lQX``z l1pjȁo|w[n[R>o15ld>2k&8z=y;s=S W_3Tzym77F fY^/dxJny e_6bd770%fsd zTQnEW`H/P6f,mG0T%A8@Ib jH%>kFxs3#%N,Ld z>bx a Y6Kqk7@ z99; }kmh^mدx; NEؿ`'!``j0aGB6v0kbBJO> D%NH}UT|?dyh-;t[  17Q&2975AƁK;fpAϔMRpp& thYJU 01VA#0{blndF+&EpuGXЃIw0w/Lh"^?,UE<+P)"LJlVl7}yƬlc,f7$n'c\N>?1cWfjTہq2<ܣa^,V%U@[#}I+  EO6pnw/|pTou &W.Zm(ne4X}~*p ~y% `^UwFD8d !D5  e?|-r 0/# ;hrndg+[c36df2ЂE'G[σq>oq nl˜GvdG8Rkog#rril3 >00 ۫\ҵ*'hF"CTCs)*DR6^fU<ז8)F4sF$&3J.0\m2{+s#%C?ʹRs-l S.S Lڵ &Fn_n͞5ˑ›&l7z}}#pb/yQyg8=Ϲ]}֎ O7T]ɁKvC{-7_d?H;p/ygE~8ߏkȶii&Got09a>ȆxlÑ3ev,5F$;51 mߛ^td_w` ;#s627bCG'`VHF>$. vsKPH1SW^ h:_4_'~y,T_{tˆFg|uAKMqf-"`pR#bĴ@[m0nn}V2UavETQn5tSCmM9ȃz7-OGscYHuZ,dj*?ѭXtC9|UlM ࢿ-=;68hfN~:q (3oDaB򛧁83vƞ5} x- b7b6y}r}pSݍhp{M20S33K`t7uL |LAa# /WzVmcer!X7_y`4`kf; WMe 7~j 0\A +c\_27 sȭs9VtTmmLۓEcz!ë >s$o_2/ץ[N4p'y"/4,̒eR`f3  m.Gf >Tax cXyY8UށD[:t5ed8/0+c_gDJM '{ kjR_nH\GL7Zj`o,VXsTJj2D3\6R٢18fN|d(*?"bpI_STZ_x¢>7 9GJ)j&X8~U-sPQ:PL/1y_8;6у_2z|Wc0d, p' _F@"ǚޜ~iގ}7 `FnPی*`2&LjW mjm{2} u9=G'"A<ȥLٓ&Q&i$MCXFiz@ /mЋ' hlޓDcc}¤FTС%V%2gt?gK*KFׁN  Dcs`冞B֞O Srx?al]EwJރ~!{U<5ȓmZSW,e j22p- pd7oeiq>[;|ؙ 3F cvo7N}+Bف!+gbN1uf0ώ)6708}"D+-~8scx(nu;ֽ@ ÁDu͸33|80Qb./r})@5?ti\.ZXkcrYdvl11y561z췜2;b-C6 H})Sb] )i[la"U?τV}TWwKq~ 8W0Gtnn86z)5C# 6Mg,Gzʍͷh2tk͂@eF~0M,Ş"] ,>vCz74[&5An\oL++J25?)n7zLo#>o$y:/ؗ˿k u 3M]H=yOE?3_Ԑsם$iQ?gw@1K9<2oj2fHWYn8%r+5]>F_kآX\c,CMPT68Wp20L@H81xY2B}tnQ րrlSZA0V斸("pV99H8ؠ+mV4=MK ZGr0;CjJ~m^xֱ\/}kZ2 YC[+*eMé߮F~ RJ|1GѠ(@9&. (?PXӣMxu//kMwyNY-ُ߶ݫVZ7 _W| (Ǩ=f#]pݟ%4H<@ek(_^ :W,q~sNį9U\N|~F՟1&w" 80z|EF<t$ľvqd_Uo2sn[\C~9380?HyVK^C^.-g@d8*J$sh 'LN!y21DwmGcGKuTw8]8Ьs(3s`a:qP<] -\/HeiOf1%#!Y~$fC#a"h#ΠD؋5Z}9ӒcY2.&3!.  7.ff9KГy~7r##Nq8D ";52{# f,'2LQcn\()Z^߲ԉ8JpW8ga(`S 9 ^) Jz2psNGV9t .7jEG>ÐeU„y^c# Ɖ fd>&f7o?o<gDsfdߴ7|&4uJ΄i6%+I-{|l*1J=Ҳ[~q7Rs(VAI{&@lnF^CM\M1`ZgW -;T\(<`0g|Km]5OE!$QG99+q+ފ:V @R4/ˁb1v~9y|tcR0'3!ظ6%ZWȱ<ܷ9GR9# @H(LY!)m6;JXmWDS(oU^Qds+1m@/z"LVnRGhF\R/XLP2s^a?2EOky*J)7\4 Pc\&\c/F0 ]&JoMw6$pkf--_Œ_hmk\cE")k{0'9f/}{= 2͗j/;fIZS%BzK~ÍZ"TUroxuuubz[ߟ!@G)؟v1>+3J-fv.@HAM/-FQX@7"@N80'Z蝤~tN[%!wx ܿ텮08}rn5wgpLc*x8~VB]蘖(:CA @9\ d>I1f/"`< Փ} dv0Z|Yv9Dӊ<y`(.2=GnNd%rq=~{ӘH`av/k9yr/μVd^i0H ݢP!EED r9+ٮTFޯWN%,-3osI ͹3{'6s|mi*[Ϗw0? i@c3Րbq`GiY\hփKUt۳ nj sNPz =0VEGFo9$A#SS=0kً " #[kV(XulHPRk4ZЌ|)>01¼D ]|`P!|̛0K@mgzA&4AI\+@|^]m+ LlHOhqօW[muʗsFmk71FQc X c`h?s29y™db,5J<71MEWcP^  `% XhyI!::"/RԩLH~ni Cx*}x4G%^1CKy`,39n')9SpCL Mu2y$< }[aT̪/e/o<>DJ>$ Y5/cL"ׯ27,ÏhH_qI?3k?:Aޤ ғޤq~ #Nm,CT!&wifNQ).Vou+!ХhmZa"(o" >4rdRx4 =&Lm@Csh`>~9w+ͮU{Ij˴z$0uѽ%Gf吹t#{6 `\PpA/~?}sffWM_[̜9%=49^p젂{1IS;8U`U/=)__som_s8ZnxZz!Y4U4 f _ber⢁}ƕ[)N|1U]qIݰ9˜[c;DǘQ2r ]XkkY>'/RM?jP>`.Jv%P*3P9IfXF#} Z~HS:9( c`L$#4mr"K+t\<SltU*nBĽp0sQo otrx:D-t9$ԕ礞fW=<1:<OfJg,ORL~sƹ~Lۑ~EP D%6iS p9VW+^bIL> MnP]hvkTslw7w궃_$s WfʋN+弿+Ob%=nY{SD=Pf?:3~ȸzmk f)\Aた~`#P`_ʬW?*{ɵ,Eiҭy,}aTɥMbk_ptPdczGT흌/|1 /2 .HMbj'b.{)06ݼ#s&e;k\?s?f1t~0l#˹gVOX(p,ؒ#t_5:sZko}i>8Lwf2O{4 $9)9 N[?\(˳l9#cY&}U3kK[s^I ̱ZJ G'Hˮ01 hc4!s,c \6jLxjw@{ sU=]}e~ĥY>\zuYo!rns}*fIP4 &$W` i!0K+njMAHؤY>26䩶9M{u˕"=rʍ\!;*tǚF5"^ߥ2?fyemŽpXN*/tOePv2!K|neVoijs݂҂W\.} 4t,#,oPy 7#Nf`satd'Um8ZC^ڗkyØoF"br~sୃvZY:ȝx#Mtw& s-9ZU ض܁ ӳ̌L9>uqrTIذrFR#L;S'\^FnsفD׆qy(n@ V Ɯ3NZ-a%"nVRBv^Ԯ᪱gƌ-1ne4Z dUg^-aiYYFyxSpQCr"{c1x >v\yUŠٴh1t=] Xex&p}9h5AI8mLXu"kGt WLK\bks-Ţ>QHG|qfΰooNd^psqf}G0,spl݂4]p]pz_rgTl֋aaN8hV]{*>]p1FCk}ljصN. W c,ϧ3bm)\Ti;s@0pUa2SJ|tPsYT1Sډ Kce@NsM 8ЪedɭN\y% w `t`<0@R@aG߿R жz,ѠBXL[''8I0.w8x @PЌexߑc};LthKLϫl=v?9Y.ƴ rv |~$`Kݍ$1 ݙ\ ٹC=7bH™1;7> {]t309U&W3  M  7DfOk[3]TV.*\[C;ƦT@- /TǾ~ysJV{`"RCVzv85V7 zl_V3.ho(F3lrvI>0ez7aBɞpp[FI:Ve䤚l(ʉ:C;G$a4M/`v07Ow8L1[-5sYv33'jl'e]0f㍆7u:י˫o/ff4wڲf8m|jWiƶ;f(@`0 ~qqӳl 'V[B+c mm5%:Duz}LJnR*`s>^\C`q``oAjNU9̂ߎH`w_+kO"eP?TڕL7wnN gsdbRl#>^ 'jaKbL4d I}{:A~\ҤXouNcyxs1L'Ld4̫WDY /ݜqy17uskɵ US8 8?j h=cK';Z>.Yy؊ kFZq g֛A7D"1/ 1D 7X),о>1 ʉZ`Y!<0s: 8mp+I\a29C5yu>0όCs0G363J"6*80,a$}"Tw )"vef26,| i{۸X]_ݔK {8fO #] mNj\L6 YGĸ}f3+83Rf}Z =T3а- s2k̶VY̏e>E&-D+6'XAQ-p pg׎Z-׎0OR,6Xb 1 je)NzA S4kCCtR)a17ױޕ@"hGsdbu\qnf sPlFد#6 r-4:Ix0> + c@Ͼ%{̀faN#(XxMBtdp]ZS=uf79Mo;#ۗm7KM!Θ7q{t'r](TXD̅\A #!@ia* 17m9?HLTg|1}2+"kx$"Rt3xq%.ha vpmC<~`+."F;asc޶R=Inba[:D{|Vkjf0Ơ p;Hq<޹'1ׅzC;o=N۞L!`HaR#~~} 2ٍ!ެ{ps1&;-#u񙟛 ncx%bS5*GTV aɲ+=}8k*ɵ a!e\Bqjv% sNBs `d$}r,P*'QĘRW3 JU[AEYXSE5aϺ04 ^\aRv7 ;SUx$#ts|I΢[] 4әz 7AB /rF t"zδ$`"Le_Xʖ@ бt`+*ddE`k%J $1Vۡ#0 l2L˅bVbDL۴C9"J!+"9œ$4NP]3d<Y= :J\0n7Au̵RݢYk `[ehhV.wSY7zϰavhty ϭ6{?oK>fbֱἏgzLxpDݑpͭ*:].ȩo}8;#(UPO->R , ,(M"l28dr1 rzX4mEbj@)SЁTcK!"`1TP]+eCa/nr;\;hYj_LaJoLuZZy\ F nV Vo9M&;8943E$žN~9tHx46M'NVVCD9ʓ: MTa2 {C^ 1an7>ĭVzHga5*a c5,] 9mmngab^fsk/6wmrnP;>tq$1/e{mp*57g`* # TiJȻL<`jAP $Y"c-"Ku " fikc6׶J ZL.l8J0nXEyjsR8MND6 z*ab෴éΆw:4]?Grnekmva Lzϥ.1wr1 NWUfanZ9S\FkڼZ$2$_ۭ̬:Q-JgB궎&.1~*cY06_2.lrԚX:565u??&v3ULg2pn=+<./Lټ>s5^nTۊo̽^N5i-* Gn.dY>9f |&&Nau^6:Aqn.7& )KaR.&AQ؉1Wil)T BbbbxAiCi[=X;tlK+VtզOr4/4G0:GVŐԆSNp28/X KSm8Palwx2.C&4 <ll0#thYsݳhJ;=1 ~=^emА?C/='h'tas!2>$[p=@~OXu@n]茉Ϩ^Yuqs%Le3̰)712f ojs f:\0cq1i$M 1 Fm6Q 6Bd\ *}]2[PkmP9jS؃|aV82>3 M{wfL; ~[psNU8 ;Ḛ~֙zXr=Kg"h/z h} N1=W+N5 #C=h-3R1.*NxV8཈YR\kMez~ݘ ߹{NϞ)`M?fx"UpZ2`ܟ;T%,B-GŎ܀gڰ;Q s%, A,wj; }C.a"b-rܱk'}eV9iKءN*Laded7KkcM Z)Lb "?lgX &SdV0b2r+i ͔1oexN2Z\{?g32e&Y6w%"I1eu{涸6wi7笙 F]35umOqD=*I)=9Atie JBB] v|ϘԻ:8/b:U-^x0u\@873$QncrӸ=kO@qfz.%JZִMҘ $)xr; 5gP *pn1`a/Z`ӊ=Z 0Eʹ^0?'\XSt  e2~"٩y-;A ܞ5^xǘ9DB0j82D*\ϙ\3E,vs#1# cȉ:E'#1?~⫽?jr(" ̦ ]޾vpܽ`ǵ2vz~}Ģ+2Kcfj{cSJSKF.8nBergؘK cΨr7ݚɒ*w:>KnrWj`&6b7IgVlf33ViPRpLg !J}SQY!#O9U99\L_i5aNetz39vW0RpW C]vUqCÎ9z2sR.ýNgVC2&BՂk9f~L3^R"9p,CrgZ6LQ',&K)V+jV) vVXNqILmJFyԝyb~V3ͥaq)Tp{. ab ٱ;6hЦ<mud_A¥*mp B԰$ 4zHu!vANr/I%HBܙNS.g$]<"t|mN.!2-Xl\d˜z9EWcg%a4 ua?[y?Wps|3IӫLʆ^;޴x5xQ[{t~0Xy# tnx,z-.3/wut^6 L:.2`E'#F.yæݎvz=:ajq[1I5] 0Xd/ er\3ւL,EF0ɤ̕ch)n%i^9SW'iC=ߙwDق])NĂVbpx6,=W!&QW K^ SݪޮSXt,𷏷zjsNqu,wڝyэcu47T8RIRmg7Y. XjJ]ܴw0CL+mfMs{vY[l|5|ւ[b5 2!Nll,|V|X8- e/`FT~N;1UN1Bf9r,lycbӴќ4'hh'cFrgEQ'ꮿά\]]n1_µ[ ?Lqf^VWӏ֑#v׎ PT}Itwixn-XY+,K' kt u3=)Yl&[{:ʰ%U."dqN-0I+XvT7㋝K^߲iܪ)aEblf4(+0I+usNNe\ eWU-Ɲ `nRG 1კa2?\dsb0Gf4#Pl[Ea=c o}IDz 0eV:&;NIؖ pWbSm50ݜdUYo/D\j־>q;|);V+Ss6Gc5[0*{g6۴""/0@o'mpˬI߷,=f] .iyk妵 F~53S rSm y(z9vQ[/|b8_׾1$zy+gizŐݝ*E,H}6.2ŬnL5 PBj1w]im1!>`W 2AW_ HINs^˫u>߸Ӕ;\u&.B^LCY S6^LH|`N7 5!Xhw3'5Aj: ϸπ722^.6^xqv{WCZ ]ѷDŽmNjbL99 lsn9Vטg ϸ$Nxk2:U$ܹ_sLS":M kMINq']d юey+fޛn6=w_SZ-y0MmJK {قuxTF0A8kn/hdL~H=N?{AB+΢C2a`yY3x|f*sA};@X!\m6jӅ Lʖځgf mamK#Vj1C`Xa /#127 i<8xrLLquS.0\1,d%^؀}50 m 69ȶߐ+,܏hA MXCx[{ }';]D01f=2Tg:t?F`6' 3_#:&E8f O輮rĔ03p"I[Ark`V zvj)R0Iڴi/pF@p Whsu^ǏXL;U=b(:Î0lnzk5S;HN \S菡[6 "ؖ}?" _8I`z!p]=*9fU<_sYo6;Z` ։b/3Nya:.`9%fN1%!2@6#4"8PH{R(:)=:3OhN3x,cV.i|wk[&4th<=dtu. iyt7S_^q&[ЫjSf<|ۃ2:~y`@Odf8gN ] tzpItXU9O[k;HFtyv~&G<:nyپ+<\ϓ_3 D@|q\v>yl 1#&ywcu6ocq\nP VUSX y}oO˞>:+ ҌK\lhgT/R#~)rORmh̆s yVqU,(ryONEaQ8T:f{E +dGB UAelajm{8p.9Gw`"FρU=]eq˲?X9ܜ]M/GY:`#TAǀ*s{pmUGX7R Dz1{ӖL@O_ .ԭpE&{ > Ob܂Q3GsΣ4Vpfx EZD\)$xHtұp8po; RеC-t{x!n?d@܎՛㘾_ױDo{@T #xp=1>Xi>>F"BVر=2ǃ 0x100Bٿ%r4$gKRz7pKYEH6<$Wpv4'Ínf+@ Xq(P ,rs +\IY"NU(YObfSP F]ӫ}!6wͽKEWt>w/2Ī~]2uqyߏc.2(4^ieESa .Ҽ금B/' ?ȖgXx?0>'ڏ_6kMNo /OeM흞"VUHhsJNwzc5)Y{  &f}(CUǘLb;6 ]-sp%qoW>S4%+8v$HvzIv}Ռ)?\O]l/ߕ~?P9T̷rnC E* צPB@N 0A]z?:&3'b1{?Ƃ#i aQ26gO}|ǜ |v``\RC."T"PM50Qmp%X,;1 L'2bBJ2#tn%J^ {>F'˕\7G!Aax 賿 zgܽOV4msj (5#8f/U7DF`.!顳4xw:khQ4PJlw +]>z;T{z Õ0~9 (bP$л 8 eJ+t<>]l> 7K|Nu;c*ݙ Rs; &+v\Fa|`όpf(= w1}{qE,0N`BX8^~hXy#Ft7o/m܄->2?`@XGWj *n4y! H``͓ ۫aF 9/V$ c&YA3Ɇ vH؅o~dx? v cXKT<_=ɮSL9=W9>G뜞#οm~}SXf`N$@j0_b.?Z_[^f= <@nq{sʛ!@̟s iY+i0B"{$z|v%]eMkˌ@LFG^α+]??@|<7NbPxe/::d: gۉT)j$UN84k(, |HgBtG(P0⟷897Ȉ紊͘tƬޏyK՟erpA1>aE>?e("D_MLP)P.vat"b%Z'YvZnBf;\)n ڼF h;w7Ѐ2n&>z-`fm![?&0}Z~5KgA0"nR2)$ MwNp5np8}%]%3)Ytgڷ/ tҚu:>\}:z$i[7'";Nќ`{4gStQwy뗿m˅v}ގߎ,]6 0 /\霺йuG'bŧN%*' ]4Φ ΘLL.KNFB+2-3= 4:h_ E8\ƴ'z@\`Az,=|0?cj?+/Wzݼ28q{}  &QYg:ЌEm|!TnCY_Ƴ7OL/= }zۉиTA\xsPlEWNVйaxܾP:ˆ[f0㫆xO0a='h<߶74wIcAK rhjbu?Ss‰NsCHvMœ [3Zrn;K/Bssh^O쯋8a~n ɀYO&ok?K7+M_06'9Vi%v?\e q8=E,j|z_O@_.{kE񑎕xs򂀏ΩSBϒcYїYх0 l3e\˻&]g ,KsR  ʧ@B'ׯ5fM 1qcCLmɄ7 t0omܾ7>7?7 "t*ڿ\<tٟ,1a^ץ\ 26Lxb.L~E{BXh#bBnG 0Yt|!N~0Fn7x/^WYL 3*s/ OŸ Ģ ,%&Rc& {_9/T}"vB;$/@ц8i7 ~GOqfղ@ D0sàt>Ik.;X8֬M+f!PۭvH> Zt>5h3ddaYzpe}n|_XL] /"!rFz"Ot_Y䂓6f(TEձuVS:#3?ON3G\|Y j99r`k(Ő捲@'v33ll̙sg3G|~ ;Ol4YʓI- 1|$i >&EX !rqΛ+1fqs:'E.l&@(I~"ܺ122]  FJ4qժdq1\% [ucA.iO"J01j5X !/x-!& :_GQ'a[k?P@' O[, E)ͪ&F[$qd0H 4%[8Ԭ;)eN1w.}ق@fju5л}awQcI bNSF|t 0sOMz;YtvGpossv߷j~C`{ G[ԞNQ:|ˁޯ`ZIfm"n090V09¹;"f<|}F5WW~\pyAc/k;u|Gm'hq腣\b=f/XQ|/O3ņwH3oQ;Hw9Nl:+R5I_32n|V h|V[kmWa`N@? 8CRc=@6C8q~a+шO $60 f,c04n%@>.nJ/ry+`׫;Z3zdJ ˑ bm,EY$,Û8(#@<Mf0$F[ PB`>ɯSNo Arj.nD)̊%Fo GDRŕն- #ݖ#8H-b-=O'P"v.iZ z; ő` 1 3^\c٘EVu ։99cemod78,mj7 ~tq}ď[pןx; pCؗ܁YsE4n*s>28*9:n~h}CM,<*Á6UaȀ-\YyɬWsg³0 Np臥;WR/-ul:p[BK9\QF,<S8r+ NǻfAb#w_9B "Zk:)_>&-y$ "c-VyEW")D'Rh$Pd:>%-:@``z wlۥcy? <;E(k>!]΅۞~@E!DrP zbi酎3ǽΐί#3',Km:gcÅpi95vT޶3Ʒ'[4~ޣ>g `N? 9I5 شY8%֋Ê㲭J #f QL.E`YiPX͚/rĪMu,.U *ҙ&8s}@ DC- WTn# 9@ /XQ^N [%7eO`d!.@g]Dh3:0'٭Cd`&^Z\maBVZ@X J0G !yHd\b y?`l}rptzLzg`,>@ W?oN0!rtA_t'vudg?I*ƟIb,h΄_JY q Y ֑t{ <?\|`s˶_u['^s"pYzy9]4NqrqL퉀 <~ho9?.lUx z`+9Ӆk:Z]LҹVΌ{#*#Bۿ6L40`x;1, 0b93#{=-n8_7 ?/-K E{0.Y#`:-ۃAqն⦌J1l 5?Z jP}~盘|{+e؝,"q䄿gT κ?.Bf'2`3[+tp ~ jkT%X:3# ,/Qґg_ʼnс[*GD^54NvVbbR`'d\f J0Vja!9D(Ncʚjչ(Cl ~Dp`]g{"hkyON!oMtM4#|OE 3H>韙bGȠQ=^6 McG-8&~akX:ߘ<~ϿO Y_;5vpf7E#5bQF `%#%WR f[UcPޘ1/c0kzQ7ָ]n\ @ÕXNcP0%+~L'!.0T?e>@'8@Jukchʎ.+6'AX ,.W\E:ER IG.gM@0m<=IB_;cf#{Ag-ŸyI}ԥ0]2È`΀[g2֊1k}3U +38Cg?=mjKzW36?ramqMa -q>t~V_9'3#o{N>0D3!rNog]^'6w9^JU,c;մzF>4c'/s6߅>sM /a9#M#E3,z2qں:^z\sv68TGZ 90,Guj?@ :p̹?sUC4gxN;Xr[<!~!:TjDZYRZR-/R݊f   zS^CݖCB } ó.:ofTcf޳z;o-T̤#2_jip 0zLTMWv9O[ vtX\i3d`sP.Ci["OL^9A6:W#\O?.{ Q`cK v]A6P0=_$\?tnuB/[&:p ;"hLdCLJ~Ǭbvo,<)V20b|ք7c%=23Z+3A:t@A2_L<ׄ'tk˳GKE'0țG}"W*/)Б q9`+E.CXmv&;w"pض!'OL^NfO~ȸ<> X|-a0ؠaN8q_2vEݬ4&yezN;W=E>g_Wxq#18\ K.cOV{׀}n sQHBKDxFfb0 ],'xoBq@?6H>6 O{Wì/%Y~U"U` qcmaq@=2tX^c!V]ɋW jG?#40Q*'} v;TcZ`pO3 3VˋEA2hryb =D}RMa깸Ȱ^0iכ'魈AW"spVi#,}ǸeSt爙z/cmp*Y\ 4eRq77iySJ.lЎkpv7J.(tXOu#p-xVߦs. pgz]/l vss³pVX`r l7:Go9VAG,9'C-[ĞAho)pP{wj 9*&aѕrJ;rEVɍ}V +ˈ\cW|0fab8ÙEж!Ge1bP@ $EZ"p!/+o '`#e5za`3A2[W)0ɈΆGpB rp ;B&;@o l{VW d}5_qׁIjऻSnzo:ֳtLAׂ" Ŭ0x<؝@Z N%gb ~Wox8 ߳s @N$EL[`VF`aWCP/ABea~>0Ķy#] 6]D(-E8V6~WP WQzu~w])TetD' ]Hΐi 4HGǡŐ6 S4wp@Sǯg{zz`q0{~m;+)Ek\[wtD#c~SNUa`y"A0"Bkl AL1{7胆dWnbzkWHs$ʼnji56IW9^0+QX+|K0ڼ`ct[o2t!.:Js0c `!0@4'û]$bͅSp˔=<!y0vH%xofʚGzz͙ Uw)[pkmTΨMA2дr\oߝxn@()Gt2AZ"K-*#_@sC,ڈLvYa?7Ds m~Ng? +ߕ"+ YrX&L@ċHaB^9~r}o zآ3_\_?wfbB蒘1 N|AeUS1(~Ե! w!;&rjU%oؽ$j2, >;VSҜ'|011;At,=c]~~|?(|~}c6ǚ6g蹙afYvu`O y8>Ü X~p*& a`)(H<+q:B2q)x?eΰP3Zs&ni.-kJ1lo5e4ĵnёŸEQnZu1?. `V[L'?+9剣I l}m39i ɡqb,j8'A޺(}[#҅?rh[7jr2-u1dȬe&~Ja <2b|&@C{Q?4"޿c}@ p?dӌ:huJ|-u|,q.NA+v:@)"f`U`O+5Ruj'=]p9 _R3_fmqF :J?34t|,6Okt~ww?:a*/ 3?x ۏ9Mǚ.-ވa[g$|_$\h;;wf0yWDXqh2ƔEaC\uB#O/ϔHY( |oe!lZt/|&H!qql eH"X TXi @;>8 - N'v.-gBgF1CfDlfNEW'\+TsAzηl|<ûTnc;ZHf1^:jo{)hQD.|FK%? eɹJ;期pnOop49O/X; xXxqdCɯNkinS׫rqg~h &SP| `S4`eTnKRfCHP {3?lS P4|! M HNL虚Ă83sG`Tu%ptY PTo`{YcZ./hm{|0 RAI )Ss&~J'H`;A:RvL*0.>t.39bz{. O /f $և :? 1_5/eRlqNZr6R0}gvS|RfgO#jz;GIhpZðƊ.ou{k21BNa9{teNڜKuռNcQAh%'0dz_W\,]OVms|,O _8OOag"#5,榅vze.X4MW}+mo4:<+Ko__({axʎf!^zV *d::/S%yQP_﯒sIc_)}$&|CG}bu1Q_G)Lkt/GDXm\"C&9DW ]499W ᕠe=,<] N>c𖫑n:œv!>g]~e^ ؍|Mwx`(_R-qyxevGuAlG;2-p#[a$Z[#eRv|!Ocܬ}W.3 AL>[ʟn _ӗ$ ϿIPK!(.|Ƈ: VFVbN"\_ C4Ɨu~R+p;s =ɵ]l1*CsZ~[ ~K[MB؟߯wV^S9+_FBA$ֈTcL{:5f<CubZ!*S eXFB0>wnI& ^C}7dH)Ù^Ν e4dݨ"3>shsQ "Bf~ΐq1Ǩs_Ny1z1CS$x: OY]Iń`S Vv# c;tn8 gnBW&r10@61Ϝ!,%Wٚv5kf{faıwb YL]UFˆ ܧie[k3B+,ժ6RӦPkO=h0p.3f2_j_$oB<_Kː@nLK_?=R:cX`=3 (Hd*ޜa"V}~;ݞ1@,qzQ}KQ8nϟgV#W 9@ 0Ka,< z6Y; GaaݐA}w<>t" Q1̫y|?'vbzlu9q.2gÐ,|Dh s|ŋΕa[agVD g2i9#4P]UKZrC=k 3QgM%3\Ow^ .Yy\Lb-,rw^X$OڕUw3IBl0ȶA1ʽZ|aP@2>>_[獌&蘖@ja z82s4Mq_Kwiyo}H@7*7/oN/'/(ICtn5HK)`wM ykzJ˷ kxOsA NjS./t N!J7F Rk^cksm?ٗ:}?] Ga,Izŏ03ZV-Bw Aswx1V30.!xv Y ۝֙owqR-qx+풘~pLcK۩nU.V! F |'m2bJlٹ BicaNp9ATPIU TN' [KE4jCbW d;wBŗU3;a6`h4mvKml9=gr m&U" ϐ|ȨdJcQR= x}qTNt`sM$;ofY<0@0p*N,wü#19 1^_`e|O0eo)ᰁ ~r1N!dAZD',P/6XHϬJ8+ \xurˊ.d0HW# >Nn0T *L6"im$Ԡ`!!2]y8i33]IƧ f&+p.X;AثF+Qrt) vK4]wdX:3PWst1 ]. FuS5%L70T=&whv0 c.r fXi~/^RNϰ\^`TXL*`\޳_9^ >k3)m9E)W9-g`Xjz ,1b5y S*m9*94h9Eua 7S=,: 70ۄ]rR=rE򙳆'TjPҙ:L]k:bjQ*0Ak,tMRt2Nxwmɥy)h[kWZ`ȰX3,EV Tek av.GXGp';Cx)~|p<.## 1qULj3>)?Yɘ^ϕdL.'x8ŰcAf1?  y/(. CFVPޅyڄŞsǽ(_cgG }V$ơ򴬴:i(j5΍MCos[S o?o~\70 mU]zyQ dAad̽1νƓ!WSmW{n1>3Lv08Ͳ+v9m^ܿ~sw$C!dWk3ð*fXչ3F8@Y'hAP9ł#'$0X &sr-GV]  !7Ěb ^7턉n5QE.ld[BF& Wu<#tg嶼O'BeG, [u񡳡mqK*ʐ3`ϟ /oYA)] L:cºT=Iսu4&`RnP2+XMf ۍ``i}`%n)uԶȰk m:}ȴwsc0o_/{,'ͽR'f`.2;=b0`{6K,|TQG&1oκexgA0~yٙ_,Ԙt#gNbʹƘ{0N-n{ \_()V"< ';1i "`\(rGs'm΅Q G 5HyD^d &35o`AvX3lp0Dt[GcE48>JDp쳴vEWf銩sҜT,(@6KGh;^SQWX^c7q":C./\cEUXs\4,8cz89!ǗA/}Aj^gr7SK@7qoˁOZYT2Z$aTURY㱙 o .<龫Y7yܟeiw WbjF0EPת](t3N#IA.-{PX]# ?`!nȑ$JPwvǙ#/ҕB< ˰VM~^l>ߩ ~oy^zes @V0}k]th: N6w6[ :sAp1øN+1ְuX:1=@]wW< >扠 SԠCa'kuP0g[s.BqⲤf^ީށy~)hUǫ ' ( 2i-@k}[,iLdEUf^IeH~(@xϟo !%[ r+3¬)7|C6Zx1h20ͧzN rzU]c9:@y|gy[~ļcvQ:0l|2ϫʇx{wov@GU y @+Ͽ1X~WYoWׅv L/ O{7Qp0 = Xn-u.Rb &VfyVBmrP݇0?.3{~UFT f7Z1~.F`./E@+R%-߈!\ YJY aMfpX-4&Y"GMBYx{={(L.0bKÒB 8so";4gg p(šȱ$ޞ^8yS;W9N=s_o6o_ 1ukݗϏw0}[6V|0BaTXz17:aC q`J҂Wz!oHQ~z%'0Οf TrXX Vdox1$rxb9]<&k MoOy'YmlE` w: 4%%776 b.ʒ ^̪4`F}d~/ $9_'kUw|Fi) I2_c=#WAm"[e[x֜sl1?<WX"Cx sCEU#Ӟ D;w̐n -5g _Co Sp-=۳j[,tVX+|.èV?=xQG!V'A/\`j" \/6+&:{tZ2 ?OG~PyL< e 9?A kxUWwFB`0RcL蜀B`MϽy@ɖ5UZf.UE^Jj1=TM]":GJh0 ߵn ?-72Nenഢ 4w>}I? >8qw< [99q#(1mys@[IvV1؆y :2)Uܜ$<{e"תi\^r\\u?ƅnaZN) eDY_3[z/9fs魽'#0 !pi1NE |$W0ox$r҂(f&˱\al2qNo= 8Iy9B|~=?鹔cӵؠ}rG> -`^scBi5B10@\ܢG\SR020tx ~Ŕp;ߝ1Kn4yڑҤ$ҹ&M'\t=n03P3H#6S0HF%~@SXGM5~ȿ`E`0x9MKqȥ#)QSzގ7O7e7]Ε4>|ctK߳^O={;o]SGlǁ/;/sfwuz#Pwf[^:@h`>hZ۞'ܪX y?~cE_o\9 &-CHV 7qƒa#3t~< q 87 D'8[0 yVmHNv\`p=mvd(9D :5KQgo;KgssM/|IM@/@8Oö9@TK6`~X!F?oC X>`1H8jMeaoO_-ɡ8al0Ӎ@n8<?pA䐯Nߕ7Vn C}3\@ q1\Ƅs(Pw\U{m;V1ь פ0@ _O Gzc8`:WK~N8/d>3lq"NtӶ}X8 ڭI W:tXsJ;_Su+&}1ً`Ubc_l̀ 1 K9u,$> tx xVEowXGE:i<&|K˕9Bz›0XmnCu޾T《9^[ +qyhg~Q=| eOCb|W8[ P!$Z:/z'r l@ M?re_Mt2>&^Nk1燿9^ڇ@-8^|pdž z¬LphM<[Q% O|.T;ly;v;hػmV]6Ã`n.d J% S֊xq@n;|jTz`x?n `jG~$lc'q_i{D_s@ƍ6O`"aB 0 _%+i\:,'o3=@Aמ;Åh+A/.P2|2[>3:S?mcBML[g2ͺfpnhNђDBjT)0~a87V'g@pAYP~r2c cCa%o t:X+sQ,9*0;7Bc_Ha*P ?_zm%OpN!~WW/'Tk ?XĊ>L=~V>0X~VZёyNG` ],R 5emz7OE}!t@ r!|M  Y7\]/I>wSz|' Vz)td."5 8x|N.BxQ 0t`49] !\^֑ ~M2(8 +]g@ t" C#v^>d pVCIp>z89tvf`Xr/93ӏN?*]@3MR0gaaH`aMޓ2\ĸyWjx}\')+mx<&|h~SxXC=`5\hJtqA73X5oL^7,cdzH,2sSF{AY+VpTdg=r~Z=<`B~^'`m?Mf`sZ(yUb  D:@3< k8yLCbP 8(-=Xus hU!|_Eܖu '4zfU^(e6p>[ҩ?NKGx-El]?ho|+b7ES< +.}a^/\~:.LYC[A?rVDo:}`%*"db+ ]KCZ*v8wzVAs;T9@^UQTcо[Ҏԋ @,|J&ƭbBp"6ṇ̀ҪVDB$&QoKಳ 3W𥤈A]Px"4gZ>|#y]RQ͟;6KdrMJ.$M 8v)ޞY1FqCq 4bpnf.s6sI?:'ha o2GE^ດE^*%I%O?&tQN ɷզ &<i Q!3K FLKY%b@zBŎ_6}'mr-|E@4{G+. N0q>K-TZ/~?blZ7<+W]:0/s{l /JK:?FHTtRP̶/r8T%Ρm|m -LPLvl:Y'7jTئ}`tT1yd40:&tz^~gM_7` edUL7SeZB]W9߾nD~\]0ZC]2:9Fװ4jЩ\CaGRX6HtQV t@L:qq WI} IUqζOU"}'z+X"a78(]m@L\mzdǩw0C2sQ涥* B`/8?_]HR((vcA'ރ|Wf>{1xEn&ћVE ^,ᮊvd`#-cg0%:>2|_ZhV,lƺeLHs: CyN;vu(轏-UѲN?^ЋTKe'vu0wr3O34*vԘX\m¬l6w8{ΙOVމFgR@j Ԏm;<`Do_&$h5E!9C%d~=fb$3oIzLƃЎzϪ9MzkuiL}"37δ |1 5{a_.ϕB+ԕtztj/v񍋥[|oG͟"vQE mNmVA߾\OY&>0= .pS߸ھ쒘REպ0F9/MD:B;%K`=^jG2hI>,H}n^YF f^}=E|^g珽ĵy$PyS0LKw0=_5;/HCXފGuQܟz7x>- !8pSzV7QyTZBH,R~Ŭ[Ƞz1)ZFV+2]DG_,lEtC>!NҖ? l+]u#h=;3re**/*akZ\\ 1CS)}j 30RPOb!6jҽ8Wmc1/e߯5Os:!1"_t[ҳnE'ə)9Ŀ|~J-AuX:v'6 /Yo+[`zAgw!:mtώ nd/`Y3 &kՋ.25i]U 8Alf{@0>87 7v^o=8mWa#ƐyUO7KI؀3δfǍٮzܫm\4u;]#q߲&'"2x,7Qtܼ1ۆ1@2G|n@ Gd]ڦq,~4iL}ˣ?9;\-OsWΐ9p|S>ÓE-^mw[M.b&l]APA$69Q kd c }ϬdD\fa+cђYRV nc;3l)-sGSr_"CA:\ YxQ![<\T-q0F`!0Z0]V)x<=m rh,8b=ݯ ȾPsb秅:1P@qָą=tܠz 9:iVikSȱ2%(Vc~L%aN|0D xrJ*+a/*i4=#$iA#3Mtm#1@Ӥ@fw1Arw`9M}~ڃncAqP@1ST_n6YSh5)y:Խfo6G"MXO:Oـ49r6ݦ6@gc~vs0 U*KvR,-ʔљ\ƾˊb/XX*P"I,[v0g`W9BGS`*Y IBa2m;U|rslͥe+ W2eZ2I܄G:[kz0Z76QVp Μ \b1Hx?86'2wYk:ˬRK|_ܿ˯i%( 赆 Gybzs_M**._Vʌ*/0/wҶ3pud^-=1& iAsV  mtl v `ent7 ds֛?/Ufo `KK˕||P]R]wΓ~2%imZIn ۖJASRa= yHi70,בG9AIq1E#Kp^jS.]j/2i}837Usj^԰Ǹ$z8fZa~)jM[ `T96Pd WJ5T,^gL-/j Yg[I*1f~v<2̳ [+aV%’+1?2uB r Qqvf^5[Z3r&9AIMۥmz9`$@|,9?0GC]93}~_Q #F9r^e91te[Gk-J(O%*gjÏ]`e?XH 4̥]7k.2|$$%3<20AF 62G Nd~n6=- +ݺzX*5M16ȉ кЛ̐k pPOOf>Ĕvnβ+lf2cф-ܔAP-D6d`;)Z@Il8Pxqfi|d7/2cc~V]<<,"jSOR|ZopQ): LN,h]ШLi 1P =+ɽb?<),ElhAb,DT6Tw~"k 1fiV>}O۝^<Izhzdla PZyP(Y_Rc|j__ Iۍah8z: wdbXhEoW{Gd%tjXYچ0Vsn2c:n/,ik{nзh)*Msn̫{6MuUz-ǫx$ߗ{\{~H4D&(0VCd~3c3ͱt29iՙ# AGAjcqfg~Ba{< |Ao`VX.68?k.,l: ؽM=J+*3!"n(#/akz> P .Zv' >4vXYmw"aX L`)Fj\YZ#zR&u[Ó4#5Dp)1;mR@NfUv<Ē 17xGzh}2nDk}c6 Zpm d!I3n,7jػ5c63;/ 1+^>+l~o66 kX$Orr;p%[!5UJ1A6}(V֎ s`〫W]Wm pw y,2FYI"tFA3x;r8Ix,lgkaqIƼJ8I0mNa@0bAN-v:=A' ~|8g`y!d?KbN5ϣQ>ٰn&TiU!۪`ٲ@,}~F3&l9A 3CГK G%$C(s;u;Щ*Rx1-~hk˳2XLjY!'h%ZB~QJjO߬3H$@~3HȢ#79 knRkR֋7&) (ëfXesUB:LecJAk0\H`9~oeO/ \<'!X a9@Ⱈ.ߪ,ޭ~&".f} ߗ6fe~.R%%6P(]O 8g(ٜW_B:#.QJgiIQ,T< 8EE5a9m,uН:EDfʢK*ڱL`Up;^ª8?5U8 Ua+W'LtRފfuAZ"ae%A䘤GhjDaEY !9ή TsU2O%g죑mZlZV1\D=*upjC$;֡8RC,5]ᏧFbZ9ܺ>oQ&}ۘTFJhgSqIEvv9 ]Pzn(L[WVW)b`{yCػM2Z W"D]\9-HeL ՔKct!L6'*L~@'if@"5\69{U|Z3Csǹ$̘jAsP6 HkhV:0ZQS9,XzQrR0@X0!Vh2WGhg9fMƫ8ĠH~|^_rҋG~S洺Yf!T*pA6ph !'g9s~]`M9"-/&vƮǛ1CdNKZ(BviPK@`4wj ׼fOi C7 @ @`Ucu 䭄Zuf!3L11`a KLߊ,Y*ʤ9߬ #;P h$ZڎsJ(ByAQ>kF4`eay<Lʴ* u-rs,I4k m`pf1HAruKSHr 0uJcb6Vf 9*_q:g-h N_-'Vbme:A[h[0$BҕN H% J1 a3LퟧndHa8t by?ρ1'&U~ c.هϘVE KcxEs r^UFKL lW'Ukyl{B4hJ+ܬz y˽DZ/y鸟2EUnKI %xQzLT x=lN-e2&XU[4~ƖF:E*.Ld}S;*, -!Hj,bg Ci y|? ZTҒRSy[@P'-sXV`TΏ,PDȗya$<MLLF"ch wمõ Ven/7/20JTȯ=UAh~^aV= r?!gWKRfrz掏SIPhK6k6z(5ǂĦLr. mZ V&y;ʿМM^Ab3ur 99]$7kZAd3PF$~6+`d_ v0dn7 =NQj;UiS̩*\]k 6Sz rEQB3AH􀵆lF2Bza۲IUZHsqn49h{iaR`-BqZ 4N&Yl3b08̢K7ÌjD9D MzZb_o rs'H2(Yn-p3Ó ɻ5*iPM]B' ~Z]ۢ1mwIqKC̰֚ױ! И]^PF7*iҡJLЋ%*j`֭ &+Wk-Z}2ϧ{aJgiWp^ 8Hz\L3;m9." fL Kn:ڣ7 sث-kAs! FԍZV}޲ӹ4ec]΋G\ sQ"i"vxĜWsCNJ1BY%>fpCkE0JNk3x,u0sXӧl%=PksS rviA vyh8t5tAѤwy X`ߕRaT??Rr~VjRClA, yDBkm6H%f\ (Go5{ L^7_aD+zK*2YgmBkj7"z[@vu4 =Ohj5kZkX=Qiկ]..$L: V듀s"CŅ=V_ 7viV6<2_ث~g2G"HӎWkU#@vW 0Es)($+Klu q *. :/YkkNT`l'UA+D& 'r8+D-{PU/^,%-T&9Sл"|_ PÏ\UO9WOZE/Z_Aq4¹K)Ubׄ.n֔L`0L-5z  $n5L02%dGQu'9L-UL~:Ej%YXRk2>ߊL 9DR0r97 &%\9)ol* [ 2|9FR& S05KcKsbb2U0+HQ!Rzh 1iQ\2Ja`Udf]%mo/GX@"9?c^h.{$ءܨ,5ZN z -k&&HXa 'guV02YOgt2ga+̘ZNr3ū;ëhnmmg/|cVuV>9 ʨTq9™@" (WXOUI[Ri<چ̒}\6(⅊9V%%ׅZ j@Գ}R;=d6qL@y5{[ SXnʘ&$ʇslCW:^p wN|1S~|\^8yjyM"J^f_حaTP3ƶjUC%3J靳Lq5.pY%hIQ0o<,ƹnpV{7?,6P7v#`}qvxMeW 7 tu{aGk薼`X#eŪ¹Ef]-jjp*Ρ##T*6M[Gq$wqܥ2KV\ >TtxϚ(bOdHYO f XW-2 Sױn]/y.})n2L:Y!Te2k"R2;M ]i\*!l9vx4/K!Zq=ai+28QK(@V ^u9&͡iFJg|?ɏk qN@Ӆc~z|<|>Jkn|$t<-#}f[nrU`Wza_?~:gaؚ%?Z}60~ٜ:c^сx|pB xL] D=_yCMk0au(ИvtY3g2< ebxIk5Eי%ɫF̬@D~9Hj*+XvgNX*%FVh1:ddTƬ =-8cp)ʪ>'Nà<0KxZ5Z.A23*zYEd֙r.f0/K $Tie^2TaÊ*awU,09kJՖV%jn5côiu 7b}pICz {~Qa$eZc"8fGG( ?N&YxR]ږ^8$VeV'`xrm\T G]4&=Ȭ4u3Ӄ踱*/dߧmmd;5P u0w\B%JfYZ "*`%ZUiǹ`ߕ D628 laq L%wP-5ܭb/\qqyH>Bb3,cMMc"\@w"6Y)L G~$xqQL߆H;L:Hk[m4AFr%#Z01=.9rC"Ufc ºęR$LF˪oa!-Zfmt^nmG4IMwQz}_@zK`bp.Vp b8L ;Y XA`̑0kj$l fj?8adڅP =N2??H@}V}%$NJb[ez q*]xL 5 )*єu2M~:K` +Z' [3~'oh~ cڠA_ `HpNuajngzA~FYqA'cƶs\1}?c).j^/= Cou: kӚFocmz6=_SV+cءC "\cBKl&d-<*w mΧmafʦ pl7ϷRs4Y d^}XB5gw.s nvu}WB^t,Ow{`BJahtp0h&$ H*ZWuKy n[sS.* CiÕ9gc֖Jxaz6 ҁHe;QNZ>B}cvfZMa+=E'a8txgJ j0Frvd\܃N{a߶{|c.$#hd? y_;ݡ9 SwQeNi?m*t{|,qt|'Fpݨ7$~7tu`wLI a<IW0&_sV#A]tz=GȮWƧ>gxZvZ\.gX˭b9:sBME 7r<|`2S7R;8ixsg^R,6<{kL2/W[s> J2pik3;/̻2r{  '*#~̵ccC~p֠&1=Eoߘ~QF\K|.]++Zk5w͝!وѦ8mJL7{Ϛ?<˹K7Fm2̯Ӈ㉺qB2AvTL4 j*eH01=.l-w.ut`Z\v|,=)}fs FeAСsI@^ZlÖ>3~0b>X}_)#)M2x! Ho=4<6VMP>^tRٸNMpAlI ^vj\:tj8WkZdE&!7 =瞮 X:S'9>2@ON'QzƅZ*VZx|:*&ׯ;O>C #xn05ffb|biLH ѨTqeNrj#E:-XrC C[- 8k/6`Ui~copUrVҴLV1fxu[N+ȜaqCXi"d_,|C.̙^C=W-:B|2C0"`]n<~Η,mm׵x|o[a[w̰}S6=<4Zig&m`_+(zT8[sю[7tDh4âݐxEs}H1pY$lN(f $0 |0~'jwhXrGyg"l {~B2Mr'ufUj{E~9M!pE5~ eka>djE]١XwZXEhRDw.a?4s:GQ rgȔn( VGFC%t^-Zy۩ӊ >玒Z+Œ˫ G`ao:I]QPV%-^G 4塂 5} V pEá—/rmK|^_嗇?9?{_|rv{i95qvޚg]gBsimgֆ\aV[UfgZձʼB c Ȁm.6[ f`->DZtP*.s5 ޏVa@mA.13c!P+)(ԕXbQRJ~Bh ,Χmp }h |Üli.@La(@_;+8tS9(,'m,Üĵc1BPr}`n.8֔O3K, %DW Sւlce5  -VmAFքX:~x?+}߿ܟ|Iiydz>Ǐ9|:>h3ۿ}tn?gWYP2glT?L'x g#+bfevYsܞV!|u_BӹvpBH8@R-Ս@KN|MW*e&lvqg,pEl52 gu{(xZΏ;Ke7*boWC/v昻+UNbMݐ. ,4mbՃ-3|+Z}^NOÂj+1lkZцd Y`<a 4>1Cñ-LWɌ0a^`k72BcZ`o k\gE@y>b2\?|_9~žόÎG+r?v#F Pⲫ*lut\5A:Rm5wyMc`ns.(M*2FW2j"jܪ\Sxռ5\ƙo|edz'-G]\h/6Jg WAw|)f $<n$/R蘘 I gJw̕ .Ȗ/sRRpAN[guXv8x`TRk4W{nV9wcQ[eXf4sfm[- j!m=DŽD~ Y ZE.yϟ??fe1ӟ_ 8ϟ)֯G$AjuYت9bADtϑ`S,n9Ngͥ1/}k3hr!]B9ai7/ڬaY?jnS6.qqɋײ_*e*3.©nd:<~h#X0ǸpPIGX(`n/ R1J8"7d4Qf,aJ~F7 8q&vAr<]z%3Fv*Ė MgC$HPl@Pw hIˎ {f"Jf}QO#m&*h=W}o~lP7ZJhh@moDkb)C Ֆe~kǖY0alSBkpXAΏYV_ eo[/NVUhTvM\  O45 MUٞ:6 ޫaCX̶6]8hdaY\Fk:l>}qDܜP\"?'|N`0t^0|pE輥{,T\,Z'L\3 cC^%-HlkUV~pQu0 {;N?{? LjADYsy;e;#%uf*;@Ag,^٥{ͺ:@.sZ;K s MN5hHŎJk v ~V8lf&J.OЩd*>G&݄)Z5WHoD0hwSfzbk@^{O¾?ܧ?_}+nP|?67M)sZmE+/`gT!OG5eXz+^4Ձ[ 0N6LjR`l1<'1[7]e>Kenl&㫶'341VM9Z `vs)UY:J\jhI[= #0nh=w0qzWh0lPaL4KX8`W)O6mPXv!$^+yI@tY.u8AE9<:aj.-\HQN0̆@Ws.Ov^rx߮_WyG`gy{yG~& 0;5 !5zYEF ͹Wy.uFYeI.*Z3X*cM֪/yH{}T)w<֛5^EvPS0*,᱁wmg\;m \5Lj"Wi+4tL'DX &Z,Z2eJj'^H]% >d9#Cod|^!Ktfo8IsU]' i y^)@!5X"KjzIMWɋ$'D8 )!w F_ׯ챾?nq_Ws!}'H.{u|mOO}qþ{787 󳀑!QI+f^IC#8J?\6i HeUss}K| ݊F|jN$9jϠ(hEr}-[0*/;:#_:%:a6H) 'H[XЀUEsv JXUb [2pxdIAt~܎V(i*izK\3Dme}$W0CiQ0<24 ƅ,9NDg-^.V:yo)[\3ǰg|{ >}#|zyQ?{(=1U1$BKliN" 04=q;pW?/? 10g)'5+jn]Wwybuտj%5 n!o!$6=nE/9"a/Mٽ*:%x S*_r67-]C[ͥ3ꢽ^EF^ ޿TPuyR@Wj CKay(8L=YuAvW}b]OiW%[FF"H$b W ^1"bD#^1rgOթsxH'} n*NUK1 VW51 9Ӷu=A0EC>"3 04[>"<24NQOl jy3E~1Ӭ B {+>j}mۻJKef>-t<oN_G@!x>z{ǝN/7{-$:o_um3RtIJn,S!#=Vé(K'q,4~[/"ҪS\-=s`*u}eYyCDXNs!opUڬna$3Dr,%>ZwVck8że]W9.ּFй}3UdY.WIzސquy_+&\$fJQSD{1gKsIqq8_0rL fSG͕SI@Y|CAތ'$'@SԜ^~o7W< 0F9EfHۑn;j|9Kydjп6s)H>kO{"t RF :Pzv{)g}`L.o(/xoǃ.s=~u @W_F(rRMϹVOZ|Rtnl`28lc8q{n=>":A1~b^_1o+֯ i)"`Pja*фAX41)/i}ĈYddi@ѝEFBy]z$x[&"Wtj:TR\aԈ6hsż-@I$GF(NIuOw{g"=ڞj/wM< } Ȯ, e ,*G͔yE.0 EӤdߎ;9ZxJfn4 fޛBGЭ Ux]"AHZdxjE{}fQ40(n { rs|XZe |fG $+|i7Cf;F`HL.T ћy?1e1>\וֹI \v>!ED>s(#)5>2cYi0,ii Yfi[|`kﳧav"ksC{$씙2]K.g(1"tWx|˿#5U^U.T]JMubOgV#rv>+֙i8=4 xSTMfe7r#%guuwh4tix!)1}"4I {;>#>Bjxs.ns`ů+akrlPX@8ҴsC3EL'uQs Q;*gS8@33Fnrn y\37ߐ^/ã߼G7zC۴7<}tD#M3?c)PF&~3 kFvzЕx`PU4хu$TAuc2R$)0R{Y ukS^32.Mw6 F6 #O8pn83֙ >= v4'5Q)% }͵47Ps"Ae^؟nX/c)L<DfFAG/t߂OQNXJI~t^oo[|X]k54VM]w?Jϭ=6&U)D-n.Ys&3E'6d}KLĕWG2SqDv5$Us7t`.ЂWW2S似6`nE~J/B6(4@^&FEfHPAZZV@F7( ]obim)N^CNSC^Z[0kl*s(/Zl0@ؔYJ+] V=@KBч ST9-wCeY{k| $lDMFCd]\Oo=e5;Cߏ0Pamg}! ¶n ;Q?_|#?$D1:G-7? j(~"d}tKo^dF4fy ,ݰܶPQ(&B7 yH?͸7X}kl ǀ5uwzc7yQ:NxMyꘓE^K4y].%}\+2jH5ʨQ)QkNy ?%|'`44hRVuYƇIF4!̽f\Hiz{]R\hYzT9;S9K80DoPM)tuDZXS >ްZ{vrGoX\'31wuKj0;DB<#u`Ci8CpJM0i~v9ߜף>7 k@)%߼aXM7%ܰI`PI(veĦ1ƥ4Q*$Itُ!Il)ǝ*6G) I-TPzq%oKNkIvӕi5A6o0qR&')v@j Pbr๲r6R>V*p_D j 11v!%gYFHHE撀`mc&Q\J _H9AbNxYG xS|5q޸Z &tYM0]x&O9OWG=#`y{ 0ہ.iX{&&gvڝ;vCv]kG-}WY[YY&gt2UKx fVOp嶘HY{d07iw.>µ+3'wJ`3MdI"e>#,T,Y=ڏ .|WJLYs0?Q,I@.el4a4D "0.qYvdnpHpkP@VDSHQ_ns}iϿVx.5tU<pd~:G+55\n'4Χ)= -moXr\ib%A}?ތ}"bM7K/Gp?x>p0ow^#s~XXSv.hg_+cb]~|&>@Јhx55hĦ01m(1]n VF[>ɚkq':0>b Pj.zTs{8ZJ,nDW],J\e&npA~vb©==>,m2 ?*;IA:2)KywFm:Ws)D#6$ Rj' AgB9٣D ͿՌHQ`.StuMsf7!cfl[S@M>e 2w!L ޮA!L>\^sΰq hJ2@<=!֏Z~/M@yM pKU<1}Fs4%>x __6 :  );۾s/(~8؃sʁ8Ze1d]G 3_ݨȻ)Ԋe9x0[5ҦI /6%NZD6d81e2}gI`AOtS[bx!GT_Ǯ,!6B6=nan:ǮuN6+ ' '5#LBRp++!ZRbR\%4ؔH] *ޜht59aB6LcWÏ @`J e7=0hߛE]2?E^ #!-yfIz0Pw|J5˻ǹ &pS Q7R\Wd EQo!Ƣ)hʺnkDdTGזh,:~-#!=Ypa56-:6rOafNa&n]l!2U#ev|.H%>["S4XѹD\o]]HOpuAJwFP8vVxExXT)gx$} H Zhb)LͿY#wfX3Dm8FFDcp BE&Es MM\inrX:(g$x%)1"#~[wo*s?(Gz,"=\׿޹9Yd4K %urTÉ>J2Z8d]f5"0S> ^ZǨoILAUoimX{eDXNvqgakVɊͣMl4#d*4꺐//{ۡdHn^)2hj8| rJs@Q֙7>'r̜=ɳ{5ps^19شH֙V7~' m/9 j -nKFMa#4_͔?(k[|/^l`\%_GMta9\n'u nsSv@ ]$HCo:4i 3sGD*Amk vܘ0evm%׆®FA4U%j.Z7 Q >bZ+Ӯ+-Ue>]w4ѷ; voyV}(?F#}@$HAq]vG{mu"@r&LzR\ i5BU DWcd9Qz6}Ik9׎KOOCGxu7S ER-CӁ&^)ҥi{<-:j JS [sC 8e~B,Ah\z:kQ"@,XŌj |"E{ۨzטS!XxkEڞ*J3- ХT0Km`7)B"C(c!ڠ? 2r^ؼFx|\/wⳏy:q=r 1Ҝ'\~[YQZll疗`mo D ִQ.@$-F-zF}?.א!TJ;Y{5uI џXQխlV-ʋQc76~m?QUwל+퍓JiIz~UL)SX*Y]d#=H~BG&@O?i+]M.%w~* cHN7:fiN0=bANx움hbIԵ zL 0l=2cip4!Un` NmWk֢ &sbF5Bz€ 6| pI3򵉯b\ay7:khy|8o&<.1k}Es4kG4o&HO&\Afys4zrÛx|0z"1: [؈d_ T9 tGoAUIZѽI?*aS P5Fp$2(V@w%i Rm}a6xZ]E WvRiv5HuO8ZjqByw_x_W^kEfR햢ﯪ-VWSԐ(8qTtd˺֙\9ԟddEɜes4Fq19‰ +wFĉ@>n 9K#`Cь3"2 MofZ i8%0Ewi^v6IpZ7@(I^tG&AZ+YMi ;p2#Px`>.F z^ctunAdo w@{HOi/fsp~>{!D諌w#<9FuװEzfsk9 |&=yCui}΍]bSU+ nã2ގ{gFiw#ѐA׷h׫"4i,$]ixyIN] բML[$I膦IH?7 "9C'5LQihaXR5BNx]ꋙ!U@Mt|45ԐHPB=t(!- b5JQ;DTX~1J[LiTU`{bvB7{ˮ'eoL;X `~Nv,F7$ReSgs)}0]ȷ`PAgK7fBZlGf\KK-٩̨VE%Zng-?3/jGqŲ226fCXMl KͦL<J0Az l9w1Dc9Wl5`4W(?t!`#su !t揂xٙGǗT{yb2eaҔRɬ"ި)7i~ޱ#1O87sss-?'sZRT^/gXCdz_V2bPj*wt"+* | EOqXOM( ui9HJ ?:Ldl4fi',"2,CA<p05 STRYN@YSé0g rh1Yig m2mkWfݰ6ٙgLh:j -:~ިfص3u@!6H ՛f.4Ez9ȭrC$YD7V?vLMI.bM:_hXZ `t58˷I@92Qǧ f@ξ>]E/4Dbѷ̀^ֽ=Xzx5mfp$t$aA4ؽ3J@Mܬ;X Ή ;eʍ{jdxʺĦQ8E씖$ּfvƥ<v 'N?YRuKeu4B_Ė]b u}?!i4H93̉ .S32$vU7q*)1j1k6Ƽ𴚀H78hoI2-EF:=+uٽMQ":՚w6F># D )6!Quzc"%Rp]/X Y!}cg, nkmźk`t TQEwmI`FS^~E`;弛q'R E>̕GΪIΞ,,f'Ex Bd)Tx8*t.2#L'BGH؈oÉt#,u!@kܷ6:H=ʀj=?8yaZk Z74.U)ϔ9m=^L~k)b"]:&[u<1&HZ&`ۍu.gY`ʝu4Pv}G{|zNEi.&- qAC6v[*L##/fc)q}_eJF@M qs 3jGke512=QtVHa9͢hh4hi{ԝTjL"LQ)BWO({o* OFxP hۓ:gIɨ!)\ XpR-ϢJ-KfԐG.L\Z\WMcQ "7sd3A+;PaR(N9Rg.ЅJ3#> (5Fo(8##up8J(ވQd ݠz٣=>ĭ}鹼=mEŴul3덬N࿾F5<o>je6{Ki Y*ïT42y ϚLbtcksDlUIG>Q Ӂ}=v7ss5ƴٍ}P sN*6&ShJus*L0dA|"Q"dz"+FTzR)H1|GvkLOA4IhpQ(s2KRhBA$HѰa^%kHt si:>Fr9r.WSHa{5x}Y5 b0xo""ÍA) |.?{X= ?N tk u&)Kh l˹gCGaBOiD ,DM}{jX9o1Ɨr4v% K.[`LMgۥfTIY*ɺwvJsqHtnpDwg5Rr62wj>8OC@|̎N'fܮ؍$iCAg Lv"UH ّ\z U:R^LљUxLp^F@((4R]f10\WZqLSz? >#xr 퍝o2c~HrÈ{<&?>"(A~hH7Wys,F9R͸ q5,D26RkPZ*%yYy8h3W<]}}}}}}}疵׽<}}}}}ޖ}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}u}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}׽8<}}}}}}}}}]ƶU8]}}}}}}}}}]<0ӜӜӜӜӜ>G7<]}}}}}}}<疵]}}}}}}}8}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}u}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}ﶵy]}}}}}}}}}}Y׽u<}}}}}}}}}}]<6O <]}}}}}}}8u}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}yu}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}ﶵuy}}}}}}}}}}}Y׽׽}}}}}}}}}}]<6?78<}}~]]zޞ]zƚ֞=֚֚֚֚֚~֖ƞ]z9=׽uY}ƶ8]}}<8q6G8]}疵ޖ~]~ޖ~]8Ɩ~9֖~֖ƞ=~ޖ=׽u]ﶵY}}]8Ӝ{{qq{678]}疵疵z֞~׽9Ξ=9Ξ~׽9Ξ֖=֖׽֖ƞ~8ƞƖz֞֞YΖޞ}]8ZR{qqRZ.? 8]}疵8Ɩ֖YΖޖ׽}֖8ƾޖ}׽8]׽U4־u׽<}]8}ss{Qqqqqqq8Qs{&'Y8<]Yξ8888}8Ɩ]8Ɩ޾}綵޾֖8ƾ֖8ƺ֞ƶUUYΞ}Yu׽޾]<8s0mk]Qqqqq]Z{0/ Y<]8Ɩ]־8Ɩ־}8Ɩ־]YΖ}־֖8ƾ֖׽ƶUUu׽׽u׽UYξ]______________________  Y,* 2K:)EF!G:RD XWX WX  WXWX WWXX   W      \ \\ \\ \ \\ \\\]\    \ \\      $C[As{Hd^ {@ŀÀ̀Հ@Hb [ cIJKcIJJJ [J [kUYƖ׵YZ׽ߚVQZ9ZRIJR kRRRNsZZ c kb{{ӜY֙ޚZYv______________________________________________    #* 2[)GG)) kQT|V$ X W  X  X X                      \ \ ] [\ [\ \ [\ \ \] \      \           E[BcMD_c@«@´¼̀͂ՀCɬIKkZ ckR [sJ|Ӕs׵UZƚ]YY]YU{IB JZZ Bb cZZNs kZZNs kNssΚޜΚYΎ[______________________________________    T4"1JBE!F HRLRSdXX     X X                 \  \ \ \ \ ] \] \ \ \ \ \ \\ \          _3 D Të@@@̀LJ ck{J|sOcӌԔsXY׽Y֚YYYY׽M[JJJ cIB JZZ Bb cZR kMkMksЃQVƛޚޚZXޒ! ____________________________________________________    4*G!bMk9 HG)1rLXW                            \ \ \ \ \] ] \] \] ] \ ] \ ] \ ]          _Z _Y%A{@A@@͂݀ՂGLϔNkєsOcӔP|׭ӌ׵֚ƚYZؽUQNcJBkZJJBbRJRZ kRbss c׽֚ޙޙr_____________________________________   =N3I:kJ! FF1YքUYV   X W                     ] \] ] ] \\[] \\[] \\ \ \\      __D@@@@ŀŃJO|Ss|kӔֵԌU֖׽YƖޚYY׵\ԌQRKJ [bB BRZBZRZNksZNk{UZYΛ֚ޚޚ֕) ^____________________________________________________________________    MX91I:IBG)I9E!FAa լ]VX WX X X W X                        \  \ \ \ \ ] ] ] \] \] \] \ \ \ \           __ U@Àք|U|UԔYΗ׽ؽ֛׽ޚ׵ޛ׽ӌU|ZJJMkZJRJJ cRJRZbR{NkNkYΛ֚ޚӜ!B V _____________________________________   U<Λ!!1ГE)YjWΖBZRJ :NZ1      T:1) *2J!!G1AAGbrɊt  V XWX WX W WX W W W W  W                     \ \ [\ [] \ \  \             T-sBB@@@@ ΤUUUӜUԜ׵VƖؽӜؽYƗ||Jk SՔRNckRؽZYΚYYYYYZXΚXֽUb)  !bsTD__________________________________________'ZTRbjscSRNRMbb״        S,BD11IBB9F9G19IRrzMRV<V VW W X VWWX WXWX WXW W W W W X W             \ \\ [\ [ \ \ \ \            I\{@@@A@´ǬάΜќӜUӔVUV׵VӔ׽ӜV׵ƖY|NcNcԔԜ [kԔ|J|sؽYYYYZX͕IR  HJ{[___________________________[>uSLZQIbԃRscRZRORLJZ{     U,LSF!19:IB9G9E!1IZbz PLVVXXWX X W WX WX WX W W X  W W  X                 \ \ \ \] \ \ \              DC@A@@AAAȴͤМҤҜUUVVӜVU׵ؽӔؽؽVƗYsWUOksWԔZkkZ՜kصؽZYZ֛ޛޛ֕ӤF1(  HJsO{_________________________________=t׼Z9NBJJ9bTRk)) 2JJ BIz     SG)!9BE)19IIFZj HT$X  XWVW VWVW VX WX WXWW VWV WW             [\ \ [\ \\[\\           sBs£@B@A@A´ìʬФќҜԜӜӔUӔ׵׵UԜ׵׵ؽQQՔRUBRc SԔ{V׽ֽ׽YYYXΚX֖b  D1{jNS___________________-sS 2: :9AbNkVcPJBBM*)bU U T U U V V V  eE)1)1LRR91AAIER s{IΣ U W V WW V XVW W W W W W W W W                  \ \ \ \] [] \ \                  \sBsB@A@@ȤΤΤФМҤќӔUӜUV׽ӜVVӔؽ׵U׽U׵RӔؽ՜ [ӔZVNcsVRؽ׽ŗŖZY֚ӬJB  1sM[___________________________\Ut|cBBM:NB 2 :LR{]QkRJJMJR UU   E !)G1Ak 211AIERb{K|+X XW WWWXXW WX WX WX WWVX WXW WW             [ [\ [\[\ \\ \ \\              {BcAA@@Ǭ˜ΤѤќҜӜUӔUԜ׵׵ӔV׵׽׭՜RԔTRNc׵OkRԔU׽׽Ŗ׽YZYӤ) ss kP3______________\d R[jRMJ B 2N: 2NRs{jMRLJZMRkԃ   V UW V WeCF)G)G1RR19AFRJGbrǂ ҃\V W X X WX WX VX WY WX W W WX VX W W X                [ \ \ \ \] \ \ \                   GdkA@@@ʬҤӜVӜԜUUӔVԜؽVƖS׵ؽROk||s׽׽ؽŒ)( 1 s{cZ______________________'U,k JJrMbMR :NB BORRRk9 )9NZ Rbc|     W~J)9E)BJHB9AJIQrrՄ< VV V V W WWWX VVWVW WX WXW W W  W W      [\ [\ [\ [\[ \\               5lDs@{@@@A@ԤӜԤӔӜԤUӔUӔ׵U׽ӔRؽ׵skRصԔQU׽ֽؽؽ׽ֽ׽{ F1{Ѓj,_________WTtKR RNRL:RK: 21 J JORB *I!J1A RMRQkQk    U  u 2!K)919 :JLB)PkU֜R 1I9JA)H! 2 BIUUH900Szr҃)ĻK͉IL Ԍ I܆jsrrHRFJ|IRGR΋Is{rƊFHJ ȂL Gđ̌LM ĉ ՊHԎȳ Lč μɣF FȫKL I ϬδKK H ɃJTD [ [ [ \[\ [\[\ [ [\ [ [ \ [             ____________________5J9K19) :9MJAAH19׬ RA !1)1) BTUH)1990YO)!1H9»ǣF̉ 勼̋MI ʫǂ zrbbbJsA9 kAHbF{jjEFȓ˛ȚKRՏ݊H K˼ J  ͋ HˣʛM ɫGFDNϴK˴̴ČJI ɋ | L|tD%  [ [ \ [[ Z\ [[ Z\ [\ [\ [ [ [ \             ______________________^.\6JjY 1! 2 : J!1ZbQ{R BA ZbALJQc :) BLJ1H) R{! :I) L˴ Kǻ L̎̃srGb {мR0{Ib͓DbFjzGGʋ˓ MGLݔ GćȳʂΒ J J̼̼NʫHłGɳūǛʛȣLOĎ ʛ˓ ǃʳ dDY  \\ [ \[\ [ \[\ [ [\ [ [             ______________________._ߦJ* S!)) *!11I1c՜LbG19IQӣ{U{J :MB1 JJ:IIPKj[))ʓʹI ćH܊̌čPÕjjcjbb { ʛ9I{ k 9bBjssL ȫ³ԅ̇E {{ EFG JdzJMLLKĈKțțJ ɫGɛ ̓GH L K Ǔ{ M ϤdM[YY  Z [ [ [ [ Z[ Z\ [[ Z [ [        ___________________________^6P3 JHA :1Is :!1)1!!Zj)!H)QQ Z Zzқ[ JNJ :A9I99H1G) 2 B Jʓ J ʛGʫ ̳УQrr { sjjCREbrZ KZD)D9 9KEjZ{GKJ ԆBFJLĆ ̋O LLʫ JJI ͇ ʴ˴I IJN KҼ ͏FGNRM{k II N||U= [[  Z[  [ \ [[ Z [Z[[ [ [ [             ________________________&_C!  1A0rJ9!9 *)!19{ :H91A9 B BBKb jQ BLBMRAI9 JK2L*K21sWJJjsI{ʓțIJMMM{P͓sHsZrbQQHbGZjFZ {jA1RJA0 rGrjrEŒƒK˫H݃DԉDF̣˛ ˣ˛ KNL K ʛ˫ƫGJER ͉ͼ KQ˳Qk HKIII LʋMdD  [ [ [ [ [ Z[Z [  [ [ [ \             ____________________________^_oOSDM") !H!1IԋKI ! ))1(jPc11I)I)I1IHQQJJRYMR :9IAI9B2 1{zʂ { k szJ G { s{{ sσJ rrzFzIIbjJbZGbI{ EjAjDA( IKjÚƢȚͳ̴GDĄ̈ȫG˼Kɛɛ LˬΣėݒPs{Γͼ ͋  ƳԆ HYO݋PRϓˋJ ȓKLLKOtdD=Z%   [  [ [ [Z [ [[ [ [          ___________________________V_11H))J!1!HS *J! )I0 ZQs!AJAK9J)J9AHAYYؼԣrK)cRAIIQ 1{Iz{˃I{IJI { sЋMk cLkJs {GbHjFbbEJJRjEbjbEjrGs)A90FQjEZBZrzD ECǫGIɫɳ KTM z J ˣɛǣF̈ij»ED ݉̎ ˤŎNݔѬMJHJKLK\5Y        Z [ [ Z[ Z [ [               ___________________________^^>_XJ! )I)H )I99HI!9b9M2)JS!)I9 1I!!) BH){פ J œ 2L2! RR1(׬NR) MK ȋ L {brj s kIFjaiɂjFbZbEZFjFjzY rȋjRsA1AGRsZRCjzǒLP܃F E L J UP̏N˻ ̊Lȣģ̇EDHIIONݒ֓RͬK˴˴̴ōMOT<%Y     [ ZZ [Z \[ [ [[  [           _______________________-=E\Os!H) !J!9IA9I)AT)9WIFJ)! 2 * 2A jsMRMR RzՙH1M2 :1z9:JIɣʓMc [Qss ɂHjr{EbjrDbCjjjbHbZYjssID9 (AJYQYrBƒȊлLIDŒGFȫKGIݚLIʳȫR͐ H ĆJȣćųE͊Ԅԉ  TMPRдάʹI ʴˬˬϜdD4$        [Z [ Z[ [[ [\ [\ [ [ [ \                  ____________________^. 6UD :0Ir9)!!J)!19H) уQғ9rX :1! IAzLJZkk(rœ{RkRWcH1 J1ţɻʳ LjK[kLssIr sjj FjEbYGzEjEbrˣ{jRL {0(9rZIbbz zjzɊJŒP˳ L{΋N͐Q̬ ĻILL̊Ԉ̊J ԊԌPOKǫ̊PѴдPQ̴ʴKLŋJM|D=    [ [Z[ [\ [\ [ [ [ [          ___________________^Z%VtRNB *) : : "1I19 B J J J JG9I R1OB!LB JbA9!9!!DG)֌|I)I)AZk JIQKQE(1[RJJrMbj J Z ZQHǣHƂFjrjrrIbb ckZIZZ z krrdzĒ{Ekcbb˃ZHY ǂHAA9ZzG˻ȚŊFĻIʫKJ L ɫzj kZKbbK ɂ ʣɣɳH ŋS^]͓V\Y֙ZQMɛJάάk< TV            Z Z [ Z [ Z[ Z[ Z[Z [ [  [ \ \\ \ \ [ \[ [ [ \\       }{MjcJ::L2R)H!G)AA1LB BQzj ( I!IH :LbD9 9(!9)JAELRV1 I R{ ZA9QA bP{1KBRrbjKR J91E) ̼Gąs{Errb sHZJZHRZb {Ij PKĄzrErz{Ūdzɓb s˃{AAA b9B9B1ArGĊ ǫƫŻCŠI̊KɣNJzr zzrrj zsJʛʣ̫K ̏dzTUXY LN ՏQՔT՝]մWKͼPdL$UU          Z Z [ [ Z[ Z[ [\ [ [ [ \ \  \ \ \      \ \ \ \ \ \ \ \    ]  6T BARRZLBR B9I19G1 *M2)IIMb Z{cOKPCL: JjςK1 )9 )I I1TjI9H9rsII) B)9ALJZKbj R JA9!F9 ĉijEʫƂYHZbjGbbjGjGjHzrHŒĂzDjrłȣj jCRʃLaCI0R̓AICYDQCAZFFNŻCCzĂEDȳ ƫ r {j˂j sjrr {Is͛NΛMQ KMOOSN݄ ͌ԎOP]]YլQTXޗދ N̬M|T#T T T U              ZZ [ [ Z[ \ [[ [\ [\[\ [\[ \\[ \\[ \\ [ [\ [\ [\ [\ [\ [\ [\[\ \\\ \ []   [MVd1 BRLB 2 :MR 19I!! B *:)9{kQ{ZMR1RN{AI9 R֬cH)0( 1RG !)! 2A09A bZZAiiIG9I19Yɴ FG ɣJrrzNJzȊǂzƒĂFǂHHƓǓjJˋDZ{ rjHj0 FzXEqQZrzγ݊īšDqzƻF Kǻɻɣ rJ{bZb s { ̛̣ϼ͓KԌO O ɳJMLMNݑP\\Λ٤TӜҼQ͍ ݊ʹM\vJ       O P OO O Oz֔ *=0[kU}\[}eeEDE$$2K            OP OP O   }P"[8ƚs89D!e)))))B!1C!C)B!)!)!B!!1)1)1)1)1)1):1919191A1A9IAA]}<}}}sIC9!1CIAIQQIZIQBAIQYA88CQAAYZQ08IEZJRBRQCA9JHRQAAJAABB1GRR1DAb(HJK{ BF  FBØ(   CD1͓ {GbRAD9IE1IRDI r(9HAAjGQפMJMJLP  O\yΚκEeNeEDD$$a%rt kq        O OOP   OP Opv}^ׄ~yY2 [B!))1)919)1)1)1)1!1)1)1):)1)1)11:1B9A9A9A9AAA1y8Y]sARIRZIZQQCIQQYCAQQYACICIDIZFjZQ0QZQREZAERA9AIQIQRGZJ0D(CDC Z kbbjC1A(b̓kZKsIb!HBpĸMԎˎˎc C 8 D(Bb HjHbE9IR11 RE9 sb98IIAMZқҫ9 BPss3   \׽ƙ5n^}^$$$aaANK JP         U  P N   P  O N#K<Q! JiII""I2!2)*!2)1):)1)1)11:)1)1)1)9191919191919syuuU׽Y]]| c1I%RZRI9IIQIZEZEZIIIQICA1IAQRCI9QARIIAIIR19AI9bAAD C B 1 cRREJKsB)kRL{IB(B#HB`c"BàE )D( (C Ijj sjIDAb(1AA k΋A)G!)IKzĕLZIs|;O OOo2L|T׽}$$DdƸƄaaA@b*   OP PPP P NO  POq O/ N+KtuΞ "Jk /  H"HI"I*"2*2*:2:1:::191A1A191919199B:EJBFJB kUvuUUUUUU׽Y\y0B99ZFJFJBFJEJZZQQEbZZIYEZGbAIIIAFbYCQQbRAQYIFZRIC9HZ9C9GRj0BCBC 9RZRJKkIRCDC)HJ{IsEZAB D(C((B &ap"AjrB C((D0 C (bYIzYQEzYCA1(j ZF91!JjSrLRrsU"  O O QP sD*KMY΋M##CcCbaAaA  PJ             PP O O O OP   p OO  N;QdUΞ "k . /NON     II IHH"H*"G**G2*:191119191A9B:B:B:EJBEB:EJER ksqUUUUUTUUU׵8ƺ]u|9&:J22H"*:G*IJBRRZQYYFRJAAQA9IQbIRERJ1YbQIQJIJA)9RQ(CB B!BRBHRZ s1AD C HR˃ɃI(C )F!QY( ( @@EQzFjRFrZA E9KIYbrL{PNLbG)9 BI[;P OPP P P  lT"!!AAA!@  0} l.o P       P   P +#S4S44$  N .  N;rluY}4. O P PO O NP P  PP   KI"I"I*"G2*G::B:B:BBJBB:FJ:EBBFJD:JBJAA9BRlsUUUUUU5UU8ƚ}֒lk&2h2"  "2*H:H:JFJGRAZRIIYDZEbQQQIAFbRZIZIIAR:JHRJZE1 9:R9C AAJRk9 C Aj9D(BCE1JBGD @zΛ( 0BBE(D G9IBHJIrZJ!F1j͛{b911H1J]   P       %BBbbMTKP PQ P          Q,S,LmZuU]DL<4$,R,R4$Q$O N / - M+S|}NN O O    N O OO      K J  J""!*":2::EB:DB:EJBDJBDJ:B2EBBB:fRZs/UUUUUUUTU54Ӕ* k        ":GJZQIIaQBIIQBARIIAIB19Jb1E: [1D1AZFB@CF1{MkA@@ bKkIkJcR1IHR sbZIB2": @ j{D1G)!0II)*BIEQ( H)KBb9 B9 B BbRC )@H9}   P    P  PMi!!!!@@a`,LqO     QS,DmU]TDLTUYZZإƛ[[ؕmLQ,,R,RL\QL4 P.o OCnCk . O OP O   O P O OO O      K J I J JH"G**G:F2:F:GB:FJEBFJBEJBEJBDJDJERBEJBeZb{PU5UUU5q|  O OP O O O OOP O O " ;:GBIIQEbAIAIIQIIAAC1AJ91AJIJZjJC CCBIJs sD9(C1 kKkKk{sMs kb1F))G)(8A ( RIZ11(900F19I 1G) B1 !( 0H9ӓs A9Am            O @             M]ۦYY۾W֭׽Ԝ\\YlL\uZإd#MP O  //    NP OO    P OO NO O        K   I J JII"H**G22F::EB:EJ:EB:FBEBJEBFJF:H2G"*MCS0ddC PP NONO O OPOO  #G::JJIA9AII9QIIAIAZFJ9)E1E) :BbC1  C B kbbIkIkRGR1)BBCC!)F)!C((9() B BB9!D Ib()rNk\Q Q      P    J      P  P   ϛX]] {PT{sQU[UZ_]ߞ]YQd3NO PPO OP NP PO O O P   ONO OP OP O     O      JIJ"I"I*G"2G*2G*2H*2I*I2I""POP      OOP P P P P PP  P;BJAAAGbRRRZIQIRGRbJRAF11RJ)BC(DC)Z k c[Z)D(C( D(D 0 )C)1D E(D(CC FARI D  1 As[AH1Y[Γd      Q      PQ Pq      Q Q Q    ]ߚYỸ cUϬGZKkP|Zֵ֚YΚ׽cK+NN O ON M NONO N N P    OP N     N N              PP  O   OP O O OO O  OPOPM3G*:9QIERJERIRJJJHRJGJGJGJ11 AbZ B B  C C )RR cGJ)AC C (B0C C((D CCBBFAB  B A  (8 )9DE D C(C 0(AEAj{k18l        P Q      P   P PZ]]]ߚΙ_{\ގ^X֑σ[^Y\TXTt3+   N  O OO OO P O P     O OP  O   N        P O P     O N O   O  O   OP P   O      N "BFBQBFRERRRbFRZGRRBGJFBHRJRBFJFRbR0000((9 cjb(A  !(B((0C AAJZ R9AAAG)H1H9I B)D ( ( (1FAIQRуRI1HAbC         P        P         PQ P]ߟ\[\[[Q{UZ^]]WΛY]ץPdM3    O OP ON O OP         O OO            O P O  ON ON  OP O OP O O   OO     O O #:BFB:RIRRFZEJFRFRGZFJRFBFRFZGZBAAjjHZ A @ 00AGRKsJkD9 BBA 0QJR: :!H!)D)G!G!D( BAAIIbZAF1A c+                PQ P        P ^]\Wƒ l{҃M{ӤP{MsWXƙZֽ׵ƜΜYޗS  O  O  O O         POP  OP     O        O  O O P O OP O PP P P O    P P O O O P  P P P PP3M;JBJFZIRDJZERbFZGZBFJBJIZA99GZ k c19C1IQA(D9Bk [9C0AIZb k1CD (09C D ( D((00YYQIJZAA) [          T           PQ Q   P     ΚM{MsKkމ{ЃV׽YYֽֽWTYߚl3 M  NO   K     NO            N O O P O OOOP O O NO NP O  OOP OO OO PO OOO32GBJZAJRbFRFZFRGRBERJEbDZQ9B91I ssJkbZbZR1RZ sFbI@(0(9C1GR cKkRHJC)B (B  F)) C((C(8R9ADIQ99BBQIb       P       OPP OP P OPOPY֌H{{ cZ [ZZRiJiJRAd1ARiR(B(B(BRiJHJHBhJ(BB(BIJiJRRiJ(BIJJJJRBi:21)))2!eeffffffffEff     O     Efeffffffffffffffffffff  O      O #; ;RFRZIRIIIZZbZI1#)!#)!#) $!$)#)D)D)d1D)D)$!$)$)D1d1d1d11e1e1d)$) (C( (()0 (C (A(  9(9AZYYIR9FII{ U TT   l  P  P P P  P P ][ȋsHJ9:iJiJRZZRRBA(JZZZBe1B(B:iJJHJ9(BHJB9IJiJA9BIBjJJJiBJJJBiJJiJ:!%eeffffffffff )         f feFEfEfefefefffefEffffffffffK P O P    O O +:REZZIERJRRbBd1#)#!C)C)C)c)C)#)#) #)#)C)C)$)!!#)$)#)D)C)C)c)d)#!!D CCCBCDC (B )(9AQQbQYIAC1ISS U  K PP OP OPPO P PP Q PحYT΍911:919)JRZRiR(JIJ(BhJRhJBHJ(B9(BJRHJB99BHJR(B9(BRRiJiJRJRRR(BAIJiJ)fEfffEfffffffff        EEefEfEfEfefefffffefffefffffffff  O P      O OP OO+2RFZbFRbbb1d)$)#)C)d)C)d)C)c1C1C1#)C)#)#)#!C)D)d)c)d1D)D)D)D)#)d1c)#)#!! ) (CD CD CDD CDC B )AQHrYYGZGZIQQRZ3  U * OP P OP PQ PQP YƙŧZ11999d)19IBJZZZRiJB(BiR(JhJ(B19BRRhR9AHJiJHJJHBRRjJIBiJiJHB(BHBJHJ9B(B)efEE%Feffffffffef  J   ) efEEefefEfEE%FeeEeEfefeeEfefefffffffffffffffJ OPO  O O ONONN*JFRZRZ#1)#!!!#)#)!#!d1C)#)#!D)d)d)C)C)#!D)#!D)d)e)d)1d)c)c)c)!)!)#)#)  BBBCD CBBC18DQaYYGjRIAIQ k  K P P OO NPQ׵ZΙ\9199A9911:RZZRZZRHJJ19119IJRRRR(JABHJJJ(BIJiJRJiJ:9(BRRRiRHB!eeeEfffffefeffffffe       efEfEfffffffEEEfefED D e E E efffefEffffffffe e E E E e fffP O    OP O P P N OO# 3JGRGZI)#)#)!(!#)#)#)d1D)!#)d)d)D)d)d)D)$!$)#!$!#!D)d)1d1c1) !#)#)c1")) (A B B  ABDB (AEQCIQQFjiaQQJZ S P O  f  P  P P O QP O ZΙα(J999d1d1d)d))HB(J9:iJB(BRZiR(B999iRRiRB(BiJiR99(BiJ:(BiJiJiBHBB9)(BiJRRRiJ\kEEeefeeEfefEfEfffef  K    fEE$ E $ % $ % D E D E EEEfEE \cE D fEfefeeEfEfeffffe % U% E fffffJ P OO N O OPOP OP OP OO  N B:1!#)#)! !! #!C)C!d)D)#!d)d)D)#!#!C!d)C!C)C)d1#) #1C1\s)sdQ {DAR}9Ζ{s JZjJRJZkJRosЃmk(J9R9AZZ BA9 JJJ{̓Ls{|RIJ [s [RR cZLssKkFJGJGJJRdOC"""J"K" JUyΚ]}YƶU4U׵׵vC;KKK  ""      L   K  K KK K )+ ]m2ܾܾKJ     O O O O O OO NN     PP O OO P O O O P No3+OO    ȉfȂ֑֙d  K      K K K K K  RRc[tԌՔSSktlttԄҌL:=f=$${>lQO~Q [(B!19 B)E9e9$11Z C1199AAbssR9JKkJGBF:GBJ [ [kZGZJB:s{QkNS:J"IJJJJQt8֝}}}~ߖ4׵uu|KJ   J         K I K K )IhTmmt=>>>>>>=hL    O    O NONO       O OO OO  o#|ŵ4tQ\K;.  0D "&؄<o+   J  K   KJ JJJIJRJO[kS||tkkkkc|T|Ք-^f=,,-zN]}}}]]]\=<49( *J sZD9(EA1) B||LcJZRHRBB9HJZLsZRJHR1ZPtQttMK:* ;22*2lu}~}}]\]QuvuUU5541l   #               K J K) *)]muuڶ>>^^^^^^^^>>=u ]"   N O NO   P NO O O     O O NP OP O  OO  3Tޘx8սS0lS|ƫҥȅdeet       K   KK JK J J KRHJRJ [N[OcNcst|tR|sss|3dz=n=$$=-=5l]}}}}\\<<<ގs  G1 csPB110 !1RJJG:BAA9A9ZbGR9J):k{R [[cckM[c޺Yƚ]}}}}]\\]<<ӜQuvuUUU45  K    IJJ      L  KK JJ J ( 2Iemuu vdzl:\~~~~^^^>=}ie)]*    O  O NNNOO      O NO O OP NO C\xx֕޹޹xΘ\׎%fCD9ܾܾטqt     KLK  KK JL JK JL KRZRJJJOkOkssRR|SRSStLze^>z$,-~-y=N֝0S*UK;Ξ~}]\]<=<<޻ֶf1B F) cMsA(0(1 (1:2G:19199A9GJJBJA cRHJJJJZ c{|}}]}}}}}}}\]\<<4|UvvuvuUU5544j kk K K#  KJK #L   N    K  i *Iemuu v)vlI~i~i~k~k~~~~^^^ٮRmIe\*   O     O N O NO P O      P OP O OQ\03ޙXSPRޘ޹ߙ֎fIe!"zǼܾo;          KL KL bGJHJBIJ [sk|RQ|RRtR>ZLV,$,>-#5=Ϟ،P S"+3d}}}}84<yU9abF199 9DAA)0D1C)1::9B9B9B19C)9RKkRBBGBF:HJBJHJR]<\]]}\\<<<ߺӜ0Ӝ))Jkk J             kh"]iemu v)vIvIvi~i~]Ǯi~i~i~~~)v nmmie)eT   N      O O      O. |WrkxWQPvޙ޹ގEiɧZS K  K    KKJ cHRBHJBZkQRSRRRd_.:\-$,=->-:$pQ T"3;9DL}}\qQq! A)9 (9YZI)(11)99A9RGZRAA9b΃KsGRABGJ9B9Jf9׽Y8YΚ<<<<<]>z$,-^-#n~[9[6+;9DT=}Uu89Y׵ֵ1aaa(D1 A((RB!1ZJJZ kZbJs̋ sbR sO{{9B1{]޺׽u8<ֺֺֻ֚֚֚<׽{0F  J )KJ"      N      H3)]iemm v)vIvIvIvIvׯi~i~i~k~~ n nmeie ]T2                 N+T\]v(jwQPޖ޹0F#b"&<\6Ύ;  K    J K   K K K  cR9JRb cRB SN[Oc [kR9O*>.%83n<-,-^-y<^ƽ:=߽}<璌vXYyκyuZaA 1I 1((91AZ k{OMДМФOOskk{sJ1BXY޺ֺֺ֚ΙΚyyyYY80sQ!F %% F JK"        )TIemmm)v)v)vIvIvIvi~i~i~i~ϐ nmeee)]\K) O   N N       N      S}\6>.:y |eF,$=-y=Nqz}癕=}}Ӝ89Yyκ81@A FI bY RjA1js{Lck[s|O|P tkR ks kB)14ֺֺֻ֚֚֚zyyyYY8Y85k1Ӕ!% %E F J JJJ  N  NM   N jH3 ]Ieem n n)v)vIvIvIv:i~i~i~i~,vee]I]T)T*         O       ŝ}\<]t(I(ޘQPޕQ JNZܶ\}ޑt KKJ  J KJK JK J K JJMc Sct|JIAKRZsj8K*.>.}-x n>>-,Z,MUXyƘ{}]8YYz׵AA00911E1 D)s|scK[BJL[kss΃{ |OJcGRGR{ cZZbu8ƚκֺֺֺֺֺ֚֚ΚΚyyyYYY888vmk!% E E J  #         +L ]ieem n n)v)v)v9~k~IvIvk~[<~; nee]I]T)ThCJ   O    O O     O O   Jս\}\]j(}uPR޹޹TTxƼƙΙ\;|\WίK     K   KL KK JK1BccStSltltNcRKJN[k|Mss...$ 6 "eF5Fl{Ӝ<纭|=||y}}}89XyΚߙv,cA@ a C)!B [JRR2H:2:FBZRZHJRR csKsFRAKsҤTu׽8Y9YYYYXYYY8888mk|!$%%E  K            "T)]ieeem n n)v;IvIv\\;^^ vee]I]TITCI      O    O   K ь޾}}\t(I;ޘQPޕWֵw;\\<ޚֵ8\[;|T #        KK JK F)RRKBL:S[cckcscN[N[kk--.-|-x :LUWm{|ξ:[~dwl[~}}8Yκ94AAA$9)((G9F199:9RJA9HJRRBJBA9GJRsJsb9jOsXƷ4UU׵׽׽׽mk{!E E F %  "#K#  N  K     O   H3T)]i]eemm n nL~;IvҖϲ+vM~ee]i]IUTILC*      O   O  O O O N   NKWΞ}}|(<ޖOR޹7͙WֹޙֺXֵֵy[]޲|       K KK LARIJ1 BRkccN[RRcJR Ssz=---->>9$87Js{{k:[[}}}<粔׵89ƙӔaA(#1) E91F9E11C!1:1D)HJsO k9C19)11GJZ kC9AZBHBRc}}$,$s{{{P[vt|ڥ}}}Yβ8Yƺ޺֖{Ab F9QD9D91A9F9D)1!HB)HJsP |{BD119199RJkL{AABJ99J<yξ}ޚ8ƖU󜒌4ksr`c!% %   "JK" ""#K      K HDT Ui]]eeem n n]_mee]i]IU UL)LC3J      NP N ONO O  O     1dxkI]ޕ/Qޘޘ޸ޘtTt򜕭8tY$$,s{|s{ƞ9|~}}U48YY4)B E9IZAIC90 9)GB)BKcs SZJcR)D1)1C!19R{ sJJ911 c4444}]yӜ4{sr`"j F ) "JKJ"J     hLT UI]i]]eeemm____^ee/~tei]IUTL Dg;';        O          }ZR@I(WXW֘ҔC ӄ          RbbMkZZJRR [JB9KRJA) 2---->>-$,d{|s.SՄ[:ξ;}}ߒ׵8ƙ ($)hRjQFbZC9HRHJR cJGRjA99HRAC1C)E1C!D))ABKs kRAA9 kLk0454545~}}\]<1Ӕvssr@& f  kK""K "*"L"KK   N  LT UIUi]]]e/~=_____________Ǔr>mIU)ULiLCG;'3K l    O     O O O    K ޴ba(a7wWt}d Bj^____________=>>uIU)U ML)DCG;3e kJ      O       ;b(|W56XxWֹ<{ @ fյ  K   JRBBA B1 :J:BBB1BRGJ!D1#---->>-%-|{q[tXz:}}}u8YyUb (I 9 cM{GR99J199B91C)1C!111!D1D1D1)D1HBPќѤ FRB cBB}445444}}}]\\<<r41srD AE Jj """""""JKJK N   ]}L M5~sP~>>^^______^^٦vvei]IU)U MLiDD;'33% k Kl         O    O     ֵvHA((s4ΘA!@D޺\҄  K        91AD1D1)F1)) *JB11JRB))---->--\-\~Y~8t||~\Ƴ׽8ƚBAA#90#9Q kK{99)1D)119C!C!C!B!E)C!))D9)(C1ktssќNb9J954444444֚<}}]\]<<uqPsr!`%E j KK"J JJ J K    JlLqmݦ>>>>>>>>>>>v]]i]IUIU)U MLLHDCg;'32% K k K        O      Q\5s[[b((}7իժիKC!AB;\;WK K J      )D1C1D9C11 1F!1I:R19BB(194--|--><-\%[5|\;=֝5ӔT9޻ֶR:iJ cZ s͋NK{͋ {I()B1CAC)1D))!D)!C1C!D1)1)1)IRk [ [kkN kA9I445454\ӜuuŚκޖQq{% G *"j"K"""K""JJJ*"""*  J ;}u|ݞ>>>>>>>>ڦe]i]i]IUIU)U MLLhD D;G33*Ff J       O O  O  OO    .#]9<իիBAhյ KL        RAA1E9 ( ))9GB9D19))E1 JC-|%-|->-\--\{Qxƾ}U׵88yuR)JiBjZ k{{J{I{ {AIbjAC))) ) !!!) )BJGBGBJKk |Hs9B1C1Kky544448y8ζ4ӜӜҜy0󜲔`QE f*""KJ "IJJ"JKJ"JJJ"+wu[ݞݞ>>>2~i]i]IUIU)U U MLDD(D6uK'33&"F I         O    ޾|||I\7);޹\xΌ  K   sbA9D1D9(0(F99R1E19JRZRMc:-|-|-|-->|---1t{1Sttמֵ8Yƺ8f)%!9AAIZ΃ sj sKs{ZAC)E9(( ( )B)D1C)D)D!)HB19C!RNѤN{ZB)U44544y}}]]ޚ8׽0Ԝ`ࢀ$) ) ""K*"*""K""J""J #  #*\}:{ݞݞަ4IU)U)U U MLLDDHDLLtd(32            O O     CW\|[[]4et\75}<\t        {ZI1D9D1D1 )1AB:) R [kLcMc [C|-\%|-\-65--=|QӜ4׽ޞ}Y4׽89ƺ׵q)RbEAbNs kHRRIRbR9!)!) )!D1C)D1D!1)9BsO |ќќsJX4444}}}]]\<<޺ֻ{Ӕ4a)"I"JJ  KJ "J""JJJJ" OLxuڅ;{ݞݞަ4~ U M MLLDDDHDCdddT2F  J                t=#jJx5W76Ι\\<Ų|-#      LcMsbQ91!E)!)1 [RI JZKJZ c kIR,\-\%\-|->>--.zE|Rtlrt|8]}U׵׽9Yzu c@e!99FAF9AAFA(A99E)D)!!)!E))9D)E)D19A!9sP|sksk{R3~y4־ﶵӜӜuUYy޺ֺֺ{ӔUbi))2-C.C;** "J"J""""""""""  #KJC7m}};{ݞݞݞޞަ6LL UmqmLDHD; Lddt\TT2f  )                  1dx|]E(x5XWWW֘};҄o+          cNScMkZ !E!!AJB [L[ZD99RMk SJO3\-[-<-;-56-.>.UF5?>=NlqY}}8Ʒ׵Xy89!  E)E9((((9:1)1 ) ( )BF1Lks1HBLcK[L[K[ cZ [RGJ{44u֝}YuҜ׽ֻ֚֚Q5eYJJ'2RJJB2IJJ"" "J"JJJ"JJ K  KJI"dmUmu;[|ݞݞݞݞݞݞݞzY{;tuH<;;QTdu\T\Tgfjk            N   +5j}667ֹս3n3   RJB cZD9!11 BJ: SR99 k cZJIJ,;%<%;-\-66.V}M5#8=U}}}8ƚ)"!D)D)AQA9E9 kb11(0   9Ms cB1BBHBH:JGBHRGBHJ9b4ӜӜӜ=<<]<ޙY׵ru֚ΚΚQn(B,[[RGBRJM[ K:J"KJ*""""J"KJ"J K " oC3;L}~;[|||[};;i;\\T\4TP;ff+ Jk               N- 9<)IxִŴų6Ŵս3;          N A1 :I2RGB(@ !911I: c9ARRHJR:*;-<%-%<-6,TU}5-^--{,=N<]]]]<<ߚ֖nk'21d)E)!jL{Ib0B A k9)(0 )@ E119!11A9:)11919ӜQ0{{{4Xyκ޺ֻ֚Κyy-ay(:JkcJFBJ SZ:J* JJJ KJ" "J"" JJJ  JJ!)33+Dxuu}~;[\|||||||\[;څ}XuC'3Lu\TT4TCff f f)K                  ;7<;սtսŴŴtC           E1911I: S9( ((0)9ZbAIAIRBB1#-%--%]_>,-]--#{,<^Yƙ<<84JJ1! JJbJE9D)bJ1!) !1D)( A(A A )1D)E))99A19119AA5y֒/+hfF&&&&&&UyΚκֺֺΚ֙zyYoQ1HBRsRJB [bJ "K"J"J"J"""""""""J""" N  K*""3h3\Xmuu}~~;;;;;;;څ}xu8mdsTCuTTT4TK "% F %  )K               O#U<\(kŕN; +nCSdqttսt1\  MN          D1)1F!)H:HB!1D1A99D)9RA)E9D1199D$-$$, X3^-$-^--=5xeU8yκ1!D)1{REJA)JZJ)1!D)D)) @BAA)D!D!C)1C9)C11B9R9A` #``@`@``@@ eIӜUYyzyyyYҤ`#11:hJRZ9GJRZB*IJ"J"""""JKI"   J*&"2I3\mXmyuuuu}}}}}}}}yuXu8md\\TuTTTLK2%% $ F J*)J            K Qdw\\[Sjս-3" M# ;o3 K   K       991F9E!1H: B:R [ c [J1RI1)D9D1A1E)$,$$:$V 5 m>>,$-]--?>Ee9ALk|5׽׽4-c(JR{PNJkZRJ9919C1D1D)D) ( AB A D)CD!B!9)1)J c̃ JsA``@@`@```@`@ `@ K򤖭YYY94`C)d19:Z kHRBHJGJJ 3 +""""" #""""""JK"J" &"*J;\\d7mXmXmxmyuyuyuyuXuXm8med\TTuTTT4LLK2E f  ) J K    N            -#\;;\ƎC"N# #+    K KL K  O O O  O    J9A1D1E) BIBIJ:JBI:RJIJ1R(0((!D)C!(0#,$,$ #n5$,,]--$,1)(e1ZMkP1k! DH1ӃcOkkZ9AC11C)D111 !! D19A !C!D!1C)C)C)9FB c cJssZ ```@`@`@`@ `@@ˠ糮v88va#)D9d1BZ{ kHR9JGB:JI ""JJ"JJ J KJ j"*n;LUTT\\ddddd\\TTTuTUTTL4LLKK2E)k* JJ J JK      J        ||;\K".# #-# J      K K K   O NO    JBBGR999 J991 :9JRbHZJ9 ( A cs cbx,$$$ Dn5z$,,]-Z>6 )B 9")D1E)E)jMR1JB BD()9D)D)D)D1!!E1! 9Z FRB19)129)92GBGBJ: cGB @```@```@ `@ àÀ`m30$)#1A9R k̓GJBIHJ2*""""*""""JK"J"K* "K*JKJ  NK "Kg*M;KLL4LTTUTuTuTuTuLULULTLTL4LLLKKqC!f Ij   J K            Lƾ;\S. ##. K           PNP O OO   KBBRH::2911E)1)9:RJA@ A(@B[c Scc[$,$Z$XXDn5$$-Fg!@ E1E)D)G)G1E1JE1E9FRA D1!D)!) :MckkFB1B)2)1)A:B1A91"Z` @`@`@ `@ ˠà``@ cǫઠ !#9C9JGRj cb1B1I:J*" "JJ"JJ"I"I  "+ )"* 3CKKLLLLLLLLKKKKC2g K   JJ K   J          S<}\S        N     O O  BJRJJBJ:B1F9D)1:R KISRIZRJZL[cLScctcT$$$xLnF}5-]F(*  A@D)D)1!!()IIIIRb1 !)  !!IJ ScckRAD111:1AAI1999)1&JÒۀ@`` àÀ`@ `#!d!BHRR c c kZGR1:::"""""""K""K" J"""* " "K""KlK K "" 3CKKKKKKKKCCqC2 ** L     K  K  K              QdW\0\           O       O OP OP  GBBA1GBJHJ19()) BBSN[k [ k [kS[cccl[3T$$$:9,enVNUAB D!CC) ( A9ARjbj1    )1HBG2BJGJC)))1)A99B1C9B1919)2)E)!jǻ` ӀӠ܀`@ ˠÀ`@@@8D)9RRKcKckJcsJkB*2""""J"JJI""J*JJ"J"J  j*;QCqCqCqCqCQC;2 "  j  k     J JJ "  K       K K     K K-+ս0\  J     N     N NONO kskZ1GBB9 !1 : B BZ KM[M[NcLSMS KJB[lPtlL$$z$zZ:Z,<6<A( !()!DE!!) F9JR c [kkZGRC1 ( )))))1!D1191B9B9D1)1!C1C1D9)1)9C1D111)11BBksm*E`@ `ˠÀÀ`@ E9e1ZJ [R [Jc{ |{LC2""""" #*"""""""" #MN#kj ) gF%F! "fKj  O N O K  "" K    K K    Qd1\  K              O OP OP O O {kOLER)HJD))R [ZB K[tcL[H::1I:JcPl|OlPdW4$$[CE)!1 !)))) R{c [SR A@@A:9)1)99AB1C1B)C)! )!))B1B)C1!C)!9 BBBO[k|k{P9AYy` `@ 99RGBJBRJcs S2""""I  K"*  j) jjK   K   N     J J K  JK JK KJ  Ct; J K     KK JKJ        O ONbLRJJ΃ZB119!D19RMk|P|ҌO|tcGB)BJ S KclQ|Nl[ CnKCTD4w,:  C((F91E1E99)!1AAAjkRHBZ9)9GR kJ99HR:911B9AB9C9B)1B))  !E1)ABZR9 91ABIJBZO{HR))1H:A"AibxyRA19AIAJJb cZ** "J"""""" #"J"*K"*JJ"J"""K""  Jk"*k"k  #"  N       KK    JK   K       JK       L KK      O O Z J9ZKkB11E1BGJ kZIR JZktsc tJ9:BBMSMScdl[k SRS [JcJA (!11ZR1 F! :RKkFRE9IJRZBJB!Z|ќNJkGJ1919C)1B1C9B1C1!) ( )!)!9B [ksB)19F)999ZQ [91s [)BsLcbYDaDYEaEYbkR9191IAJFJZR:" #JK"KJ""J"JK"IK"J"K"""" "JJIJ*J           KJ  J          J  K    K KJ      O O ZRB [s {QIZJZbGZ9JHBRRLkck tkGB:BM[ccLSM[M[s|M|kckN9AB F1)JBckB9B1) Bc|sER)AJZR  kPtN|{NR119C1D1B1C)91D1 0(()E9E199JJRZR!)E!1999R SkQ|s1F1E)BMkMkBRJIRBHBHBHJIZ [ [ [Ms9EA1BBRJFZFRGR22 # #""*K*""" #"" +"JK"""2""#""  #"* K""""" K*"*" "K""# M  N    "K        K       K   O  ON O    HZJJJkKcZRbB19GZ9B9AIJbLckN[RZlS[JK KBJR[d҄|sZLcd9 @019KB [KJ J11!(A S SZAD9)A c sBHJk|clPlфόZB9(D1!!1B)C)) (!C)!111*:1:)! E)F1E191:HB [L[kLc1C!11HBBJBB9A9:2JGBRJZb9)99JARRQIBH2* JK """""JKJ"   "JK"JJ  " +JK"*J    N     J  JJ J     J J J  M   J IJ     N NONO   NONHR R BZJZZZQD9C)GRFRIB9AAGZZL[ctltN[ SJ SJJ:RM[c[|O kGB Scs{IJF1E) J911QIAE19IBbR9)ZkQtcB[SS[|S{8 1 ))!D)C) D1))1):11D)D) 1E1E1D)99B:HJJ c:11919199JBJ19ABBBAIZC)99A9AAJAIBB"  K" # #"+"*" #   #"""""""    #"K*"* K"JJ"K*J ##              J        K JJ  N      O O O NO  O NP NO ZBRs cD)AR91IJA(AJ cks[\\[KKB2JJBHJ9ABRLS[cPsLkK[tcttRs cIJHRD1EADIDAC191JAA1Jt[K[ SRH:**::J Cd|єK{J IsI)9C)))!)!119C)C1!) ) ()C1A9BBJRJD1E9E1E1E11)99I9C1B)AJKkRJ9919199JAAAJH2*     """" # #""""   O# """K*J"J"            K    JK  K KK J K     J K  J K N       O OOO  O NO OO OHRJ Bs|JkJI9jKFZIAHRRsskSNSBB211B9A9JHJRJM[kllN[M[M[|QPP΃bR9RAHRZsckL[[JB1B9B9B9JJ KcSRJ{AB19C)) ))1C)9C1C9)) !E1!D1)))D919AJGJJD1D9C199I19D1919D1A1LcOZ1D1E11A9JGJZIIBI:J"    """"""""*"""*"*++J"J**J"* + +""J K""""   N K      K KJKK J   K  K  J  K    N      O O OP NO O O O QF9 J:RLkb9R kJ{bjGZHRJRL[M[ KRIBHB11)99AB))B)A1BBM[ckcllkJJ Sc[lQl|JcZEZGJ csL[RH:BJS c919BAA9A91BcQtl|HkRA9((( !C))1()A!!D!D!1D)1!)B!99ID1)C)C1B1B9AC)D1)D)C)99B KtЌs )999B:Z cGZ9H:I*"         #""""J""""   JJ"J"""J "J      O     J      J K  J IJJ  KJ  JJ  J    O NO NON O O FZRZRBZHJIRHRbZbGZbRRRZR cRAC1$)#!!!#!D)!!#!D!e))e!e))1e)$)$!))))))119111sK[HBHBMkK[KcRB1JFBB1919) : :JSdlkP{bB9@ A 1))1( !  !e))111#! !$!$!$!%!)199e) #!$!#!#!d)1D)D)!2 # N#    # #  # # # N##)efefEfEE%EEFEfEfeeefeeEEEeEeeeEEEeEfeEefefff   OO O     %E%%%%EeEEEE%E%E%EEfEfffefefefeE%E%%%eefefEfEfffffff k    cR kJk cGJAD1AAGZZQIJZJZZ΃K{AC)#!!e)11d)d)D!D)D!d)D!$!$!e!))D!e)e)e)e!)f!)))19)$!)(BGBHJKc [GBJ2BGBGB21B!1C)9D)1JRRLS SkNb0@(!)   E)191911D)!!$!$!$!$!e)))19e1 !!#!!d))e)$!Ef    "  N )ffffefee%EEe%eeEEeeeEfEfeEE%EeEE%EEEEE%eeeEeEee     N      FE%%%%%E%EEE%E%%%EEEEEEEEeEfefEfffefeE%EEEEfefeffffffffff j   Mc{{σbIAA)ARYEIY RZbjj sA9d1! !$!$$!d)1))e)D)D!D!!e!)))1e)$!e))1e))))1:e)$!$!1GBZGBJRJ1919C!D))IBGR:BJHR:HJRk tNϔ D1 !  $!f)e)))111D!$!#!$!$!$!$!D!e!))91$) !#)d)1e!e!eff"#"# #  # #  # #N+ Efffffffefff%efEEe!ffEfef%E$EE%EfeE%EEeEf%%%fefEf       Ef%E%EEeeeEE%EEE%%EEEEEf%E%EefeffFEfffefFfefffefffffff   NcMcRIBBBR909IAIE9AI RQbd9d1e19 $)#!#!#!#!C)d)d)e)D!$!#$!e!f!e!1e)e)d)E)e)1e)D)e)1)9!$!e)IB [FJ9A1C1)9)!!BGBFB:I9:*99GZ9IR [Lc|Ф1A $!%!E!))11d)#!#!#!$!!$!$!D!d!e))e1 (!#)#!D)D!eEEEfe "     #   # )efEffffffffefeffe%EEEEfefEEEE$$$$%eefeeEEeeeeEEeEEE N       ) ee%%%%EEEEfffEeEEEEEE$$$E%%%Eefffffefeeee%%feEEEEffeffffj PPO J J199IA J900EI9DABA99DI#9$)$)e1e1e9#)!D)11111#!D)C!d)d!d)#!!!%!$DD!e)e!)$!f)f)$!E!e!)11e)e)!99AJJ{kER9A9BBJFBB1A:ABIAD99JGBLkOZ  !$!$!e!)1d)#!#!$!!!D)D!d)d)D)$!))#)D)D)$!EEEEffff"""## # N+ # ##  ffffffffffEfffefefEeffffefEEEE%EEf%%EfEfefefefN    fff%E%%%E%EefffefEE%E%EEEEfEEEfffffE E E E E fff%%EEEfEfffffff  I)( 991F1IA 0EAIRrbDbQ0 $)E)f1D)#!#d!e)e)D!)))\sd)d))1#!!$$LLk ۂhA t:hA t:Eƺlv9zުԹ>o{;9qꜪ}sjA1tabbbbbbn"311111הMͷȏ4/~p \ *?e/{ˤ 5q*ȴ ރV,vnTL7l:uy_n~//H¹q \ *ӫ_j>2!DsZ&]v=(ormviT_w)?<ҡCy'KkYe|裏.S ĵ#NE߃1'Ny9ru]??^:s]7oJ]r{#ڭtqs jkiW`)0{|J)_+R8\ ׬Lzիj٧h٘g*<+++Cy||~A؏Dq 򕯔c(crO''Ϳ˿6??a>?9.菚??cGnUW}Wɀ/ &7aTZ6 W&ŶqRu}oecڞ3GGs3eKKܜ9w^c@ưXS.oo6KKK浯}ۿ[y{׼51n2> !3_E_d>,e9Bl黾z\|r`i';;e@wyti # /c=V8aD3 l+9pͺʴ.맟~Z.\ lL3}< uQHUs~WUrl///Pz U.\Woo _tF gP|7}YXX0nn߾-dĔR$}~+$??79S3?P|07E΃kQ&Q6gӯy>l V?(Lu?Sv`_%QPEۧzJ mϪʅ(k׮ ??rQ\?z,~{''?!iwwwo A_Uҷ}۷yMeOOc^3fwcl _{z#HcD,>><#ӟٟøVX\2a es a[6g4:|yW||M[[be "XJ`^҆[jD" f;w/a{\*ׯٟA$1ho~e{1'cIϗ&6ֈ͛雚w{w3;~VX:3{ cM # FQY0G=L)uHF.7}86yecڞ~ް Zx<7< l#XxJLe 05n7<8'+.- B? fj g% p`7-Dc3hw+D'_aNbe^\P<L8'ڪh`@k.﹞sY1MKc N&RO cq~SZUجs畍i{&Cǫ$ O> Ao]ϔ]ZquT\b__N#]X A2vD 5S [70_c0/;A_mohwhsY8WLZXkϋyCD^^"Z\D^a0q޽{E66, 4ieVz衇zr P+#"څ{,YLY( u1m{`UFϵ=MaNϔ[..w^jPu }5̅;#Xw<55%S\0q~ɗ|68u_e"m@oP.*R0!!G}GCl]h_OhX&wk^+}nLLL?Bx`Z}> oo6o#ց41pens > #`>vϏk-NJ±Y+LYԞM¶3+"1 YF>0*Bg Ŵ" ӛ'I&:"ªbІyfulI2_wrT(~b_J:~9&?Do+,z ooM3QP_y] bC<{.ԟl #ʲ4Hf'{]SO:7$\Ll? ) 9p2ecڞ XDz+W?Jc+̄@ѷPEυ čhc_GQ?ڗ}"] (AZ|땱_O7U7`ՄU }I sw<ᖰ K;2U%V;aB~s u7S&! ;a{]S쿣0' 8p2eBd)v jGs4*L٘gnBK‹Le -|yrx0?Sg!b ]apW0|ˆ.rt qP` -/ˋ}WAGṁ2JµpM\l=bʑK\)`JIx1P\*ʏr>x`5@Pf{+灚1J h)Ir7a$ko6|p;7YLpֹƴ=S{`'ߜPPs^3U.{Nr`^\B"6Ps+X yF. pqNX͠rDj7$("ae>.X9\xyե$\jX S3Zdzac|_>poӿa"1+WV%u8Md4>>(8ō6 ,S&,9`.S6MXh^\v0VB ʅsiD8Hȱ( PvJnHY}~jO_H𡻀2k~~al(i槟+}}mJn|X_ڞhh,ewpM.zU]nG0PӵD|7( *Ep ٴo>Yŗ`vVo9x4eʤ\!,0Ρq2ecڞ)=-ȯ(*{r!Yg" iB䫾y *+T SqV@b.1)0'aE#2#:KV 1cCIϾM~}J`oVo%:(,!]s32U]&`eoicKțH;˻K-؄m1jB%oX^ Mc܄HC3|`s1P.Z_crU/Mф'kHxk).= ,]lL3) ,3~0o|cbh]TPt,0;""=J NN#a C(Q}׉y*&rL;eߡVѩLj .B4,̑E'@{}>\<7XPE t|;cK_&-҆XZd;;,AL0у8M\o7+lc rl/] owb li< eʄ nYƱY+L3g"uȱ %p]T "wk0-#{|P _)۾o ^A\ `XThEb%uEǷ>';Ҏ@La ŻۚWXzs=@U\D>^  6ڦa=NIL1w#Y&U$=g*1!K 32d/(W2x!ī KX7J 0%0ֱ!Enާc ,Q/o>_˂) qPp`F,Z{nP` |p.9`o S~  -Y 0^Ί$4RsE΃kQ&T"F6`rW>ط1mTͽnT^@` u$O *:+(Eć ZѨtvR Cg&at|PYY[?vIJ.@;ʑv,t{.[_ ޷7z#oRk|f|d>6S(h!/\WOi0Vڱuz+bRWDˣKcX0lz0 ٛY hvq-\2Օi{&Ap~ 借GL9r(Q=4 jQ؆r>(+/HG03>@+| Uk_ZQܤ},<Նձ`r+M#j" %0^ Eǵpͬ2lG|s֕pM\J=2mT=ATh{U4u:Q c]aj"1 j&`쇈T(9J@9cB @GԳcaJG6~G0i1S6A·`>Tф(ܸkf /)+a#DsZ&]v=yߊ媚0 yij5-p F%]h*^:#d|e҄kᚸvzdiA}+k;N?LLLLLLLMcox pcbbbbbbn;3ȴA6mnv{lʊabbbbbbn[j pRebbbbbbnLLLLLL#Sebbbbbbnr`LLLLLL#XA1t Lln%3wbz9!3{l,[ȿnMnsy KrKf憘rz`x~lȼn 6nmq>,0w|,_Zkߺ]>">z Ή/]0g?zc^s_R_LPoHrwkP?~<;_5/^6^8y=VL ;m3r{>k.366L>idNsbx`Œ?0. We>7?}~s ן=:+q= A؝ XI/ڻm3@Aݻw'ʂf$|An{_oW@Vyu ]3wrN%Q_y{ap}p>)7I+SM%:q˽gwիГ)3 018(}/K)ieTVq=AԣA>PiD}{@94ش@$yyQ> ,YDU({^.SVuA *΁LY9p^<_ ggd@@$`bwߋ&ۙ0:ν.w8G{rI*<\bZ-zFA>2 H 2|?$x `AM<9x=}(Aº} (yi3us9["=^ϭXUc\ 01raEg%Ϝ{_ov}KHJ&Mt C@0HϽ!01.SP>}a0R˾{8(U3dg#b6aֽ{(y״ obu <Y$}bN*Asӫ ;b%tn0gH5spLRSX&y&Kf^{9G+&tL/ڛK%X(:WK_V1H`RopPi[*_J%4RkYfn[Il(>X9 %wmr'013rT=Hp*`1;$*:?Cw͸vQa+WW}׹= pɗOzoe1K#`O\Kk@fYAIR6QGi[Olk ۮ]h9x`@ۍ%O$`b@:/M̸)`(A׷ Sn4|s3u*/;|{>_*T`%SUQGy߮YaP'9 vS$`Q SHt}SSOyE{a:lPtPS̢sQ ۽ m߹5p(?u\>Fp\zEaAEպ&kMA+?knJm0A& Dk~-"r{"`}aLE#s^=g"`("eR3{WWE+H6'`;M h$Tv^g `*WU X撦: RBΞYlI{HYN E&GBȊks<: v$Uﮠ L/ RMۯ eet2:)Lx!lEI$ _1Ri[ rϩ 0#asc+}:S#thZdw*R7:Vx//_ >gfniKiڰWݚ|($Q$`b߄jrM23]Gen#2h}ftLL)𕧠i]g2FA_XvSQ8Tfu gYE=ZWYtPV] fn{BջrC[}P o"T=dq/iƶx'iШp{0} 1pWߜik}A2^4H+Mڊ^hB|):oKK.=tzΠ;RZ;"qŏ Z 0#F6PxLjDv^M "!#F: K;㕾d @^Q>yP ྋF0_ۅa++` 0!Y "OE(0ή"tOM_`Je\Yy=7#mkֿJq.MX^CUJXT 1H[)3n$4jY;Q{xa{ ~TұQA΃Z C *(F; doG"| ~X|o.E|2"g1{&闎}(9?EL0A&v$t c~]~= }{zm"pM_f}#WLpMc]^dn>u(:{,.̱|ѓaL0/mb;`ȅʔM2@1ܤYk=|57Z,G}Y %$`bK~2:ktPn R~d:1*HT#bY6T~ezg?|0=|HP~$D#+&S/dגF8'&P(FQlpOK2}17r[i|L!h/);G: Ho$ *@zE 1@OX i_9B9õ6綩o?I$`ZS~ W 8my&Cy:i^1[]~Xn{5اS S28ȋ|iuYv&ez#-Uv0C$`(Ln;!NpOsf1z(?/$-~A/Ld˘ 17A Wk9lWwD=+$aپK"A bd H\2Do.l9֭ -\bvGﮐ #LC'`r Rސ!f U' .y 0AEFDlbl Ά_ aЕ ::BrK$`!iu-5|7P&zO|Hjδդgsq* ў =F ޘg AU@E"pF4n{*zsA|]-s\ݡOXMOiW8M 0A4Մ{' +J.A5PƝt`*aG5 tm"zhKG|A&N_a^<ӎ2ֿÿ su@~BhbN{{}cnD{c>$Lr>?WMAYWAXXnDN *H̀XO{VƶzFXC5CG ߐ I!nF϶#fPZ%0AlE|Z"@Uy)o_R7fbSsp|}6 SX">`]l%fC@ 2gY;ZL { HM~[LXg;mfqs lUۤOA&cOAT0cЅ E| ֳCוp_DU "aA&dZ ~t;$UA& 0AA$`  AA& 0AA  A  xȲabт9S:LLLLNC1u4(3gΜ9s_1LLLLLLLMc'WUÜ9s̙3^>v9xz R}(g{]Sx?o<ء3ϮDY'?).9 :cXauv^?s53s>9yhss:;s} !۳939۳}n3SܥaK̙3gΜ9y"//GAZ6-rۙۙf=*mns5lIEo9ȗ:c)Lmp{jKN:6n)//Zm̙3gμ9yO=EOx2[pv*"Hvj.iJӍjmlĉ6oo6CzZn[QڝNm3gΜ9s[1['[궒z@ī7;|L6@noI;wLLLLLLLJi|li=am|jc]3q6T~E># ΄2gΜ9snwL[q0AAqon* &NqsAQN*" KG;X0AAQtS5$f0AATTT~Nc%*` [Y@ܴi?{3EAT#KX}גwdsTa &);&I, }' RJ:ݓ$x0A c]zH3KV4jKB,ruKpCւvmNu;k+ӈ5fRWQM}*"<1*yٺnQ8{0+wA]uKSׂv 6'`D D d9!$rQdτ\4njr8]34]8 b+|%TAi8a<ׯ}*γeT}Kg:T.x8svO9=jS=''bݭ b(F|L{LͶoYlC:!:Ys}P|Y(ZGt.TA_7{n̙J!9NY%{K77Ak[ =zy6N]虞VP }>_jm>ApMuKpC|>5'aE4kh=EN.Cl(蔯7RE-=@nQp#|B6\y^BE"IW[H Zr5XU<pmuKBAU {ɢ99+7#aЖ!ԥUh%,_P_)L͊Đbybpu]z$b)Msɾ\A[( :&7g͢c(JYK9[T 1>`!TϢdBSIYq{ 3t|S~XU7}:{3Ek_[A[G7LA;I7LA;C7LA;I7LA_LA#SAP0}A12L0AA ULA#SAPpC|тNǬhKkfš9$̙3g [;H}Bwᳫf%3ȼ3gΜy#y3yd>jm" Q cĴpeL]6{Z4хr6pOY+-~@>`!f戒9s̷d'.kҿ \qJzm4ǣ/r6G7b迃|e߉^1Z>ߠ1Ѩ,'Qԙ%3v̟[2 /3gΜ9Q SfȒbBD!;`0I.AaAC_X1WW *is̙3a$Uxa̜Y6SA ѿ #z6_ P0Zڍ5qc#on3gΜD?O/]Z~K5C_GN4iz릎,ʈjj8ڒׇ H[-̙3g>f+E%3}l':h؍Dʗ˜9sce % to<=ltv>,v8dVlm䭖9s̙(w0kۇN.4ٶnnh` :\+[PF;طI`g@!-ynmns#v~+ʱzmŴ\5saʘsA0xL# 4-c7\ b>`U,#0OKiI0܅ct kJtyQ].H${zc̉={|15{ɽ{jtZ9qa_8(!c6h$v `{9s̙/WF_~rGX_2]APXAX@vs4JG9ұArs!| ڍC;|,T0ig|oo梀o9s̙0WQfh:R }'Pk5WE񊂅ڍ5lyDƢ'4 %׼ޡ> 8F.=H[6lGd͖CܿXMΝ볡|[&?bZ3v{- pXxNn3gΜ0s }3RWBV,,SgM725S%APPHԴ{ >, VnR4~kya֡x|j e@pʴ|Kqtl~n @,ۇ4Ɯo >>H _:e6 hp}Ґsӓ}gΜ9sc몀1y:^$woF/HvAXk~F/]4Gg%g}0g|`kpE]- k Q%!mfs(`kk.^0s焈f͚Sf|Hm{BLF8H˗e}rNvHxScS{C!+_7ƴL(XB0?\0`|LiIlGz˗zEozeΜ'Эp.ps]_y3"SЕ tdF<(o{T/Y`frT1) {/l%/<# Xop\w}6P|:=O8# >++eթUZ)g+V :aI#qpdxNigT|Ʋk+uG|mDECe\[_u  i+`A uv *WvN/6w춥 L9pծH] rk5H+/$~r_ݡ/)kэ``Y_!%b Qr5wb,_239}?SıƇmS8MK:,o^$(vz^V~\^~s-8-,/w7!e@~&irmri F(C0 'ٲϭ6:Qyf. gʼy5m[F3ZZQ_Lg&m$;v4"Oõp;I2e)[Do\\ZBYTfw*={>vB;?GKl=i-ΫYٻ u}_@LdSupbɩ6R/>y A ڕysWu%z>γ@llb.NO ˔q2׵0G3}vϮDĨ:d QlRR RGb Z51tZ߽vV\Uʢ4mõ~AEwϯe@PVʘ@HR&Et7[6tp?918jNR._\9?O)B'\=nI>Y &^Pzin6 nA=EeVeV=|zܧISa+f(`o*b>`,loWqQ.}yҴE9*]g" 8FnPRc,K+Z2amR;+NJ>isШn=;8𑠽G5AY.Udݮ7x_|V=FEӮ?W ]6}RFXsS}9P-l}z">t/LLpFԍn엹φ>,= ]͎/lnzynϯ/x^tr~tY9?q~v]r$N"m!S] kAzmcpE ]1:M‘x <XWrrx|n~( ksA[|b)=n2s#u!\%NHcc>ax_%)r*ֿާF`ㅋsKXr-sQ6qEvncrXdW ,خø:ڕs_R|^C?/]#L7J\]C]G=weQr0?Z 36,1)}EZ濾NINHӉEGIc;{,Y7=\09,qn[-奘8J-Q__c+%&]kt:Ci*0vejnIXo*`dIEzu6TY'x Y ߍ>kS_]7|"TWy Xmwݎjԇ1;";mZ*]'V|E磌Ϛ2G9mT[DG|l)[E|k\-$)>lIf]Mo?|\}Ҭ&Y~~;;lסs}= "Y^W)\ J;>פ(%"Ip\lRW r9"NiJR`E>߰ڨ33 X|% z}Ɋv)I>NU0~"FAHt|˙>`{$ӐFng,e9y#sv'tsdmec+q)_Ο0Ad+sA*%m_NU~\2;=oR-y4j^sڋ>ø~,T S>v\vlOrpp\<`HA[;aplf}6?M;nfRګ(UUR6kpQU0(u: KRMROն=)Z6B2gfPCLpJXW‚9Oɦ)`wK~)m] `Vڲ>`]; ᭍xUMӔfrPk#T'l)1}:Gܛ~fQۖxEsR}ɾ&8.4㕣9(^+~{f#3#+{ ~8 {Y zҿuN/|QJtm>vG 0HFtWH:{-= XY *<DzJc*`7Իp<8V\-E:HTM +o3#JXTi#>A0}y_Cڸ>5$lA5]_C>!mY0Ѕ80rG%cd'k^r,y|akYwE GCiTQ+5&_v5HΥ;#PBmp6|2NEYeA<ɫOAf<3JS; N#Jcxepڱ߽_Qr|`.Kkfbbbbj$ag}VvsF&ˑ99+FPԘG,Md+`]2복~p6mn7D@D4tD¥~(J;mS'v<`&&&&c볛b2k?ם_@FNmnsnVjMQvNeфLZ\id>`7" ]-VJu.=5|76#eLdydT/|ЧiGk_5yk~Xsa̙3g>.^T <+V9.$*Sv"e>*`efΜ9su=qU0A>]]iXM0'o|k2х-_!/6=mܫ}$ c,Ca>.<\_|i:TC f%a̙3g>-#}O-(jT&FU c`ƶVw̙3g>\ozWo+e3̢& X85#yb~s+fYk R_cΜ9sͥ_?FB_}Xz#y߉]5G<5h>O6l;/B’|E߉Wo5i].mns[dџ:";`0reZ1أ=#)̙3g|^O.Wҵ 2l}Vv+PWԱeh,mns!lέvDLA4_~-@8<4kvlV…5ABO-3ԂoJ{2AA4C7|u#V =/ͮAȼ(cJw׃bY?k&_0c!. ϞYa AV#w3\%!e$goA><'*5 AD3pE,@PcbZMӢnCs~Z4ς?LD5ZU;~tEzlf/mH 'H}P3'C}pӏ BzKfN~GMCS4౥>(쩓f3Kt@.\0X^[#Dۺa=[\7 AVP PP V4-z>Rxiծ\έ//rےcfG0N# ]Fon<`Ri[}>T+/jP # $*z%>_-U*)g S V'Zǐi0 Y5qlpܸlέXg{2./煫0}GVSFF!ϯ/a'w̚l/^kvZOp^\焊ھ8pMv[M](WZ;펙>8*qcuwrPQǾ /Z_Y5?hWϽ|+-y'< y(s_e6ӫ3Lk~߻ZM+`)FD|N>`uf,\\m!חJ| X[c^fATa%YU.MKhAPFD4y|% z*ܗQƝ!/.헶;=4Zl=(ʡe(q\ѡ:,JZ8׈->PQvYɬXWzTu\} q Rui>.N}2UnmwgOm~޻2{AYLa&ֲUGI<2 SoN9H W#߰:ELY"WyAН>YM;![5atwй:=/4vxؑҔ^8"Χ\I(} j0Hgi@y?Cp Z}<v86P4gRD=ڝ[ѽQ.۪ݺ{q%oeމBw}ۼGۤkY ]?mf˞6" CǗ% akoܹ+2%iv, zWd}aHe.N3ͥ A>hߋFJ*(krtԧWvvGTwfv]-333!Q}CN9SdpwP}"moW {W9m#>`l@,!6}q5͖e4}+ЉC#tn'/ovHiӮꌂ7WR LW8o %)ˇ_܇^Ul'~@.y*qĉ}сMe߉BUnok{>U-6"}f\D'l<+!ABC #)vy~휔 ui桼wa0:.1!`Cv>դXNkYymjھږ([cm.N}꼯"u[vPU߻*GUo e*˒>2@@W]lw>_FqiIGʶKzŨSD2V% X˂ >: w BݣE}EEIV֨>o_pV㛆T(,y_L U*GUOvyU0~IlNh2m#<3d9+*aFgP&vZiºN@Rf* xm_Tf9ӧN{꼯8bSe*VNsTi#W7n[ĀX͔vv@E9XWU$h5)wT~sA>ҏgT?^j`&Dg}kk{#짍>`wGӐv5h ݙ+]ӔmQsN؎6iGP3VťSA0;wJ^ռWL5 :7](,y_u/Y!ʽU=g?mD*1wX+VYzFO7v6U/xݨHgwGx9A /feGyE%&`ڮJR#`t\n]ً7_\hάy<&ǞfM\ݾng](,d}AriˬZ笣U }2T05 k ]@_.]H[!ˎԕlQbjJj WCByt^aijO^n].0^ȷrWͅt%$:a]V²ö4Y~b~v}sݕ}Q˼E`]G+]G2_ |78Pӊ;Ƌto Gl!:1\/tRkk}./)tv͹ut{:zWBMn2U^Ț %%ϵg,N}CWr됧}]6C9t rւ.zκڈ>`ipn-OYT-_0V"AGƷ60 4eLQU7,NE ;;|*4[O<>/DsiT-$`b x av5 >°K4;[^X'k$`b >`@ļ1. 4:gMݒ' ZO[v:V>Wì3CDn}zJ%??>gÇ?;-pZ'P꼷N|D:ovpt]I?ב\k }Zz x>,\_ dZ&ΕTV%z;k¡GzZ|]H6ކ ?5?\4093^g$`"_7Fk(7zOA'4,sV#øvoey X*zoxV#XL/Uv"`)ɉ-Oyj[JݞegI-+_LԫHHw%%F%w;>#\j\xU!`\G:q9Ox1s"cܤU|ܑݛJ +,Wi"Q-oAug DYHX[RmXnYSRgU28uSM07m*~#ݫ>Kub mm~ 嬲,8*N2;jyyA/N>Z+<FPIo+>/ckQyHI;8( %|tEDm{p~u8VIKXY~WQ*~@(s?W>6hRqG4I7JE)OYS:< *4,yOUml%l AE>vY#}K3qy|bcߜ] |S9i>ecMP-p\ ˺m(GJGVW\/P}¬BKy8 kMO{?R?牧~VQt?y 1>`1wj TV'RE>i|n:sBLE8:W^[ V?f9FM i\.yoYq)ǷS-,5kΰ#1UGFq"vY<7#&#{{#Vt:uK S|e!L(NE9GmLM~U[˶EZV/*AIrFyϲ=vTcr3Kk;o,Ui] M"RSm&9 Xz%pڹJK4*D443\ l[ x|*`uOU~` JH|TJ8ٳO\׳O  ˘Z2fُnZq/&ug}+J䍺 =jv+;K 8.ǢS5y>`<;{Jn J=S_h_\ӳTG o }*Ʈmô*4 ڣ|U#bR:}$e,TŖ&(%`rAE>F=EAXT{Jx h9Y4sݏnZvaOFV+5XʰV*8V_4UOǴT>b_a\p}m'Puv:=b02+2)~gy=X y0)<HpSr 86a+aL." KH+;xcr_2ƫ E輴&༯!s( ²ޣ8h,p6KgZuPfV=>r"בHe]A/e>s\׳T2Nu xXĀpf1 f?j>q ++[qD6^<3 L;OEgR;AeĈzS+ ќ#ѐ b*XaCD SLjcXyfZcդ5]ͼe}˒Ca)Z۳b NJr/طgw*DE}hV3EDspc|y 5WsT3*:%HбSsQ+!8'~k!q aoLӀz1*  8W6h@TEvu66$k QmB癨b~ BSMu#nj{b``UU*^J뤎g *1G HU,2;jfwUV͵蒑v"}::2zW/HgVEvo_a+,v)] B(jyHSǪm" 5QEy|ٝvcls]VQ}0WǚGR+1HUXT݃krKY7X"?qMۊ=δƋgl{hҮbpAUÑ=2*@}i<hTlEg=%e;<DY|i>`f]uWTs5#ElvK:8P"+3Q=ϗMuRZ6kg`3Q=O:X3EDspVvi*|kFML#`:&ܼ(gߩ1z/ sHy)rep?AMT[dBL껳;* X&"dm kPZ=,ݠ(%/곮Ty&ʶGl%H T@3EDspkoOEZVyvgWL,Ev&`N<=i.Vªc^j|/AϟV4,27gl{}S%"f2AMT[NK RsՋt/j[C;fEc!5 45"0עK|l֕k]v=uJN±٪DsJձKu%ru`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >` Dݽ ׍پgz&M& V5s >`   _Z^LA(.}AQ+wvLNǴm  SݸankfiIUAD][_3˗™Et~Izuլ^_nn >` !໑YNGu伙=6kO͛ B+WL0Aq/g[I[!v#]8`=uz9AsӇY3{|VwL@"\& v.Y:$D;)3q`xsw!ကg̘]<&h @%ݸ2ǵGI3^/&_CSBӏM ;1'Nj>o)kADLk7w 3a%b2<&X?q<;cf&Exn3K& va/,6Bf* ]%[,$;,?!'G 9! LADnE-H*$*ʢx%׿G8T Z4F0A 6jFP><- ج*PNIϺ?t' M"4?Qs%Ӑ8 ؖĵ.0Cn*dV4T 8AR>ler ^߃wFK(Z9"JrMouK(gGJ_ J|܀V.HC>񗎛}/d;~G ?AK!V \m8P^4}A0 ʕ S~@B˝$eHjA˵o@r{VžmUSDcR$W|gDALA [횵kk`ҤlmssU>Ww;K0H5 z.|Y Q(a%[|5( \b(7}J!YoDkxp}(Ӹ-9yAj$p I27opmkڶOx|/WQw7<ܾ,ͅ&hZAQFq xWTmfq]|܉m]}[{k| u-hru>_cĎD-k?!zԾ +>}'H7';<뵕ӵ%|ZY>`~(ά 2#̹s/~)M0>`ǽ")UOt}&=~³>വ=|k (VmV.zǛR~R|=VdP;O7H|ƶ"wov}1CO+EF276ZIעP[Z2 G2C pũ'>٬\cm;n)yxk ٝ=Z*5I+aPoh;IBo/HZ :ގu+hwl;q +Y/].nRKS~6JʐPգx2OW&y&t{|Ў:?零wׂB }^{J1`$ĺ! ېL`01><7ٽb#e +N]]̷SUTF7{| p?>Si}k^ Y Z3"R0"ܦpPA2JCL Yï\Aڸ rBķ{.R1QGM bK|Ms*XK>(]^C Ċϓr{&lw̵{|m3ǷS-E-RLC \H"cb fSlSL`%l-O1i3ŀS (E )TPB{}dv^1FXe{sN}8t?倷n%mHau>s|e5+ÑpϦaДD_mcl}-5[w.n'yo$&jJvqurէ{'WCUo:qR\x{a~檬ho-8ox>Cva[b\c:1ˡ9}>6~~jCֶH"ޅܑp_vsxTӟb'=_o,r; fU>TI뎫0=z~=~kW֎{WzFDvF~y?>:[<^q pn-\06ڇqM }- e޶a=jKϙ mH2WOtSo>3t'y\ѯm;0}xPc"^Owȳ%.wOrNrE=/[RڢYyÏkbV]M¢o"t=Mmq('{L*y,aX;tniPJ{k_/ n՗^OZ[0DsO \  Ĝ]WD|;?t8X0q2 8\;͞voB%w;sDv]ذpev!rv$bϯLD[ܦ{j.|cසуQ;)F`ȏ.5Tຠ"*Sm| }3{O )eC8>8ޓqmG)ZCo~Uʕ,7^כ!h'O EA\;QI1X_$d+]?Cγi6ߛqN I8.GЉ'^Y@=K;'G] ޡH?Z&6DyݎaAw ,2o^L泎8eх Ed)HKE@2%r]}ZZF5 $r)!8>[֐{%˹t|Tκ? g,i͂F孯oioWk#8wd[õbː):o;kz9qR?߬PEp@9'=3AVw|o׺{i$M.Qk}!$7+yv-_20//Qcu*yOW' 4Qx]r͌{2q5}DߢgqZ5_t% c]nIIL O&后Kη ~鮗*熧%P˥z:{=Ojq•+vaпUM j 8`G! =WAwYп)ȡ[n%$'nj-I"B/j@ư As¼Ӭw[q8y~9"8j ~w _i8Zlh|\@%CPeTe85lN2Tӈ_w}Yߕ跃 k@7<,n[o}7_Ylg/Dtz[><~T͌u87\Ý86 %>/ 9iޥЩ<7-kwߑ~4*%v/%Tq 1`H*URCTzrbFT@a}؆"~ yRjݬ}rq1|1{?ʿ;r-=8;J8C9"׷92dul}sgEDO!] [YM7h w}owIhbD,!A`[om@m{ruXqR @>  [ ^cNdW8YGΙN%a}08٦+8'i~\l2syn.psrdbSvm׶-v4sݤ ѽfe3t(|ԑp ف/x=&nQ$Ga=q6H׸\]rl;7.gyG_y4 3C2MMKGT-5?Pyl"1  =mu'lc|~J${gH" 츗]TK:LUݗme8^Guv8if{S:ff6 ˊ/ t䆯N> Vg?G7T:-jZvth|Z[::>oS瀃9k88!up;J~UqRHzqu5!W X Upxx|X<.n-=ޕ$.'CCZNG7_ڑTo9 l _lwsE- +\#2M^"K" _E,s#&w̡at_]]C,sY˶e1ܯ?fA?!aYSHx9M"\ej~/ps}O_˸^z^±_x# \+TБ#ƾ>߰]"ي8r } oct8D!`Wqa dN^NSȶ.hU~Dܜd8":Pϧs|,SK%8SE0q6kΑ{ҧPwC0crLX>-]\N*5pVw-WL ޮ)aͿПix||acI}{B8xi=X$rڶ2\sSb C!D@]k.]Jp{ײ՜б طyVDUF[r+Nz>=i~]}@\I:gޫp]tU5"O|YdWq58\pP'oEqq?ȍ W4aJk&ѷ9B|*9 lHRmJ/C̎/HF*hmqyRUPcM (XҎ62h]J''Lz D[Gھ-_e_VZt}""؊pt=ejUܖ"ZP 2ڀ:͒Ld"(q'awKhHT 0 orTm?*]~唃?7y<2=>x^ɑeA?)kj$,m[:*eqtN-'x h=JUъ )zPI2+z~ YI<ʤnp?r)*CEU?|˃z7+?[цt;KºW쳗A9J/dq a#?YH$[8),/sԪk\qKEly؆ՄpU-}^x@ͿR [)8x2qih50+ Ɲ9[v5C. ' Cf2sTM:_/\9J1@d.47w;2#{qy?Wz7=x<I۠^os/'cOדJNZo;kymEO0ԵЎ2:fMj_؇b鶐ilg 6j#"j-" ~oN8/(4C xlЏ[11ۻfI=J8(;嫦6+S.^Hw!ⶕ/xJ-?]/ON1!F= l ʪ(^0px!ɓ)ێ+wV9ЎT dIv$!>|v,Ѕ$_tt{`%H+9Ǽ՜Ā0t.ӿx#^{`8CVpp;5Utv(oT 7#2tV!ߖzzP HBM]f/Y~ ]7rқ:w}rԯߟHOmCY|v}rj֒&OqߜmHDX|3N4 / )SW7FK˹gI`gׯ `DθG\㄃LrrekVS*omE=lHP_Y4 LzؑTh!bw !ۊ[rUOBǮexs~a[m=G#BN*jZ~$fLOr'ms S!pؙօM+q, ھH*ٖR bC ɶa\ƌ ["m u-pC+k|ޯ'\ \mE*8A_dGtdIYC*0\Ln< ڐ.xJX7ܑ-P՜,Ǒ*?pP&2ۅ x]7 9px\Q֣YhDV`柍,+%u䫱5_/S!%/+~涶l?8A̒ ?χn1-JdOUt|/<^? 0L*W+}7"835q ]k>҇{q"qp[YϫmQsZ8(*q6tgSĦW۠6~Z]QgTܑ ~׸PEy;~eǭLRo8j%#7 &n!^ݱmٶߘqȈvӃQ+-9_$Ypd9)ˬjllGh/m[6 8 OهâgekVhF //0֤^03_D-K /.p˄yFr#\7⿗ȸ[kS Ko[}v0VdA?S'dvp+"eU  n툕' O4Zѷtg7 FEr܎!\AV /\䥨m:G2)*{3_x_|'86Aqʼ,­,)vxncnj8''\X㎚ⳙZs#pk-/!=P/ ۍ'v{m+o?*Mk¨ފDAR?_+88Pu؋7r҅Z{ڒ D,"q>Ae g>zsVk(d.=hO>XsŎO 5'O-GՌJQͷr#],'.pu[N(t@LrdEu>vomvI(W|@ma"۔ߪHLXVk{M1 xrk>_'ɟ"U_.,v;O~lifF{qEͷ%anR]_=ʻƣhqTe"e2׈wO>nH,SL }$C—Qi)3oL%Zeh#GDoogj@+_pv {sjf }Uy%(ؓ#5|ߗS -Gp!#}gq,ZS6C߳30e$+p ɠXD&%kۿ} ȇeJrRB"Yf,(cF}]dp K:/_zkY1똈wmu-|c6Y΃),r19vy;{^v|%'윱ŇJօê2lM<.?*tO_$WQoȿA .hY5VakwGiam\A Gh:bbdCh_8v|N/c !ܼn }cV}S<?&b `ݍXk,A6͸ \0Ӭ6kȸ 4fIgJe_o[T:G?.U-仙kmJ7=Ȓ2phC56h3?Hƭ Lٵ@A{?|g0Y; lnDUlWexh'b~⨶nqyjR/U#IRCNӶI?44pr";G+?zmRDN+(g_BmDzI6%kQD@8Lu,^^NCZr\µn֩ e"dy4dacnEq}ޟCCÁlJ*}&"嫥KOZYϾ_|L Dl᪾3^˚ OAuutxvjYV%be^+1% ZTЫRyqiCw;T/]}i!n0+.k-0Ė6rEh3+lҭW>}Yp_k-TAIۀb}ɹ#RW ۝?%ϯ(&UŽB/{3XYWeqI='wiCW/  {è'] `9>sIbyϑ²Ӑ!]`m)9fR #c1C sXyϭk\>m%Zڡ/^OBOvrĤVYQ4YR@>|?kHSEKN2a;? EYLYaw,9cZyhgA%ȓ;*8}"7=_8}e@˓93ⳏϟ;>NʓEY(U`4Oi19b<gjz}ڮ$Ɣr.R9a Ѽfa }uTA/x|n_;G9 G†FFmn $[AYtkpS0><3VxQWO:!l/tK1>~o|h$]w=)H8МG4{rr|Amĭjd[pkW)*'8""]w/OPf],>ɜM92!8 CLQri@Rα}RԹ[څ\7мƶ"O:L֤{VfO?1yfjKcuQ.SQ['by-D)[[lLIjgČi+>mߕ%IәJj;:vWUle ޡ v$l-Vzh=Z βWֲ7*Y녚$ۯ&_3#W; ݾ.8椪²3 p,y_˂2#>w0JQIQӾ!Z= 涳XMSZe|D.-'?mŢ.jHVC<"euD4!pxqi;Y8KJ ~8mX5| …[ҿE\ TI;qď '~տ7d+7\YA`;>9AUŧ ~s, >Jv,N *ڜfȇge{C|}6Eba1Z]ݾ]aSC_.8aQMӂ:r1+:+pKۙ/jV?罼jjJoBȉN~Ԟ.'!L]V-Q@4Y}F9`ٞ9@xl'c5f}HAgybȕ'wK%|%[hP'z,K%9L"y>g|[9C[qzKs#cegJǓT0 z&Du#"bOK"lֳWw^OI]\ jP~lxa|?}خwMH''?|i¬}V CȊ>C(3F5TNL5$eYѩ1Uѱ}WEYR{/t7z2]+}gFǸɲ/;.Un%hLºt~Ilea)klT)WUֽjop5t8&a}[/+э}obrr8ϓpjL^ӰT6+IuĭC̯{.yn{2|j$B &+hu3l;a'Y=CxS_t+|P0W8*5ɄZ\ς9^^n熄)zoz_JȺ@q0-tTy~߄3-[Йx<;7p@X)KmKPVtsAy,e1zu [TbQm^Pؑ-I]n)+Wm[EBIȶpG;ig~XdT=BQ6' E*?x/  pQ1V"kI^lR|4l۳y}HBO\FZ 53u.|xoΩ+ţn /p -kzj4>t;ʝx|#ji[ clXEܻRGkUt: W9x\:B>\<)] v{zv["ᄎg>`A }{8 s+ v"}[VJk8|yN2'aՒNpH$C9Hwdét3$ҍu[o"(EiUo#بcE-*h▻>ef=S ~_>j*K>g\tbe%M}DqIPC"=LU2m[p  }儛nxD&e/8_UG眰 gOZTz/2}%̟}>ޚʺmpg*iWKW]ɂs#N,ǞrEFX֤/h% #j ljH3rD+Bwq8nl:xAQ%6 |AKjz G1TcRz=%A#L\L9}H>.7V3}QrwyŸJnrw}Dz%GmI{ވD)}ŌQ?ٿ_4,s08 #ompL刄Ta8SesKu\ YA\f4g`oaI+ #b~.z3St>\=;g}W$Uhs7w );+-p?%QbA?Z}KJaC9a7pCT=,9QNs¿ owP u|/\tHrens!2zVԀ̾hL5Px&RF8G`UAopQ* %<VFok IV&% XJ~u&n~ 7\Mȶ6&W"}fIK1],U|l-8z`3ċ _<ۢLZL@꧁ P5nz>5%?1ϔD뽘ϻ!>KMן.oua,'LV?;G6ȅ\ЖzQӨۢ Tђ:l D9؍XO\lzy*ل[/>H s\amjgsג 6MM|Щ0l" Z 3">IB=RG6^PYi㜔pNߌ=Շ[S'8BQ|Bra=@Yo:)e&/yoルظ<*- +9\q9gC6$/|PG/Sb'~R.,]U? q->˔EŴKPE7C@8ʘYDfMZZalNi7jùpUct֪t ;> ΞO/xeY* rPDaR}vx[rpr )1"UfPҘ̢tZ9CP'׎2>N Qf塰Iw|G^Iȓ$*)*+ټx$_:}:&:4/X+e`e(E[whˎf6%6=+Lgnj2N˳n}*EPX8o'*zFU'ђD$Lop^J?WO*~@SΛ^ǻYAn{9>ۮNxdA & 21@'Vs#;-X"MfD J޹ ogFx|oQdm~Q1qʶgض_ x4NpQ{"yvDJSr ua'qIȀ.a. 4T?SZF4s10~I_q̊>xh}Q * ۾TɢqYV4[ $Zd>Hx-+d(ܘ̶n&\+kP̶0OσJnE*88">0Y|: d+xX+Bu]\!᷊~QOIjwT 5 c(Vl 禟leɬ$lz7L`Y޶d5Xޏo5LE*Yˑ#X Pķ'ކ跹LlHؼX/113 ?%g}5es? pl ,+CX=d5kjIH(9a~Y>`׻Qd5>^=Aknsp;.zaB)rpIC>C/POYmxUy怋 KK TA6ls g֢BCRzwg݅XAUzT֡WmNB<'Q ޣwP;VuJy.>I1ǯohCEh9NlU~!ߏg5{ƱڱՀ  gmxZ̠ڐ ɒeK{5"OUU"0%;epD:pՒku|8`*e2K2"ߣ+ϧ=W&]s=ۓ`5C$ ۔@KYzp'ֳ?KU;էBKߊ$\`I'DḞOynbBlAvN‚Ĉݺ~#5tjhOZ34Zʾ̲Sr%bms/q y%=YOpO9KŲ?5) V=u{[W/9J#̒PUOW IZcrzyv|=n?'?ppz`o+B5t3&gn%fxgj[dqzc/mOJ_~IPX`RSsn8mP-Ԩ~R='4gxa=|c{B+e4fMϜ^k왉={ۣ_#/X/$$BE?^^t2W!I;m?.h ܜ[Njh"s*`k8tCߨ[>Nx>i|P- |-{_c/ZQDkq t֤?4+Sݨ|h78îYϸmC:W# vV2GmY:jbB -h}݆8}a-i*++Fb1[F9g[/;'v+wb|'f_?;9'o%a;gǕ GcG/^G ۱q,0 'yC.rDaw C\8=OB<k', flBYh"+d3A a,t?CٳCopt3$Zp̖ΐpθܨՀ%MDM?O̖<^r35{YfݶskqJ)C87-ȇwt/G)Zgn3Ng)պtMW]F4{f^ģ\ר}$l G^7|<ō#4=+:J5{߱TgcYss,Ex; i/,R?lL&0(kWCkR̎h k@Z&m8B=UD^ eV5kU8B*fAmm;-wbKҮu@~JUD|[ORs r{19q-r<|SF͕;5elߝ`5gY0XaSlRmlV ߳E}όe NXVq-i]bbEb}!9;n6'I&E4v!ϩdjM$d,ov CU_0DUt; $E(l!/uEu )7}i;{B!0D #kCo*)f*rPIt炍<8d݇(Yv`ڙ?Hhwf1{~Aż * &ϸkCvMdalTL?%U0-d@ٸ{橆}ul3\#RwuCA#1/f0]Y2;LWǝBd» /szs0Ec \09),r$/UY^tyf'84%[傋L*PlD19, 'L$ݼ~M!%aڐ>J5D2|x :(>ĺ$ 4gLj9jp|C=e/W;i7<]` } xb3>L7q\'`)/yn2nK~金ԝ/eeTHzuX-sr`ʳNJ~9A ֗[" 54e-I>|Ұa[~Scs˗[pmư8 jF p8E)*;چDd;mK #K;_}m×1wkCL։JtHZuKmN2ՉlW~b ;^w<M'kv tmYu8٢cnB*o:=(o_ݚL*}$t'/;Trw ٓ.<哧IÁM<{{!u/6xL%j6)0<׮T8КĬM0"͍Z7,%2]e^@]8٘TUuhBYG@OЕe6$͂Nu><_,apZذ"߀v^%glɉM*nNtb~˙ysaq8E!Z yevמlJg+cUµ^wY=<䫱[L*s϶psc ad4rn KTw9#Ø_!Βg"kMl znfY¬h>`Y= DHGd8.["Ƅog-M!ȺDϧ†r .ڙZ`U޿ %nrZQmL# a.3xNsG)Y$E̷,H8xwuļ-I /^?ƑGܾN?ia4aC[ y$ՁDdb'sEI?8-9OlH7's p2~/KߍfŮYwfEcz0 dzȊ^gaCχZ neG8>8_C28af>+E;'ZТ䃵.3n>_+}}* Bhm{2!K"5rɭ!9+>~V&uu>e>36<3tm'eD3b@Ab,;b=ʭ'BQs9b)'y?fCK`|FN=Y1V>mAw :z9FN; .Hܽ3;{-: 1#zt= %e}KmF7]W<(⛍}3,}7ͼG8 z/ψ'yV.[g ^U?0qEyp6$a.n4>Vҹ6 ʌC?JJιPq>W}^auUo ę3dj*`}l%.Cdf/A3N®l&lf֚~ʲS?] ?vbya6!#ߊ mymBsm(~eYQ aG_02d/˾ y.YRVOAy2V+kS \ _ro>j͓NQ !py8өqQG\"clڶ2n,5 3)g\˙sDAȉ^\&xߥ8ȭ_Q& ~7t8_l?ֱ i..y!1[4m)/"ʅ>Ƕzz!nG6[w'\CDY9zҿ'_%({4rznDȸ*/@BfoϸV_btdzXXw[ uC$psX^ye4%8`oLZKof^d@ 얠BʈMMDƽ`Rz+;6t-Xcx@uᲠH#lC1A qSЊg@O*UAh'sĉf6>N~0=|-Ffg`=1NZ^qg¸ֽu}w~%nJs߷|`gBVqs(b*u(VͿh.!_2 Hd?g&0ϗ>6JmǵadAkWAglAx,3}VCqK͋<.YFtEG5| ~rLPY!*gmk QQ{{S0> I̲߳sCDPphb8^s2 )vx?-LȈ])bST/{prƐ^oGqLpoߔ%]W#p6ujN mk{i9w]CMAkeA$s}>Sm ]VlLcUt<-/6#Lzvo7'CM*XZPtYs1\򓖷-Y06D3>:>kmZ-n; ۪EpeBė'GyolW[$`ez- WUKڏ~ߠȎn!xڐ6t\/Z,@lkhWpD bV6C]z;X yb =xL?Urz~ϖm=PlSѿ! n"d|]>P{)+d>3+}1FN߸aጟMssx ˬ- H@pbYb?j!^U>2۹Pӧ;nOU- []njپJ9`AO*< kER孷cfa'9C! oGy:1k_`dk!d,`324< a@r6.L:Y&-gT *4@rYG)93/E"4ejЗ|Eo |55tνjnޯ;\~Hl d2OD5r%>`Qs? 8OfY2_g-D̡IS^oW3kK]FFe #`X !6zUTAdQ>cu_Ԣsm\{ YxmB%YA-Q 3ے]Ɋ.z,y+Rz6[HELlxz6-L 6 pS^dAG^ S{h6;+ЖLд,l#j]p^a+xKb/&́&$lj/ Ӱ]Ʉ. l6הϬgB N&^w;(#Bw63U4ȊV-r;CYp9\fES]&Y4oA`-m8+2*皋~%kz%!'>) +"yq4409R=ˬdI մ-AKn?hEcbA~w XRR Ld.^+nOrßI)x1'^ 2~ڮB([N ߸xy1  owdo}u#{]T5r v}-Z8z[BU/c%N8"NHy+ jD_렂9Z$$ Q&eͯJZG2D-V8cW?bPU\S4qc؀$# _%&aMNj q:GlZ9oωo&;/L3b"on>X=TKػS,\"_\2γYs|{r8w!*r.cvs o7}Ń8a- *h&a[>#'1jتIm&>͆')r}' G߰=/je{Qhe"p{.)0ڙvq"-+;I*WS{ҚqÒe(E*oHYЭĬ~[5=X+}=Me wy݆t"Bse7hN>Rğ_^ao0QPy. 㤼 _A N%4b*-#:u1'4IސClwtC߾ŋjRmGhZ2KmHw/_-]WUT-ܕH`d bcd%۱hx:C-0ˆneQoֳjU}|N:P%r9nm{>NVgZRWAjR}@} ! 533M-g4eI}p|a[rXdQ,+ ߇J/Qy}Q(;?C >:b}]!&5C^UE'NWnL K|0ktϓK*~}eVp^+;ڇf'C48` 8`fAoggd:3gD'pA, _j0bchFv H"^_3#}0 mG:hct"Q.ϕPPeP %KY C9<ɊiR̊3/KȲ8^XT+m}(W9~U*k{|=$o|w8ߙqҋgUK_|yϷu;pr2U΂ODҳ,jg'5=oo sSEK0V0s 29};da'To0{1 xξQExFKj_XV#¦=H[׶d"+zhAżЎVF T`flBk~ Q+,;'y¡-?Ipl5bX,/\~m7{$",Y'.ڐ 9SWvE+~;po7w_n)(dҕsO.bxWǝ̊蟔m`rYzulKh2Lc-E-گpũ@i —|?Y = *jQE6 {~8OW&X8Zp\L(q8]~U_~~—G֣(Y $jg ]9S3Nx}c%C7"YІǿ N5Z&rHv~:oV bT7xmIҁ,$?X/Io0p̊vN8M)+Rs˿_ rÉCn#_C_ 72_!`#~<η]/I8бw2\<8^}E~r./bbcȃ*!OH*iqL*=,/b3%964wypun9z9xƋ.o Z|7>VY@{O[xA UN'x'j96 dEڿX/g`%4 p*6 y0n6I@ƝR=o'|.e.v X}Y,iȂQJ8GoybֺY$aL1p3.D†|S`^E++}ñ7n$.[;^[H(>\+"Mq? ;(L1Ek_ב/ЍO^u7vp8V&tNCqP\.$[t۞Y\j-,6î]bcŏg"Cf1N"ϸͭC]Mmj+zSI_ͳ|gi~*Rц׋ܯm1jBfx +b*z;X>0NYb% F=/ؼ( WipW){ޟcWEC ؉̚m5K a3!zђ>ބdRu\f7o,j6߈cAV޲>`ܑ6~#˂C+ ]Opz;cc!9uDN3/p}ړwx|wGguINLJ0"5}gX\=cM>dz#]qvAѪz*ڲ%gˊnGސ|p̊&\9׋^bTfwX6ϺM]k,}-E!>|w>OȉaX\GAnZQdrY9 C͵8j8'ɺAg#648ppCmf G$5 5N>f3ܝ?ej,`\E 1znsܽpxCҤc܎\{LK} MȂ*zpo+`cbmGy-V)ͭzXw?WGWoۀvA9qgnhl3JaQɮ[ i!?>9e`^-!-LL!bU9N>aVv>^p$b.ArlbY(Z<3Ttb%S?NB:M]~y?X_{ Ьh$+z#W% k9d堖}m\SA_Křϗ&uP^+|SvnPA_8 Ezkz.!"#wh涨%h= ٶS ˢ 5=Sl =JW27qe ).;ČZ}t!i 3ng_'eS%9?Fιnje_諽ef;gړJoTG3޺'cyQq[<2NsV8ጃvsw, ޷,h8s ~:VI[0`HSEPTi A5,6E _,&n͜:V37SONJC[f#|C, /oX<-$RNRo6f/g`DPTnD/pzb&u9[H6L8wv /ޭ!H"^siNPOFvrh?w:쮈5xd,V'^a*ײmF77?MwlBb 6΅RZ/&_q"(ñ` F|eb!BCS;32FCS=0G͛XvdߓmdBU_ YW#K-+:q`=pOH˭m Y#r|tkXa(0' R~1 c|PlYLTvx_ kQ-B3GT;l y.:<2jh,P% iS>eXLJ-:3wwyfya%;-p3k$&]lxק?g/s]=ߊfLe-p {L d6z9=".S} \"}mH?v}۪"愄amIaq{ Z7lOnYvDqXE +N 07w rRm5|Y=Vu&|]o8s$?snק r"HUEFҟؐ|B8"_/̰AFOmYs.q\֧B-o#,*AKU=ٞtrΑCU|vcvfRK;|-+Ed Cx+1 ZqG8 A dɤyᱵ}{ϳI~WHȎKp+aȊܰ=jKmomhAʓcqY[Ua,XwRh E^_ uIhxޗnp.)l͚f݂|ZIJ-IBop+)p95ЎdKt},hSA~G   @13l iY?Oֵ j'5 +0> siu.bo|AUۀsX/; KUÑ0 nSîUjVl7k} XpL(Ð-"P^L¬ÔLͅ e 2e|+Xj v_Ղ f8<㙉X6$^nǤ[JF<, f;wE8Go{۶!u[؊^\BD,'y<a7NXOLzs>ap "[gTFcT63Kn ,Gg^wCvNlU!;k I)jYYqF|?,1' [ Ns# L Y .rBYpK8f?Gvݔ9<%a@@{w$q~f vfD+ok<Ŭ\{2dN͢kA2})9_IgI-aj%{̓d%l#Sjr}WM\be!U5fܣn-nrFW+#uљ͝-6GIpeׇ!`0!C<׼ίM9SUӖbDׂ~/0CÌ9`W;Gt51ZPGG+E1 Z:Qx˂C$\^ '¡M̾W򈇛=bn$U=N'#,w-QNhbu^ߴ V;C_Y͚ al{bd=77oT[3e) H=]jČq Nj`DM>cc-O[҉v3 0R ƮlGiz dDyw09`YeQ`j0*fjԘ!~y:Ky6_l+::A^.xAw-HxfeDA D8pi'- h2꭯o4??p2›7F=fF[ # bS ZΫec鈔jh ( #u;x}KDz2?NC\$Hx/ݖjn@S_$2&!pTI3!KBRөo㡻|ůUj̫:4܏ a;IM݆ꍪhoM IkOx,ls0taKMR-׉>) ::]$8G-`#IE1J0mwdwDff1NuvWWZ͗?6+4{|QɑnǕ8nml_M "CdE{o$-Y.So0kv||L܃I,)X( L%z~ދ4)YC, ЇG_pD1!5υ&ڥ(Fp[Aռ5IK\qAYK48࿮ȂquR~Cx$Ļ" m*N^`:0Iwf[Kw} :&?:! dgIU*`- w k|JVi c%HkNڬr";Oӆw~'0B: Zj%߮W1 B-l/f JcR3t!j=.B'/"'|IV9]LSZ$0Jߴ{*]>5 㼧CxWR[Ɋ^ٜ$9E"Uj%JgV]xpTAbaPN20U vRR_L{A IjNbSz>T/&熌A\ǫmJvm{uO/ۀXN'‰ CX%1U:4_U[r8`lCAO}(d[hYRy]ԁlǠD2C[vT~*)hc3UΎ|gjgnsW"&PgKKB!Rf^25t f+R >VKRȒ}-fk}C݇%a :GĞ$E|A@@!ݶso=61X $Â۪prOx勌|+\f-RWE5 T cͳ!L5Cԙ7-U%1e [W8?͇(wƏ)ԅYO"_eل\LV#{#ч!d:,J*&wڍK{5zT[3?p@[9?˔vnd䞷^`Ϡ>w<0`(hg)|AټYn56r&HW`^?$,)hhNR(n`"$ ,C.>-(XB8Cp kx,$ƄGe\e(t`/n6w9|&1? *c]s[6$Ij4=ϐK6:-G5Ww˧ގTQ.vePpYozf6rr 'vp I!W opSVtlYTgL M8. nG$*nahjOڄ&RejQ $-S.92䬄B G KjՄD$? kZq,-sl媣){z~d9.mG;9s"N#udk2fBz{y9  39 9E`Xv ( | u]Jµ/s0$v?^v.ckQ>_tsr59zݕ}lH;8bi6!pl[ A˟L7 3GbnaP*[g $\pQl;Y}/; +"N70\T{/^6ϟܹ'yR܇ZBي 3yB#T--nflK}9.gs/-`hYo`U{u;zP͊Y f}BԊ\q[vvvmVjjQAoyeA!VXv,>U / P2q ΤYAqylۢjpaϻy @eSp[^!bb|09asC,= YXǎx')CdDe3יC};޷c*A]9XͥƤٳY6C.ThRڿ](8TJ VTfä+ZU'oqs}҆d*hUL+CN25$!Nn;V'<>0d$Nz3_g;̲D*ۑdHK}mIXˬ," d§0FVB5$ٰ':Muf~gr WE[oC!!E6,#Y=;:Kagkxk݆O> rFfe8c〥 @ ;0{y+L$vtv3VNx.x?]¹Sv YKOhҪG{Do } #{Ɔd[Ը~_LφWx[R.Kae+V% ;6$1Pmz㋍ڢ&4B2)-?Pmѡo*[3_7YdZ^cϐ֝ _z.Ur8aPeȓuz _ɑ+)βG  .9˚&,hpڐ|*~@_$^LIY*5xRYNJEnC825c;Q#a[$~w,h*z&7m.5KG6El=Y40BDղ,R,^XycD+vwk:fj [s?_[L勹{%fc% 9aLXzb娊)&h_+.r"FGKk c:w۝-b ard4/_-Li{|,/o{%iEh% "S$T@j Ɏ{7zuGTº+ 5T*Y?1ܞ45:)->zE\-YbNYz2[Wvv^0nd[Lϛvb]TZVBW͐0*!$Xp+R8?+&a7?{kɠm_[!81_/9DIOIr(mgn]8w߁0O ֒dne?C<yV1 b&haXPEIMLU\cX{0{mH22[pbbWG2=ʸYsʶaw{{1% 1rR:MV[B|ketNd]r4IYWSqC®0k-o5tL6t?B8 X(i'a>`qXہ`C%;.x8r4&;D5&laWu<0-<]LJdR}<6al0w0Db;]gq޳$Y7ۑ/ˍw5{W{Y;N(7z6l|ce a-|[sELp Dٚ֔Wz=(a%>bApV d!r@>DzL`5x09M5G qIibl} !bnO*Rm2E2V2zinD%'θJϧ.szwedn J 6T-׆tkR/26$<$f4W`nOGW!]nosmH8`\QWK+ˡq`uǁRG»^껈ə5۳؆7v岬|o-K t,KJAL22yWn3v 9T~22=G/-E/u8m+c*p|՘Ty(qgǥ~[P[hGJ|a`qOb0*21aGa{|)ڐ ZjkBF} qx| ;"\ek e6G~edRP;{K\1 6"{wqx\"ƽw}rT\BS"Gg[g.Sss8RXF$MUnCU^&")k[xH(:գHv߂SvglV8@5l> "qPZ.cRk̪C{s-ʾLus|,/m"nN;n[,gDܳʻsi1JDؒ*W7rm(\˯E;g{-7f- 0T_e (iq|〻g9Ť,f86+W{ W$cVT!{{P%Ɛ^yG=JЏ{0H>~Zv ~ж_bHRĐb )&0 b+&g")BH1)IpSƘ8E)NP*RH…4}G\EgmS6u(=ZUe\t 7DE2q@ ]FZ*gwS{c'4|ߝ:dn&,6W?sV dJRŤy=_+}sV~TWK0D#jfXt% uWapC A )V{.ֳs-WnHz0CP9s_l Hߜ4eUV 鴧k۪[є4WdZƭJl jS|nw;:SƉKZf1 UAo'/h)S,@'@#IDATw/*S[R.-+VN_荬-kE2/eK>[Q5 /j 9bF$ h_E6OZR8v?̣,=/w|%mUVu]%M!6ߴ*opS]V#LyM[ldt~)|^/3Hmd W{[rV?)Fq>쫕j(Jߚd-E+WCK6elv$-1[ Z9`vd +r:L:BUt ?frǎٟp !j2q\4i)jfA)BYK!ր@e^fSgsޏF?=$!ċF GhCN"NV ~+ڵ%IUtuY\.R]LKR_4x!rd9iI #",G8cDF*}D&(,SSrgZ@U}1,tu Z5GA{ ]ki\oC1qz}}^q#TT"c^сͼm(A%|M+}C?泀ou?9ɦ)r?[2x%aAf0Aޙ*L\p)VQiKڜᗮڜWr|1wS4GׇԤ?r);4ɛ{Y^6o8fZGJU'~; "FdUW&sK֩IWlj[*|u#* Zy<0sI8ܙn}L)kS x=jp5.cop)_ wӎpT7K?`垓fZ,GlVzu3ĩ*a ^V\o0j-l`^K1);kM*_0s} 9am>O˜&I whVEoH@: 鹫ƣ\=)FQ~nnZ !M[ԮtB&yL A@/1 %*zE?v^7$*Լ2ͼ+p\p>8 VN^(W+wWqj fΦ!&S +yAf0AQ L |`h 0(R*V%$O1l~_>^wtv4l^*zV3(|zsӋTHa.*QW{ K%0B=qZRP|݈+)\ yY†2S^6xMsqsSyU"ۀ]m뱼'zSBgj7V쳵\trO SK+l$3 IWze΋r^Qe~^UE?ҺJ[xXс\Hh]uY?ӒJ/U0sQOJ *U>asjVҶ1%yQ IZٞ暷s t^/`W mϱnp&F^fSiUO~0ǭ0 g^EE X>27-lsCyAyq0YWvaGDs-S_D#y9GPa;~0Ej~Uϛ{7w:g#™!hG7cU -ղ]U huU]Uttf4eB-1I9ࠀ7jA#0q \*a vm/:0;XY߯)^)4&hQrnv;z0K9%_Wx ? Y nJQܯYrnaP^.>\f &w k f6gW66)^ڔ"Te9XUXUlVn奃qHTf^+qW/b_p._ɧ%-\ŋa?r̾O8`-ԺSs^&w6!(J ~3/>^n`ѓY#+@+8j*RT2K{--y+Z+јL43K-$ұwc{apl ^q;slɫP 3[>psT;O ջ4&Otm͓3KZO(S3(rL <`6Gך传. +a(8?򐚶sVS˭X]% xpÝyA3L] @~VdRԕ9T#s2g! MтFh~?3+JntI'`q9>8C~ڒԂ,a9(t,W{C;e:^б?o x!sę"yTlέxp,Ԭ"bT󨪵6J~.VD[a# JY#VJ<{KcC=zqcμwUo%pCBі= .Y/󁾞ܶ2ohIx2, NX'h[9FJ'2J 4 ۵9G?y*U\mV$(^l100-kJr=*]bq#a^:AE U;/vCUcJX5\YB:ŁiK~QiL7e鶛L/h~$k9+@2QfV}/Kr^~_] kHj~W],^k "8Salt3ƛ %c1(c>dK,}57xK[`ThDL rº~.z[,FE1#sDӰp?vdbd5ArvɢdZLdd-8wΦD2g<ϵӖ UosT{M6 L..3v^'Gvu HN;ەuU/g -b#h6VEWD^WiIJb|1 Cp| õTe}aơ9`kMl>d*TB9ʯ>l,T382vBj($U6m|"-$;g (ƜV.U3SispڍU!ܜz-|MC\'', %fGq'$rt|\!I7s_T#k8cJב)C9TۑCuE~ 6v-7}GX?"JT1o78U2O!]hHTm*w kS6@ G䂯ި(a'Ѧ,,^?>ջ+wWG:\LPlJ'!]GeQŒgjN'~uP[,'Y 9PMbj۶jɓYi8:{|c! ;HGׇ- 3{J#AgJٿ42~ @XĠCGZ.7D/ Ӓ+ZRTkr۱8BO*wL7fdۍ6M'\mڔl)[8H7.?>&6ecRsvա* wS9Ox1P\_ir慲52f2AG-bMEG :+:0rpJ9./jn~tOaA*h,[8B\UYx$)dċ(s!Y_w9blK<ݬZTV\9ROQ!OUP凌<{L{򽟯qrJ{Ectp5iC5M3Y)UԤ}+{Ak%4JuK+rÄ9b3r8NцK `>U'm} ۏR}^.d@ѾL~Os-{.KKS!a ߳Wіh]vY>\zE9gԤl~?n m%)<EXUA3|N^,*>UNջ+-o*ݡm5*=xڇ? ۚ">دHjLZu+mT=s׍ټVl9\,:[A C5Ƚ=ČăܬrUяCWr>|IAƕ9`?5b/IHcC޶ ?-K#k3(=N/{ S@QL a 'x&N6(I[ ]1#}~Nk9~5%k\D`' ,wk#$,rCڸo_QǼJ9*_nё f?l`Wt6~5^[ν+c.6$?juJhԶ/)EPߺկ9e89T N$(EqN UҢVלLq[5te S[6H!'@dM튺}#"u\<*. bNuއHW4Wh CKyҥJZL8V zMG>Ԇ]-Xh{ #-v.Pf1 906|tL{eUuck'=Kˏ㤶kx]OѳmA{8:~wv bnްԤ #*^њ FHx 1̐FsU^B%Wqcl^PVTrfVb@ۡT YH-HR\酂&7Jpu-v=r%.g?;rʾL>CL&}Mp]ۑ )8WeӒ.zE2x rYhC4TAkބNX3Rv?*a 3kγ$WU2R È=ٞwdTnAtc{Ew|]?ֆ%&T1|F.'sSĻ;{95v3A]PH7+3LM5K3+e}u@4$#:/h?®B^UrE>W¾EKW–/jPә{:g6_SV۫/6EFv:u 7NniF~L`z.oѳ$3[vtvHK:;8_]^R3d)p(𢜌TkLWx%}iJȫ}V72yݔ6ŪČul^ҍscth悔[Ť9bz|>yKGK 8J!mj^ g+]aY.2 [ގah u+[XPg_8qg)~eXi~o˅tf*]8>Y&`sA|L-Iedܰ.[ ӗPeŠa|J2}-8v v9<r6w S@m=ۊf !y2y39HJ$#?&k^Ϣn~>_/A[S@uCzEGDh))G*:da=9ay˱/+`s’>X튰893=o7TNF(hPGUqVmqPruӇl{7/CKUjw>Nޣ:%b1 KڷR elb݊M6bnTAd2zAoeӐ>N 3|ƕ%ٗPW; {[oo~A/\ e6SZqnZ‚s~<};qB\Ϟ׿+RbOU7H~<' b! Ņ;ݑ !`6.iSzyM/zE/_I9`(` CgEB 8F*`> ۤ"dÉtYܩH!X S@z6=`ϔkR23W\H{?Wmg/uloc+ML"A[A N@$:W[C,䫡AK?OV ӏN%KUejtouy;;rQJvՎ}ܯZ]߿މ/rRlUTAdj?4+$z t9@򁆎3G^zro*g (B:P^nR#dí (IGf \-ևV*BU r- msjpҒ>`LCD$x#YQj a}QWxWR1U#KHZ~ZK) V]vl-٪cw/ju3P|{Ces2 IXBvSU4iw4 ?\JfsBH::d])- SBwVA3|f 8csBNUr>[Re-66ZnۭS_p^n>JD* zE @d*k֤J ?8+ĺ"`G) X[q'_5bmtyv}"3?x^s5|l=?N9jgiD< 8A؀|MWVGб%|59eapxU#ߔgU_|<`Z-z<۴-d\8[IKrJ#UѶӔll`BDK)pOkdp3hq$"*s-,hTG_I&WoD\BW19";A%9 viD QzEKTn=݊ET`?|ܭ4BQnw Zu={n+^\ًKΉaQ[^0|T`!4?8)RU޻ ,0u𢟘?Aę}MuBNI aJ ^ TMJw_;思 Gf XX.Qe^␥`WYKq_ x-m"[U& R ^1,jIQ^ % ,dAbt儗/A 6,}ORtFPm E'07]07lzev%eZ&2]XXb@Y+VA в0A`T cXԚ04kP7VB=ƼC9`NUA %զk'!7CE06Qu}9# 9` b׼YUYZu_w~p|2'` ET-=\$ mQ,6$igz&"T 9` b0V M-aGRa X, eVA#,Z:af XDxEN^PP2QFr,ݗ'QA GTEժI^Yx7d5=X ]{9` nkU,- WP=[X(Z C%0LAp3͵ Y%k(Br}%(! 0sADMMLl*kA@ \`0 tr0A$|Xj/(`FnX c", b2^W4-ڒ\,(`(k sA1UfS+eiMz5zo5FA$\TEKkTL ,,A[(A4$ b<+ѪҚd  F!(Gy8#MAD֤89H~FA֧AJ0ps'(a"YJil7ÿ_ߟyl<@=dvݗ+)7PP[ . ui <}ލn6۱`c/Gm`[J<?~ocXmp+oN<@+ 嵇|_GKv!s`رɍ3F95y:%qG&&lb\Lq! QXN, SBjz! .ߖLSkE4ۏƜ>ÿ +Fsa>~"x|{,7'v|W}q2?ֱx>7ΧwQyɶ}}ox TflAe_a@C$X\훚).1OYɹCA$uq%r |>a[~G%`QxީiScY;w93b|űъIΩ{qkz [E؈_G/pEUЍ@CaD\ 6dT%yr%V HǼj?kI]Iu{T"qߗ:wjB:&LzN59O $)@: ]cT³P*3#Uah O6 ?O7C `#F|޺܄k<s<b{G9UQIA̿y8* Rpz % -g9bEuxuρj\'qjnOՃ/i]x|$9qMܸ Q7+<9<:G #b^chm2k@yع_OV3`€Uӓ8=E脚-wK',-1^Ц|EjayNX6$ ? ٘vz{"k Y5Ks5-iDT4d| Ιq87qxK?ؼ{ x!4[G苠rbX d`^#Ƥ.;=~I|~* 5|OV*/hg [OrNM|04A;}L9ap"59`rAL 0_oH ;΍oHHw@;QIAq ;_k|m7kJhɐ b%IWQnv;{w1xJn;U-MFeq $ߏ  ` #a(qUldbۭNa;?ԓ&z? b2-FBXQS0*HuYUl*rm*/lOO ^[a"_T&"ͥ4".Cul ar{niIA19LjdAiWf=9yABظ*7\Ed8p梅/>J ( ׋(k ;NYqJ\Gd \iqmG=IAQESTI<` Uã䇐hYA Aߓ'+๝IFDNLC"AAAHO *yM$T/Z<<`0A'.B%ji8Ah<}L8FYX`k  f)$'9`Z@ľg @e 8>9` 8[ ,*s& Y  Βf  & )` b 9` f  & )` b 9` f  O8p… .RO,̃̅ .\4 ` 9` `}Aq }A1U0AAL3LASUA0sA1U0AAL3LASUA0sA1U0AAL3||n˃qFsulW~=>/1l7/ݕϳ})oZr:v&s{_:re]kONƦxۖ=~׫ҟ{B+U\w9.@>pC wy< 9wco< ?w 5v/jP8f%fA7=> 8k-un5"zsR3,%I?¾q]YT–QQp}sՀ/Rl㸔Vw估e#3<_ 9Fl{ bOy…BV>ByE?^Xpȗ``|}F\mv?.# |~\Kv_y):/ϣfx !BOΪ EE+]. k?ۗ2䆋?*^m_I57UQ߽{+oyQ1:Ԇ/;. ?GJB1|BMi֊ߛ}e4>|)`+ON;BYX.\/wVWۏ( ;qRׅ`ơ.f3"?p,lwӸiˎKYsPu>) 4Rwux9q>o*`)3#;KOǨ;zy W+` *[pcT`Y8ٶQu!Iim,PTR*TЏDžpJ*ۗ.{W憚N뢆ǫƫm6wsԋ}M.M xܾ۹j2%ߡ@x GW08QnAf8*[WUH؊UpvbNBPF lw\tT<0U.GY=VXs]U x q||u}s>c2TMɇ;h˭7uY<]݌kTU8ھccPFelձei^6nIrz;NA$iJkt?&!` , i\Et5,ЬwX;S;l}Iݟ󨀙["J,p}N| `!:wUmHv?~%KTMVgc ^ZUw/wCI8Wp,oS\qkpݴeJ5wf7$I?.[fMMcRciZAe˹q7o(c*Ye:қ }LǸ< 9s8]&d7KQ{ yQ#t0sX?xQ#I*`*'LF\%O3LASUA0sA1U0AAL3LASUAT(9`pד a! bބOñrB=*a h| rnlJ0lzf  կW[$^3XGc뷻=ed )q$mʷs[>=z8J'lTEі׹usg`n]yWw s\:׹Y:׹us맳?:׹us};s\:ק0׹uss\:׹0׹us\?^ DiIENDB`chimpchat/src/test/resources/com/android/chimpchat/image2.raw0100644 0000000 0000000 00005670531 12747325007 023372 0ustar000000000 0000000 srScom.android.monkeyrunner.adb.image.CaptureRawAndConvertedImage$MonkeyRunnerRawImage7t.I alpha_lengthI alpha_offsetI blue_lengthI blue_offsetIbppI green_lengthI green_offsetIheightI red_lengthI red_offsetIsizeIversionIwidth[datat[Bxp  pur[BTxpp>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>DDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDBBBXXXdddgggddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddgggdddXXXBBBDDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDDDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDDDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDbbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeejjjiiieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbb```YYYeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeYYY```bbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbbbbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbbbbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbbZZZaaacccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaZZZZZZaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaZZZZZZaaacccccccccccccccccccccccccccccccccccccccccccccccccccaaaZZZZZZaaaccccccccceeexxxwwweeecccccccccaaaZZZNNNeeexxxZZZaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccrrraaaZZZXXX`````````````````````````````````````````````````````````````````````XXXXXX``````````````````````````````````````````````````````````````````````````````XXXXXX``````````````````````````````````````````````````````XXXXXX``````kkkiii``````XXXyyyVVVeee___fffsssXXX````````````````````````````````````````````````````````````````````````XXXYYY$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$GGGVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]rrrppp]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]qqqqqq]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVVVV]]]]]]]]]]]]VVV]]]ppprrraaaiiiVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVkkk;;;RRRaaagggggggggggggggggggggggggggggggggggggggggggggggggggcccXXXAAACCCSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ^^^\\\ZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSSSSZZZZZZ}}}nnnnnn}}}ZZZZZZSSSuuuUUU{{{kkkbbblllSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSGGG`b[}0(SVNGGGPPPWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWPPPPPPWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWPPPPPPWWWWWWWWWeeeoooWWWWWWWWWWWWYYYWWWWWWWWWWWWWWWPPPPPPWWWWWWyyypppWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWmmmWWWWWWPPP```pppYYY{{{```\\\oooPPPWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWPPPOOOWWW!%NNN|||MMMTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMMMMMMTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMMMMMMTTTTTTbbbtttTTTTTTTTTvvvVVVTTTTTTTTTTTTMMMMMMTTTTTTUUU\\\TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT\\\UUUTTTTTTMMMqqqxxxUUUgggSSSGGGMMMTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMMMSSS]_ZVVVXXXlll,,, !!!]]]kkkbbbkkkbbbJJJPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPJJJJJJPPPPPPPPPPPPPPPPPPPPP||||||PPPPPPPPPPPPPPPPPPPPPJJJJJJPPPPPPyyyPPPPPPPPPwwwRRRPPPPPPPPPJJJJJJPPPPPPPPPPPPPPPPPP}}}yyyPPPPPPPPPPPPPPPPPPJJJ{{{~~~NNNuuuiii___JJJPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPJJJOOO`iLWWWPPP444WWWGGGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGGGGGGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGGGGGGMMMMMMMMMMMMMMMMMMsssMMMMMMMMMGGGGGGMMMMMMMMMMMMlllsssMMMMMMMMMMMMGGGjjjfffRRR{{{YYYpppGGGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMSSSdddMMMMMMMMMGGGFFFaoATTTMMMppp&&&  BBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHBBBBBBHHHHHHHHHJJJNNNHHHHHHHHHBBByyykkkIII^^^hhhPPPBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH~~~HHHBBB:::666HHHq#OOOIIIUUUOOO nnn nnn@@@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE@@@@@@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE@@@@@@EEEEEEEEEEEEEEE|||EEEEEEEEEEEEEEE@@@@@@EEEEEEEEEEEEjjjjjjEEEEEEEEEEEE@@@ZZZ~~~EEEoooPPPRRR@@@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE|||EEE@@@///???Zj5|JJJDDD>>>RRR999OOO<<<CCCxxx<<<CCCxxx<<>>@@@555888================================================888888========================888888=====================ttt=====================888888==================================================================888eee<<>>???:::OOOFFFyyy555444888888888888888888888888888888FFF888888888888888444444888888888888888888888888444444888888888888888888888888ppp888888888888888888888888444444888888888888888888??????888888888888888888444lll<<>>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!)))&&&!!!!!!!!!!!!!!!!!!!!!!!!RRR+++```zzzGGG333IIIooo<<<)))IIIxxx!!!!!!!!!ooo!!!!!!!!!!!!bbb!!!8882>xxxxxxxxxxxxxxxxxxxxxxxrUUU'''jjjQQQQQQAAAiiixxxUUU333kkkkkkgggGGG$$$111!!!!!!...###___###<<>> Pj^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}AT11/HHHEEE???&&&^^^AAA,,, ...ZZZfffdddVVVWWW... JJJqqq    |||XXX888&&&$$$SSSccc^^^ {{{___ QQQ      }}} xxxVVVFFF}}}www ``` 444 nnn @@@  ~~~uuuooo^^^DDD222ggg,,,LLLEEE  EEEIII$$$   $$$IIIEEE  EEEEEE  EEEEEE  EEERTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRjqjjmjsqsjmj)()1,19<9949141141141)()141)()1,11019<9    949 {}{949141949989 $ 101  )()sqs RPR101IHIbeb  jij),)A@Ababjmj RPRZ]ZIHIsqsZ]Z Z]Zbeb $ Z]Zjij{y{)$)babRPR101 beb989 {y{ susIHIIDI jmjA@AsqsZYZILI)()A ddms/app/.project0100644 0000000 0000000 00000000553 12747325007 012775 0ustar000000000 0000000 ddms org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature ddms/app/.settings/0040755 0000000 0000000 00000000000 12747325007 013244 5ustar000000000 0000000 ddms/app/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 020231 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 ddms/app/NOTICE0100644 0000000 0000000 00000024707 12747325007 012241 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ddms/app/README0100644 0000000 0000000 00000004504 12747325007 012206 0ustar000000000 0000000 Using the Eclipse project DDMS ------------------------------ DDMS requires some external libraries to compile. If you build DDMS using the makefile, you have nothing to configure. However if you want to develop on DDMS using Eclipse, you need to perform the following configuration. ------- 1- Projects required in Eclipse ------- To run DDMS from Eclipse, you need to import the following 5 projects: - sdk/androidpprefs: project AndroidPrefs - sdk/sdkstats: project SdkStatsService - sdk/ddms/app: project Ddms - sdk/ddms/libs/ddmlib: project Ddmlib - sdk/ddms/libs/ddmuilib: project Ddmuilib ------- 2- DDMS requires some SWT and OSGI JARs to compile. ------- SWT is available in the tree under prebuild//swt Because the build path cannot contain relative path that are not inside the project directory, the .classpath file references a user library called ANDROID_SWT. SWT depends on OSGI, so we'll also create an ANDROID_OSGI library for that. In order to compile the project: - Open Preferences > Java > Build Path > User Libraries - Create a new user library named ANDROID_SWT - Add the following 4 JAR files: - prebuilt//swt/swt.jar - prebuilt/common/eclipse/org.eclipse.core.commands_3.*.jar - prebuilt/common/eclipse/org.eclipse.equinox.common_3.*.jar - prebuilt/common/eclipse/org.eclipse.jface_3.*.jar - Create a new user library named ANDROID_OSGI - Add the following JAR file: - prebuilt/common/eclipse/org.eclipse.osgi_3.*.jar ------- 3- DDMS also requires the compiled SwtMenuBar library. ------- Build the swtmenubar library: $ cd $TOP (top of Android tree) $ . build/envsetup.sh && lunch sdk-eng $ sdk/eclipse/scripts/create_sdkman_symlinks.sh Define a classpath variable in Eclipse: - Open Preferences > Java > Build Path > Classpath Variables - Create a new classpath variable named ANDROID_OUT_FRAMEWORK - Set its folder value to /out/host//framework - Create a new classpath variable named ANDROID_SRC - Set its folder value to You might need to clean the ddms project (Project > Clean...) after you add the new classpath variable, otherwise previous errors might not go away automatically. The ANDROID_SRC part should be optional. It allows you to have access to the SwtMenuBar generic parts from the Java editor. -- EOF ddms/app/build.gradle0100644 0000000 0000000 00000001071 12747325007 013601 0ustar000000000 0000000 group = 'com.android.tools.ddms' archivesBaseName = 'ddms' dependencies { compile project(':base:common') compile project(':base:ddmlib') compile project(':swt:ddmuilib') compile project(':swt:sdkstats') compile project(':swt:swtmenubar') } sdk { linux { item('etc/ddms') { executable true } } mac { item('etc/ddms') { executable true } } windows { item 'etc/ddms.bat' } } // configure the manifest of the buildDistributionJar task. sdkJar.manifest.attributes("Main-Class": "com.android.ddms.Main") ddms/app/ddms.iml0100644 0000000 0000000 00000001442 12747325007 012756 0ustar000000000 0000000 ddms/app/etc/0040755 0000000 0000000 00000000000 12747325007 012101 5ustar000000000 0000000 ddms/app/etc/ddms0100755 0000000 0000000 00000006326 12747325007 012762 0ustar000000000 0000000 #!/bin/bash # Copyright 2005-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. # Set up prog to be the path of this script, including following symlinks, # and set up progdir to be the fully-qualified pathname of its directory. prog="$0" while [ -h "${prog}" ]; do newProg=`/bin/ls -ld "${prog}"` newProg=`expr "${newProg}" : ".* -> \(.*\)$"` if expr "x${newProg}" : 'x/' >/dev/null; then prog="${newProg}" else progdir=`dirname "${prog}"` prog="${progdir}/${newProg}" fi done oldwd=`pwd` progdir=`dirname "${prog}"` cd "${progdir}" progdir=`pwd` prog="${progdir}"/`basename "${prog}"` cd "${oldwd}" jarfile=ddms.jar frameworkdir="$progdir" libdir="$progdir" if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/tools/lib libdir=`dirname "$progdir"`/tools/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/framework libdir=`dirname "$progdir"`/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then echo `basename "$prog"`": can't find $jarfile" exit 1 fi # Check args. if [ debug = "$1" ]; then # add this in for debugging java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y shift 1 else java_debug= fi javaCmd="java" # Mac OS X needs an additional arg, or you get an "illegal thread" complaint. if [ `uname` = "Darwin" ]; then os_opts="-XstartOnFirstThread" else os_opts= fi if [ `uname` = "Linux" ]; then export GDK_NATIVE_WINDOWS=true fi jarpath="$frameworkdir/$jarfile:$frameworkdir/swtmenubar.jar" # Figure out the path to the swt.jar for the current architecture. # if ANDROID_SWT is defined, then just use this. # else, if running in the Android source tree, then look for the correct swt folder in prebuilt # else, look for the correct swt folder in the SDK under tools/lib/ swtpath="" if [ -n "$ANDROID_SWT" ]; then swtpath="$ANDROID_SWT" else vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` if [ -n "$ANDROID_BUILD_TOP" ]; then osname=`uname -s | tr A-Z a-z` swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" else swtpath="${frameworkdir}/${vmarch}" fi fi if [ ! -d "$swtpath" ]; then echo "SWT folder '${swtpath}' does not exist." echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." exit 1 fi if [ -x $progdir/monitor ]; then echo "The standalone version of DDMS is deprecated." echo "Please use Android Device Monitor (tools/monitor) instead." fi exec "$javaCmd" \ -Xmx256M $os_opts $java_debug \ -Dcom.android.ddms.bindir="$progdir" \ -classpath "$jarpath:$swtpath/swt.jar" \ com.android.ddms.Main "$@" ddms/app/etc/ddms.bat0100755 0000000 0000000 00000004612 12747325007 013523 0ustar000000000 0000000 @echo off rem Copyright (C) 2007 The Android Open Source Project rem rem Licensed under the Apache License, Version 2.0 (the "License"); rem you may not use this file except in compliance with the License. rem You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem don't modify the caller's environment setlocal rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 rem Change current directory and drive to where the script is, to avoid rem issues with directories containing whitespaces. cd /d %~dp0 rem Get the CWD as a full path with short names only (without spaces) for %%i in ("%cd%") do set prog_dir=%%~fsi rem Check we have a valid Java.exe in the path. set java_exe= call lib\find_java.bat if not defined java_exe goto :EOF set jarfile=ddms.jar set frameworkdir=. if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=lib if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=..\framework :JarFileOk if debug NEQ "%1" goto NoDebug set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y shift 1 :NoDebug set jarpath=%frameworkdir%\%jarfile%;%frameworkdir%\swtmenubar.jar if not defined ANDROID_SWT goto QueryArch set swt_path=%ANDROID_SWT% goto SwtDone :QueryArch for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a :SwtDone if exist "%swt_path%" goto SetPath echo SWT folder '%swt_path%' does not exist. echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. exit /B :SetPath set javaextdirs=%swt_path%;%frameworkdir% echo The standalone version of DDMS is deprecated. echo Please use Android Device Monitor (monitor.bat) instead. call "%java_exe%" %java_debug% "-Dcom.android.ddms.bindir=%prog_dir%" -classpath "%jarpath%;%swt_path%\swt.jar" com.android.ddms.Main %* ddms/app/src/0040755 0000000 0000000 00000000000 12747325007 012115 5ustar000000000 0000000 ddms/app/src/main/0040755 0000000 0000000 00000000000 12747325007 013041 5ustar000000000 0000000 ddms/app/src/main/java/0040755 0000000 0000000 00000000000 12747325007 013762 5ustar000000000 0000000 ddms/app/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 014540 5ustar000000000 0000000 ddms/app/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016160 5ustar000000000 0000000 ddms/app/src/main/java/com/android/ddms/0040755 0000000 0000000 00000000000 12747325007 017107 5ustar000000000 0000000 ddms/app/src/main/java/com/android/ddms/AboutDialog.java0100644 0000000 0000000 00000011414 12747325007 022142 0ustar000000000 0000000 /* //device/tools/ddms/src/com/android/ddms/AboutDialog.java ** ** Copyright 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.ddms; import com.android.ddmlib.Log; import com.android.ddmuilib.ImageLoader; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import java.io.InputStream; /** * Our "about" box. */ public class AboutDialog extends Dialog { private Image logoImage; /** * Create with default style. */ public AboutDialog(Shell parent) { this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); } /** * Create with app-defined style. */ public AboutDialog(Shell parent, int style) { super(parent, style); } /** * Prepare and display the dialog. */ public void open() { Shell parent = getParent(); Shell shell = new Shell(parent, getStyle()); shell.setText("About..."); logoImage = loadImage(shell, "ddms-128.png"); //$NON-NLS-1$ createContents(shell); shell.pack(); shell.open(); Display display = parent.getDisplay(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } logoImage.dispose(); } /* * Load an image file from a resource. * * This depends on Display, so I'm not sure what the rules are for * loading once and caching in a static class field. */ private Image loadImage(Shell shell, String fileName) { InputStream imageStream; String pathName = "/images/" + fileName; //$NON-NLS-1$ imageStream = this.getClass().getResourceAsStream(pathName); if (imageStream == null) { //throw new NullPointerException("couldn't find " + pathName); Log.w("ddms", "Couldn't load " + pathName); Display display = shell.getDisplay(); return ImageLoader.createPlaceHolderArt(display, 100, 50, display.getSystemColor(SWT.COLOR_BLUE)); } Image img = new Image(shell.getDisplay(), imageStream); if (img == null) throw new NullPointerException("couldn't load " + pathName); return img; } /* * Create the about box contents. */ private void createContents(final Shell shell) { GridLayout layout; GridData data; Label label; shell.setLayout(new GridLayout(2, false)); // Fancy logo Label logo = new Label(shell, SWT.BORDER); logo.setImage(logoImage); // Text Area Composite textArea = new Composite(shell, SWT.NONE); layout = new GridLayout(1, true); textArea.setLayout(layout); // Text lines label = new Label(textArea, SWT.NONE); if (Main.sRevision != null && Main.sRevision.length() > 0) { label.setText("Dalvik Debug Monitor Revision " + Main.sRevision); } else { label.setText("Dalvik Debug Monitor"); } label = new Label(textArea, SWT.NONE); // TODO: update with new year date (search this to find other occurrences to update) label.setText("Copyright 2007-2012, The Android Open Source Project"); label = new Label(textArea, SWT.NONE); label.setText("All Rights Reserved."); // blank spot in grid label = new Label(shell, SWT.NONE); // "OK" button Button ok = new Button(shell, SWT.PUSH); ok.setText("OK"); data = new GridData(GridData.HORIZONTAL_ALIGN_END); data.widthHint = 80; ok.setLayoutData(data); ok.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { shell.close(); } }); shell.pack(); shell.setDefaultButton(ok); } } ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java0100644 0000000 0000000 00000013073 12747325007 023361 0ustar000000000 0000000 /* * 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.ddms; import com.android.ddmlib.DebugPortManager.IDebugPortProvider; import com.android.ddmlib.IDevice; import org.eclipse.jface.preference.IPreferenceStore; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * DDMS implementation of the IDebugPortProvider interface. * This class handles saving/loading the list of static debug port from * the preference store and provides the port number to the Device Monitor. */ public class DebugPortProvider implements IDebugPortProvider { private static DebugPortProvider sThis = new DebugPortProvider(); /** Preference name for the static port list. */ public static final String PREFS_STATIC_PORT_LIST = "android.staticPortList"; //$NON-NLS-1$ /** * Mapping device serial numbers to maps. The embedded maps are mapping application names to * debugger ports. */ private Map> mMap; public static DebugPortProvider getInstance() { return sThis; } private DebugPortProvider() { computePortList(); } /** * Returns a static debug port for the specified application running on the * specified {@link IDevice}. * @param device The device the application is running on. * @param appName The application name, as defined in the * AndroidManifest.xml package attribute. * @return The static debug port or {@link #NO_STATIC_PORT} if there is none setup. * * @see IDebugPortProvider#getPort(IDevice, String) */ @Override public int getPort(IDevice device, String appName) { if (mMap != null) { Map deviceMap = mMap.get(device.getSerialNumber()); if (deviceMap != null) { Integer i = deviceMap.get(appName); if (i != null) { return i.intValue(); } } } return IDebugPortProvider.NO_STATIC_PORT; } /** * Returns the map of Static debugger ports. The map links device serial numbers to * a map linking application name to debugger ports. */ public Map> getPortList() { return mMap; } /** * Create the map member from the values contained in the Preference Store. */ private void computePortList() { mMap = new HashMap>(); // get the prefs store IPreferenceStore store = PrefsDialog.getStore(); String value = store.getString(PREFS_STATIC_PORT_LIST); if (value != null && value.length() > 0) { // format is // port1|port2|port3|... // where port# is // appPackageName:appPortNumber:device-serial-number String[] portSegments = value.split("\\|"); //$NON-NLS-1$ for (String seg : portSegments) { String[] entry = seg.split(":"); //$NON-NLS-1$ // backward compatibility support. if we have only 2 entry, we default // to the first emulator. String deviceName = null; if (entry.length == 3) { deviceName = entry[2]; } else { deviceName = IDevice.FIRST_EMULATOR_SN; } // get the device map Map deviceMap = mMap.get(deviceName); if (deviceMap == null) { deviceMap = new HashMap(); mMap.put(deviceName, deviceMap); } deviceMap.put(entry[0], Integer.valueOf(entry[1])); } } } /** * Sets new [device, app, port] values. * The values are also sync'ed in the preference store. * @param map The map containing the new values. */ public void setPortList(Map> map) { // update the member map. mMap.clear(); mMap.putAll(map); // create the value to store in the preference store. // see format definition in getPortList StringBuilder sb = new StringBuilder(); Set deviceKeys = map.keySet(); for (String deviceKey : deviceKeys) { Map deviceMap = map.get(deviceKey); if (deviceMap != null) { Set appKeys = deviceMap.keySet(); for (String appKey : appKeys) { Integer port = deviceMap.get(appKey); if (port != null) { sb.append(appKey).append(':').append(port.intValue()).append(':'). append(deviceKey).append('|'); } } } } String value = sb.toString(); // get the prefs store. IPreferenceStore store = PrefsDialog.getStore(); // and give it the new value. store.setValue(PREFS_STATIC_PORT_LIST, value); } } ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java0100644 0000000 0000000 00000033366 12747325007 023600 0ustar000000000 0000000 /* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java ** ** Copyright 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.ddms; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice; import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.Log; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; /** * Execute a command on an ADB-attached device and save the output. * * There are several ways to do this. One is to run a single command * and show the output. Another is to have several possible commands and * let the user click a button next to the one (or ones) they want. This * currently uses the simple 1:1 form. */ public class DeviceCommandDialog extends Dialog { public static final int DEVICE_STATE = 0; public static final int APP_STATE = 1; public static final int RADIO_STATE = 2; public static final int LOGCAT = 3; private String mCommand; private String mFileName; private Label mStatusLabel; private Button mCancelDone; private Button mSave; private Text mText; private Font mFont = null; private boolean mCancel; private boolean mFinished; /** * Create with default style. */ public DeviceCommandDialog(String command, String fileName, Shell parent) { // don't want a close button, but it seems hard to get rid of on GTK // keep it on all platforms for consistency this(command, fileName, parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE); } /** * Create with app-defined style. */ public DeviceCommandDialog(String command, String fileName, Shell parent, int style) { super(parent, style); mCommand = command; mFileName = fileName; } /** * Prepare and display the dialog. * @param currentDevice */ public void open(IDevice currentDevice) { Shell parent = getParent(); Shell shell = new Shell(parent, getStyle()); shell.setText("Remote Command"); mFinished = false; mFont = findFont(shell.getDisplay()); createContents(shell); // Getting weird layout behavior under Linux when Text is added -- // looks like text widget has min width of 400 when FILL_HORIZONTAL // is used, and layout gets tweaked to force this. (Might be even // more with the scroll bars in place -- it wigged out when the // file save dialog was invoked.) shell.setMinimumSize(500, 200); shell.setSize(800, 600); shell.open(); executeCommand(shell, currentDevice); Display display = parent.getDisplay(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } if (mFont != null) mFont.dispose(); } /* * Create a text widget to show the output and some buttons to * manage things. */ private void createContents(final Shell shell) { GridData data; shell.setLayout(new GridLayout(2, true)); shell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { if (!mFinished) { Log.d("ddms", "NOT closing - cancelling command"); event.doit = false; mCancel = true; } } }); mStatusLabel = new Label(shell, SWT.NONE); mStatusLabel.setText("Executing '" + shortCommandString() + "'"); data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); data.horizontalSpan = 2; mStatusLabel.setLayoutData(data); mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); mText.setEditable(false); mText.setFont(mFont); data = new GridData(GridData.FILL_BOTH); data.horizontalSpan = 2; mText.setLayoutData(data); // "save" button mSave = new Button(shell, SWT.PUSH); mSave.setText("Save"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; mSave.setLayoutData(data); mSave.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { saveText(shell); } }); mSave.setEnabled(false); // "cancel/done" button mCancelDone = new Button(shell, SWT.PUSH); mCancelDone.setText("Cancel"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; mCancelDone.setLayoutData(data); mCancelDone.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (!mFinished) mCancel = true; else shell.close(); } }); } /* * Figure out what font to use. * * Returns "null" if we can't figure it out, which SWT understands to * mean "use default system font". */ private Font findFont(Display display) { String fontStr = PrefsDialog.getStore().getString("textOutputFont"); if (fontStr != null) { FontData fdat = new FontData(fontStr); if (fdat != null) return new Font(display, fdat); } return null; } /* * Callback class for command execution. */ class Gatherer extends Thread implements IShellOutputReceiver { public static final int RESULT_UNKNOWN = 0; public static final int RESULT_SUCCESS = 1; public static final int RESULT_FAILURE = 2; public static final int RESULT_CANCELLED = 3; private Shell mShell; private String mCommand; private Text mText; private int mResult; private IDevice mDevice; /** * Constructor; pass in the text widget that will receive the output. * @param device */ public Gatherer(Shell shell, IDevice device, String command, Text text) { mShell = shell; mDevice = device; mCommand = command; mText = text; mResult = RESULT_UNKNOWN; // this is in outer class mCancel = false; } /** * Thread entry point. */ @Override public void run() { if (mDevice == null) { Log.w("ddms", "Cannot execute command: no device selected."); mResult = RESULT_FAILURE; } else { try { mDevice.executeShellCommand(mCommand, this); if (mCancel) mResult = RESULT_CANCELLED; else mResult = RESULT_SUCCESS; } catch (IOException ioe) { Log.w("ddms", "Remote exec failed: " + ioe.getMessage()); mResult = RESULT_FAILURE; } catch (TimeoutException e) { Log.w("ddms", "Remote exec failed: " + e.getMessage()); mResult = RESULT_FAILURE; } catch (AdbCommandRejectedException e) { Log.w("ddms", "Remote exec failed: " + e.getMessage()); mResult = RESULT_FAILURE; } catch (ShellCommandUnresponsiveException e) { Log.w("ddms", "Remote exec failed: " + e.getMessage()); mResult = RESULT_FAILURE; } } mShell.getDisplay().asyncExec(new Runnable() { @Override public void run() { updateForResult(mResult); } }); } /** * Called by executeRemoteCommand(). */ @Override public void addOutput(byte[] data, int offset, int length) { Log.v("ddms", "received " + length + " bytes"); try { final String text; text = new String(data, offset, length, "ISO-8859-1"); // add to text widget; must do in UI thread mText.getDisplay().asyncExec(new Runnable() { @Override public void run() { mText.append(text); } }); } catch (UnsupportedEncodingException uee) { uee.printStackTrace(); // not expected } } @Override public void flush() { // nothing to flush. } /** * Called by executeRemoteCommand(). */ @Override public boolean isCancelled() { return mCancel; } }; /* * Execute a remote command, add the output to the text widget, and * update controls. * * We have to run the command in a thread so that the UI continues * to work. */ private void executeCommand(Shell shell, IDevice device) { Gatherer gath = new Gatherer(shell, device, commandString(), mText); gath.start(); } /* * Update the controls after the remote operation completes. This * must be called from the UI thread. */ private void updateForResult(int result) { if (result == Gatherer.RESULT_SUCCESS) { mStatusLabel.setText("Successfully executed '" + shortCommandString() + "'"); mSave.setEnabled(true); } else if (result == Gatherer.RESULT_CANCELLED) { mStatusLabel.setText("Execution cancelled; partial results below"); mSave.setEnabled(true); // save partial } else if (result == Gatherer.RESULT_FAILURE) { mStatusLabel.setText("Failed"); } mStatusLabel.pack(); mCancelDone.setText("Done"); mFinished = true; } /* * Allow the user to save the contents of the text dialog. */ private void saveText(Shell shell) { FileDialog dlg = new FileDialog(shell, SWT.SAVE); String fileName; dlg.setText("Save output..."); dlg.setFileName(defaultFileName()); dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir")); dlg.setFilterNames(new String[] { "Text Files (*.txt)" }); dlg.setFilterExtensions(new String[] { "*.txt" }); fileName = dlg.open(); if (fileName != null) { PrefsDialog.getStore().setValue("lastTextSaveDir", dlg.getFilterPath()); Log.d("ddms", "Saving output to " + fileName); /* * Convert to 8-bit characters. */ String text = mText.getText(); byte[] ascii; try { ascii = text.getBytes("ISO-8859-1"); } catch (UnsupportedEncodingException uee) { uee.printStackTrace(); ascii = new byte[0]; } /* * Output data, converting CRLF to LF. */ try { int length = ascii.length; FileOutputStream outFile = new FileOutputStream(fileName); BufferedOutputStream out = new BufferedOutputStream(outFile); for (int i = 0; i < length; i++) { if (i < length-1 && ascii[i] == 0x0d && ascii[i+1] == 0x0a) { continue; } out.write(ascii[i]); } out.close(); // flush buffer, close file } catch (IOException ioe) { Log.w("ddms", "Unable to save " + fileName + ": " + ioe); } } } /* * Return the shell command we're going to use. */ private String commandString() { return mCommand; } /* * Return a default filename for the "save" command. */ private String defaultFileName() { return mFileName; } /* * Like commandString(), but length-limited. */ private String shortCommandString() { String str = commandString(); if (str.length() > 50) return str.substring(0, 50) + "..."; else return str; } } ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java0100644 0000000 0000000 00000005077 12747325007 025130 0ustar000000000 0000000 /* //device/tools/ddms/src/com/android/ddms/DropdownSelectionListener.java ** ** Copyright 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.ddms; import com.android.ddmlib.Log; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.ToolItem; /** * Helper class for drop-down menus in toolbars. */ public class DropdownSelectionListener extends SelectionAdapter { private Menu mMenu; private ToolItem mDropdown; /** * Basic constructor. Creates an empty Menu to hold items. */ public DropdownSelectionListener(ToolItem item) { mDropdown = item; mMenu = new Menu(item.getParent().getShell(), SWT.POP_UP); } /** * Add an item to the dropdown menu. */ public void add(String label) { MenuItem item = new MenuItem(mMenu, SWT.NONE); item.setText(label); item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // update the dropdown's text to match the selection MenuItem sel = (MenuItem) e.widget; mDropdown.setText(sel.getText()); } }); } /** * Invoked when dropdown or neighboring arrow is clicked. */ @Override public void widgetSelected(SelectionEvent e) { if (e.detail == SWT.ARROW) { // arrow clicked, show menu ToolItem item = (ToolItem) e.widget; Rectangle rect = item.getBounds(); Point pt = item.getParent().toDisplay(new Point(rect.x, rect.y)); mMenu.setLocation(pt.x, pt.y + rect.height); mMenu.setVisible(true); } else { // button clicked Log.d("ddms", mDropdown.getText() + " Pressed"); } } } ddms/app/src/main/java/com/android/ddms/Main.java0100644 0000000 0000000 00000013371 12747325007 020640 0ustar000000000 0000000 /* * 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.ddms; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.DebugPortManager; import com.android.ddmlib.Log; import com.android.sdkstats.SdkStatsService; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.util.Properties; /** * Start the UI and network. */ public class Main { public static String sRevision; public Main() { } /* * If a thread bails with an uncaught exception, bring the whole * thing down. */ private static class UncaughtHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { Log.e("ddms", "shutting down due to uncaught exception"); Log.e("ddms", e); System.exit(1); } } /** * Parse args, start threads. */ public static void main(String[] args) { // In order to have the AWT/SWT bridge work on Leopard, we do this little hack. if (isMac()) { RuntimeMXBean rt = ManagementFactory.getRuntimeMXBean(); System.setProperty( "JAVA_STARTED_ON_FIRST_THREAD_" + (rt.getName().split("@"))[0], //$NON-NLS-1$ "1"); //$NON-NLS-1$ } Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler()); // load prefs and init the default values PrefsDialog.init(); Log.d("ddms", "Initializing"); // Create an initial shell display with the correct app name. Display.setAppName(UIThread.APP_NAME); Shell shell = new Shell(Display.getDefault()); // if this is the first time using ddms or adt, open up the stats service // opt out dialog, and request user for permissions. SdkStatsService stats = new SdkStatsService(); stats.checkUserPermissionForPing(shell); // the "ping" argument means to check in with the server and exit // the application name and version number must also be supplied if (args.length >= 3 && args[0].equals("ping")) { stats.ping(args); return; } else if (args.length > 0) { Log.e("ddms", "Unknown argument: " + args[0]); System.exit(1); } // get the ddms parent folder location String ddmsParentLocation = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$ if (ddmsParentLocation == null) { // Tip: for debugging DDMS in eclipse, set this env var to the SDK/tools // directory path. ddmsParentLocation = System.getenv("com.android.ddms.bindir"); //$NON-NLS-1$ } // we're past the point where ddms can be called just to send a ping, so we can // ping for ddms itself. ping(stats, ddmsParentLocation); stats = null; DebugPortManager.setProvider(DebugPortProvider.getInstance()); // create the three main threads UIThread ui = UIThread.getInstance(); try { ui.runUI(ddmsParentLocation); } finally { PrefsDialog.save(); AndroidDebugBridge.terminate(); } Log.d("ddms", "Bye"); // this is kinda bad, but on MacOS the shutdown doesn't seem to finish because of // a thread called AWT-Shutdown. This will help while I track this down. System.exit(0); } /** Return true iff we're running on a Mac */ static boolean isMac() { // TODO: Replace usages of this method with // org.eclipse.jface.util.Util#isMac() when we switch to Eclipse 3.5 // (ddms is currently built with SWT 3.4.2 from ANDROID_SWT) return System.getProperty("os.name").startsWith("Mac OS"); //$NON-NLS-1$ //$NON-NLS-2$ } private static void ping(SdkStatsService stats, String ddmsParentLocation) { Properties p = new Properties(); try{ File sourceProp; if (ddmsParentLocation != null && ddmsParentLocation.length() > 0) { sourceProp = new File(ddmsParentLocation, "source.properties"); //$NON-NLS-1$ } else { sourceProp = new File("source.properties"); //$NON-NLS-1$ } FileInputStream fis = null; try { fis = new FileInputStream(sourceProp); p.load(fis); } finally { if (fis != null) { try { fis.close(); } catch (IOException ignore) { } } } sRevision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ if (sRevision != null && sRevision.length() > 0) { stats.ping("ddms", sRevision); //$NON-NLS-1$ } } catch (FileNotFoundException e) { // couldn't find the file? don't ping. } catch (IOException e) { // couldn't find the file? don't ping. } } } ddms/app/src/main/java/com/android/ddms/PrefsDialog.java0100644 0000000 0000000 00000055452 12747325007 022161 0ustar000000000 0000000 /* * 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.ddms; import com.android.ddmlib.DdmConstants; import com.android.ddmlib.DdmPreferences; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.logcat.LogCatMessageList; import com.android.ddmuilib.logcat.LogCatPanel; import com.android.sdkstats.DdmsPreferenceStore; import com.android.sdkstats.SdkStatsPermissionDialog; import org.eclipse.jface.preference.BooleanFieldEditor; import org.eclipse.jface.preference.DirectoryFieldEditor; import org.eclipse.jface.preference.FieldEditorPreferencePage; import org.eclipse.jface.preference.FontFieldEditor; import org.eclipse.jface.preference.IntegerFieldEditor; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.preference.PreferenceManager; import org.eclipse.jface.preference.PreferenceNode; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.preference.PreferenceStore; import org.eclipse.jface.preference.RadioGroupFieldEditor; import org.eclipse.jface.preference.StringFieldEditor; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Shell; import java.io.File; import java.io.IOException; /** * Preferences dialog. */ public final class PrefsDialog { // public const values for storage public final static String SHELL_X = "shellX"; //$NON-NLS-1$ public final static String SHELL_Y = "shellY"; //$NON-NLS-1$ public final static String SHELL_WIDTH = "shellWidth"; //$NON-NLS-1$ public final static String SHELL_HEIGHT = "shellHeight"; //$NON-NLS-1$ public final static String EXPLORER_SHELL_X = "explorerShellX"; //$NON-NLS-1$ public final static String EXPLORER_SHELL_Y = "explorerShellY"; //$NON-NLS-1$ public final static String EXPLORER_SHELL_WIDTH = "explorerShellWidth"; //$NON-NLS-1$ public final static String EXPLORER_SHELL_HEIGHT = "explorerShellHeight"; //$NON-NLS-1$ public final static String SHOW_NATIVE_HEAP = "native"; //$NON-NLS-1$ public final static String LOGCAT_COLUMN_MODE = "ddmsLogColumnMode"; //$NON-NLS-1$ public final static String LOGCAT_FONT = "ddmsLogFont"; //$NON-NLS-1$ public final static String LOGCAT_COLUMN_MODE_AUTO = "auto"; //$NON-NLS-1$ public final static String LOGCAT_COLUMN_MODE_MANUAL = "manual"; //$NON-NLS-1$ private final static String PREFS_DEBUG_PORT_BASE = "adbDebugBasePort"; //$NON-NLS-1$ private final static String PREFS_SELECTED_DEBUG_PORT = "debugSelectedPort"; //$NON-NLS-1$ private final static String PREFS_DEFAULT_THREAD_UPDATE = "defaultThreadUpdateEnabled"; //$NON-NLS-1$ private final static String PREFS_DEFAULT_HEAP_UPDATE = "defaultHeapUpdateEnabled"; //$NON-NLS-1$ private final static String PREFS_THREAD_REFRESH_INTERVAL = "threadStatusInterval"; //$NON-NLS-1$ private final static String PREFS_LOG_LEVEL = "ddmsLogLevel"; //$NON-NLS-1$ private final static String PREFS_TIMEOUT = "timeOut"; //$NON-NLS-1$ private final static String PREFS_PROFILER_BUFFER_SIZE_MB = "profilerBufferSizeMb"; //$NON-NLS-1$ private final static String PREFS_USE_ADBHOST = "useAdbHost"; //$NON-NLS-1$ private final static String PREFS_ADBHOST_VALUE = "adbHostValue"; //$NON-NLS-1$ // Preference store. private static DdmsPreferenceStore mStore = new DdmsPreferenceStore(); /** * Private constructor -- do not instantiate. */ private PrefsDialog() {} /** * Return the PreferenceStore that holds our values. * * @deprecated Callers should use {@link DdmsPreferenceStore} directly. */ @Deprecated public static PreferenceStore getStore() { return mStore.getPreferenceStore(); } /** * Save the prefs to the config file. * * @deprecated Callers should use {@link DdmsPreferenceStore} directly. */ @Deprecated public static void save() { try { mStore.getPreferenceStore().save(); } catch (IOException ioe) { Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage()); } } /** * Do some one-time prep. * * The original plan was to let the individual classes define their * own defaults, which we would get and then override with the config * file. However, PreferencesStore.load() doesn't trigger the "changed" * events, which means we have to pull the loaded config values out by * hand. * * So, we set the defaults, load the values from the config file, and * then run through and manually export the values. Then we duplicate * the second part later on for the "changed" events. */ public static void init() { PreferenceStore prefStore = mStore.getPreferenceStore(); if (prefStore == null) { // we have a serious issue here... Log.e("ddms", "failed to access both the user HOME directory and the system wide temp folder. Quitting."); System.exit(1); } // configure default values setDefaults(System.getProperty("user.home")); //$NON-NLS-1$ // listen for changes prefStore.addPropertyChangeListener(new ChangeListener()); // Now we initialize the value of the preference, from the values in the store. // First the ddm lib. DdmPreferences.setDebugPortBase(prefStore.getInt(PREFS_DEBUG_PORT_BASE)); DdmPreferences.setSelectedDebugPort(prefStore.getInt(PREFS_SELECTED_DEBUG_PORT)); DdmPreferences.setLogLevel(prefStore.getString(PREFS_LOG_LEVEL)); DdmPreferences.setInitialThreadUpdate(prefStore.getBoolean(PREFS_DEFAULT_THREAD_UPDATE)); DdmPreferences.setInitialHeapUpdate(prefStore.getBoolean(PREFS_DEFAULT_HEAP_UPDATE)); DdmPreferences.setTimeOut(prefStore.getInt(PREFS_TIMEOUT)); DdmPreferences.setProfilerBufferSizeMb(prefStore.getInt(PREFS_PROFILER_BUFFER_SIZE_MB)); DdmPreferences.setUseAdbHost(prefStore.getBoolean(PREFS_USE_ADBHOST)); DdmPreferences.setAdbHostValue(prefStore.getString(PREFS_ADBHOST_VALUE)); // some static values String out = System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ DdmUiPreferences.setSymbolsLocation(out + File.separator + "symbols"); //$NON-NLS-1$ DdmUiPreferences.setAddr2LineLocation("arm-linux-androideabi-addr2line"); //$NON-NLS-1$ DdmUiPreferences.setAddr2LineLocation64("aarch64-linux-android-addr2line"); String traceview = System.getProperty("com.android.ddms.bindir"); //$NON-NLS-1$ if (traceview != null && traceview.length() != 0) { traceview += File.separator + DdmConstants.FN_TRACEVIEW; } else { traceview = DdmConstants.FN_TRACEVIEW; } DdmUiPreferences.setTraceviewLocation(traceview); // Now the ddmui lib DdmUiPreferences.setStore(prefStore); DdmUiPreferences.setThreadRefreshInterval(prefStore.getInt(PREFS_THREAD_REFRESH_INTERVAL)); } /* * Set default values for all preferences. These are either defined * statically or are based on the values set by the class initializers * in other classes. * * The other threads (e.g. VMWatcherThread) haven't been created yet, * so we want to use static values rather than reading fields from * class.getInstance(). */ private static void setDefaults(String homeDir) { PreferenceStore prefStore = mStore.getPreferenceStore(); prefStore.setDefault(PREFS_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE); prefStore.setDefault(PREFS_SELECTED_DEBUG_PORT, DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT); prefStore.setDefault(PREFS_USE_ADBHOST, DdmPreferences.DEFAULT_USE_ADBHOST); prefStore.setDefault(PREFS_ADBHOST_VALUE, DdmPreferences.DEFAULT_ADBHOST_VALUE); prefStore.setDefault(PREFS_DEFAULT_THREAD_UPDATE, true); prefStore.setDefault(PREFS_DEFAULT_HEAP_UPDATE, false); prefStore.setDefault(PREFS_THREAD_REFRESH_INTERVAL, DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL); prefStore.setDefault("textSaveDir", homeDir); //$NON-NLS-1$ prefStore.setDefault("imageSaveDir", homeDir); //$NON-NLS-1$ prefStore.setDefault(PREFS_LOG_LEVEL, "info"); //$NON-NLS-1$ prefStore.setDefault(PREFS_TIMEOUT, DdmPreferences.DEFAULT_TIMEOUT); prefStore.setDefault(PREFS_PROFILER_BUFFER_SIZE_MB, DdmPreferences.DEFAULT_PROFILER_BUFFER_SIZE_MB); // choose a default font for the text output FontData fdat = new FontData("Courier", 10, SWT.NORMAL); //$NON-NLS-1$ prefStore.setDefault("textOutputFont", fdat.toString()); //$NON-NLS-1$ // layout information. prefStore.setDefault(SHELL_X, 100); prefStore.setDefault(SHELL_Y, 100); prefStore.setDefault(SHELL_WIDTH, 800); prefStore.setDefault(SHELL_HEIGHT, 600); prefStore.setDefault(EXPLORER_SHELL_X, 50); prefStore.setDefault(EXPLORER_SHELL_Y, 50); prefStore.setDefault(SHOW_NATIVE_HEAP, false); } /* * Create a "listener" to take action when preferences change. These are * required for ongoing activities that don't check prefs on each use. * * This is only invoked when something explicitly changes the value of * a preference (e.g. not when the prefs file is loaded). */ private static class ChangeListener implements IPropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent event) { String changed = event.getProperty(); PreferenceStore prefStore = mStore.getPreferenceStore(); if (changed.equals(PREFS_DEBUG_PORT_BASE)) { DdmPreferences.setDebugPortBase(prefStore.getInt(PREFS_DEBUG_PORT_BASE)); } else if (changed.equals(PREFS_SELECTED_DEBUG_PORT)) { DdmPreferences.setSelectedDebugPort(prefStore.getInt(PREFS_SELECTED_DEBUG_PORT)); } else if (changed.equals(PREFS_LOG_LEVEL)) { DdmPreferences.setLogLevel((String)event.getNewValue()); } else if (changed.equals("textSaveDir")) { prefStore.setValue("lastTextSaveDir", (String) event.getNewValue()); } else if (changed.equals("imageSaveDir")) { prefStore.setValue("lastImageSaveDir", (String) event.getNewValue()); } else if (changed.equals(PREFS_TIMEOUT)) { DdmPreferences.setTimeOut(prefStore.getInt(PREFS_TIMEOUT)); } else if (changed.equals(PREFS_PROFILER_BUFFER_SIZE_MB)) { DdmPreferences.setProfilerBufferSizeMb( prefStore.getInt(PREFS_PROFILER_BUFFER_SIZE_MB)); } else if (changed.equals(PREFS_USE_ADBHOST)) { DdmPreferences.setUseAdbHost(prefStore.getBoolean(PREFS_USE_ADBHOST)); } else if (changed.equals(PREFS_ADBHOST_VALUE)) { DdmPreferences.setAdbHostValue(prefStore.getString(PREFS_ADBHOST_VALUE)); } else { Log.v("ddms", "Preference change: " + event.getProperty() + ": '" + event.getOldValue() + "' --> '" + event.getNewValue() + "'"); } } } /** * Create and display the dialog. */ public static void run(Shell shell) { PreferenceStore prefStore = mStore.getPreferenceStore(); assert prefStore != null; PreferenceManager prefMgr = new PreferenceManager(); PreferenceNode node, subNode; // this didn't work -- got NPE, possibly from class lookup: //PreferenceNode app = new PreferenceNode("app", "Application", null, // AppPrefs.class.getName()); node = new PreferenceNode("debugger", new DebuggerPrefs()); prefMgr.addToRoot(node); subNode = new PreferenceNode("panel", new PanelPrefs()); //prefMgr.addTo(node.getId(), subNode); prefMgr.addToRoot(subNode); node = new PreferenceNode("LogCat", new LogCatPrefs()); prefMgr.addToRoot(node); node = new PreferenceNode("misc", new MiscPrefs()); prefMgr.addToRoot(node); node = new PreferenceNode("stats", new UsageStatsPrefs()); prefMgr.addToRoot(node); PreferenceDialog dlg = new PreferenceDialog(shell, prefMgr); dlg.setPreferenceStore(prefStore); // run it try { dlg.open(); } catch (Throwable t) { Log.e("ddms", t); } // save prefs try { prefStore.save(); } catch (IOException ioe) { } // discard the stuff we created //prefMgr.dispose(); //dlg.dispose(); } /** * "Debugger" prefs page. */ private static class DebuggerPrefs extends FieldEditorPreferencePage { private BooleanFieldEditor mUseAdbHost; private StringFieldEditor mAdbHostValue; /** * Basic constructor. */ public DebuggerPrefs() { super(GRID); // use "grid" layout so edit boxes line up setTitle("Debugger"); } /** * Create field editors. */ @Override protected void createFieldEditors() { IntegerFieldEditor ife; ife = new IntegerFieldEditor(PREFS_DEBUG_PORT_BASE, "Starting value for local port:", getFieldEditorParent()); ife.setValidRange(1024, 32767); addField(ife); ife = new IntegerFieldEditor(PREFS_SELECTED_DEBUG_PORT, "Port of Selected VM:", getFieldEditorParent()); ife.setValidRange(1024, 32767); addField(ife); mUseAdbHost = new BooleanFieldEditor(PREFS_USE_ADBHOST, "Use ADBHOST", getFieldEditorParent()); addField(mUseAdbHost); mAdbHostValue = new StringFieldEditor(PREFS_ADBHOST_VALUE, "ADBHOST value:", getFieldEditorParent()); mAdbHostValue.setEnabled(getPreferenceStore() .getBoolean(PREFS_USE_ADBHOST), getFieldEditorParent()); addField(mAdbHostValue); } @Override public void propertyChange(PropertyChangeEvent event) { // TODO Auto-generated method stub if (event.getSource().equals(mUseAdbHost)) { mAdbHostValue.setEnabled(mUseAdbHost.getBooleanValue(), getFieldEditorParent()); } super.propertyChange(event); } } /** * "Panel" prefs page. */ private static class PanelPrefs extends FieldEditorPreferencePage { /** * Basic constructor. */ public PanelPrefs() { super(FLAT); // use "flat" layout setTitle("Info Panels"); } /** * Create field editors. */ @Override protected void createFieldEditors() { BooleanFieldEditor bfe; IntegerFieldEditor ife; bfe = new BooleanFieldEditor(PREFS_DEFAULT_THREAD_UPDATE, "Thread updates enabled by default", getFieldEditorParent()); addField(bfe); bfe = new BooleanFieldEditor(PREFS_DEFAULT_HEAP_UPDATE, "Heap updates enabled by default", getFieldEditorParent()); addField(bfe); ife = new IntegerFieldEditor(PREFS_THREAD_REFRESH_INTERVAL, "Thread status interval (seconds):", getFieldEditorParent()); ife.setValidRange(1, 60); addField(ife); } } /** * "logcat" prefs page. */ private static class LogCatPrefs extends FieldEditorPreferencePage { /** * Basic constructor. */ public LogCatPrefs() { super(FLAT); // use "flat" layout setTitle("Logcat"); } /** * Create field editors. */ @Override protected void createFieldEditors() { if (UIThread.useOldLogCatView()) { RadioGroupFieldEditor rgfe; rgfe = new RadioGroupFieldEditor(PrefsDialog.LOGCAT_COLUMN_MODE, "Message Column Resizing Mode", 1, new String[][] { { "Manual", PrefsDialog.LOGCAT_COLUMN_MODE_MANUAL }, { "Automatic", PrefsDialog.LOGCAT_COLUMN_MODE_AUTO }, }, getFieldEditorParent(), true); addField(rgfe); FontFieldEditor ffe = new FontFieldEditor(PrefsDialog.LOGCAT_FONT, "Text output font:", getFieldEditorParent()); addField(ffe); } else { FontFieldEditor ffe = new FontFieldEditor(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY, "Text output font:", getFieldEditorParent()); addField(ffe); IntegerFieldEditor maxMessages = new IntegerFieldEditor( LogCatMessageList.MAX_MESSAGES_PREFKEY, "Maximum number of logcat messages to buffer", getFieldEditorParent()); addField(maxMessages); BooleanFieldEditor autoScrollLock = new BooleanFieldEditor( LogCatPanel.AUTO_SCROLL_LOCK_PREFKEY, "Automatically enable/disable scroll lock based on the scrollbar position", getFieldEditorParent()); addField(autoScrollLock); } } } /** * "misc" prefs page. */ private static class MiscPrefs extends FieldEditorPreferencePage { /** * Basic constructor. */ public MiscPrefs() { super(FLAT); // use "flat" layout setTitle("Misc"); } /** * Create field editors. */ @Override protected void createFieldEditors() { DirectoryFieldEditor dfe; FontFieldEditor ffe; IntegerFieldEditor ife = new IntegerFieldEditor(PREFS_TIMEOUT, "ADB connection time out (ms):", getFieldEditorParent()); addField(ife); ife = new IntegerFieldEditor(PREFS_PROFILER_BUFFER_SIZE_MB, "Profiler buffer size (MB):", getFieldEditorParent()); addField(ife); dfe = new DirectoryFieldEditor("textSaveDir", "Default text save dir:", getFieldEditorParent()); addField(dfe); dfe = new DirectoryFieldEditor("imageSaveDir", "Default image save dir:", getFieldEditorParent()); addField(dfe); ffe = new FontFieldEditor("textOutputFont", "Text output font:", getFieldEditorParent()); addField(ffe); RadioGroupFieldEditor rgfe; rgfe = new RadioGroupFieldEditor(PREFS_LOG_LEVEL, "Logging Level", 1, new String[][] { { "Verbose", LogLevel.VERBOSE.getStringValue() }, { "Debug", LogLevel.DEBUG.getStringValue() }, { "Info", LogLevel.INFO.getStringValue() }, { "Warning", LogLevel.WARN.getStringValue() }, { "Error", LogLevel.ERROR.getStringValue() }, { "Assert", LogLevel.ASSERT.getStringValue() }, }, getFieldEditorParent(), true); addField(rgfe); } } /** * "Device" prefs page. */ private static class UsageStatsPrefs extends PreferencePage { private BooleanFieldEditor mOptInCheckbox; private Composite mTop; /** * Basic constructor. */ public UsageStatsPrefs() { setTitle("Usage Stats"); } @Override protected Control createContents(Composite parent) { mTop = new Composite(parent, SWT.NONE); mTop.setLayout(new GridLayout(1, false)); mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); Label text = new Label(mTop, SWT.WRAP); text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); text.setText(SdkStatsPermissionDialog.BODY_TEXT); Link privacyPolicyLink = new Link(mTop, SWT.WRAP); privacyPolicyLink.setText(SdkStatsPermissionDialog.PRIVACY_POLICY_LINK_TEXT); privacyPolicyLink.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { SdkStatsPermissionDialog.openUrl(event.text); } }); mOptInCheckbox = new BooleanFieldEditor(DdmsPreferenceStore.PING_OPT_IN, SdkStatsPermissionDialog.CHECKBOX_TEXT, mTop); mOptInCheckbox.setPage(this); mOptInCheckbox.setPreferenceStore(getPreferenceStore()); mOptInCheckbox.load(); return null; } @Override protected Point doComputeSize() { if (mTop != null) { return mTop.computeSize(450, SWT.DEFAULT, true); } return super.doComputeSize(); } @Override protected void performDefaults() { if (mOptInCheckbox != null) { mOptInCheckbox.loadDefault(); } super.performDefaults(); } @Override public void performApply() { if (mOptInCheckbox != null) { mOptInCheckbox.store(); } super.performApply(); } @Override public boolean performOk() { if (mOptInCheckbox != null) { mOptInCheckbox.store(); } return super.performOk(); } } } ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java0100644 0000000 0000000 00000031770 12747325007 024321 0ustar000000000 0000000 /* * 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.ddms; import com.android.ddmuilib.TableHelper; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * Dialog to configure the static debug ports. * */ public class StaticPortConfigDialog extends Dialog { /** Preference name for the 0th column width */ private static final String PREFS_DEVICE_COL = "spcd.deviceColumn"; //$NON-NLS-1$ /** Preference name for the 1st column width */ private static final String PREFS_APP_COL = "spcd.AppColumn"; //$NON-NLS-1$ /** Preference name for the 2nd column width */ private static final String PREFS_PORT_COL = "spcd.PortColumn"; //$NON-NLS-1$ private static final int COL_DEVICE = 0; private static final int COL_APPLICATION = 1; private static final int COL_PORT = 2; private static final int DLG_WIDTH = 500; private static final int DLG_HEIGHT = 300; private Shell mShell; private Shell mParent; private Table mPortTable; /** * Array containing the list of already used static port to avoid * duplication. */ private ArrayList mPorts = new ArrayList(); /** * Basic constructor. * @param parent */ public StaticPortConfigDialog(Shell parent) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); } /** * Open and display the dialog. This method returns only when the * user closes the dialog somehow. * */ public void open() { createUI(); if (mParent == null || mShell == null) { return; } updateFromStore(); // Set the dialog size. mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); Rectangle r = mParent.getBounds(); // get the center new top left. int cx = r.x + r.width/2; int x = cx - DLG_WIDTH / 2; int cy = r.y + r.height/2; int y = cy - DLG_HEIGHT / 2; mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); mShell.pack(); // actually open the dialog mShell.open(); // event loop until the dialog is closed. Display display = mParent.getDisplay(); while (!mShell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } } /** * Creates the dialog ui. */ private void createUI() { mParent = getParent(); mShell = new Shell(mParent, getStyle()); mShell.setText("Static Port Configuration"); mShell.setLayout(new GridLayout(1, true)); mShell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { event.doit = true; } }); // center part with the list on the left and the buttons // on the right. Composite main = new Composite(mShell, SWT.NONE); main.setLayoutData(new GridData(GridData.FILL_BOTH)); main.setLayout(new GridLayout(2, false)); // left part: list view mPortTable = new Table(main, SWT.SINGLE | SWT.FULL_SELECTION); mPortTable.setLayoutData(new GridData(GridData.FILL_BOTH)); mPortTable.setHeaderVisible(true); mPortTable.setLinesVisible(true); TableHelper.createTableColumn(mPortTable, "Device Serial Number", SWT.LEFT, "emulator-5554", //$NON-NLS-1$ PREFS_DEVICE_COL, PrefsDialog.getStore()); TableHelper.createTableColumn(mPortTable, "Application Package", SWT.LEFT, "com.android.samples.phone", //$NON-NLS-1$ PREFS_APP_COL, PrefsDialog.getStore()); TableHelper.createTableColumn(mPortTable, "Debug Port", SWT.RIGHT, "Debug Port", //$NON-NLS-1$ PREFS_PORT_COL, PrefsDialog.getStore()); // right part: buttons Composite buttons = new Composite(main, SWT.NONE); buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); buttons.setLayout(new GridLayout(1, true)); Button newButton = new Button(buttons, SWT.NONE); newButton.setText("New..."); newButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { StaticPortEditDialog dlg = new StaticPortEditDialog(mShell, mPorts); if (dlg.open()) { // get the text String device = dlg.getDeviceSN(); String app = dlg.getAppName(); int port = dlg.getPortNumber(); // add it to the list addEntry(device, app, port); } } }); final Button editButton = new Button(buttons, SWT.NONE); editButton.setText("Edit..."); editButton.setEnabled(false); editButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { int index = mPortTable.getSelectionIndex(); String oldDeviceName = getDeviceName(index); String oldAppName = getAppName(index); String oldPortNumber = getPortNumber(index); StaticPortEditDialog dlg = new StaticPortEditDialog(mShell, mPorts, oldDeviceName, oldAppName, oldPortNumber); if (dlg.open()) { // get the text String deviceName = dlg.getDeviceSN(); String app = dlg.getAppName(); int port = dlg.getPortNumber(); // add it to the list replaceEntry(index, deviceName, app, port); } } }); final Button deleteButton = new Button(buttons, SWT.NONE); deleteButton.setText("Delete"); deleteButton.setEnabled(false); deleteButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { int index = mPortTable.getSelectionIndex(); removeEntry(index); } }); // bottom part with the ok/cancel Composite bottomComp = new Composite(mShell, SWT.NONE); bottomComp.setLayoutData(new GridData( GridData.HORIZONTAL_ALIGN_CENTER)); bottomComp.setLayout(new GridLayout(2, true)); Button okButton = new Button(bottomComp, SWT.NONE); okButton.setText("OK"); okButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { updateStore(); mShell.close(); } }); Button cancelButton = new Button(bottomComp, SWT.NONE); cancelButton.setText("Cancel"); cancelButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mShell.close(); } }); mPortTable.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // get the selection index int index = mPortTable.getSelectionIndex(); boolean enabled = index != -1; editButton.setEnabled(enabled); deleteButton.setEnabled(enabled); } }); mShell.pack(); } /** * Add a new entry in the list. * @param deviceName the serial number of the device * @param appName java package for the application * @param portNumber port number */ private void addEntry(String deviceName, String appName, int portNumber) { // create a new item for the table TableItem item = new TableItem(mPortTable, SWT.NONE); item.setText(COL_DEVICE, deviceName); item.setText(COL_APPLICATION, appName); item.setText(COL_PORT, Integer.toString(portNumber)); // add the port to the list of port number used. mPorts.add(portNumber); } /** * Remove an entry from the list. * @param index The index of the entry to be removed */ private void removeEntry(int index) { // remove from the ui mPortTable.remove(index); // and from the port list. mPorts.remove(index); } /** * Replace an entry in the list with new values. * @param index The index of the item to be replaced * @param deviceName the serial number of the device * @param appName The new java package for the application * @param portNumber The new port number. */ private void replaceEntry(int index, String deviceName, String appName, int portNumber) { // get the table item by index TableItem item = mPortTable.getItem(index); // set its new value item.setText(COL_DEVICE, deviceName); item.setText(COL_APPLICATION, appName); item.setText(COL_PORT, Integer.toString(portNumber)); // and replace the port number in the port list. mPorts.set(index, portNumber); } /** * Returns the device name for a specific index * @param index The index * @return the java package name of the application */ private String getDeviceName(int index) { TableItem item = mPortTable.getItem(index); return item.getText(COL_DEVICE); } /** * Returns the application name for a specific index * @param index The index * @return the java package name of the application */ private String getAppName(int index) { TableItem item = mPortTable.getItem(index); return item.getText(COL_APPLICATION); } /** * Returns the port number for a specific index * @param index The index * @return the port number */ private String getPortNumber(int index) { TableItem item = mPortTable.getItem(index); return item.getText(COL_PORT); } /** * Updates the ui from the value in the preference store. */ private void updateFromStore() { // get the map from the debug port manager DebugPortProvider provider = DebugPortProvider.getInstance(); Map> map = provider.getPortList(); // we're going to loop on the keys and fill the table. Set deviceKeys = map.keySet(); for (String deviceKey : deviceKeys) { Map deviceMap = map.get(deviceKey); if (deviceMap != null) { Set appKeys = deviceMap.keySet(); for (String appKey : appKeys) { Integer port = deviceMap.get(appKey); if (port != null) { addEntry(deviceKey, appKey, port); } } } } } /** * Update the store from the content of the ui. */ private void updateStore() { // create a new Map object and fill it. HashMap> map = new HashMap>(); int count = mPortTable.getItemCount(); for (int i = 0 ; i < count ; i++) { TableItem item = mPortTable.getItem(i); String deviceName = item.getText(COL_DEVICE); Map deviceMap = map.get(deviceName); if (deviceMap == null) { deviceMap = new HashMap(); map.put(deviceName, deviceMap); } deviceMap.put(item.getText(COL_APPLICATION), Integer.valueOf(item.getText(COL_PORT))); } // set it in the store through the debug port manager. DebugPortProvider provider = DebugPortProvider.getInstance(); provider.setPortList(map); } } ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java0100644 0000000 0000000 00000025141 12747325007 023774 0ustar000000000 0000000 /* * 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.ddms; import com.android.ddmlib.IDevice; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.util.ArrayList; /** * Small dialog box to edit a static port number. */ public class StaticPortEditDialog extends Dialog { private static final int DLG_WIDTH = 400; private static final int DLG_HEIGHT = 200; private Shell mParent; private Shell mShell; private boolean mOk = false; private String mAppName; private String mPortNumber; private Button mOkButton; private Label mWarning; /** List of ports already in use */ private ArrayList mPorts; /** This is the port being edited. */ private int mEditPort = -1; private String mDeviceSn; /** * Creates a dialog with empty fields. * @param parent The parent Shell * @param ports The list of already used port numbers. */ public StaticPortEditDialog(Shell parent, ArrayList ports) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); mPorts = ports; mDeviceSn = IDevice.FIRST_EMULATOR_SN; } /** * Creates a dialog with predefined values. * @param shell The parent shell * @param ports The list of already used port numbers. * @param oldDeviceSN the device serial number to display * @param oldAppName The application name to display * @param oldPortNumber The port number to display */ public StaticPortEditDialog(Shell shell, ArrayList ports, String oldDeviceSN, String oldAppName, String oldPortNumber) { this(shell, ports); mDeviceSn = oldDeviceSN; mAppName = oldAppName; mPortNumber = oldPortNumber; mEditPort = Integer.valueOf(mPortNumber); } /** * Opens the dialog. The method will return when the user closes the dialog * somehow. * * @return true if ok was pressed, false if cancelled. */ public boolean open() { createUI(); if (mParent == null || mShell == null) { return false; } mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); Rectangle r = mParent.getBounds(); // get the center new top left. int cx = r.x + r.width/2; int x = cx - DLG_WIDTH / 2; int cy = r.y + r.height/2; int y = cy - DLG_HEIGHT / 2; mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); mShell.open(); Display display = mParent.getDisplay(); while (!mShell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } return mOk; } public String getDeviceSN() { return mDeviceSn; } public String getAppName() { return mAppName; } public int getPortNumber() { return Integer.valueOf(mPortNumber); } private void createUI() { mParent = getParent(); mShell = new Shell(mParent, getStyle()); mShell.setText("Static Port"); mShell.setLayout(new GridLayout(1, false)); mShell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { } }); // center part with the edit field Composite main = new Composite(mShell, SWT.NONE); main.setLayoutData(new GridData(GridData.FILL_BOTH)); main.setLayout(new GridLayout(2, false)); Label l0 = new Label(main, SWT.NONE); l0.setText("Device Name:"); final Text deviceSNText = new Text(main, SWT.SINGLE | SWT.BORDER); deviceSNText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); if (mDeviceSn != null) { deviceSNText.setText(mDeviceSn); } deviceSNText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mDeviceSn = deviceSNText.getText().trim(); validate(); } }); Label l = new Label(main, SWT.NONE); l.setText("Application Name:"); final Text appNameText = new Text(main, SWT.SINGLE | SWT.BORDER); if (mAppName != null) { appNameText.setText(mAppName); } appNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); appNameText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mAppName = appNameText.getText().trim(); validate(); } }); Label l2 = new Label(main, SWT.NONE); l2.setText("Debug Port:"); final Text debugPortText = new Text(main, SWT.SINGLE | SWT.BORDER); if (mPortNumber != null) { debugPortText.setText(mPortNumber); } debugPortText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); debugPortText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mPortNumber = debugPortText.getText().trim(); validate(); } }); // warning label Composite warningComp = new Composite(mShell, SWT.NONE); warningComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); warningComp.setLayout(new GridLayout(1, true)); mWarning = new Label(warningComp, SWT.NONE); mWarning.setText(""); mWarning.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // bottom part with the ok/cancel Composite bottomComp = new Composite(mShell, SWT.NONE); bottomComp .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); bottomComp.setLayout(new GridLayout(2, true)); mOkButton = new Button(bottomComp, SWT.NONE); mOkButton.setText("OK"); mOkButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mOk = true; mShell.close(); } }); mOkButton.setEnabled(false); mShell.setDefaultButton(mOkButton); Button cancelButton = new Button(bottomComp, SWT.NONE); cancelButton.setText("Cancel"); cancelButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mShell.close(); } }); validate(); } /** * Validates the content of the 2 text fields and enable/disable "ok", while * setting up the warning/error message. */ private void validate() { // first we reset the warning dialog. This allows us to latter // display warnings. mWarning.setText(""); //$NON-NLS-1$ // check the device name field is not empty if (mDeviceSn == null || mDeviceSn.length() == 0) { mWarning.setText("Device name missing."); mOkButton.setEnabled(false); return; } // check the application name field is not empty if (mAppName == null || mAppName.length() == 0) { mWarning.setText("Application name missing."); mOkButton.setEnabled(false); return; } String packageError = "Application name must be a valid Java package name."; // validate the package name as well. It must be a fully qualified // java package. String[] packageSegments = mAppName.split("\\."); //$NON-NLS-1$ for (String p : packageSegments) { if (p.matches("^[a-zA-Z][a-zA-Z0-9]*") == false) { //$NON-NLS-1$ mWarning.setText(packageError); mOkButton.setEnabled(false); return; } // lets also display a warning if the package contains upper case // letters. if (p.matches("^[a-z][a-z0-9]*") == false) { //$NON-NLS-1$ mWarning.setText("Lower case is recommended for Java packages."); } } // the split will not detect the last char being a '.' // so we test it manually if (mAppName.charAt(mAppName.length()-1) == '.') { mWarning.setText(packageError); mOkButton.setEnabled(false); return; } // now we test the package name field is not empty. if (mPortNumber == null || mPortNumber.length() == 0) { mWarning.setText("Port Number missing."); mOkButton.setEnabled(false); return; } // then we check it only contains digits. if (mPortNumber.matches("[0-9]*") == false) { //$NON-NLS-1$ mWarning.setText("Port Number invalid."); mOkButton.setEnabled(false); return; } // get the int from the port number to validate long port = Long.valueOf(mPortNumber); if (port >= 32767) { mOkButton.setEnabled(false); return; } // check if its in the list of already used ports if (port != mEditPort) { for (Integer i : mPorts) { if (port == i.intValue()) { mWarning.setText("Port already in use."); mOkButton.setEnabled(false); return; } } } // at this point there's not error, so we enable the ok button. mOkButton.setEnabled(true); } } ddms/app/src/main/java/com/android/ddms/UIThread.java0100644 0000000 0000000 00000212132 12747325007 021415 0ustar000000000 0000000 /* * 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.ddms; import com.android.SdkConstants; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.ClientData.IHprofDumpHandler; import com.android.ddmlib.ClientData.MethodProfilingStatus; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.Log.ILogOutput; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.SyncException; import com.android.ddmlib.SyncService; import com.android.ddmuilib.AllocationPanel; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.DevicePanel; import com.android.ddmuilib.DevicePanel.IUiSelectionListener; import com.android.ddmuilib.EmulatorControlPanel; import com.android.ddmuilib.HeapPanel; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.InfoPanel; import com.android.ddmuilib.NativeHeapPanel; import com.android.ddmuilib.ScreenShotDialog; import com.android.ddmuilib.SysinfoPanel; import com.android.ddmuilib.TablePanel; import com.android.ddmuilib.ThreadPanel; import com.android.ddmuilib.actions.ToolItemAction; import com.android.ddmuilib.explorer.DeviceExplorer; import com.android.ddmuilib.handler.BaseFileHandler; import com.android.ddmuilib.handler.MethodProfilingHandler; import com.android.ddmuilib.log.event.EventLogPanel; import com.android.ddmuilib.logcat.LogCatPanel; import com.android.ddmuilib.logcat.LogColors; import com.android.ddmuilib.logcat.LogFilter; import com.android.ddmuilib.logcat.LogPanel; import com.android.ddmuilib.logcat.LogPanel.ILogFilterStorageManager; import com.android.ddmuilib.net.NetworkPanel; import com.android.ddmuilib.screenrecord.ScreenRecorderAction; import com.android.menubar.IMenuBarCallback; import com.android.menubar.IMenuBarEnhancer; import com.android.menubar.IMenuBarEnhancer.MenuBarMode; import com.android.menubar.MenuBarEnhancer; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTError; import org.eclipse.swt.SWTException; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.MenuAdapter; import org.eclipse.swt.events.MenuEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.events.ShellListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.TabFolder; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import java.io.File; import java.util.ArrayList; /** * This acts as the UI builder. This cannot be its own thread since this prevent using AWT in an * SWT application. So this class mainly builds the ui, and manages communication between the panels * when {@link IDevice} / {@link Client} selection changes. */ public class UIThread implements IUiSelectionListener, IClientChangeListener { public static final String APP_NAME = "DDMS"; /* * UI tab panel definitions. The constants here must match up with the array * indices in mPanels. PANEL_CLIENT_LIST is a "virtual" panel representing * the client list. */ public static final int PANEL_CLIENT_LIST = -1; public static final int PANEL_INFO = 0; public static final int PANEL_THREAD = 1; public static final int PANEL_HEAP = 2; private static final int PANEL_NATIVE_HEAP = 3; private static final int PANEL_ALLOCATIONS = 4; private static final int PANEL_SYSINFO = 5; private static final int PANEL_NETWORK = 6; private static final int PANEL_COUNT = 7; /** Content is setup in the constructor */ private static TablePanel[] mPanels = new TablePanel[PANEL_COUNT]; private static final String[] mPanelNames = new String[] { "Info", "Threads", "VM Heap", "Native Heap", "Allocation Tracker", "Sysinfo", "Network" }; private static final String[] mPanelTips = new String[] { "Client information", "Thread status", "VM heap status", "Native heap status", "Allocation Tracker", "Sysinfo graphs", "Network usage" }; private static final String PREFERENCE_LOGSASH = "logSashLocation"; //$NON-NLS-1$ private static final String PREFERENCE_SASH = "sashLocation"; //$NON-NLS-1$ private static final String PREFS_COL_TIME = "logcat.time"; //$NON-NLS-1$ private static final String PREFS_COL_LEVEL = "logcat.level"; //$NON-NLS-1$ private static final String PREFS_COL_PID = "logcat.pid"; //$NON-NLS-1$ private static final String PREFS_COL_TAG = "logcat.tag"; //$NON-NLS-1$ private static final String PREFS_COL_MESSAGE = "logcat.message"; //$NON-NLS-1$ private static final String PREFS_FILTERS = "logcat.filter"; //$NON-NLS-1$ // singleton instance private static UIThread mInstance = new UIThread(); // our display private Display mDisplay; // the table we show in the left-hand pane private DevicePanel mDevicePanel; private IDevice mCurrentDevice = null; private Client mCurrentClient = null; // status line at the bottom of the app window private Label mStatusLine; // some toolbar items we need to update private ToolItem mTBShowThreadUpdates; private ToolItem mTBShowHeapUpdates; private ToolItem mTBHalt; private ToolItem mTBCauseGc; private ToolItem mTBDumpHprof; private ToolItem mTBProfiling; private final class FilterStorage implements ILogFilterStorageManager { @Override public LogFilter[] getFilterFromStore() { String filterPrefs = PrefsDialog.getStore().getString( PREFS_FILTERS); // split in a string per filter String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$ ArrayList list = new ArrayList(filters.length); for (String f : filters) { if (f.length() > 0) { LogFilter logFilter = new LogFilter(); if (logFilter.loadFromString(f)) { list.add(logFilter); } } } return list.toArray(new LogFilter[list.size()]); } @Override public void saveFilters(LogFilter[] filters) { StringBuilder sb = new StringBuilder(); for (LogFilter f : filters) { String filterString = f.toString(); sb.append(filterString); sb.append('|'); } PrefsDialog.getStore().setValue(PREFS_FILTERS, sb.toString()); } @Override public boolean requiresDefaultFilter() { return true; } } /** * Flag to indicate whether to use the old or the new logcat view. This is a * temporary workaround that will be removed once the new view is complete. */ private static final String USE_OLD_LOGCAT_VIEW = System.getenv("ANDROID_USE_OLD_LOGCAT_VIEW"); public static boolean useOldLogCatView() { return USE_OLD_LOGCAT_VIEW != null; } private LogPanel mLogPanel; /* only valid when useOldLogCatView() == true */ private LogCatPanel mLogCatPanel; /* only valid when useOldLogCatView() == false */ private ToolItemAction mCreateFilterAction; private ToolItemAction mDeleteFilterAction; private ToolItemAction mEditFilterAction; private ToolItemAction mExportAction; private ToolItemAction mClearAction; private ToolItemAction[] mLogLevelActions; private String[] mLogLevelIcons = { "v.png", //$NON-NLS-1S "d.png", //$NON-NLS-1S "i.png", //$NON-NLS-1S "w.png", //$NON-NLS-1S "e.png", //$NON-NLS-1S }; protected Clipboard mClipboard; private MenuItem mCopyMenuItem; private MenuItem mSelectAllMenuItem; private TableFocusListener mTableListener; private DeviceExplorer mExplorer = null; private Shell mExplorerShell = null; private EmulatorControlPanel mEmulatorPanel; private EventLogPanel mEventLogPanel; private Image mTracingStartImage; private Image mTracingStopImage; private ImageLoader mDdmUiLibLoader; private class TableFocusListener implements ITableFocusListener { private IFocusedTableActivator mCurrentActivator; @Override public void focusGained(IFocusedTableActivator activator) { mCurrentActivator = activator; if (mCopyMenuItem.isDisposed() == false) { mCopyMenuItem.setEnabled(true); mSelectAllMenuItem.setEnabled(true); } } @Override public void focusLost(IFocusedTableActivator activator) { // if we move from one table to another, it's unclear // if the old table lose its focus before the new // one gets the focus, so we need to check. if (activator == mCurrentActivator) { activator = null; if (mCopyMenuItem.isDisposed() == false) { mCopyMenuItem.setEnabled(false); mSelectAllMenuItem.setEnabled(false); } } } public void copy(Clipboard clipboard) { if (mCurrentActivator != null) { mCurrentActivator.copy(clipboard); } } public void selectAll() { if (mCurrentActivator != null) { mCurrentActivator.selectAll(); } } } /** * Handler for HPROF dumps. * This will always prompt the user to save the HPROF file. */ private class HProfHandler extends BaseFileHandler implements IHprofDumpHandler { public HProfHandler(Shell parentShell) { super(parentShell); } @Override public void onEndFailure(final Client client, final String message) { mDisplay.asyncExec(new Runnable() { @Override public void run() { try { displayErrorFromUiThread( "Unable to create HPROF file for application '%1$s'\n\n%2$s" + "Check logcat for more information.", client.getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); } finally { // this will make sure the dump hprof button is re-enabled for the // current selection. as the client is finished dumping an hprof file enableButtons(); } } }); } @Override public void onSuccess(final String remoteFilePath, final Client client) { mDisplay.asyncExec(new Runnable() { @Override public void run() { final IDevice device = client.getDevice(); try { // get the sync service to pull the HPROF file final SyncService sync = client.getDevice().getSyncService(); if (sync != null) { promptAndPull(sync, client.getClientData().getClientDescription() + ".hprof", remoteFilePath, "Save HPROF file"); } else { displayErrorFromUiThread( "Unable to download HPROF file from device '%1$s'.", device.getSerialNumber()); } } catch (SyncException e) { if (e.wasCanceled() == false) { displayErrorFromUiThread( "Unable to download HPROF file from device '%1$s'.\n\n%2$s", device.getSerialNumber(), e.getMessage()); } } catch (Exception e) { displayErrorFromUiThread("Unable to download HPROF file from device '%1$s'.", device.getSerialNumber()); } finally { // this will make sure the dump hprof button is re-enabled for the // current selection. as the client is finished dumping an hprof file enableButtons(); } } }); } @Override public void onSuccess(final byte[] data, final Client client) { mDisplay.asyncExec(new Runnable() { @Override public void run() { promptAndSave(client.getClientData().getClientDescription() + ".hprof", data, "Save HPROF file"); } }); } @Override protected String getDialogTitle() { return "HPROF Error"; } } /** * Generic constructor. */ private UIThread() { mPanels[PANEL_INFO] = new InfoPanel(); mPanels[PANEL_THREAD] = new ThreadPanel(); mPanels[PANEL_HEAP] = new HeapPanel(); if (PrefsDialog.getStore().getBoolean(PrefsDialog.SHOW_NATIVE_HEAP)) { if (System.getenv("ANDROID_DDMS_OLD_HEAP_PANEL") != null) { mPanels[PANEL_NATIVE_HEAP] = new NativeHeapPanel(); } else { mPanels[PANEL_NATIVE_HEAP] = new com.android.ddmuilib.heap.NativeHeapPanel(getStore()); } } else { mPanels[PANEL_NATIVE_HEAP] = null; } mPanels[PANEL_ALLOCATIONS] = new AllocationPanel(); mPanels[PANEL_SYSINFO] = new SysinfoPanel(); mPanels[PANEL_NETWORK] = new NetworkPanel(); } /** * Get singleton instance of the UI thread. */ public static UIThread getInstance() { return mInstance; } /** * Return the Display. Don't try this unless you're in the UI thread. */ public Display getDisplay() { return mDisplay; } public void asyncExec(Runnable r) { if (mDisplay != null && mDisplay.isDisposed() == false) { mDisplay.asyncExec(r); } } /** returns the IPreferenceStore */ public IPreferenceStore getStore() { return PrefsDialog.getStore(); } /** * Create SWT objects and drive the user interface event loop. * @param ddmsParentLocation location of the folder that contains ddms. */ public void runUI(String ddmsParentLocation) { Display.setAppName(APP_NAME); mDisplay = Display.getDefault(); final Shell shell = new Shell(mDisplay, SWT.SHELL_TRIM); // create the image loaders for DDMS and DDMUILIB mDdmUiLibLoader = ImageLoader.getDdmUiLibLoader(); shell.setImage(ImageLoader.getLoader(this.getClass()).loadImage(mDisplay, "ddms-128.png", //$NON-NLS-1$ 100, 50, null)); Log.addLogger(new ILogOutput() { @Override public void printAndPromptLog(final LogLevel logLevel, final String tag, final String message) { Log.printLog(logLevel, tag, message); // dialog box only run in UI thread.. mDisplay.asyncExec(new Runnable() { @Override public void run() { Shell activeShell = mDisplay.getActiveShell(); if (logLevel == LogLevel.ERROR) { MessageDialog.openError(activeShell, tag, message); } else { MessageDialog.openWarning(activeShell, tag, message); } } }); } @Override public void printLog(LogLevel logLevel, String tag, String message) { Log.printLog(logLevel, tag, message); } }); // set the handler for hprof dump ClientData.setHprofDumpHandler(new HProfHandler(shell)); ClientData.setMethodProfilingHandler(new MethodProfilingHandler(shell)); // [try to] ensure ADB is running // in the new SDK, adb is in the platform-tools, but when run from the command line // in the Android source tree, then adb is next to ddms. String adbLocation; if (ddmsParentLocation != null && ddmsParentLocation.length() != 0) { // check if there's a platform-tools folder File platformTools = new File(new File(ddmsParentLocation).getParent(), "platform-tools"); //$NON-NLS-1$ if (platformTools.isDirectory()) { adbLocation = platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB; } else { // we're in the Android source tree, then adb is in $ANDROID_HOST_OUT/bin/adb String androidOut = System.getenv("ANDROID_HOST_OUT"); if (androidOut != null) { adbLocation = androidOut + File.separator + "bin" + File.separator + SdkConstants.FN_ADB; } else { adbLocation = SdkConstants.FN_ADB; } } } else { adbLocation = SdkConstants.FN_ADB; } AndroidDebugBridge.init(true /* debugger support */); AndroidDebugBridge.createBridge(adbLocation, true /* forceNewBridge */); // we need to listen to client change to be notified of client status (profiling) change AndroidDebugBridge.addClientChangeListener(this); shell.setText("Dalvik Debug Monitor"); setConfirmClose(shell); createMenus(shell); createWidgets(shell); shell.pack(); setSizeAndPosition(shell); shell.open(); Log.d("ddms", "UI is up"); while (!shell.isDisposed()) { if (!mDisplay.readAndDispatch()) mDisplay.sleep(); } if (useOldLogCatView()) { mLogPanel.stopLogCat(true); } mDevicePanel.dispose(); for (TablePanel panel : mPanels) { if (panel != null) { panel.dispose(); } } ImageLoader.dispose(); mDisplay.dispose(); Log.d("ddms", "UI is down"); } /** * Set the size and position of the main window from the preference, and * setup listeners for control events (resize/move of the window) */ private void setSizeAndPosition(final Shell shell) { shell.setMinimumSize(400, 200); // get the x/y and w/h from the prefs PreferenceStore prefs = PrefsDialog.getStore(); int x = prefs.getInt(PrefsDialog.SHELL_X); int y = prefs.getInt(PrefsDialog.SHELL_Y); int w = prefs.getInt(PrefsDialog.SHELL_WIDTH); int h = prefs.getInt(PrefsDialog.SHELL_HEIGHT); // check that we're not out of the display area Rectangle rect = mDisplay.getClientArea(); // first check the width/height if (w > rect.width) { w = rect.width; prefs.setValue(PrefsDialog.SHELL_WIDTH, rect.width); } if (h > rect.height) { h = rect.height; prefs.setValue(PrefsDialog.SHELL_HEIGHT, rect.height); } // then check x. Make sure the left corner is in the screen if (x < rect.x) { x = rect.x; prefs.setValue(PrefsDialog.SHELL_X, rect.x); } else if (x >= rect.x + rect.width) { x = rect.x + rect.width - w; prefs.setValue(PrefsDialog.SHELL_X, rect.x); } // then check y. Make sure the left corner is in the screen if (y < rect.y) { y = rect.y; prefs.setValue(PrefsDialog.SHELL_Y, rect.y); } else if (y >= rect.y + rect.height) { y = rect.y + rect.height - h; prefs.setValue(PrefsDialog.SHELL_Y, rect.y); } // now we can set the location/size shell.setBounds(x, y, w, h); // add listener for resize/move shell.addControlListener(new ControlListener() { @Override public void controlMoved(ControlEvent e) { // get the new x/y Rectangle controlBounds = shell.getBounds(); // store in pref file PreferenceStore currentPrefs = PrefsDialog.getStore(); currentPrefs.setValue(PrefsDialog.SHELL_X, controlBounds.x); currentPrefs.setValue(PrefsDialog.SHELL_Y, controlBounds.y); } @Override public void controlResized(ControlEvent e) { // get the new w/h Rectangle controlBounds = shell.getBounds(); // store in pref file PreferenceStore currentPrefs = PrefsDialog.getStore(); currentPrefs.setValue(PrefsDialog.SHELL_WIDTH, controlBounds.width); currentPrefs.setValue(PrefsDialog.SHELL_HEIGHT, controlBounds.height); } }); } /** * Set the size and position of the file explorer window from the * preference, and setup listeners for control events (resize/move of * the window) */ private void setExplorerSizeAndPosition(final Shell shell) { shell.setMinimumSize(400, 200); // get the x/y and w/h from the prefs PreferenceStore prefs = PrefsDialog.getStore(); int x = prefs.getInt(PrefsDialog.EXPLORER_SHELL_X); int y = prefs.getInt(PrefsDialog.EXPLORER_SHELL_Y); int w = prefs.getInt(PrefsDialog.EXPLORER_SHELL_WIDTH); int h = prefs.getInt(PrefsDialog.EXPLORER_SHELL_HEIGHT); // check that we're not out of the display area Rectangle rect = mDisplay.getClientArea(); // first check the width/height if (w > rect.width) { w = rect.width; prefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, rect.width); } if (h > rect.height) { h = rect.height; prefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, rect.height); } // then check x. Make sure the left corner is in the screen if (x < rect.x) { x = rect.x; prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); } else if (x >= rect.x + rect.width) { x = rect.x + rect.width - w; prefs.setValue(PrefsDialog.EXPLORER_SHELL_X, rect.x); } // then check y. Make sure the left corner is in the screen if (y < rect.y) { y = rect.y; prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); } else if (y >= rect.y + rect.height) { y = rect.y + rect.height - h; prefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, rect.y); } // now we can set the location/size shell.setBounds(x, y, w, h); // add listener for resize/move shell.addControlListener(new ControlListener() { @Override public void controlMoved(ControlEvent e) { // get the new x/y Rectangle controlBounds = shell.getBounds(); // store in pref file PreferenceStore currentPrefs = PrefsDialog.getStore(); currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_X, controlBounds.x); currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_Y, controlBounds.y); } @Override public void controlResized(ControlEvent e) { // get the new w/h Rectangle controlBounds = shell.getBounds(); // store in pref file PreferenceStore currentPrefs = PrefsDialog.getStore(); currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_WIDTH, controlBounds.width); currentPrefs.setValue(PrefsDialog.EXPLORER_SHELL_HEIGHT, controlBounds.height); } }); } /* * Set the confirm-before-close dialog. */ private void setConfirmClose(final Shell shell) { // Note: there was some commented out code to display a confirmation box // when closing. The feature seems unnecessary and the code was not being // used, so it has been removed. } /* * Create the menu bar and items. */ private void createMenus(final Shell shell) { // create menu bar Menu menuBar = new Menu(shell, SWT.BAR); // create top-level items MenuItem fileItem = new MenuItem(menuBar, SWT.CASCADE); fileItem.setText("&File"); MenuItem editItem = new MenuItem(menuBar, SWT.CASCADE); editItem.setText("&Edit"); MenuItem actionItem = new MenuItem(menuBar, SWT.CASCADE); actionItem.setText("&Actions"); MenuItem deviceItem = new MenuItem(menuBar, SWT.CASCADE); deviceItem.setText("&Device"); // create top-level menus Menu fileMenu = new Menu(menuBar); fileItem.setMenu(fileMenu); Menu editMenu = new Menu(menuBar); editItem.setMenu(editMenu); Menu actionMenu = new Menu(menuBar); actionItem.setMenu(actionMenu); Menu deviceMenu = new Menu(menuBar); deviceItem.setMenu(deviceMenu); MenuItem item; // create File menu items item = new MenuItem(fileMenu, SWT.NONE); item.setText("&Static Port Configuration..."); item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { StaticPortConfigDialog dlg = new StaticPortConfigDialog(shell); dlg.open(); } }); IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenu(APP_NAME, fileMenu, new IMenuBarCallback() { @Override public void printError(String format, Object... args) { Log.e("DDMS Menu Bar", String.format(format, args)); } @Override public void onPreferencesMenuSelected() { PrefsDialog.run(shell); } @Override public void onAboutMenuSelected() { AboutDialog dlg = new AboutDialog(shell); dlg.open(); } }); if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) { new MenuItem(fileMenu, SWT.SEPARATOR); item = new MenuItem(fileMenu, SWT.NONE); item.setText("E&xit\tCtrl-Q"); item.setAccelerator('Q' | SWT.MOD1); item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { shell.close(); } }); } // create edit menu items mCopyMenuItem = new MenuItem(editMenu, SWT.NONE); mCopyMenuItem.setText("&Copy\tCtrl-C"); mCopyMenuItem.setAccelerator('C' | SWT.MOD1); mCopyMenuItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mTableListener.copy(mClipboard); } }); new MenuItem(editMenu, SWT.SEPARATOR); mSelectAllMenuItem = new MenuItem(editMenu, SWT.NONE); mSelectAllMenuItem.setText("Select &All\tCtrl-A"); mSelectAllMenuItem.setAccelerator('A' | SWT.MOD1); mSelectAllMenuItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mTableListener.selectAll(); } }); // create Action menu items // TODO: this should come with a confirmation dialog final MenuItem actionHaltItem = new MenuItem(actionMenu, SWT.NONE); actionHaltItem.setText("&Halt VM"); actionHaltItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mDevicePanel.killSelectedClient(); } }); final MenuItem actionCauseGcItem = new MenuItem(actionMenu, SWT.NONE); actionCauseGcItem.setText("Cause &GC"); actionCauseGcItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mDevicePanel.forceGcOnSelectedClient(); } }); final MenuItem actionResetAdb = new MenuItem(actionMenu, SWT.NONE); actionResetAdb.setText("&Reset adb"); actionResetAdb.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); if (bridge != null) { bridge.restart(); } } }); // configure Action items based on current state actionMenu.addMenuListener(new MenuAdapter() { @Override public void menuShown(MenuEvent e) { actionHaltItem.setEnabled(mTBHalt.getEnabled() && mCurrentClient != null); actionCauseGcItem.setEnabled(mTBCauseGc.getEnabled() && mCurrentClient != null); actionResetAdb.setEnabled(true); } }); // create Device menu items final MenuItem screenShotItem = new MenuItem(deviceMenu, SWT.NONE); // The \tCtrl-S "keybinding text" here isn't right for the Mac - but // it's stripped out and replaced by the proper keyboard accelerator // text (e.g. the unicode symbol for the command key + S) anyway // so it's fine to leave it there for the other platforms. screenShotItem.setText("&Screen capture...\tCtrl-S"); screenShotItem.setAccelerator('S' | SWT.MOD1); final MenuItem screenRecordItem = new MenuItem(deviceMenu, SWT.NONE); screenRecordItem.setText("Screen Record"); SelectionListener selectionListener = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mCurrentDevice == null) { return; } if (e.getSource() == screenShotItem) { ScreenShotDialog dlg = new ScreenShotDialog(shell); dlg.open(mCurrentDevice); } else if (e.getSource() == screenRecordItem) { new ScreenRecorderAction(shell, mCurrentDevice).performAction(); } } }; screenShotItem.addSelectionListener(selectionListener); screenRecordItem.addSelectionListener(selectionListener); new MenuItem(deviceMenu, SWT.SEPARATOR); final MenuItem explorerItem = new MenuItem(deviceMenu, SWT.NONE); explorerItem.setText("File Explorer..."); explorerItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { createFileExplorer(); } }); new MenuItem(deviceMenu, SWT.SEPARATOR); final MenuItem processItem = new MenuItem(deviceMenu, SWT.NONE); processItem.setText("Show &process status..."); processItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { DeviceCommandDialog dlg; dlg = new DeviceCommandDialog("ps -x", "ps-x.txt", shell); dlg.open(mCurrentDevice); } }); final MenuItem deviceStateItem = new MenuItem(deviceMenu, SWT.NONE); deviceStateItem.setText("Dump &device state..."); deviceStateItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { DeviceCommandDialog dlg; dlg = new DeviceCommandDialog("/system/bin/dumpstate /proc/self/fd/0", "device-state.txt", shell); dlg.open(mCurrentDevice); } }); final MenuItem appStateItem = new MenuItem(deviceMenu, SWT.NONE); appStateItem.setText("Dump &app state..."); appStateItem.setEnabled(false); appStateItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { DeviceCommandDialog dlg; dlg = new DeviceCommandDialog("dumpsys", "app-state.txt", shell); dlg.open(mCurrentDevice); } }); final MenuItem radioStateItem = new MenuItem(deviceMenu, SWT.NONE); radioStateItem.setText("Dump &radio state..."); radioStateItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { DeviceCommandDialog dlg; dlg = new DeviceCommandDialog( "cat /data/logs/radio.4 /data/logs/radio.3" + " /data/logs/radio.2 /data/logs/radio.1" + " /data/logs/radio", "radio-state.txt", shell); dlg.open(mCurrentDevice); } }); final MenuItem logCatItem = new MenuItem(deviceMenu, SWT.NONE); logCatItem.setText("Run &logcat..."); logCatItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { DeviceCommandDialog dlg; dlg = new DeviceCommandDialog("logcat '*:d jdwp:w'", "log.txt", shell); dlg.open(mCurrentDevice); } }); // configure Action items based on current state deviceMenu.addMenuListener(new MenuAdapter() { @Override public void menuShown(MenuEvent e) { boolean deviceEnabled = mCurrentDevice != null; screenShotItem.setEnabled(deviceEnabled); explorerItem.setEnabled(deviceEnabled); processItem.setEnabled(deviceEnabled); deviceStateItem.setEnabled(deviceEnabled); appStateItem.setEnabled(deviceEnabled); radioStateItem.setEnabled(deviceEnabled); logCatItem.setEnabled(deviceEnabled); screenRecordItem.setEnabled(mCurrentDevice != null && mCurrentDevice.supportsFeature(IDevice.Feature.SCREEN_RECORD)); } }); // tell the shell to use this menu shell.setMenuBar(menuBar); } /* * Create the widgets in the main application window. The basic layout is a * two-panel sash, with a scrolling list of VMs on the left and detailed * output for a single VM on the right. */ private void createWidgets(final Shell shell) { Color darkGray = shell.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); /* * Create three areas: tool bar, split panels, status line */ shell.setLayout(new GridLayout(1, false)); // 1. panel area final Composite panelArea = new Composite(shell, SWT.BORDER); // make the panel area absorb all space panelArea.setLayoutData(new GridData(GridData.FILL_BOTH)); // 2. status line. mStatusLine = new Label(shell, SWT.NONE); // make status line extend all the way across mStatusLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mStatusLine.setText("Initializing..."); /* * Configure the split-panel area. */ final PreferenceStore prefs = PrefsDialog.getStore(); Composite topPanel = new Composite(panelArea, SWT.NONE); final Sash sash = new Sash(panelArea, SWT.HORIZONTAL); sash.setBackground(darkGray); Composite bottomPanel = new Composite(panelArea, SWT.NONE); panelArea.setLayout(new FormLayout()); createTopPanel(topPanel, darkGray); mClipboard = new Clipboard(panelArea.getDisplay()); if (useOldLogCatView()) { createBottomPanel(bottomPanel); } else { createLogCatView(bottomPanel); } // form layout data FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); topPanel.setLayoutData(data); final FormData sashData = new FormData(); if (prefs != null && prefs.contains(PREFERENCE_LOGSASH)) { sashData.top = new FormAttachment(0, prefs.getInt( PREFERENCE_LOGSASH)); } else { sashData.top = new FormAttachment(50,0); // 50% across } sashData.left = new FormAttachment(0, 0); sashData.right = new FormAttachment(100, 0); sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(sash, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); bottomPanel.setLayoutData(data); // allow resizes, but cap at minPanelWidth sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = panelArea.getClientArea(); int bottom = panelRect.height - sashRect.height - 100; e.y = Math.max(Math.min(e.y, bottom), 100); if (e.y != sashRect.y) { sashData.top = new FormAttachment(0, e.y); if (prefs != null) { prefs.setValue(PREFERENCE_LOGSASH, e.y); } panelArea.layout(); } } }); // add a global focus listener for all the tables mTableListener = new TableFocusListener(); // now set up the listener in the various panels if (useOldLogCatView()) { mLogPanel.setTableFocusListener(mTableListener); } else { mLogCatPanel.setTableFocusListener(mTableListener); } mEventLogPanel.setTableFocusListener(mTableListener); for (TablePanel p : mPanels) { if (p != null) { p.setTableFocusListener(mTableListener); } } mStatusLine.setText(""); } /* * Populate the tool bar. */ private void createDevicePanelToolBar(ToolBar toolBar) { Display display = toolBar.getDisplay(); // add "show heap updates" button mTBShowHeapUpdates = new ToolItem(toolBar, SWT.CHECK); mTBShowHeapUpdates.setImage(mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_HEAP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mTBShowHeapUpdates.setToolTipText("Show heap updates"); mTBShowHeapUpdates.setEnabled(false); mTBShowHeapUpdates.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mCurrentClient != null) { // boolean status = ((ToolItem)e.item).getSelection(); // invert previous state boolean enable = !mCurrentClient.isHeapUpdateEnabled(); mCurrentClient.setHeapUpdateEnabled(enable); } else { e.doit = false; // this has no effect? } } }); // add "dump HPROF" button mTBDumpHprof = new ToolItem(toolBar, SWT.PUSH); mTBDumpHprof.setToolTipText("Dump HPROF file"); mTBDumpHprof.setEnabled(false); mTBDumpHprof.setImage(mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_HPROF, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mTBDumpHprof.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mDevicePanel.dumpHprof(); // this will make sure the dump hprof button is disabled for the current selection // as the client is already dumping an hprof file enableButtons(); } }); // add "cause GC" button mTBCauseGc = new ToolItem(toolBar, SWT.PUSH); mTBCauseGc.setToolTipText("Cause an immediate GC"); mTBCauseGc.setEnabled(false); mTBCauseGc.setImage(mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_GC, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mTBCauseGc.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mDevicePanel.forceGcOnSelectedClient(); } }); new ToolItem(toolBar, SWT.SEPARATOR); // add "show thread updates" button mTBShowThreadUpdates = new ToolItem(toolBar, SWT.CHECK); mTBShowThreadUpdates.setImage(mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_THREAD, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mTBShowThreadUpdates.setToolTipText("Show thread updates"); mTBShowThreadUpdates.setEnabled(false); mTBShowThreadUpdates.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mCurrentClient != null) { // boolean status = ((ToolItem)e.item).getSelection(); // invert previous state boolean enable = !mCurrentClient.isThreadUpdateEnabled(); mCurrentClient.setThreadUpdateEnabled(enable); } else { e.doit = false; // this has no effect? } } }); // add a start/stop method tracing mTracingStartImage = mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_TRACING_START, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null); mTracingStopImage = mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_TRACING_STOP, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null); mTBProfiling = new ToolItem(toolBar, SWT.PUSH); mTBProfiling.setToolTipText("Start Method Profiling"); mTBProfiling.setEnabled(false); mTBProfiling.setImage(mTracingStartImage); mTBProfiling.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mDevicePanel.toggleMethodProfiling(); } }); new ToolItem(toolBar, SWT.SEPARATOR); // add "kill VM" button; need to make this visually distinct from // the status update buttons mTBHalt = new ToolItem(toolBar, SWT.PUSH); mTBHalt.setToolTipText("Halt the target VM"); mTBHalt.setEnabled(false); mTBHalt.setImage(mDdmUiLibLoader.loadImage(display, DevicePanel.ICON_HALT, DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mTBHalt.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mDevicePanel.killSelectedClient(); } }); toolBar.pack(); } private void createTopPanel(final Composite comp, Color darkGray) { final PreferenceStore prefs = PrefsDialog.getStore(); comp.setLayout(new FormLayout()); Composite leftPanel = new Composite(comp, SWT.NONE); final Sash sash = new Sash(comp, SWT.VERTICAL); sash.setBackground(darkGray); Composite rightPanel = new Composite(comp, SWT.NONE); createLeftPanel(leftPanel); createRightPanel(rightPanel); FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(sash, 0); leftPanel.setLayoutData(data); final FormData sashData = new FormData(); sashData.top = new FormAttachment(0, 0); sashData.bottom = new FormAttachment(100, 0); if (prefs != null && prefs.contains(PREFERENCE_SASH)) { sashData.left = new FormAttachment(0, prefs.getInt( PREFERENCE_SASH)); } else { // position the sash 380 from the right instead of x% (done by using // FormAttachment(x, 0)) in order to keep the sash at the same // position // from the left when the window is resized. // 380px is just enough to display the left table with no horizontal // scrollbar with the default font. sashData.left = new FormAttachment(0, 380); } sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(sash, 0); data.right = new FormAttachment(100, 0); rightPanel.setLayoutData(data); final int minPanelWidth = 60; // allow resizes, but cap at minPanelWidth sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = comp.getClientArea(); int right = panelRect.width - sashRect.width - minPanelWidth; e.x = Math.max(Math.min(e.x, right), minPanelWidth); if (e.x != sashRect.x) { sashData.left = new FormAttachment(0, e.x); if (prefs != null) { prefs.setValue(PREFERENCE_SASH, e.x); } comp.layout(); } } }); } private void createBottomPanel(final Composite comp) { final PreferenceStore prefs = PrefsDialog.getStore(); // create clipboard Display display = comp.getDisplay(); LogColors colors = new LogColors(); colors.infoColor = new Color(display, 0, 127, 0); colors.debugColor = new Color(display, 0, 0, 127); colors.errorColor = new Color(display, 255, 0, 0); colors.warningColor = new Color(display, 255, 127, 0); colors.verboseColor = new Color(display, 0, 0, 0); // set the preferences names LogPanel.PREFS_TIME = PREFS_COL_TIME; LogPanel.PREFS_LEVEL = PREFS_COL_LEVEL; LogPanel.PREFS_PID = PREFS_COL_PID; LogPanel.PREFS_TAG = PREFS_COL_TAG; LogPanel.PREFS_MESSAGE = PREFS_COL_MESSAGE; comp.setLayout(new GridLayout(1, false)); ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL); mCreateFilterAction = new ToolItemAction(toolBar, SWT.PUSH); mCreateFilterAction.item.setToolTipText("Create Filter"); mCreateFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay, "add.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mCreateFilterAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mLogPanel.addFilter(); } }); mEditFilterAction = new ToolItemAction(toolBar, SWT.PUSH); mEditFilterAction.item.setToolTipText("Edit Filter"); mEditFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay, "edit.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mEditFilterAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mLogPanel.editFilter(); } }); mDeleteFilterAction = new ToolItemAction(toolBar, SWT.PUSH); mDeleteFilterAction.item.setToolTipText("Delete Filter"); mDeleteFilterAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay, "delete.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mDeleteFilterAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mLogPanel.deleteFilter(); } }); new ToolItem(toolBar, SWT.SEPARATOR); LogLevel[] levels = LogLevel.values(); mLogLevelActions = new ToolItemAction[mLogLevelIcons.length]; for (int i = 0 ; i < mLogLevelActions.length; i++) { String name = levels[i].getStringValue(); final ToolItemAction newAction = new ToolItemAction(toolBar, SWT.CHECK); mLogLevelActions[i] = newAction; //newAction.item.setText(name); newAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // disable the other actions and record current index for (int k = 0 ; k < mLogLevelActions.length; k++) { ToolItemAction a = mLogLevelActions[k]; if (a == newAction) { a.setChecked(true); // set the log level mLogPanel.setCurrentFilterLogLevel(k+2); } else { a.setChecked(false); } } } }); newAction.item.setToolTipText(name); newAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay, mLogLevelIcons[i], DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); } new ToolItem(toolBar, SWT.SEPARATOR); mClearAction = new ToolItemAction(toolBar, SWT.PUSH); mClearAction.item.setToolTipText("Clear Log"); mClearAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay, "clear.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mClearAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mLogPanel.clear(); } }); new ToolItem(toolBar, SWT.SEPARATOR); mExportAction = new ToolItemAction(toolBar, SWT.PUSH); mExportAction.item.setToolTipText("Export Selection As Text..."); mExportAction.item.setImage(mDdmUiLibLoader.loadImage(mDisplay, "save.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); mExportAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mLogPanel.save(); } }); toolBar.pack(); // now create the log view mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL); mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); String colMode = prefs.getString(PrefsDialog.LOGCAT_COLUMN_MODE); if (PrefsDialog.LOGCAT_COLUMN_MODE_AUTO.equals(colMode)) { mLogPanel.setColumnMode(LogPanel.COLUMN_MODE_AUTO); } String fontStr = PrefsDialog.getStore().getString(PrefsDialog.LOGCAT_FONT); if (fontStr != null) { try { FontData fdat = new FontData(fontStr); mLogPanel.setFont(new Font(display, fdat)); } catch (IllegalArgumentException e) { // Looks like fontStr isn't a valid font representation. // We do nothing in this case, the logcat view will use the default font. } catch (SWTError e2) { // Looks like the Font() constructor failed. // We do nothing in this case, the logcat view will use the default font. } } mLogPanel.createPanel(comp); // and start the logcat mLogPanel.startLogCat(mCurrentDevice); } private void createLogCatView(Composite parent) { IPreferenceStore prefStore = DdmUiPreferences.getStore(); mLogCatPanel = new LogCatPanel(prefStore); mLogCatPanel.createPanel(parent); if (mCurrentDevice != null) { mLogCatPanel.deviceSelected(mCurrentDevice); } } /* * Create the contents of the left panel: a table of VMs. */ private void createLeftPanel(final Composite comp) { comp.setLayout(new GridLayout(1, false)); ToolBar toolBar = new ToolBar(comp, SWT.HORIZONTAL | SWT.RIGHT | SWT.WRAP); toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); createDevicePanelToolBar(toolBar); Composite c = new Composite(comp, SWT.NONE); c.setLayoutData(new GridData(GridData.FILL_BOTH)); mDevicePanel = new DevicePanel(true /* showPorts */); mDevicePanel.createPanel(c); // add ourselves to the device panel selection listener mDevicePanel.addSelectionListener(this); } /* * Create the contents of the right panel: tabs with VM information. */ private void createRightPanel(final Composite comp) { TabItem item; TabFolder tabFolder; comp.setLayout(new FillLayout()); tabFolder = new TabFolder(comp, SWT.NONE); for (int i = 0; i < mPanels.length; i++) { if (mPanels[i] != null) { item = new TabItem(tabFolder, SWT.NONE); item.setText(mPanelNames[i]); item.setToolTipText(mPanelTips[i]); item.setControl(mPanels[i].createPanel(tabFolder)); } } // add the emulator control panel to the folders. item = new TabItem(tabFolder, SWT.NONE); item.setText("Emulator Control"); item.setToolTipText("Emulator Control Panel"); mEmulatorPanel = new EmulatorControlPanel(); item.setControl(mEmulatorPanel.createPanel(tabFolder)); // add the event log panel to the folders. item = new TabItem(tabFolder, SWT.NONE); item.setText("Event Log"); item.setToolTipText("Event Log"); // create the composite that will hold the toolbar and the event log panel. Composite eventLogTopComposite = new Composite(tabFolder, SWT.NONE); item.setControl(eventLogTopComposite); eventLogTopComposite.setLayout(new GridLayout(1, false)); // create the toolbar and the actions ToolBar toolbar = new ToolBar(eventLogTopComposite, SWT.HORIZONTAL); toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); ToolItemAction optionsAction = new ToolItemAction(toolbar, SWT.PUSH); optionsAction.item.setToolTipText("Opens the options panel"); optionsAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(), "edit.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); ToolItemAction clearAction = new ToolItemAction(toolbar, SWT.PUSH); clearAction.item.setToolTipText("Clears the event log"); clearAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(), "clear.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); new ToolItem(toolbar, SWT.SEPARATOR); ToolItemAction saveAction = new ToolItemAction(toolbar, SWT.PUSH); saveAction.item.setToolTipText("Saves the event log"); saveAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(), "save.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); ToolItemAction loadAction = new ToolItemAction(toolbar, SWT.PUSH); loadAction.item.setToolTipText("Loads an event log"); loadAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(), "load.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); ToolItemAction importBugAction = new ToolItemAction(toolbar, SWT.PUSH); importBugAction.item.setToolTipText("Imports a bug report"); importBugAction.item.setImage(mDdmUiLibLoader.loadImage(comp.getDisplay(), "importBug.png", //$NON-NLS-1$ DevicePanel.ICON_WIDTH, DevicePanel.ICON_WIDTH, null)); // create the event log panel mEventLogPanel = new EventLogPanel(); // set the external actions mEventLogPanel.setActions(optionsAction, clearAction, saveAction, loadAction, importBugAction); // create the panel mEventLogPanel.createPanel(eventLogTopComposite); } private void createFileExplorer() { if (mExplorer == null) { mExplorerShell = new Shell(mDisplay); // create the ui mExplorerShell.setLayout(new GridLayout(1, false)); // toolbar + action ToolBar toolBar = new ToolBar(mExplorerShell, SWT.HORIZONTAL); toolBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); ToolItemAction pullAction = new ToolItemAction(toolBar, SWT.PUSH); pullAction.item.setToolTipText("Pull File from Device"); Image image = mDdmUiLibLoader.loadImage("pull.png", mDisplay); //$NON-NLS-1$ if (image != null) { pullAction.item.setImage(image); } else { // this is for debugging purpose when the icon is missing pullAction.item.setText("Pull"); //$NON-NLS-1$ } ToolItemAction pushAction = new ToolItemAction(toolBar, SWT.PUSH); pushAction.item.setToolTipText("Push file onto Device"); image = mDdmUiLibLoader.loadImage("push.png", mDisplay); //$NON-NLS-1$ if (image != null) { pushAction.item.setImage(image); } else { // this is for debugging purpose when the icon is missing pushAction.item.setText("Push"); //$NON-NLS-1$ } ToolItemAction deleteAction = new ToolItemAction(toolBar, SWT.PUSH); deleteAction.item.setToolTipText("Delete"); image = mDdmUiLibLoader.loadImage("delete.png", mDisplay); //$NON-NLS-1$ if (image != null) { deleteAction.item.setImage(image); } else { // this is for debugging purpose when the icon is missing deleteAction.item.setText("Delete"); //$NON-NLS-1$ } ToolItemAction createNewFolderAction = new ToolItemAction(toolBar, SWT.PUSH); createNewFolderAction.item.setToolTipText("New Folder"); image = mDdmUiLibLoader.loadImage("add.png", mDisplay); //$NON-NLS-1$ if (image != null) { createNewFolderAction.item.setImage(image); } else { // this is for debugging purpose when the icon is missing createNewFolderAction.item.setText("New Folder"); //$NON-NLS-1$ } // device explorer mExplorer = new DeviceExplorer(); mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction); pullAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mExplorer.pullSelection(); } }); pullAction.setEnabled(false); pushAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mExplorer.pushIntoSelection(); } }); pushAction.setEnabled(false); deleteAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mExplorer.deleteSelection(); } }); deleteAction.setEnabled(false); createNewFolderAction.item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mExplorer.createNewFolderInSelection(); } }); createNewFolderAction.setEnabled(false); Composite parent = new Composite(mExplorerShell, SWT.NONE); parent.setLayoutData(new GridData(GridData.FILL_BOTH)); mExplorer.createPanel(parent); mExplorer.switchDevice(mCurrentDevice); mExplorerShell.addShellListener(new ShellListener() { @Override public void shellActivated(ShellEvent e) { // pass } @Override public void shellClosed(ShellEvent e) { mExplorer = null; mExplorerShell = null; } @Override public void shellDeactivated(ShellEvent e) { // pass } @Override public void shellDeiconified(ShellEvent e) { // pass } @Override public void shellIconified(ShellEvent e) { // pass } }); mExplorerShell.pack(); setExplorerSizeAndPosition(mExplorerShell); mExplorerShell.open(); } else { if (mExplorerShell != null) { mExplorerShell.forceActive(); } } } /** * Set the status line. TODO: make this a stack, so we can safely have * multiple things trying to set it all at once. Also specify an expiration? */ public void setStatusLine(final String str) { try { mDisplay.asyncExec(new Runnable() { @Override public void run() { doSetStatusLine(str); } }); } catch (SWTException swte) { if (!mDisplay.isDisposed()) throw swte; } } private void doSetStatusLine(String str) { if (mStatusLine.isDisposed()) return; if (!mStatusLine.getText().equals(str)) { mStatusLine.setText(str); // try { Thread.sleep(100); } // catch (InterruptedException ie) {} } } public void displayError(final String msg) { try { mDisplay.syncExec(new Runnable() { @Override public void run() { MessageDialog.openError(mDisplay.getActiveShell(), "Error", msg); } }); } catch (SWTException swte) { if (!mDisplay.isDisposed()) throw swte; } } private void enableButtons() { if (mCurrentClient != null) { mTBShowThreadUpdates.setSelection(mCurrentClient.isThreadUpdateEnabled()); mTBShowThreadUpdates.setEnabled(true); mTBShowHeapUpdates.setSelection(mCurrentClient.isHeapUpdateEnabled()); mTBShowHeapUpdates.setEnabled(true); mTBHalt.setEnabled(true); mTBCauseGc.setEnabled(true); ClientData data = mCurrentClient.getClientData(); if (data.hasFeature(ClientData.FEATURE_HPROF)) { mTBDumpHprof.setEnabled(data.hasPendingHprofDump() == false); mTBDumpHprof.setToolTipText("Dump HPROF file"); } else { mTBDumpHprof.setEnabled(false); mTBDumpHprof.setToolTipText("Dump HPROF file (not supported by this VM)"); } if (data.hasFeature(ClientData.FEATURE_PROFILING)) { mTBProfiling.setEnabled(true); if (data.getMethodProfilingStatus() == MethodProfilingStatus.TRACER_ON || data.getMethodProfilingStatus() == MethodProfilingStatus.SAMPLER_ON) { mTBProfiling.setToolTipText("Stop Method Profiling"); mTBProfiling.setImage(mTracingStopImage); } else { mTBProfiling.setToolTipText("Start Method Profiling"); mTBProfiling.setImage(mTracingStartImage); } } else { mTBProfiling.setEnabled(false); mTBProfiling.setImage(mTracingStartImage); mTBProfiling.setToolTipText("Method Profiling (not supported by this VM)"); } } else { // list is empty, disable these mTBShowThreadUpdates.setSelection(false); mTBShowThreadUpdates.setEnabled(false); mTBShowHeapUpdates.setSelection(false); mTBShowHeapUpdates.setEnabled(false); mTBHalt.setEnabled(false); mTBCauseGc.setEnabled(false); mTBDumpHprof.setEnabled(false); mTBDumpHprof.setToolTipText("Dump HPROF file"); mTBProfiling.setEnabled(false); mTBProfiling.setImage(mTracingStartImage); mTBProfiling.setToolTipText("Start Method Profiling"); } } /** * Sent when a new {@link IDevice} and {@link Client} are selected. * @param selectedDevice the selected device. If null, no devices are selected. * @param selectedClient The selected client. If null, no clients are selected. * * @see IUiSelectionListener */ @Override public void selectionChanged(IDevice selectedDevice, Client selectedClient) { if (mCurrentDevice != selectedDevice) { mCurrentDevice = selectedDevice; for (TablePanel panel : mPanels) { if (panel != null) { panel.deviceSelected(mCurrentDevice); } } mEmulatorPanel.deviceSelected(mCurrentDevice); if (useOldLogCatView()) { mLogPanel.deviceSelected(mCurrentDevice); } else { mLogCatPanel.deviceSelected(mCurrentDevice); } if (mEventLogPanel != null) { mEventLogPanel.deviceSelected(mCurrentDevice); } if (mExplorer != null) { mExplorer.switchDevice(mCurrentDevice); } } if (mCurrentClient != selectedClient) { AndroidDebugBridge.getBridge().setSelectedClient(selectedClient); mCurrentClient = selectedClient; for (TablePanel panel : mPanels) { if (panel != null) { panel.clientSelected(mCurrentClient); } } enableButtons(); } } @Override public void clientChanged(Client client, int changeMask) { if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) == Client.CHANGE_METHOD_PROFILING_STATUS) { if (mCurrentClient == client) { mDisplay.asyncExec(new Runnable() { @Override public void run() { // force refresh of the button enabled state. enableButtons(); } }); } } } } ddms/app/src/main/resources/0040755 0000000 0000000 00000000000 12747325007 015053 5ustar000000000 0000000 ddms/app/src/main/resources/images/0040755 0000000 0000000 00000000000 12747325007 016320 5ustar000000000 0000000 ddms/app/src/main/resources/images/ddms-128.png0100644 0000000 0000000 00000042434 12747325007 020271 0ustar000000000 0000000 PNG  IHDR>atEXtSoftwareAdobe ImageReadyqe<fiTXtXML:com.adobe.xmp -ALIDATx}y7wozQGB Hh ı`%q$;v1c\0IPz;ݽ3{;ۓ'1{vz;NonS_i|NDݩmR.v?v ]]Qt݅㭏VՎԇcAh.Q*u\RANIkjVP5[S7ڢl ǚi5'ڳui;(VUUrd?69H?{>snKzDw>,;*'Ԏ\3!t={< `n#Ct.G2b S WN%2 uGw{fk57Hg f2ᯟyZx?}>&៱o;nvbBD/JO +G""21B:XthK_wV4ۍ+1;#ŏu91'Dt9pշO1U+_0RPA\Ϲ,Xvŵ eB3~)2ol9i:r6 Hjķkbd܇;{緼7̃T8jb " obXė5}OD#;<[FM 5=Gx[xwjNWKPupѝsKd=y|+C Kr4$!:w ;-+c/GsiBobU IQ3{+ʮ@£ݮ1GAtY3Bc:z g>M_,$%0/n^f T V RH?1&(ܔT`.='6@F{P+W[eF@&G6d&Ͽ)f;nsLv+y>;$Kg@;ʿTPh(wB{;ߥA}.R t x<%u:2X( vjaBa*5PՠB(*jI\)\q]1"Ӏ J1Huw ~sD` Qo 4_? 𙧝$ Q|‰S?DAⓝϏ i5\^3: E޸; 5mgҝ@,̟A%u_aM0L>C$s-3eY\ä I킱]p4/ԍE-tY]Y@&fa0is׽ 38&1ډ} p|/>, zp?'1eee@N|7#RP5Uh® *lx]j \Rϴn؁D\reM!Kd8 *{Ed=/xx/-Ix4˥yNJNۈ]D9sD^U0߮`c;#{Ұ8yI_adDG˒H!apjEs#K$vRw ]Em|~'r6@{;7cI1CO\JdW]gI5l_c]u˲m 4VN ly3WY>I??tꬊ*'AS{A0q0MfEUH»K@1u**عq0"]DaSH8۠$OLDD1;`.bba,*i֘'ϫqOE1"퇉Z׍SS6mZbڼ3xeߣ ?w&U|$&^&>z]Gu2MXG7=B8"&F.ؙ6"s$ סs{|.b*dD6k;!jW9F=,p(<ލ˓ ֿGX4ip&Si~ʈ"]<<7Ne9Z{7،3nkRH<{2]Oq1&vl)lx]y ҽunh_獛P쫵:4h> b=sJع}s =b^jK{7 {>PwCQʋba񒿌3b(.{jDCytCSϞUyy q^)Ơ-cr8D\J{%%?$Qmx`" m{9$ K8w=3i +s0= vL@<ůD577> oh6~- 22зt\{3qԘ^>l}H [H2rYVu+1C`{7E9ȃm:ԆLJ%}F-BބZDv9Y ;]IsE)̐ @ 5*{"?TϜm^0fLggN CArPL̼2^Ap0C)#@ @J6D4LE5ȗc;9L&%Fs-eSTRC8#C4סz3"`=SN!bǨoۤv(N\rOP(XEmMz(<قF ܓWlWN܅Խ.Eu{8SR ;b`71WDtrPL/~C]k NK=XQ!Iw:k; mo![|H- (kKLpay[eބ G qa7I\?vTolޚ? Bao[(8(% iB#Nd/>=e,z` ב9#tAVKd^䃓J#qQ2Qd ۠'w,U~s.h>Sj%Iڨ#~Wu U! =AWmkRPQ/so,> \:0wCU[bzFI|ߝ_07XRLUa9SGfgc{e(U#Ќȣo(B5F@K×A҉PXl#mArWb٘S ArHX#> ޏK@bsP ƞ%9B46M.uHxDeLo}L!ɗV 0׻Р]"O]t/Rb"Tw)~Nooc= |φe~%}YM10-#>liܬ#5~⿦24I#&Yؾ. e nɟ}T.)◯ױ?| yߝD 4C$?G{W6B85I ׍=B ++aȵS!l8ĀOg&uM\O`Y X76T"_C:&_&eX;>c t_j0mvl?Ni?<,+[p!;a͚5oC~^MDN#P _n~Zy}(D eiD?裎fPr)B6A7ۛ@k m'J'ڭ1J RA&biئAO"n%[G">8 o|(1CHU/n>4l9r:O$:ǎ3f ~;û:kl*VN:^sا`A5"yNF&y( qoR;JeG-Z;;hdu0$o~7?ǿ[c&X(ޏj>~)<29/?O(Log<&4\k8?bekhaOۙs `qbbѳB]2_m=4v2w͚B3 vsML4B|ܹg\"~Jh!TԾ: YьtjR;sœ+0q{~Ɍsᴷd :^c5K*|sE Ir2}4ʱ ڱ-ˆ Mo{΁Lsfլ܄C9.fv/R^ ; 2Z 5~$A:/ f8##ʂ5 =}i6ۋiO1+(S9i;Ye@\IC`*h:ire 'y 1l9@[H&f3 m_H\9XĮ5~ŷ}CJ, G/d j"q GĜ~O`xwlZ( E\mGXvcf** T" ąjdwJV/',z]LQ/SL ?_>.]f)n'}mev:{〮VV7?]RQ9AW]N[:6TX f@ j_ Ohd>b|5 <Ʊt;.?Ҏ[A Լa @I<$[IkuwwyOOYzD*]on[g,y2^ #~$?k었$/]/0^fN̈kuaAwqď2k0손3sGGAO#Q'ݶXj4) 3!#h|t]ל +/_dΣbnr]΢\UU'A+0رk*NvԹo\-okz|HH0~4Tv:̏mOq8y-j^h,!e"{rH0ty:UU"ͫIfxS.l~5H\qFDV(Ų:%@qQzUM]L},;2GWTTYo{v4"bk{Y ]%^AanqLG*hUn֪ǎu'MU>E=]|Vin$n9АhHL555#ˮ*'~C( W˩-[ibr &g"Ҝ(/sfCkV#hev7¤4 gjY,W^yT4"`" $Ѿ@nY\&8"7 Za r50&:^p$(\yRSwm_%TR'2=zqx2(dEqGW_| Mgh[RADAE!:|Jd0},h Z?#|%#NT*>fMs|8@ (&N?0 pםwΖ8UKa"q y̿`  dU[$wf"ɴKsH5;1?gŖ!:05B FÿJ`g4ngqK>CȐYPk| mU;SesPj!ϞzITɃ/FJ8=?ƞ7+53bOun8Z63`ZT8aWae~Z&/4p .@-J.1c{^t ǨbxXYxQ NA/yq@e5.˃:_~kv""푗&6ގ%)RuI|Tbh9hFpE;30&8B|yݶVsW+^p`FN|[ax6-\;8핑}n1$a@;]Ch'`?uM.K 8#kNh%:kz 03cҧ=$ C_vCZz15лLTZGڏ(2w2g`#iCff̊ʥ#KM'3Ǡ|46 .6N\ as\*Ux=u&tn?@ϳ\T-ff"}d<*{Ƨt2u3+ iR.LFviw3j ޑˮ:PeP "WR[)Wkn+M%ػL" /sߵ3 /x{®b z:lHHFgsq*WVVSO= oMLLq|wn4['o^ 'Foݕs%eO_g =PPvwr$Y=E'Ia={I|K.<1 b )b[`l.É7(P? 7|gLG?|iRac \@!7fo˜<< ?z98ðmF6bz:>&5KkL}[athh[:o&|^abDl^`O"Up HLH:(1L`U'a^dFr. p[I[jj9;w#gR;?݊M,]ͭhJ?2k ?[{|&nj관X*L3 ֎x"{C_IUӞ@,_ HMC{>L8+H,ñbT2ƳC{aBbۚٳAMU˱ͭsaX 5QoAXB䕆) |rѽ@`vȲӺy[lGOKے)|p1Ui9˫Tkym(j3*khǴ4Q CԌVD(Mݭ=!d0[F9d[e$VTnkup֌jx/s㒿_z$/v5VJՋ\)'),e)6KgM۸'?vBhpjeT^/nԡ]:)?^L޿@Q_ 蠺%RV=OfHbj''cT+]v^QvUM.3#odDGyhrبBLÜ[|H\G20QXb HK Ձ8`-Es>8, ^.? 2<4~-틎&ɵvb8%.MT-W#ҳYͽA~mf~*#HҒEE_/Qlh3<@mNLy<*\Ae%୆g?LnmL VM'y\a(Ij"iD~tMT`?!c/VTIz ?+T>fB[),zڵTDyn ɥ o,1q`}e-^_vA6jL,h;Z6sGê, y̗c +C '~G> ? ifBAiS\$^8ͧ?eqH?~B _!x U> =8TvzX T0/;lq4X[B3*͒g5=_"Ji9/N_84)]:sM#:{F3qj(1hĥNYpքZcF&wkE'(s O,k'Q\3é$B ` ;@0[ۉlr(K012v~)1G!qg?meSz{9S>@/ 7kPTs 1ϛy'z?1/κҙ8iZyD~!Gɿ/䈿c-=ƺofL$'rcεTKxZ1vy\6m9~@^Bod2k^^3癲a0@) f@ս|x 29h(EP/%@:bf\ؼ-(ɤS|\&#HOojN|䁢#j韦;FϡD;_^?wGisU?vtO w lSp24{lOڰHwEPGBA["0gK ySo)/qUfy.#f<qp>2f$#lI( Aս.Z3_0G ~d,êRL@)36oΘR%) j m3}Oe<ym6R )`ܨ}6zabwk)qOס@B8ظ@PĎ8>1@o0ɢGhSlǦ54ol0_^@gM l9lrdK?U~nL7Q_) 2+nznyO&=;``$kq3 )p{+q;. Jj|H*ƅ %e|粱e i+y{J ePUJ":C^YʭI"'cM!V%NIZԺ^R:CH wgzޚ|@TZ^OjA.&h3o:jw@(uoH(R r`*x clBnCTч X"|zhwd7ѮidfAɮhTP|g -t)&ays`11!p+,uCGKFiq Ey@[O;K< H0'9Bj$ v:R( ?ar cZ iQ~]}xTɢQ& m|~%` 6ǵ!K%2((b(O;AG'L'ɱkΗJ'#SʤNd џvc#{!Rb%T: ;,=ԯ\(0je8)sbLGƏʗ`06(l93&h&I?~(FG=() QQ{=b,*?7ͫbI2#U? DD;_ Ytێkתm}$)5Ѐ%_j. Ch{,weSW&;:ڏ&\̟ð>?L?j*jg7'Vl|b1b%i&ɕeOEU/! -ʈ&c4$~OWϪ?5,uu#f]>epjzf>/ڬC8p~JVѡA|y8O 3B3qtuu-eN+h=T3ApЫx<^FJhl!֥cJ>w8Af?f:/A xa!}d.*m(yJ*%S(:,ܔ3ݘUC㈫KHj&ػvN:O|2hn1'YĒLx@J8G¹N/e=p\'̩ۇ>owo׊ .y2@a{ezm*?13WnCôU{ )PqA0jղ-?qtt;f jX "n]ou t˫m߼G1*ssHA)Gs2@tfF0+1M{(IYTbGu"ˇωf e"~W[V>?O ,PLօLJ9c^g5-K1/m! ܾRXQCytMEԾ}sT{z_[O6›D/l29OfMl}]Z%![_^ ""::]M>13NTx(N}BǾ>\q/\z~;a 9͆YA:V{ޭ_=:I%L+%ʩ0(7R߼ fENףZ QxRǾ }|F^y^'c-GN-'A_&ΞG&2ݭ=+Ø`RdYE2  ~R0oPa 8J 9V UL.B/@u`r ܽ>&?ID؎4Y%3mڗN>] ƽrQեQc(=oEp'(ZHb8ϹzA| m~Og+h-g!`Sg7!!$& dF=qEݤڐ/>P~GkW<Ϣ3NȶS ޴1=#L x d tB}|/0LH%[KGN$o_CHT5yE{i/uP#cXXSH^å&? Zv yD싦^s&"7  irǞgY^ۦa4TD&Y_/< 0aָ<@g'b!&=uor(,Popy\S )A08x 괠@&w<1Qȝhi/wHx^5?k%7 B|0qoyciqn(nɅp ,ZA=v]QjVMW'q^oi` Q~3FrJ򀢦%4U1fَL*},5ag%.DWpp"1е*#'$:Om3b3.w K^umͶDN΅i$6)`N/Yvz;No`mXIENDB`ddms/ddmuilib/0040755 0000000 0000000 00000000000 12747325007 012337 5ustar000000000 0000000 ddms/ddmuilib/.classpath0100644 0000000 0000000 00000003571 12747325007 014325 0ustar000000000 0000000 ddms/ddmuilib/.project0100644 0000000 0000000 00000000557 12747325007 014012 0ustar000000000 0000000 ddmuilib org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature ddms/ddmuilib/.settings/0040755 0000000 0000000 00000000000 12747325007 014255 5ustar000000000 0000000 ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 021242 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 ddms/ddmuilib/NOTICE0100644 0000000 0000000 00000024707 12747325007 013252 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ddms/ddmuilib/README0100644 0000000 0000000 00000001055 12747325007 013215 0ustar000000000 0000000 Using the Eclipse projects for ddmuilib. ddmuilib requires SWT to compile. SWT is available in the depot under prebuild//swt Because the build path cannot contain relative path that are not inside the project directory, the .classpath file references a user library called ANDROID_SWT. In order to compile the project, make a user library called ANDROID_SWT containing the jar files available at prebuild//swt. You also need a user library called ANDROID_JFREECHART containing the jar files available at prebuild/common/jfreechart. ddms/ddmuilib/build.gradle0100644 0000000 0000000 00000000514 12747325007 014613 0ustar000000000 0000000 group = 'com.android.tools.ddms' archivesBaseName = 'ddmuilib' dependencies { compile project(':base:ddmlib') compile 'jfree:jfreechart:1.0.9' compile 'jfree:jfreechart-swt:1.0.9' testCompile 'junit:junit:3.8.1' } sourceSets { main.resources.srcDir 'src/main/java' test.resources.srcDir 'src/test/java' } ddms/ddmuilib/ddmuilib.iml0100644 0000000 0000000 00000002332 12747325007 014630 0ustar000000000 0000000 ddms/ddmuilib/src/0040755 0000000 0000000 00000000000 12747325007 013126 5ustar000000000 0000000 ddms/ddmuilib/src/main/0040755 0000000 0000000 00000000000 12747325007 014052 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/0040755 0000000 0000000 00000000000 12747325007 014773 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 015551 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 017171 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/0040755 0000000 0000000 00000000000 12747325007 020762 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java0100644 0000000 0000000 00000010000 12747325007 026456 0ustar000000000 0000000 /* * 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.ddmuilib; import java.util.regex.Pattern; /** * {@link AbstractBufferFindTarget} implements methods to find items inside a buffer. It takes * care of the logic to search backwards/forwards in the buffer, wrapping around when necessary. * The actual contents of the buffer should be provided by the classes that extend this. */ public abstract class AbstractBufferFindTarget implements IFindTarget { private int mCurrentSearchIndex; // Single element cache of the last search regex private Pattern mLastSearchPattern; private String mLastSearchText; @Override public boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward) { boolean found = false; int maxIndex = getItemCount(); synchronized (this) { // Find starting index for this search if (isNewSearch) { // for new searches, start from an appropriate place as provided by the delegate mCurrentSearchIndex = getStartingIndex(); } else { // for ongoing searches (finding next match for the same term), continue from // the current result index mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex); } // Create a regex pattern based on the search term. Pattern pattern; if (text.equals(mLastSearchText)) { pattern = mLastSearchPattern; } else { pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE); mLastSearchPattern = pattern; mLastSearchText = text; } // Iterate through the list of items. The search ends if we have gone through // all items once. int index = mCurrentSearchIndex; do { String msgText = getItem(mCurrentSearchIndex); if (msgText != null && pattern.matcher(msgText).find()) { found = true; break; } mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex); } while (index != mCurrentSearchIndex); // loop through entire contents once } if (found) { selectAndReveal(mCurrentSearchIndex); } return found; } /** Indicate that the log buffer has scrolled by certain number of elements */ public void scrollBy(int delta) { synchronized (this) { if (mCurrentSearchIndex > 0) { mCurrentSearchIndex = Math.max(0, mCurrentSearchIndex - delta); } } } private int getNext(int index, boolean searchForward, int max) { // increment or decrement index index = searchForward ? index + 1 : index - 1; // take care of underflow if (index == -1) { index = max - 1; } // ..and overflow if (index == max) { index = 0; } return index; } /** Obtain the number of items in the buffer */ public abstract int getItemCount(); /** Obtain the item at given index */ public abstract String getItem(int index); /** Select and reveal the item at given index */ public abstract void selectAndReveal(int index); /** Obtain the index from which search should begin */ public abstract int getStartingIndex(); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java0100644 0000000 0000000 00000032250 12747325007 023370 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ddmlib.Log; import com.android.ddmlib.NativeLibraryMapInfo; import com.android.ddmlib.NativeStackCallInfo; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; /** * Represents an addr2line process to get filename/method information from a * memory address.
* Each process can only handle one library, which should be provided when * creating a new process.
*
* The processes take some time to load as they need to parse the library files. * For this reason, processes cannot be manually started. Instead the class * keeps an internal list of processes and one asks for a process for a specific * library, using getProcess(String library).

* Internally, the processes are started in pipe mode to be able to query them * with multiple addresses. */ public class Addr2Line { private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS"; private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT = "Unable to locate library %s on disk. Addresses mapping to this library " + "will not be resolved. In order to fix this, set the the library search path " + "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + "."; /** * Loaded processes list. This is also used as a locking object for any * methods dealing with starting/stopping/creating processes/querying for * method. */ private static final HashMap sProcessCache = new HashMap(); /** * byte array representing a carriage return. Used to push addresses in the * process pipes. */ private static final byte[] sCrLf = { '\n' }; /** Path to the library */ private NativeLibraryMapInfo mLibrary; /** addr2line command to execute */ private String mAddr2LineCmd; /** the command line process */ private Process mProcess; /** buffer to read the result of the command line process from */ private BufferedReader mResultReader; /** * output stream to provide new addresses to decode to the command line * process */ private BufferedOutputStream mAddressWriter; private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER; static { String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR); if (symbols == null) { DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory(); } else { DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols; } } private static List mLibrarySearchPaths = new ArrayList(); /** * Set the search path where libraries should be found. * @param path search path to use, can be a colon separated list of paths if multiple folders * should be searched */ public static void setSearchPath(String path) { mLibrarySearchPaths.clear(); mLibrarySearchPaths.addAll(Arrays.asList(path.split(":"))); } /** * Returns the instance of an Addr2Line process for the specified library * and abi. *
The library should be in a format that makes
* $ANDROID_PRODUCT_OUT + "/symbols" + library a valid file. * * @param library the library in which to look for addresses. * @param abi indicates which underlying addr2line command to use. * @return a new Addr2Line object representing a started process, ready to * be queried for addresses. If any error happened when launching a * new process, null will be returned. */ public static Addr2Line getProcess(@NonNull final NativeLibraryMapInfo library, @Nullable String abi) { String libName = library.getLibraryName(); // synchronize around the hashmap object if (libName != null) { synchronized (sProcessCache) { // look for an existing process Addr2Line process = sProcessCache.get(libName); // if we don't find one, we create it if (process == null) { process = new Addr2Line(library, abi); // then we start it boolean status = process.start(); if (status) { // if starting the process worked, then we add it to the // list. sProcessCache.put(libName, process); } else { // otherwise we just drop the object, to return null process = null; } } // return the process return process; } } return null; } /** * Construct the object with a library name and abi. The library should be present * in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user * provided search path. * * @param library the library in which to look for address. * @param abi indicates which underlying addr2line command to use. */ private Addr2Line(@NonNull final NativeLibraryMapInfo library, @Nullable String abi) { mLibrary = library; // Set the addr2line command based on the abi. if (abi == null || abi.startsWith("32")) { Log.d("ddm-Addr2Line", "Using 32 bit addr2line command"); mAddr2LineCmd = System.getenv("ANDROID_ADDR2LINE"); if (mAddr2LineCmd == null) { mAddr2LineCmd = DdmUiPreferences.getAddr2Line(); } } else { Log.d("ddm-Addr2Line", "Using 64 bit addr2line command"); mAddr2LineCmd = System.getenv("ANDROID_ADDR2LINE64"); if (mAddr2LineCmd == null) { mAddr2LineCmd = DdmUiPreferences.getAddr2Line64(); } } } /** * Search for the library in the library search path and obtain the full path to where it * is found. * @return fully resolved path to the library if found in search path, null otherwise */ private String getLibraryPath(String library) { // first check the symbols folder String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library; if (new File(path).exists()) { return path; } for (String p : mLibrarySearchPaths) { // try appending the full path on device String fullPath = p + "/" + library; if (new File(fullPath).exists()) { return fullPath; } // try appending basename(library) fullPath = p + "/" + new File(library).getName(); if (new File(fullPath).exists()) { return fullPath; } } return null; } /** * Starts the command line process. * * @return true if the process was started, false if it failed to start, or * if there was any other errors. */ private boolean start() { // because this is only called from getProcess() we know we don't need // to synchronize this code. // build the command line String[] command = new String[5]; command[0] = mAddr2LineCmd; command[1] = "-C"; command[2] = "-f"; command[3] = "-e"; String fullPath = getLibraryPath(mLibrary.getLibraryName()); if (fullPath == null) { String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName()); Log.e("ddm-Addr2Line", msg); return false; } command[4] = fullPath; try { // attempt to start the process mProcess = Runtime.getRuntime().exec(command); if (mProcess != null) { // get the result reader InputStreamReader is = new InputStreamReader(mProcess .getInputStream()); mResultReader = new BufferedReader(is); // get the outstream to write the addresses mAddressWriter = new BufferedOutputStream(mProcess .getOutputStream()); // check our streams are here if (mResultReader == null || mAddressWriter == null) { // not here? stop the process and return false; mProcess.destroy(); mProcess = null; return false; } // return a success return true; } } catch (IOException e) { // log the error String msg = String.format( "Error while trying to start %1$s process for library %2$s", mAddr2LineCmd, mLibrary); Log.e("ddm-Addr2Line", msg); // drop the process just in case if (mProcess != null) { mProcess.destroy(); mProcess = null; } } // we can be here either cause the allocation of mProcess failed, or we // caught an exception return false; } /** * Stops the command line process. */ public void stop() { synchronized (sProcessCache) { if (mProcess != null) { // remove the process from the list sProcessCache.remove(mLibrary); // then stops the process mProcess.destroy(); // set the reference to null. // this allows to make sure another thread calling getAddress() // will not query a stopped thread mProcess = null; } } } /** * Stops all current running processes. */ public static void stopAll() { // because of concurrent access (and our use of HashMap.values()), we // can't rely on the synchronized inside stop(). We need to put one // around the whole loop. synchronized (sProcessCache) { // just a basic loop on all the values in the hashmap and call to // stop(); Collection col = sProcessCache.values(); for (Addr2Line a2l : col) { a2l.stop(); } } } /** * Looks up an address and returns method name, source file name, and line * number. * * @param addr the address to look up * @return a BacktraceInfo object containing the method/filename/linenumber * or null if the process we stopped before the query could be * processed, or if an IO exception happened. */ public NativeStackCallInfo getAddress(long addr) { long offset = addr - mLibrary.getStartAddress(); // even though we don't access the hashmap object, we need to // synchronized on it to prevent // another thread from stopping the process we're going to query. synchronized (sProcessCache) { // check the process is still alive/allocated if (mProcess != null) { // prepare to the write the address to the output buffer. // first, conversion to a string containing the hex value. String tmp = Long.toString(offset, 16); try { // write the address to the buffer mAddressWriter.write(tmp.getBytes()); // add CR-LF mAddressWriter.write(sCrLf); // flush it all. mAddressWriter.flush(); // read the result. We need to read 2 lines String method = mResultReader.readLine(); String source = mResultReader.readLine(); // make the backtrace object and return it if (method != null && source != null) { return new NativeStackCallInfo(addr, mLibrary.getLibraryName(), method, source); } } catch (IOException e) { // log the error Log.e("ddms", "Error while trying to get information for addr: " + tmp + " in library: " + mLibrary); // we'll return null later } } } return null; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java0100644 0000000 0000000 00000056155 12747325007 024703 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AllocationInfo; import com.android.ddmlib.AllocationInfo.AllocationSorter; import com.android.ddmlib.AllocationInfo.SortMode; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData.AllocationTrackingStatus; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; /** * Base class for our information panels. */ public class AllocationPanel extends TablePanel { private final static String PREFS_ALLOC_COL_NUMBER = "allocPanel.Col00"; //$NON-NLS-1$ private final static String PREFS_ALLOC_COL_SIZE = "allocPanel.Col0"; //$NON-NLS-1$ private final static String PREFS_ALLOC_COL_CLASS = "allocPanel.Col1"; //$NON-NLS-1$ private final static String PREFS_ALLOC_COL_THREAD = "allocPanel.Col2"; //$NON-NLS-1$ private final static String PREFS_ALLOC_COL_TRACE_CLASS = "allocPanel.Col3"; //$NON-NLS-1$ private final static String PREFS_ALLOC_COL_TRACE_METHOD = "allocPanel.Col4"; //$NON-NLS-1$ private final static String PREFS_ALLOC_SASH = "allocPanel.sash"; //$NON-NLS-1$ private static final String PREFS_STACK_COLUMN = "allocPanel.stack.col0"; //$NON-NLS-1$ private Composite mAllocationBase; private Table mAllocationTable; private TableViewer mAllocationViewer; private StackTracePanel mStackTracePanel; private Table mStackTraceTable; private Button mEnableButton; private Button mRequestButton; private Button mTraceFilterCheck; private final AllocationSorter mSorter = new AllocationSorter(); private TableColumn mSortColumn; private Image mSortUpImg; private Image mSortDownImg; private String mFilterText = null; /** * Content Provider to display the allocations of a client. * Expected input is a {@link Client} object, elements used in the table are of type * {@link AllocationInfo}. */ private class AllocationContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof Client) { AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations(); if (allocs != null) { if (mFilterText != null && mFilterText.length() > 0) { allocs = getFilteredAllocations(allocs, mFilterText); } Arrays.sort(allocs, mSorter); return allocs; } } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } } /** * A Label Provider to use with {@link AllocationContentProvider}. It expects the elements to be * of type {@link AllocationInfo}. */ private static class AllocationLabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof AllocationInfo) { AllocationInfo alloc = (AllocationInfo)element; switch (columnIndex) { case 0: return Integer.toString(alloc.getAllocNumber()); case 1: return Integer.toString(alloc.getSize()); case 2: return alloc.getAllocatedClass(); case 3: return Short.toString(alloc.getThreadId()); case 4: return alloc.getFirstTraceClassName(); case 5: return alloc.getFirstTraceMethodName(); } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } /** * Create our control(s). */ @Override protected Control createControl(Composite parent) { final IPreferenceStore store = DdmUiPreferences.getStore(); Display display = parent.getDisplay(); // get some images mSortUpImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_up.png", display); mSortDownImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_down.png", display); // base composite for selected client with enabled thread update. mAllocationBase = new Composite(parent, SWT.NONE); mAllocationBase.setLayout(new FormLayout()); // table above the sash Composite topParent = new Composite(mAllocationBase, SWT.NONE); topParent.setLayout(new GridLayout(6, false)); mEnableButton = new Button(topParent, SWT.PUSH); mEnableButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Client current = getCurrentClient(); AllocationTrackingStatus status = current.getClientData().getAllocationStatus(); if (status == AllocationTrackingStatus.ON) { current.enableAllocationTracker(false); } else { current.enableAllocationTracker(true); } current.requestAllocationStatus(); } }); mRequestButton = new Button(topParent, SWT.PUSH); mRequestButton.setText("Get Allocations"); mRequestButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { getCurrentClient().requestAllocationDetails(); } }); setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF); GridData gridData; Composite spacer = new Composite(topParent, SWT.NONE); spacer.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL)); new Label(topParent, SWT.NONE).setText("Filter:"); final Text filterText = new Text(topParent, SWT.BORDER); filterText.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL)); gridData.widthHint = 200; filterText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { mFilterText = filterText.getText().trim(); mAllocationViewer.refresh(); } }); mTraceFilterCheck = new Button(topParent, SWT.CHECK); mTraceFilterCheck.setText("Inc. trace"); mTraceFilterCheck.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { mAllocationViewer.refresh(); } }); mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION); mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH)); gridData.horizontalSpan = 6; mAllocationTable.setHeaderVisible(true); mAllocationTable.setLinesVisible(true); final TableColumn numberCol = TableHelper.createTableColumn( mAllocationTable, "Alloc Order", SWT.RIGHT, "Alloc Order", //$NON-NLS-1$ PREFS_ALLOC_COL_NUMBER, store); numberCol.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { setSortColumn(numberCol, SortMode.NUMBER); } }); final TableColumn sizeCol = TableHelper.createTableColumn( mAllocationTable, "Allocation Size", SWT.RIGHT, "888", //$NON-NLS-1$ PREFS_ALLOC_COL_SIZE, store); sizeCol.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { setSortColumn(sizeCol, SortMode.SIZE); } }); final TableColumn classCol = TableHelper.createTableColumn( mAllocationTable, "Allocated Class", SWT.LEFT, "Allocated Class", //$NON-NLS-1$ PREFS_ALLOC_COL_CLASS, store); classCol.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { setSortColumn(classCol, SortMode.CLASS); } }); final TableColumn threadCol = TableHelper.createTableColumn( mAllocationTable, "Thread Id", SWT.LEFT, "999", //$NON-NLS-1$ PREFS_ALLOC_COL_THREAD, store); threadCol.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { setSortColumn(threadCol, SortMode.THREAD); } }); final TableColumn inClassCol = TableHelper.createTableColumn( mAllocationTable, "Allocated in", SWT.LEFT, "utime", //$NON-NLS-1$ PREFS_ALLOC_COL_TRACE_CLASS, store); inClassCol.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { setSortColumn(inClassCol, SortMode.IN_CLASS); } }); final TableColumn inMethodCol = TableHelper.createTableColumn( mAllocationTable, "Allocated in", SWT.LEFT, "utime", //$NON-NLS-1$ PREFS_ALLOC_COL_TRACE_METHOD, store); inMethodCol.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { setSortColumn(inMethodCol, SortMode.IN_METHOD); } }); // init the default sort colum switch (mSorter.getSortMode()) { case SIZE: mSortColumn = sizeCol; break; case CLASS: mSortColumn = classCol; break; case THREAD: mSortColumn = threadCol; break; case IN_CLASS: mSortColumn = inClassCol; break; case IN_METHOD: mSortColumn = inMethodCol; break; } mSortColumn.setImage(mSorter.isDescending() ? mSortDownImg : mSortUpImg); mAllocationViewer = new TableViewer(mAllocationTable); mAllocationViewer.setContentProvider(new AllocationContentProvider()); mAllocationViewer.setLabelProvider(new AllocationLabelProvider()); mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection()); updateAllocationStackTrace(selectedAlloc); } }); // the separating sash final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL); Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); sash.setBackground(darkGray); // the UI below the sash mStackTracePanel = new StackTracePanel(); mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, PREFS_STACK_COLUMN, store); // now setup the sash. // form layout data FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); topParent.setLayoutData(data); final FormData sashData = new FormData(); if (store != null && store.contains(PREFS_ALLOC_SASH)) { sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH)); } else { sashData.top = new FormAttachment(50,0); // 50% across } sashData.left = new FormAttachment(0, 0); sashData.right = new FormAttachment(100, 0); sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(sash, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); mStackTraceTable.setLayoutData(data); // allow resizes, but cap at minPanelWidth sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = mAllocationBase.getClientArea(); int bottom = panelRect.height - sashRect.height - 100; e.y = Math.max(Math.min(e.y, bottom), 100); if (e.y != sashRect.y) { sashData.top = new FormAttachment(0, e.y); store.setValue(PREFS_ALLOC_SASH, e.y); mAllocationBase.layout(); } } }); return mAllocationBase; } @Override public void dispose() { mSortUpImg.dispose(); mSortDownImg.dispose(); super.dispose(); } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mAllocationTable.setFocus(); } /** * Sent when an existing client information changed. *

* This is sent from a non UI thread. * @param client the updated client. * @param changeMask the bit mask describing the changed properties. It can contain * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} * * @see IClientChangeListener#clientChanged(Client, int) */ @Override public void clientChanged(final Client client, int changeMask) { if (client == getCurrentClient()) { if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) { try { mAllocationTable.getDisplay().asyncExec(new Runnable() { @Override public void run() { mAllocationViewer.refresh(); updateAllocationStackCall(); } }); } catch (SWTException e) { // widget is disposed, we do nothing } } else if ((changeMask & Client.CHANGE_HEAP_ALLOCATION_STATUS) != 0) { try { mAllocationTable.getDisplay().asyncExec(new Runnable() { @Override public void run() { setUpButtons(true, client.getClientData().getAllocationStatus()); } }); } catch (SWTException e) { // widget is disposed, we do nothing } } } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()}. */ @Override public void deviceSelected() { // pass } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()}. */ @Override public void clientSelected() { if (mAllocationTable.isDisposed()) { return; } Client client = getCurrentClient(); mStackTracePanel.setCurrentClient(client); mStackTracePanel.setViewerInput(null); // always empty on client selection change. if (client != null) { setUpButtons(true /* enabled */, client.getClientData().getAllocationStatus()); } else { setUpButtons(false /* enabled */, AllocationTrackingStatus.OFF); } mAllocationViewer.setInput(client); } /** * Updates the stack call of the currently selected thread. *

* This must be called from the UI thread. */ private void updateAllocationStackCall() { Client client = getCurrentClient(); if (client != null) { // get the current selection in the ThreadTable AllocationInfo selectedAlloc = getAllocationSelection(null); if (selectedAlloc != null) { updateAllocationStackTrace(selectedAlloc); } else { updateAllocationStackTrace(null); } } } /** * updates the stackcall of the specified allocation. If null the UI is emptied * of current data. * @param thread */ private void updateAllocationStackTrace(AllocationInfo alloc) { mStackTracePanel.setViewerInput(alloc); } @Override protected void setTableFocusListener() { addTableToFocusListener(mAllocationTable); addTableToFocusListener(mStackTraceTable); } /** * Returns the current allocation selection or null if none is found. * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this * selection is returned, otherwise, the ISelection returned by * {@link TableViewer#getSelection()} is used. * @param selection the {@link ISelection} to use, or null */ private AllocationInfo getAllocationSelection(ISelection selection) { if (selection == null) { selection = mAllocationViewer.getSelection(); } if (selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object object = structuredSelection.getFirstElement(); if (object instanceof AllocationInfo) { return (AllocationInfo)object; } } return null; } /** * * @param enabled * @param trackingStatus */ private void setUpButtons(boolean enabled, AllocationTrackingStatus trackingStatus) { if (enabled) { switch (trackingStatus) { case UNKNOWN: mEnableButton.setText("?"); mEnableButton.setEnabled(false); mRequestButton.setEnabled(false); break; case OFF: mEnableButton.setText("Start Tracking"); mEnableButton.setEnabled(true); mRequestButton.setEnabled(false); break; case ON: mEnableButton.setText("Stop Tracking"); mEnableButton.setEnabled(true); mRequestButton.setEnabled(true); break; } } else { mEnableButton.setEnabled(false); mRequestButton.setEnabled(false); mEnableButton.setText("Start Tracking"); } } private void setSortColumn(final TableColumn column, SortMode sortMode) { // set the new sort mode mSorter.setSortMode(sortMode); mAllocationTable.setRedraw(false); // remove image from previous sort colum if (mSortColumn != column) { mSortColumn.setImage(null); } mSortColumn = column; if (mSorter.isDescending()) { mSortColumn.setImage(mSortDownImg); } else { mSortColumn.setImage(mSortUpImg); } mAllocationTable.setRedraw(true); mAllocationViewer.refresh(); } private AllocationInfo[] getFilteredAllocations(AllocationInfo[] allocations, String filterText) { ArrayList results = new ArrayList(); // Using default locale here such that the locale-specific c Locale locale = Locale.getDefault(); filterText = filterText.toLowerCase(locale); boolean fullTrace = mTraceFilterCheck.getSelection(); for (AllocationInfo info : allocations) { if (info.filter(filterText, fullTrace, locale)) { results.add(info); } } return results.toArray(new AllocationInfo[results.size()]); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java0100644 0000000 0000000 00000003071 12747325007 025032 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.Log; /** * base background thread class. The class provides a synchronous quit method * which sets a quitting flag to true. Inheriting classes should regularly test * this flag with isQuitting() and should finish if the flag is * true. */ public abstract class BackgroundThread extends Thread { private boolean mQuit = false; /** * Tell the thread to exit. This is usually called from the UI thread. The * call is synchronous and will only return once the thread has terminated * itself. */ public final void quit() { mQuit = true; Log.d("ddms", "Waiting for BackgroundThread to quit"); try { this.join(); } catch (InterruptedException ie) { ie.printStackTrace(); } } /** returns if the thread was asked to quit. */ protected final boolean isQuitting() { return mQuit; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java0100644 0000000 0000000 00000013600 12747325007 024252 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.HeapSegment; import com.android.ddmlib.ClientData.HeapData; import com.android.ddmlib.HeapSegment.HeapSegmentElement; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; /** * Base Panel for heap panels. */ public abstract class BaseHeapPanel extends TablePanel { /** store the processed heap segment, so that we don't recompute Image for nothing */ protected byte[] mProcessedHeapData; private Map> mHeapMap; /** * Serialize the heap data into an array. The resulting array is available through * getSerializedData(). * @param heapData The heap data to serialize * @return true if the data changed. */ protected boolean serializeHeapData(HeapData heapData) { Collection heapSegments; // Atomically get and clear the heap data. synchronized (heapData) { // get the segments heapSegments = heapData.getHeapSegments(); if (heapSegments != null) { // if they are not null, we never processed them. // Before we process then, we drop them from the HeapData heapData.clearHeapData(); // process them into a linear byte[] doSerializeHeapData(heapSegments); heapData.setProcessedHeapData(mProcessedHeapData); heapData.setProcessedHeapMap(mHeapMap); } else { // the heap segments are null. Let see if the heapData contains a // list that is already processed. byte[] pixData = heapData.getProcessedHeapData(); // and compare it to the one we currently have in the panel. if (pixData == mProcessedHeapData) { // looks like its the same return false; } else { mProcessedHeapData = pixData; } Map> heapMap = heapData.getProcessedHeapMap(); mHeapMap = heapMap; } } return true; } /** * Returns the serialized heap data */ protected byte[] getSerializedData() { return mProcessedHeapData; } /** * Processes and serialize the heapData. *

* The resulting serialized array is {@link #mProcessedHeapData}. *

* the resulting map is {@link #mHeapMap}. * @param heapData the collection of {@link HeapSegment} that forms the heap data. */ private void doSerializeHeapData(Collection heapData) { mHeapMap = new TreeMap>(); Iterator iterator; ByteArrayOutputStream out; out = new ByteArrayOutputStream(4 * 1024); iterator = heapData.iterator(); while (iterator.hasNext()) { HeapSegment hs = iterator.next(); HeapSegmentElement e = null; while (true) { int v; e = hs.getNextElement(null); if (e == null) { break; } if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) { v = 1; } else { v = e.getKind() + 2; } // put the element in the map ArrayList elementList = mHeapMap.get(v); if (elementList == null) { elementList = new ArrayList(); mHeapMap.put(v, elementList); } elementList.add(e); int len = e.getLength() / 8; while (len > 0) { out.write(v); --len; } } } mProcessedHeapData = out.toByteArray(); // sort the segment element in the heap info. Collection> elementLists = mHeapMap.values(); for (ArrayList elementList : elementLists) { Collections.sort(elementList); } } /** * Creates a linear image of the heap data. * @param pixData * @param h * @param palette * @return */ protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) { int w = pixData.length / h; if (pixData.length % h != 0) { w++; } // Create the heap image. ImageData id = new ImageData(w, h, 8, palette); int x = 0; int y = 0; for (byte b : pixData) { if (b >= 0) { id.setPixel(x, y, b); } y++; if (y >= h) { y = 0; x++; } } return id; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java0100644 0000000 0000000 00000002110 12747325007 025340 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; public abstract class ClientDisplayPanel extends SelectionDependentPanel implements IClientChangeListener { @Override protected void postCreation() { AndroidDebugBridge.addClientChangeListener(this); } public void dispose() { AndroidDebugBridge.removeClientChangeListener(this); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java0100644 0000000 0000000 00000004772 12747325007 025020 0ustar000000000 0000000 /* * 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.ddmuilib; import org.eclipse.jface.preference.IPreferenceStore; /** * Preference entry point for ddmuilib. Allows the lib to access a preference * store (org.eclipse.jface.preference.IPreferenceStore) defined by the * application that includes the lib. */ public final class DdmUiPreferences { public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL; private static IPreferenceStore mStore; private static String sSymbolLocation =""; //$NON-NLS-1$ private static String sAddr2LineLocation =""; //$NON-NLS-1$ private static String sAddr2LineLocation64 =""; //$NON-NLS-1$ private static String sTraceviewLocation =""; //$NON-NLS-1$ public static void setStore(IPreferenceStore store) { mStore = store; } public static IPreferenceStore getStore() { return mStore; } public static int getThreadRefreshInterval() { return sThreadRefreshInterval; } public static void setThreadRefreshInterval(int port) { sThreadRefreshInterval = port; } public static String getSymbolDirectory() { return sSymbolLocation; } public static void setSymbolsLocation(String location) { sSymbolLocation = location; } public static String getAddr2Line() { return sAddr2LineLocation; } public static void setAddr2LineLocation(String location) { sAddr2LineLocation = location; } public static String getAddr2Line64() { return sAddr2LineLocation64; } public static void setAddr2LineLocation64(String location) { sAddr2LineLocation64 = location; } public static String getTraceview() { return sTraceviewLocation; } public static void setTraceviewLocation(String location) { sTraceviewLocation = location; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java0100644 0000000 0000000 00000075630 12747325007 024014 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.annotations.NonNull; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.ClientData.DebuggerStatus; import com.android.ddmlib.DdmPreferences; import com.android.ddmlib.IDevice; import com.android.ddmlib.IDevice.DeviceState; import com.android.ddmuilib.vmtrace.VmTraceOptionsDialog; import com.google.common.base.Throwables; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import java.io.IOException; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.TimeUnit; /** * A display of both the devices and their clients. */ public final class DevicePanel extends Panel implements IDebugBridgeChangeListener, IDeviceChangeListener, IClientChangeListener { private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$ private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$ private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$ private final static int DEVICE_COL_SERIAL = 0; private final static int DEVICE_COL_STATE = 1; // col 2, 3 not used. private final static int DEVICE_COL_BUILD = 4; private final static int CLIENT_COL_NAME = 0; private final static int CLIENT_COL_PID = 1; private final static int CLIENT_COL_THREAD = 2; private final static int CLIENT_COL_HEAP = 3; private final static int CLIENT_COL_PORT = 4; public final static int ICON_WIDTH = 16; public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$ public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$ public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$ public final static String ICON_GC = "gc.png"; //$NON-NLS-1$ public final static String ICON_HPROF = "hprof.png"; //$NON-NLS-1$ public final static String ICON_TRACING_START = "tracing_start.png"; //$NON-NLS-1$ public final static String ICON_TRACING_STOP = "tracing_stop.png"; //$NON-NLS-1$ private IDevice mCurrentDevice; private Client mCurrentClient; private Tree mTree; private TreeViewer mTreeViewer; private Image mDeviceImage; private Image mEmulatorImage; private Image mThreadImage; private Image mHeapImage; private Image mWaitingImage; private Image mDebuggerImage; private Image mDebugErrorImage; private final ArrayList mListeners = new ArrayList(); private final ArrayList mDevicesToExpand = new ArrayList(); private boolean mAdvancedPortSupport; /** * A Content provider for the {@link TreeViewer}. *

* The input is a {@link AndroidDebugBridge}. First level elements are {@link IDevice} objects, * and second level elements are {@link Client} object. */ private class ContentProvider implements ITreeContentProvider { @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof IDevice) { return ((IDevice)parentElement).getClients(); } return new Object[0]; } @Override public Object getParent(Object element) { if (element instanceof Client) { return ((Client)element).getDevice(); } return null; } @Override public boolean hasChildren(Object element) { if (element instanceof IDevice) { return ((IDevice)element).hasClients(); } // Clients never have children. return false; } @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof AndroidDebugBridge) { return ((AndroidDebugBridge)inputElement).getDevices(); } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } } /** * A Label Provider for the {@link TreeViewer} in {@link DevicePanel}. It provides * labels and images for {@link IDevice} and {@link Client} objects. */ private class LabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { if (columnIndex == DEVICE_COL_SERIAL && element instanceof IDevice) { IDevice device = (IDevice)element; if (device.isEmulator()) { return mEmulatorImage; } return mDeviceImage; } else if (element instanceof Client) { Client client = (Client)element; ClientData cd = client.getClientData(); switch (columnIndex) { case CLIENT_COL_NAME: switch (cd.getDebuggerConnectionStatus()) { case DEFAULT: return null; case WAITING: return mWaitingImage; case ATTACHED: return mDebuggerImage; case ERROR: return mDebugErrorImage; } return null; case CLIENT_COL_THREAD: if (client.isThreadUpdateEnabled()) { return mThreadImage; } return null; case CLIENT_COL_HEAP: if (client.isHeapUpdateEnabled()) { return mHeapImage; } return null; } } return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof IDevice) { IDevice device = (IDevice)element; switch (columnIndex) { case DEVICE_COL_SERIAL: return device.getName(); case DEVICE_COL_STATE: return getStateString(device); case DEVICE_COL_BUILD: { String version = device.getProperty(IDevice.PROP_BUILD_VERSION); if (version != null) { String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); if (device.isEmulator()) { String avdName = device.getAvdName(); if (avdName == null) { avdName = "?"; // the device is probably not online yet, so // we don't know its AVD name just yet. } if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ return String.format("%1$s [%2$s, debug]", avdName, version); } else { return String.format("%1$s [%2$s]", avdName, version); //$NON-NLS-1$ } } else { if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ return String.format("%1$s, debug", version); } else { return String.format("%1$s", version); //$NON-NLS-1$ } } } else { return "unknown"; } } } } else if (element instanceof Client) { Client client = (Client)element; ClientData cd = client.getClientData(); switch (columnIndex) { case CLIENT_COL_NAME: String name = cd.getClientDescription(); if (name != null) { if (cd.isValidUserId() && cd.getUserId() != 0) { return String.format(Locale.US, "%s (%d)", name, cd.getUserId()); } else { return name; } } return "?"; case CLIENT_COL_PID: return Integer.toString(cd.getPid()); case CLIENT_COL_PORT: if (mAdvancedPortSupport) { int port = client.getDebuggerListenPort(); String portString = "?"; if (port != 0) { portString = Integer.toString(port); } if (client.isSelectedClient()) { return String.format("%1$s / %2$d", portString, //$NON-NLS-1$ DdmPreferences.getSelectedDebugPort()); } return portString; } } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } /** * Classes which implement this interface provide methods that deals * with {@link IDevice} and {@link Client} selection changes coming from the ui. */ public interface IUiSelectionListener { /** * Sent when a new {@link IDevice} and {@link Client} are selected. * @param selectedDevice the selected device. If null, no devices are selected. * @param selectedClient The selected client. If null, no clients are selected. */ public void selectionChanged(IDevice selectedDevice, Client selectedClient); } /** * Creates the {@link DevicePanel} object. * @param advancedPortSupport if true the device panel will add support for selected client port * and display the ports in the ui. */ public DevicePanel(boolean advancedPortSupport) { mAdvancedPortSupport = advancedPortSupport; } public void addSelectionListener(IUiSelectionListener listener) { mListeners.add(listener); } public void removeSelectionListener(IUiSelectionListener listener) { mListeners.remove(listener); } @Override protected Control createControl(Composite parent) { loadImages(parent.getDisplay()); parent.setLayout(new FillLayout()); // create the tree and its column mTree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); mTree.setHeaderVisible(true); mTree.setLinesVisible(true); IPreferenceStore store = DdmUiPreferences.getStore(); TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, "com.android.home", //$NON-NLS-1$ PREFS_COL_NAME_SERIAL, store); TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ "Offline", //$NON-NLS-1$ PREFS_COL_PID_STATE, store); TreeColumn col = new TreeColumn(mTree, SWT.NONE); col.setWidth(ICON_WIDTH + 8); col.setResizable(false); col = new TreeColumn(mTree, SWT.NONE); col.setWidth(ICON_WIDTH + 8); col.setResizable(false); TableHelper.createTreeColumn(mTree, "", SWT.LEFT, //$NON-NLS-1$ "9999-9999", //$NON-NLS-1$ PREFS_COL_PORT_BUILD, store); // create the tree viewer mTreeViewer = new TreeViewer(mTree); // make the device auto expanded. mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); // set up the content and label providers. mTreeViewer.setContentProvider(new ContentProvider()); mTreeViewer.setLabelProvider(new LabelProvider()); mTree.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { notifyListeners(); } }); return mTree; } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mTree.setFocus(); } @Override protected void postCreation() { // ask for notification of changes in AndroidDebugBridge (a new one is created when // adb is restarted from a different location), IDevice and Client objects. AndroidDebugBridge.addDebugBridgeChangeListener(this); AndroidDebugBridge.addDeviceChangeListener(this); AndroidDebugBridge.addClientChangeListener(this); } public void dispose() { AndroidDebugBridge.removeDebugBridgeChangeListener(this); AndroidDebugBridge.removeDeviceChangeListener(this); AndroidDebugBridge.removeClientChangeListener(this); } /** * Returns the selected {@link Client}. May be null. */ public Client getSelectedClient() { return mCurrentClient; } /** * Returns the selected {@link IDevice}. If a {@link Client} is selected, it returns the * IDevice object containing the client. */ public IDevice getSelectedDevice() { return mCurrentDevice; } /** * Kills the selected {@link Client} by sending its VM a halt command. */ public void killSelectedClient() { if (mCurrentClient != null) { Client client = mCurrentClient; // reset the selection to the device. TreePath treePath = new TreePath(new Object[] { mCurrentDevice }); TreeSelection treeSelection = new TreeSelection(treePath); mTreeViewer.setSelection(treeSelection); client.kill(); } } /** * Forces a GC on the selected {@link Client}. */ public void forceGcOnSelectedClient() { if (mCurrentClient != null) { mCurrentClient.executeGarbageCollector(); } } public void dumpHprof() { if (mCurrentClient != null) { mCurrentClient.dumpHprof(); } } public void toggleMethodProfiling() { if (mCurrentClient == null) { return; } try { toggleMethodProfiling(mCurrentClient); } catch (IOException e) { MessageDialog.openError(mTree.getShell(), "Method Profiling", "Unexpected I/O error while starting/stopping profiling: " + Throwables.getRootCause(e).getMessage()); } } private void toggleMethodProfiling(@NonNull Client client) throws IOException { ClientData cd = mCurrentClient.getClientData(); if (cd.getMethodProfilingStatus() == ClientData.MethodProfilingStatus.TRACER_ON) { mCurrentClient.stopMethodTracer(); } else if (cd.getMethodProfilingStatus() == ClientData.MethodProfilingStatus.SAMPLER_ON) { mCurrentClient.stopSamplingProfiler(); } else { boolean supportsSampling = cd.hasFeature(ClientData.FEATURE_SAMPLING_PROFILER); // default to tracing boolean shouldUseTracing = true; int samplingIntervalMicros = 1; // if client supports sampling, then ask the user to choose the method if (supportsSampling) { VmTraceOptionsDialog dialog = new VmTraceOptionsDialog(mTree.getShell()); if (dialog.open() == Window.CANCEL) { return; } shouldUseTracing = dialog.shouldUseTracing(); if (!shouldUseTracing) { samplingIntervalMicros = dialog.getSamplingIntervalMicros(); } } if (shouldUseTracing) { mCurrentClient.startMethodTracer(); } else { mCurrentClient.startSamplingProfiler(samplingIntervalMicros, TimeUnit.MICROSECONDS); } } } public void setEnabledHeapOnSelectedClient(boolean enable) { if (mCurrentClient != null) { mCurrentClient.setHeapUpdateEnabled(enable); } } public void setEnabledThreadOnSelectedClient(boolean enable) { if (mCurrentClient != null) { mCurrentClient.setThreadUpdateEnabled(enable); } } /** * Sent when a new {@link AndroidDebugBridge} is started. *

* This is sent from a non UI thread. * @param bridge the new {@link AndroidDebugBridge} object. */ @Override public void bridgeChanged(final AndroidDebugBridge bridge) { if (mTree.isDisposed() == false) { exec(new Runnable() { @Override public void run() { if (mTree.isDisposed() == false) { // set up the data source. mTreeViewer.setInput(bridge); // notify the listener of a possible selection change. notifyListeners(); } else { // tree is disposed, we need to do something. // lets remove ourselves from the listener. AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); } } }); } // all current devices are obsolete synchronized (mDevicesToExpand) { mDevicesToExpand.clear(); } } /** * Sent when the a device is connected to the {@link AndroidDebugBridge}. *

* This is sent from a non UI thread. * @param device the new device. * * @see IDeviceChangeListener#deviceConnected(IDevice) */ @Override public void deviceConnected(IDevice device) { exec(new Runnable() { @Override public void run() { if (mTree.isDisposed() == false) { // refresh all mTreeViewer.refresh(); // notify the listener of a possible selection change. notifyListeners(); } else { // tree is disposed, we need to do something. // lets remove ourselves from the listener. AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); } } }); // if it doesn't have clients yet, it'll need to be manually expanded when it gets them. if (device.hasClients() == false) { synchronized (mDevicesToExpand) { mDevicesToExpand.add(device); } } } /** * Sent when the a device is connected to the {@link AndroidDebugBridge}. *

* This is sent from a non UI thread. * @param device the new device. * * @see IDeviceChangeListener#deviceDisconnected(IDevice) */ @Override public void deviceDisconnected(IDevice device) { deviceConnected(device); // just in case, we remove it from the list of devices to expand. synchronized (mDevicesToExpand) { mDevicesToExpand.remove(device); } } /** * Sent when a device data changed, or when clients are started/terminated on the device. *

* This is sent from a non UI thread. * @param device the device that was updated. * @param changeMask the mask indicating what changed. * * @see IDeviceChangeListener#deviceChanged(IDevice,int) */ @Override public void deviceChanged(final IDevice device, int changeMask) { boolean expand = false; synchronized (mDevicesToExpand) { int index = mDevicesToExpand.indexOf(device); if (device.hasClients() && index != -1) { mDevicesToExpand.remove(index); expand = true; } } final boolean finalExpand = expand; exec(new Runnable() { @Override public void run() { if (mTree.isDisposed() == false) { // look if the current device is selected. This is done in case the current // client of this particular device was killed. In this case, we'll need to // manually reselect the device. IDevice selectedDevice = getSelectedDevice(); // refresh the device mTreeViewer.refresh(device); // if the selected device was the changed device and the new selection is // empty, we reselect the device. if (selectedDevice == device && mTreeViewer.getSelection().isEmpty()) { mTreeViewer.setSelection(new TreeSelection(new TreePath( new Object[] { device }))); } // notify the listener of a possible selection change. notifyListeners(); if (finalExpand) { mTreeViewer.setExpandedState(device, true); } } else { // tree is disposed, we need to do something. // lets remove ourselves from the listener. AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); } } }); } /** * Sent when an existing client information changed. *

* This is sent from a non UI thread. * @param client the updated client. * @param changeMask the bit mask describing the changed properties. It can contain * any of the following values: {@link Client#CHANGE_INFO}, * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} * * @see IClientChangeListener#clientChanged(Client, int) */ @Override public void clientChanged(final Client client, final int changeMask) { exec(new Runnable() { @Override public void run() { if (mTree.isDisposed() == false) { // refresh the client mTreeViewer.refresh(client); if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS && client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) { // make sure the device is expanded. Normally the setSelection below // will auto expand, but the children of device may not already exist // at this time. Forcing an expand will make the TreeViewer create them. IDevice device = client.getDevice(); if (mTreeViewer.getExpandedState(device) == false) { mTreeViewer.setExpandedState(device, true); } // create and set the selection TreePath treePath = new TreePath(new Object[] { device, client}); TreeSelection treeSelection = new TreeSelection(treePath); mTreeViewer.setSelection(treeSelection); if (mAdvancedPortSupport) { client.setAsSelectedClient(); } // notify the listener of a possible selection change. notifyListeners(device, client); } } else { // tree is disposed, we need to do something. // lets remove ourselves from the listener. AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); } } }); } private void loadImages(Display display) { ImageLoader loader = ImageLoader.getDdmUiLibLoader(); if (mDeviceImage == null) { mDeviceImage = loader.loadImage(display, "device.png", //$NON-NLS-1$ ICON_WIDTH, ICON_WIDTH, display.getSystemColor(SWT.COLOR_RED)); } if (mEmulatorImage == null) { mEmulatorImage = loader.loadImage(display, "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ display.getSystemColor(SWT.COLOR_BLUE)); } if (mThreadImage == null) { mThreadImage = loader.loadImage(display, ICON_THREAD, ICON_WIDTH, ICON_WIDTH, display.getSystemColor(SWT.COLOR_YELLOW)); } if (mHeapImage == null) { mHeapImage = loader.loadImage(display, ICON_HEAP, ICON_WIDTH, ICON_WIDTH, display.getSystemColor(SWT.COLOR_BLUE)); } if (mWaitingImage == null) { mWaitingImage = loader.loadImage(display, "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ display.getSystemColor(SWT.COLOR_RED)); } if (mDebuggerImage == null) { mDebuggerImage = loader.loadImage(display, "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ display.getSystemColor(SWT.COLOR_GREEN)); } if (mDebugErrorImage == null) { mDebugErrorImage = loader.loadImage(display, "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ display.getSystemColor(SWT.COLOR_RED)); } } /** * Returns a display string representing the state of the device. * @param d the device */ private static String getStateString(IDevice d) { DeviceState deviceState = d.getState(); if (deviceState == DeviceState.ONLINE) { return "Online"; } else if (deviceState == DeviceState.OFFLINE) { return "Offline"; } else if (deviceState == DeviceState.BOOTLOADER) { return "Bootloader"; } return "??"; } /** * Executes the {@link Runnable} in the UI thread. * @param runnable the runnable to execute. */ private void exec(Runnable runnable) { try { Display display = mTree.getDisplay(); display.asyncExec(runnable); } catch (SWTException e) { // tree is disposed, we need to do something. lets remove ourselves from the listener. AndroidDebugBridge.removeDebugBridgeChangeListener(this); AndroidDebugBridge.removeDeviceChangeListener(this); AndroidDebugBridge.removeClientChangeListener(this); } } private void notifyListeners() { // get the selection TreeItem[] items = mTree.getSelection(); Client client = null; IDevice device = null; if (items.length == 1) { Object object = items[0].getData(); if (object instanceof Client) { client = (Client)object; device = client.getDevice(); } else if (object instanceof IDevice) { device = (IDevice)object; } } notifyListeners(device, client); } private void notifyListeners(IDevice selectedDevice, Client selectedClient) { if (selectedDevice != mCurrentDevice || selectedClient != mCurrentClient) { mCurrentDevice = selectedDevice; mCurrentClient = selectedClient; for (IUiSelectionListener listener : mListeners) { // notify the listener with a try/catch-all to make sure this thread won't die // because of an uncaught exception before all the listeners were notified. try { listener.selectionChanged(selectedDevice, selectedClient); } catch (Exception e) { } } } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java0100644 0000000 0000000 00000163563 12747325007 025751 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.EmulatorConsole; import com.android.ddmlib.EmulatorConsole.GsmMode; import com.android.ddmlib.EmulatorConsole.GsmStatus; import com.android.ddmlib.EmulatorConsole.NetworkStatus; import com.android.ddmlib.IDevice; import com.android.ddmuilib.location.CoordinateControls; import com.android.ddmuilib.location.GpxParser; import com.android.ddmuilib.location.GpxParser.Track; import com.android.ddmuilib.location.KmlParser; import com.android.ddmuilib.location.TrackContentProvider; import com.android.ddmuilib.location.TrackLabelProvider; import com.android.ddmuilib.location.TrackPoint; import com.android.ddmuilib.location.WayPoint; import com.android.ddmuilib.location.WayPointContentProvider; import com.android.ddmuilib.location.WayPointLabelProvider; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.TabFolder; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Text; /** * Panel to control the emulator using EmulatorConsole objects. */ public class EmulatorControlPanel extends SelectionDependentPanel { // default location: Patio outside Charlie's private final static double DEFAULT_LONGITUDE = -122.084095; private final static double DEFAULT_LATITUDE = 37.422006; private final static String SPEED_FORMAT = "Speed: %1$dX"; /** * Map between the display gsm mode and the internal tag used by the display. */ private final static String[][] GSM_MODES = new String[][] { { "unregistered", GsmMode.UNREGISTERED.getTag() }, { "home", GsmMode.HOME.getTag() }, { "roaming", GsmMode.ROAMING.getTag() }, { "searching", GsmMode.SEARCHING.getTag() }, { "denied", GsmMode.DENIED.getTag() }, }; private final static String[] NETWORK_SPEEDS = new String[] { "Full", "GSM", "HSCSD", "GPRS", "EDGE", "UMTS", "HSDPA", }; private final static String[] NETWORK_LATENCIES = new String[] { "None", "GPRS", "EDGE", "UMTS", }; private final static int[] PLAY_SPEEDS = new int[] { 1, 2, 5, 10, 20, 50 }; private final static String RE_PHONE_NUMBER = "^[+#0-9]+$"; //$NON-NLS-1$ private final static String PREFS_WAYPOINT_COL_NAME = "emulatorControl.waypoint.name"; //$NON-NLS-1$ private final static String PREFS_WAYPOINT_COL_LONGITUDE = "emulatorControl.waypoint.longitude"; //$NON-NLS-1$ private final static String PREFS_WAYPOINT_COL_LATITUDE = "emulatorControl.waypoint.latitude"; //$NON-NLS-1$ private final static String PREFS_WAYPOINT_COL_ELEVATION = "emulatorControl.waypoint.elevation"; //$NON-NLS-1$ private final static String PREFS_WAYPOINT_COL_DESCRIPTION = "emulatorControl.waypoint.desc"; //$NON-NLS-1$ private final static String PREFS_TRACK_COL_NAME = "emulatorControl.track.name"; //$NON-NLS-1$ private final static String PREFS_TRACK_COL_COUNT = "emulatorControl.track.count"; //$NON-NLS-1$ private final static String PREFS_TRACK_COL_FIRST = "emulatorControl.track.first"; //$NON-NLS-1$ private final static String PREFS_TRACK_COL_LAST = "emulatorControl.track.last"; //$NON-NLS-1$ private final static String PREFS_TRACK_COL_COMMENT = "emulatorControl.track.comment"; //$NON-NLS-1$ private EmulatorConsole mEmulatorConsole; private Composite mParent; private Label mVoiceLabel; private Combo mVoiceMode; private Label mDataLabel; private Combo mDataMode; private Label mSpeedLabel; private Combo mNetworkSpeed; private Label mLatencyLabel; private Combo mNetworkLatency; private Label mNumberLabel; private Text mPhoneNumber; private Button mVoiceButton; private Button mSmsButton; private Label mMessageLabel; private Text mSmsMessage; private Button mCallButton; private Button mCancelButton; private TabFolder mLocationFolders; private Button mDecimalButton; private Button mSexagesimalButton; private CoordinateControls mLongitudeControls; private CoordinateControls mLatitudeControls; private Button mGpxUploadButton; private Table mGpxWayPointTable; private Table mGpxTrackTable; private Button mKmlUploadButton; private Table mKmlWayPointTable; private Button mPlayGpxButton; private Button mGpxBackwardButton; private Button mGpxForwardButton; private Button mGpxSpeedButton; private Button mPlayKmlButton; private Button mKmlBackwardButton; private Button mKmlForwardButton; private Button mKmlSpeedButton; private Image mPlayImage; private Image mPauseImage; private Thread mPlayingThread; private boolean mPlayingTrack; private int mPlayDirection = 1; private int mSpeed; private int mSpeedIndex; private final SelectionAdapter mDirectionButtonAdapter = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Button b = (Button)e.getSource(); if (b.getSelection() == false) { // basically the button was unselected, which we don't allow. // so we reselect it. b.setSelection(true); return; } // now handle selection change. if (b == mGpxForwardButton || b == mKmlForwardButton) { mGpxBackwardButton.setSelection(false); mGpxForwardButton.setSelection(true); mKmlBackwardButton.setSelection(false); mKmlForwardButton.setSelection(true); mPlayDirection = 1; } else { mGpxBackwardButton.setSelection(true); mGpxForwardButton.setSelection(false); mKmlBackwardButton.setSelection(true); mKmlForwardButton.setSelection(false); mPlayDirection = -1; } } }; private final SelectionAdapter mSpeedButtonAdapter = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mSpeedIndex = (mSpeedIndex+1) % PLAY_SPEEDS.length; mSpeed = PLAY_SPEEDS[mSpeedIndex]; mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); mGpxPlayControls.pack(); mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); mKmlPlayControls.pack(); if (mPlayingThread != null) { mPlayingThread.interrupt(); } } }; private Composite mKmlPlayControls; private Composite mGpxPlayControls; public EmulatorControlPanel() { } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()} */ @Override public void deviceSelected() { handleNewDevice(getCurrentDevice()); } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()} */ @Override public void clientSelected() { // pass } /** * Creates a control capable of displaying some information. This is * called once, when the application is initializing, from the UI thread. */ @Override protected Control createControl(Composite parent) { mParent = parent; final ScrolledComposite scollingParent = new ScrolledComposite(parent, SWT.V_SCROLL); scollingParent.setExpandVertical(true); scollingParent.setExpandHorizontal(true); scollingParent.setLayoutData(new GridData(GridData.FILL_BOTH)); final Composite top = new Composite(scollingParent, SWT.NONE); scollingParent.setContent(top); top.setLayout(new GridLayout(1, false)); // set the resize for the scrolling to work (why isn't that done automatically?!?) scollingParent.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Rectangle r = scollingParent.getClientArea(); scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); } }); createRadioControls(top); createCallControls(top); createLocationControls(top); doEnable(false); top.layout(); Rectangle r = scollingParent.getClientArea(); scollingParent.setMinSize(top.computeSize(r.width, SWT.DEFAULT)); return scollingParent; } /** * Create Radio (on/off/roaming, for voice/data) controls. * @param top */ private void createRadioControls(final Composite top) { Group g1 = new Group(top, SWT.NONE); g1.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); g1.setLayout(new GridLayout(2, false)); g1.setText("Telephony Status"); // the inside of the group is 2 composite so that all the column of the controls (mainly // combos) have the same width, while not taking the whole screen width Composite insideGroup = new Composite(g1, SWT.NONE); GridLayout gl = new GridLayout(4, false); gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; insideGroup.setLayout(gl); mVoiceLabel = new Label(insideGroup, SWT.NONE); mVoiceLabel.setText("Voice:"); mVoiceLabel.setAlignment(SWT.RIGHT); mVoiceMode = new Combo(insideGroup, SWT.READ_ONLY); mVoiceMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); for (String[] mode : GSM_MODES) { mVoiceMode.add(mode[0]); } mVoiceMode.addSelectionListener(new SelectionAdapter() { // called when selection changes @Override public void widgetSelected(SelectionEvent e) { setVoiceMode(mVoiceMode.getSelectionIndex()); } }); mSpeedLabel = new Label(insideGroup, SWT.NONE); mSpeedLabel.setText("Speed:"); mSpeedLabel.setAlignment(SWT.RIGHT); mNetworkSpeed = new Combo(insideGroup, SWT.READ_ONLY); mNetworkSpeed.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); for (String mode : NETWORK_SPEEDS) { mNetworkSpeed.add(mode); } mNetworkSpeed.addSelectionListener(new SelectionAdapter() { // called when selection changes @Override public void widgetSelected(SelectionEvent e) { setNetworkSpeed(mNetworkSpeed.getSelectionIndex()); } }); mDataLabel = new Label(insideGroup, SWT.NONE); mDataLabel.setText("Data:"); mDataLabel.setAlignment(SWT.RIGHT); mDataMode = new Combo(insideGroup, SWT.READ_ONLY); mDataMode.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); for (String[] mode : GSM_MODES) { mDataMode.add(mode[0]); } mDataMode.addSelectionListener(new SelectionAdapter() { // called when selection changes @Override public void widgetSelected(SelectionEvent e) { setDataMode(mDataMode.getSelectionIndex()); } }); mLatencyLabel = new Label(insideGroup, SWT.NONE); mLatencyLabel.setText("Latency:"); mLatencyLabel.setAlignment(SWT.RIGHT); mNetworkLatency = new Combo(insideGroup, SWT.READ_ONLY); mNetworkLatency.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); for (String mode : NETWORK_LATENCIES) { mNetworkLatency.add(mode); } mNetworkLatency.addSelectionListener(new SelectionAdapter() { // called when selection changes @Override public void widgetSelected(SelectionEvent e) { setNetworkLatency(mNetworkLatency.getSelectionIndex()); } }); // now an empty label to take the rest of the width of the group Label l = new Label(g1, SWT.NONE); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); } /** * Create Voice/SMS call/hang up controls * @param top */ private void createCallControls(final Composite top) { GridLayout gl; Group g2 = new Group(top, SWT.NONE); g2.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); g2.setLayout(new GridLayout(1, false)); g2.setText("Telephony Actions"); // horizontal composite for label + text field Composite phoneComp = new Composite(g2, SWT.NONE); phoneComp.setLayoutData(new GridData(GridData.FILL_BOTH)); gl = new GridLayout(2, false); gl.marginBottom = gl.marginHeight = gl.marginLeft = gl.marginRight = 0; phoneComp.setLayout(gl); mNumberLabel = new Label(phoneComp, SWT.NONE); mNumberLabel.setText("Incoming number:"); mPhoneNumber = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.SINGLE); mPhoneNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mPhoneNumber.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { // Reenable the widgets based on the content of the text. // doEnable checks the validity of the phone number to enable/disable some // widgets. // Looks like we're getting a callback at creation time, so we can't // suppose that we are enabled when the text is modified... doEnable(mEmulatorConsole != null); } }); mVoiceButton = new Button(phoneComp, SWT.RADIO); GridData gd = new GridData(); gd.horizontalSpan = 2; mVoiceButton.setText("Voice"); mVoiceButton.setLayoutData(gd); mVoiceButton.setEnabled(false); mVoiceButton.setSelection(true); mVoiceButton.addSelectionListener(new SelectionAdapter() { // called when selection changes @Override public void widgetSelected(SelectionEvent e) { doEnable(true); if (mVoiceButton.getSelection()) { mCallButton.setText("Call"); } else { mCallButton.setText("Send"); } } }); mSmsButton = new Button(phoneComp, SWT.RADIO); mSmsButton.setText("SMS"); gd = new GridData(); gd.horizontalSpan = 2; mSmsButton.setLayoutData(gd); mSmsButton.setEnabled(false); // Since there are only 2 radio buttons, we can put a listener on only one (they // are both called on select and unselect event. mMessageLabel = new Label(phoneComp, SWT.NONE); gd = new GridData(); gd.verticalAlignment = SWT.TOP; mMessageLabel.setLayoutData(gd); mMessageLabel.setText("Message:"); mMessageLabel.setEnabled(false); mSmsMessage = new Text(phoneComp, SWT.BORDER | SWT.LEFT | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL); mSmsMessage.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.heightHint = 70; mSmsMessage.setEnabled(false); // composite to put the 2 buttons horizontally Composite g2ButtonComp = new Composite(g2, SWT.NONE); g2ButtonComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); gl = new GridLayout(2, false); gl.marginWidth = gl.marginHeight = 0; g2ButtonComp.setLayout(gl); // now a button below the phone number mCallButton = new Button(g2ButtonComp, SWT.PUSH); mCallButton.setText("Call"); mCallButton.setEnabled(false); mCallButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mEmulatorConsole != null) { if (mVoiceButton.getSelection()) { processCommandResult(mEmulatorConsole.call(mPhoneNumber.getText().trim())); } else { // we need to encode the message. We need to replace the carriage return // character by the 2 character string \n. // Because of this the \ character needs to be escaped as well. // ReplaceAll() expects regexp so \ char are escaped twice. String message = mSmsMessage.getText(); message = message.replaceAll("\\\\", //$NON-NLS-1$ "\\\\\\\\"); //$NON-NLS-1$ // While the normal line delimiter is returned by Text.getLineDelimiter() // it seems copy pasting text coming from somewhere else could have another // delimited. For this reason, we'll replace is several steps // replace the dual CR-LF message = message.replaceAll("\r\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$ // replace remaining stand alone \n message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$ // replace remaining stand alone \r message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$ processCommandResult(mEmulatorConsole.sendSms(mPhoneNumber.getText().trim(), message)); } } } }); mCancelButton = new Button(g2ButtonComp, SWT.PUSH); mCancelButton.setText("Hang Up"); mCancelButton.setEnabled(false); mCancelButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mEmulatorConsole != null) { if (mVoiceButton.getSelection()) { processCommandResult(mEmulatorConsole.cancelCall( mPhoneNumber.getText().trim())); } } } }); } /** * Create Location controls. * @param top */ private void createLocationControls(final Composite top) { Label l = new Label(top, SWT.NONE); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); l.setText("Location Controls"); mLocationFolders = new TabFolder(top, SWT.NONE); mLocationFolders.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Composite manualLocationComp = new Composite(mLocationFolders, SWT.NONE); TabItem item = new TabItem(mLocationFolders, SWT.NONE); item.setText("Manual"); item.setControl(manualLocationComp); createManualLocationControl(manualLocationComp); ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mPlayImage = loader.loadImage("play.png", mParent.getDisplay()); //$NON-NLS-1$ mPauseImage = loader.loadImage("pause.png", mParent.getDisplay()); //$NON-NLS-1$ Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE); item = new TabItem(mLocationFolders, SWT.NONE); item.setText("GPX"); item.setControl(gpxLocationComp); createGpxLocationControl(gpxLocationComp); Composite kmlLocationComp = new Composite(mLocationFolders, SWT.NONE); kmlLocationComp.setLayout(new FillLayout()); item = new TabItem(mLocationFolders, SWT.NONE); item.setText("KML"); item.setControl(kmlLocationComp); createKmlLocationControl(kmlLocationComp); } private void createManualLocationControl(Composite manualLocationComp) { final StackLayout sl; GridLayout gl; Label label; manualLocationComp.setLayout(new GridLayout(1, false)); mDecimalButton = new Button(manualLocationComp, SWT.RADIO); mDecimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mDecimalButton.setText("Decimal"); mSexagesimalButton = new Button(manualLocationComp, SWT.RADIO); mSexagesimalButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mSexagesimalButton.setText("Sexagesimal"); // composite to hold and switching between the 2 modes. final Composite content = new Composite(manualLocationComp, SWT.NONE); content.setLayout(sl = new StackLayout()); // decimal display final Composite decimalContent = new Composite(content, SWT.NONE); decimalContent.setLayout(gl = new GridLayout(2, false)); gl.marginHeight = gl.marginWidth = 0; mLongitudeControls = new CoordinateControls(); mLatitudeControls = new CoordinateControls(); label = new Label(decimalContent, SWT.NONE); label.setText("Longitude"); mLongitudeControls.createDecimalText(decimalContent); label = new Label(decimalContent, SWT.NONE); label.setText("Latitude"); mLatitudeControls.createDecimalText(decimalContent); // sexagesimal content final Composite sexagesimalContent = new Composite(content, SWT.NONE); sexagesimalContent.setLayout(gl = new GridLayout(7, false)); gl.marginHeight = gl.marginWidth = 0; label = new Label(sexagesimalContent, SWT.NONE); label.setText("Longitude"); mLongitudeControls.createSexagesimalDegreeText(sexagesimalContent); label = new Label(sexagesimalContent, SWT.NONE); label.setText("\u00B0"); // degree character mLongitudeControls.createSexagesimalMinuteText(sexagesimalContent); label = new Label(sexagesimalContent, SWT.NONE); label.setText("'"); mLongitudeControls.createSexagesimalSecondText(sexagesimalContent); label = new Label(sexagesimalContent, SWT.NONE); label.setText("\""); label = new Label(sexagesimalContent, SWT.NONE); label.setText("Latitude"); mLatitudeControls.createSexagesimalDegreeText(sexagesimalContent); label = new Label(sexagesimalContent, SWT.NONE); label.setText("\u00B0"); mLatitudeControls.createSexagesimalMinuteText(sexagesimalContent); label = new Label(sexagesimalContent, SWT.NONE); label.setText("'"); mLatitudeControls.createSexagesimalSecondText(sexagesimalContent); label = new Label(sexagesimalContent, SWT.NONE); label.setText("\""); // set the default display to decimal sl.topControl = decimalContent; mDecimalButton.setSelection(true); mDecimalButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mDecimalButton.getSelection()) { sl.topControl = decimalContent; } else { sl.topControl = sexagesimalContent; } content.layout(); } }); Button sendButton = new Button(manualLocationComp, SWT.PUSH); sendButton.setText("Send"); sendButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mEmulatorConsole != null) { processCommandResult(mEmulatorConsole.sendLocation( mLongitudeControls.getValue(), mLatitudeControls.getValue(), 0)); } } }); mLongitudeControls.setValue(DEFAULT_LONGITUDE); mLatitudeControls.setValue(DEFAULT_LATITUDE); } private void createGpxLocationControl(Composite gpxLocationComp) { GridData gd; IPreferenceStore store = DdmUiPreferences.getStore(); gpxLocationComp.setLayout(new GridLayout(1, false)); mGpxUploadButton = new Button(gpxLocationComp, SWT.PUSH); mGpxUploadButton.setText("Load GPX..."); // Table for way point mGpxWayPointTable = new Table(gpxLocationComp, SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); mGpxWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.heightHint = 100; mGpxWayPointTable.setHeaderVisible(true); mGpxWayPointTable.setLinesVisible(true); TableHelper.createTableColumn(mGpxWayPointTable, "Name", SWT.LEFT, "Some Name", PREFS_WAYPOINT_COL_NAME, store); TableHelper.createTableColumn(mGpxWayPointTable, "Longitude", SWT.LEFT, "-199.999999", PREFS_WAYPOINT_COL_LONGITUDE, store); TableHelper.createTableColumn(mGpxWayPointTable, "Latitude", SWT.LEFT, "-199.999999", PREFS_WAYPOINT_COL_LATITUDE, store); TableHelper.createTableColumn(mGpxWayPointTable, "Elevation", SWT.LEFT, "99999.9", PREFS_WAYPOINT_COL_ELEVATION, store); TableHelper.createTableColumn(mGpxWayPointTable, "Description", SWT.LEFT, "Some Description", PREFS_WAYPOINT_COL_DESCRIPTION, store); final TableViewer gpxWayPointViewer = new TableViewer(mGpxWayPointTable); gpxWayPointViewer.setContentProvider(new WayPointContentProvider()); gpxWayPointViewer.setLabelProvider(new WayPointLabelProvider()); gpxWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { ISelection selection = event.getSelection(); if (selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object selectedObject = structuredSelection.getFirstElement(); if (selectedObject instanceof WayPoint) { WayPoint wayPoint = (WayPoint)selectedObject; if (mEmulatorConsole != null && mPlayingTrack == false) { processCommandResult(mEmulatorConsole.sendLocation( wayPoint.getLongitude(), wayPoint.getLatitude(), wayPoint.getElevation())); } } } } }); // table for tracks. mGpxTrackTable = new Table(gpxLocationComp, SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); mGpxTrackTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.heightHint = 100; mGpxTrackTable.setHeaderVisible(true); mGpxTrackTable.setLinesVisible(true); TableHelper.createTableColumn(mGpxTrackTable, "Name", SWT.LEFT, "Some very long name", PREFS_TRACK_COL_NAME, store); TableHelper.createTableColumn(mGpxTrackTable, "Point Count", SWT.RIGHT, "9999", PREFS_TRACK_COL_COUNT, store); TableHelper.createTableColumn(mGpxTrackTable, "First Point Time", SWT.LEFT, "999-99-99T99:99:99Z", PREFS_TRACK_COL_FIRST, store); TableHelper.createTableColumn(mGpxTrackTable, "Last Point Time", SWT.LEFT, "999-99-99T99:99:99Z", PREFS_TRACK_COL_LAST, store); TableHelper.createTableColumn(mGpxTrackTable, "Comment", SWT.LEFT, "-199.999999", PREFS_TRACK_COL_COMMENT, store); final TableViewer gpxTrackViewer = new TableViewer(mGpxTrackTable); gpxTrackViewer.setContentProvider(new TrackContentProvider()); gpxTrackViewer.setLabelProvider(new TrackLabelProvider()); gpxTrackViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { ISelection selection = event.getSelection(); if (selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object selectedObject = structuredSelection.getFirstElement(); if (selectedObject instanceof Track) { Track track = (Track)selectedObject; if (mEmulatorConsole != null && mPlayingTrack == false) { TrackPoint[] points = track.getPoints(); processCommandResult(mEmulatorConsole.sendLocation( points[0].getLongitude(), points[0].getLatitude(), points[0].getElevation())); } mPlayGpxButton.setEnabled(true); mGpxBackwardButton.setEnabled(true); mGpxForwardButton.setEnabled(true); mGpxSpeedButton.setEnabled(true); return; } } mPlayGpxButton.setEnabled(false); mGpxBackwardButton.setEnabled(false); mGpxForwardButton.setEnabled(false); mGpxSpeedButton.setEnabled(false); } }); mGpxUploadButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); fileDialog.setText("Load GPX File"); fileDialog.setFilterExtensions(new String[] { "*.gpx" } ); String fileName = fileDialog.open(); if (fileName != null) { GpxParser parser = new GpxParser(fileName); if (parser.parse()) { gpxWayPointViewer.setInput(parser.getWayPoints()); gpxTrackViewer.setInput(parser.getTracks()); } } } }); mGpxPlayControls = new Composite(gpxLocationComp, SWT.NONE); GridLayout gl; mGpxPlayControls.setLayout(gl = new GridLayout(5, false)); gl.marginHeight = gl.marginWidth = 0; mGpxPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mPlayGpxButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); mPlayGpxButton.setImage(mPlayImage); mPlayGpxButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mPlayingTrack == false) { ISelection selection = gpxTrackViewer.getSelection(); if (selection.isEmpty() == false && selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object selectedObject = structuredSelection.getFirstElement(); if (selectedObject instanceof Track) { Track track = (Track)selectedObject; playTrack(track); } } } else { // if we're playing, then we pause mPlayingTrack = false; if (mPlayingThread != null) { mPlayingThread.interrupt(); } } } }); Label separator = new Label(mGpxPlayControls, SWT.SEPARATOR | SWT.VERTICAL); separator.setLayoutData(gd = new GridData( GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); gd.heightHint = 0; ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); mGpxBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$ mGpxBackwardButton.setSelection(false); mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); mGpxForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$ mGpxForwardButton.setSelection(true); mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter); mGpxSpeedButton = new Button(mGpxPlayControls, SWT.PUSH | SWT.FLAT); mSpeedIndex = 0; mSpeed = PLAY_SPEEDS[mSpeedIndex]; mGpxSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); mGpxSpeedButton.addSelectionListener(mSpeedButtonAdapter); mPlayGpxButton.setEnabled(false); mGpxBackwardButton.setEnabled(false); mGpxForwardButton.setEnabled(false); mGpxSpeedButton.setEnabled(false); } private void createKmlLocationControl(Composite kmlLocationComp) { GridData gd; IPreferenceStore store = DdmUiPreferences.getStore(); kmlLocationComp.setLayout(new GridLayout(1, false)); mKmlUploadButton = new Button(kmlLocationComp, SWT.PUSH); mKmlUploadButton.setText("Load KML..."); // Table for way point mKmlWayPointTable = new Table(kmlLocationComp, SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION); mKmlWayPointTable.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.heightHint = 200; mKmlWayPointTable.setHeaderVisible(true); mKmlWayPointTable.setLinesVisible(true); TableHelper.createTableColumn(mKmlWayPointTable, "Name", SWT.LEFT, "Some Name", PREFS_WAYPOINT_COL_NAME, store); TableHelper.createTableColumn(mKmlWayPointTable, "Longitude", SWT.LEFT, "-199.999999", PREFS_WAYPOINT_COL_LONGITUDE, store); TableHelper.createTableColumn(mKmlWayPointTable, "Latitude", SWT.LEFT, "-199.999999", PREFS_WAYPOINT_COL_LATITUDE, store); TableHelper.createTableColumn(mKmlWayPointTable, "Elevation", SWT.LEFT, "99999.9", PREFS_WAYPOINT_COL_ELEVATION, store); TableHelper.createTableColumn(mKmlWayPointTable, "Description", SWT.LEFT, "Some Description", PREFS_WAYPOINT_COL_DESCRIPTION, store); final TableViewer kmlWayPointViewer = new TableViewer(mKmlWayPointTable); kmlWayPointViewer.setContentProvider(new WayPointContentProvider()); kmlWayPointViewer.setLabelProvider(new WayPointLabelProvider()); mKmlUploadButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); fileDialog.setText("Load KML File"); fileDialog.setFilterExtensions(new String[] { "*.kml" } ); String fileName = fileDialog.open(); if (fileName != null) { KmlParser parser = new KmlParser(fileName); if (parser.parse()) { kmlWayPointViewer.setInput(parser.getWayPoints()); mPlayKmlButton.setEnabled(true); mKmlBackwardButton.setEnabled(true); mKmlForwardButton.setEnabled(true); mKmlSpeedButton.setEnabled(true); } } } }); kmlWayPointViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { ISelection selection = event.getSelection(); if (selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object selectedObject = structuredSelection.getFirstElement(); if (selectedObject instanceof WayPoint) { WayPoint wayPoint = (WayPoint)selectedObject; if (mEmulatorConsole != null && mPlayingTrack == false) { processCommandResult(mEmulatorConsole.sendLocation( wayPoint.getLongitude(), wayPoint.getLatitude(), wayPoint.getElevation())); } } } } }); mKmlPlayControls = new Composite(kmlLocationComp, SWT.NONE); GridLayout gl; mKmlPlayControls.setLayout(gl = new GridLayout(5, false)); gl.marginHeight = gl.marginWidth = 0; mKmlPlayControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mPlayKmlButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); mPlayKmlButton.setImage(mPlayImage); mPlayKmlButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mPlayingTrack == false) { Object input = kmlWayPointViewer.getInput(); if (input instanceof WayPoint[]) { playKml((WayPoint[])input); } } else { // if we're playing, then we pause mPlayingTrack = false; if (mPlayingThread != null) { mPlayingThread.interrupt(); } } } }); Label separator = new Label(mKmlPlayControls, SWT.SEPARATOR | SWT.VERTICAL); separator.setLayoutData(gd = new GridData( GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); gd.heightHint = 0; ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); mKmlBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$ mKmlBackwardButton.setSelection(false); mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); mKmlForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$ mKmlForwardButton.setSelection(true); mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter); mKmlSpeedButton = new Button(mKmlPlayControls, SWT.PUSH | SWT.FLAT); mSpeedIndex = 0; mSpeed = PLAY_SPEEDS[mSpeedIndex]; mKmlSpeedButton.setText(String.format(SPEED_FORMAT, mSpeed)); mKmlSpeedButton.addSelectionListener(mSpeedButtonAdapter); mPlayKmlButton.setEnabled(false); mKmlBackwardButton.setEnabled(false); mKmlForwardButton.setEnabled(false); mKmlSpeedButton.setEnabled(false); } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { } @Override protected void postCreation() { // pass } private synchronized void setDataMode(int selectionIndex) { if (mEmulatorConsole != null) { processCommandResult(mEmulatorConsole.setGsmDataMode( GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); } } private synchronized void setVoiceMode(int selectionIndex) { if (mEmulatorConsole != null) { processCommandResult(mEmulatorConsole.setGsmVoiceMode( GsmMode.getEnum(GSM_MODES[selectionIndex][1]))); } } private synchronized void setNetworkLatency(int selectionIndex) { if (mEmulatorConsole != null) { processCommandResult(mEmulatorConsole.setNetworkLatency(selectionIndex)); } } private synchronized void setNetworkSpeed(int selectionIndex) { if (mEmulatorConsole != null) { processCommandResult(mEmulatorConsole.setNetworkSpeed(selectionIndex)); } } /** * Callback on device selection change. * @param device the new selected device */ public void handleNewDevice(IDevice device) { if (mParent.isDisposed()) { return; } // unlink to previous console. synchronized (this) { mEmulatorConsole = null; } try { // get the emulator console for this device // First we need the device itself if (device != null) { GsmStatus gsm = null; NetworkStatus netstatus = null; synchronized (this) { mEmulatorConsole = EmulatorConsole.getConsole(device); if (mEmulatorConsole != null) { // get the gsm status gsm = mEmulatorConsole.getGsmStatus(); netstatus = mEmulatorConsole.getNetworkStatus(); if (gsm == null || netstatus == null) { mEmulatorConsole = null; } } } if (gsm != null && netstatus != null) { Display d = mParent.getDisplay(); if (d.isDisposed() == false) { final GsmStatus f_gsm = gsm; final NetworkStatus f_netstatus = netstatus; d.asyncExec(new Runnable() { @Override public void run() { if (f_gsm.voice != GsmMode.UNKNOWN) { mVoiceMode.select(getGsmComboIndex(f_gsm.voice)); } else { mVoiceMode.clearSelection(); } if (f_gsm.data != GsmMode.UNKNOWN) { mDataMode.select(getGsmComboIndex(f_gsm.data)); } else { mDataMode.clearSelection(); } if (f_netstatus.speed != -1) { mNetworkSpeed.select(f_netstatus.speed); } else { mNetworkSpeed.clearSelection(); } if (f_netstatus.latency != -1) { mNetworkLatency.select(f_netstatus.latency); } else { mNetworkLatency.clearSelection(); } } }); } } } } finally { // enable/disable the ui boolean enable = false; synchronized (this) { enable = mEmulatorConsole != null; } enable(enable); } } /** * Enable or disable the ui. Can be called from non ui threads. * @param enabled */ private void enable(final boolean enabled) { try { Display d = mParent.getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { if (mParent.isDisposed() == false) { doEnable(enabled); } } }); } catch (SWTException e) { // disposed. do nothing } } private boolean isValidPhoneNumber() { String number = mPhoneNumber.getText().trim(); return number.matches(RE_PHONE_NUMBER); } /** * Enable or disable the ui. Cannot be called from non ui threads. * @param enabled */ protected void doEnable(boolean enabled) { mVoiceLabel.setEnabled(enabled); mVoiceMode.setEnabled(enabled); mDataLabel.setEnabled(enabled); mDataMode.setEnabled(enabled); mSpeedLabel.setEnabled(enabled); mNetworkSpeed.setEnabled(enabled); mLatencyLabel.setEnabled(enabled); mNetworkLatency.setEnabled(enabled); // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it // if we don't need to. if (mPhoneNumber.isEnabled() != enabled) { mNumberLabel.setEnabled(enabled); mPhoneNumber.setEnabled(enabled); } boolean valid = isValidPhoneNumber(); mVoiceButton.setEnabled(enabled && valid); mSmsButton.setEnabled(enabled && valid); boolean smsValid = enabled && valid && mSmsButton.getSelection(); // Calling setEnabled on a text field will trigger a modifyText event, so we don't do it // if we don't need to. if (mSmsMessage.isEnabled() != smsValid) { mMessageLabel.setEnabled(smsValid); mSmsMessage.setEnabled(smsValid); } if (enabled == false) { mSmsMessage.setText(""); //$NON-NLs-1$ } mCallButton.setEnabled(enabled && valid); mCancelButton.setEnabled(enabled && valid && mVoiceButton.getSelection()); if (enabled == false) { mVoiceMode.clearSelection(); mDataMode.clearSelection(); mNetworkSpeed.clearSelection(); mNetworkLatency.clearSelection(); if (mPhoneNumber.getText().length() > 0) { mPhoneNumber.setText(""); //$NON-NLS-1$ } } // location controls mLocationFolders.setEnabled(enabled); mDecimalButton.setEnabled(enabled); mSexagesimalButton.setEnabled(enabled); mLongitudeControls.setEnabled(enabled); mLatitudeControls.setEnabled(enabled); mGpxUploadButton.setEnabled(enabled); mGpxWayPointTable.setEnabled(enabled); mGpxTrackTable.setEnabled(enabled); mKmlUploadButton.setEnabled(enabled); mKmlWayPointTable.setEnabled(enabled); } /** * Returns the index of the combo item matching a specific GsmMode. * @param mode */ private int getGsmComboIndex(GsmMode mode) { for (int i = 0 ; i < GSM_MODES.length; i++) { String[] modes = GSM_MODES[i]; if (mode.getTag().equals(modes[1])) { return i; } } return -1; } /** * Processes the result of a command sent to the console. * @param result the result of the command. */ private boolean processCommandResult(final String result) { if (result != EmulatorConsole.RESULT_OK) { try { mParent.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (mParent.isDisposed() == false) { MessageDialog.openError(mParent.getShell(), "Emulator Console", result); } } }); } catch (SWTException e) { // we're quitting, just ignore } return false; } return true; } /** * @param track */ private void playTrack(final Track track) { // no need to synchronize this check, the worst that can happen, is we start the thread // for nothing. if (mEmulatorConsole != null) { mPlayGpxButton.setImage(mPauseImage); mPlayKmlButton.setImage(mPauseImage); mPlayingTrack = true; mPlayingThread = new Thread() { @Override public void run() { try { TrackPoint[] trackPoints = track.getPoints(); int count = trackPoints.length; // get the start index. int start = 0; if (mPlayDirection == -1) { start = count - 1; } for (int p = start; p >= 0 && p < count; p += mPlayDirection) { if (mPlayingTrack == false) { return; } // get the current point and send its location to // the emulator. final TrackPoint trackPoint = trackPoints[p]; synchronized (EmulatorControlPanel.this) { if (mEmulatorConsole == null || processCommandResult(mEmulatorConsole.sendLocation( trackPoint.getLongitude(), trackPoint.getLatitude(), trackPoint.getElevation())) == false) { return; } } // if this is not the final point, then get the next one and // compute the delta time int nextIndex = p + mPlayDirection; if (nextIndex >=0 && nextIndex < count) { TrackPoint nextPoint = trackPoints[nextIndex]; long delta = nextPoint.getTime() - trackPoint.getTime(); if (delta < 0) { delta = -delta; } long startTime = System.currentTimeMillis(); try { sleep(delta / mSpeed); } catch (InterruptedException e) { if (mPlayingTrack == false) { return; } // we got interrupted, lets make sure we can play do { long waited = System.currentTimeMillis() - startTime; long needToWait = delta / mSpeed; if (waited < needToWait) { try { sleep(needToWait - waited); } catch (InterruptedException e1) { // we'll just loop and wait again if needed. // unless we're supposed to stop if (mPlayingTrack == false) { return; } } } else { break; } } while (true); } } } } finally { mPlayingTrack = false; try { mParent.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (mPlayGpxButton.isDisposed() == false) { mPlayGpxButton.setImage(mPlayImage); mPlayKmlButton.setImage(mPlayImage); } } }); } catch (SWTException e) { // we're quitting, just ignore } } } }; mPlayingThread.start(); } } private void playKml(final WayPoint[] trackPoints) { // no need to synchronize this check, the worst that can happen, is we start the thread // for nothing. if (mEmulatorConsole != null) { mPlayGpxButton.setImage(mPauseImage); mPlayKmlButton.setImage(mPauseImage); mPlayingTrack = true; mPlayingThread = new Thread() { @Override public void run() { try { int count = trackPoints.length; // get the start index. int start = 0; if (mPlayDirection == -1) { start = count - 1; } for (int p = start; p >= 0 && p < count; p += mPlayDirection) { if (mPlayingTrack == false) { return; } // get the current point and send its location to // the emulator. WayPoint trackPoint = trackPoints[p]; synchronized (EmulatorControlPanel.this) { if (mEmulatorConsole == null || processCommandResult(mEmulatorConsole.sendLocation( trackPoint.getLongitude(), trackPoint.getLatitude(), trackPoint.getElevation())) == false) { return; } } // if this is not the final point, then get the next one and // compute the delta time int nextIndex = p + mPlayDirection; if (nextIndex >=0 && nextIndex < count) { long delta = 1000; // 1 second if (delta < 0) { delta = -delta; } long startTime = System.currentTimeMillis(); try { sleep(delta / mSpeed); } catch (InterruptedException e) { if (mPlayingTrack == false) { return; } // we got interrupted, lets make sure we can play do { long waited = System.currentTimeMillis() - startTime; long needToWait = delta / mSpeed; if (waited < needToWait) { try { sleep(needToWait - waited); } catch (InterruptedException e1) { // we'll just loop and wait again if needed. // unless we're supposed to stop if (mPlayingTrack == false) { return; } } } else { break; } } while (true); } } } } finally { mPlayingTrack = false; try { mParent.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (mPlayGpxButton.isDisposed() == false) { mPlayGpxButton.setImage(mPlayImage); mPlayKmlButton.setImage(mPlayImage); } } }); } catch (SWTException e) { // we're quitting, just ignore } } } }; mPlayingThread.start(); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java0100644 0000000 0000000 00000012306 12747325007 023624 0ustar000000000 0000000 /* * 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.ddmuilib; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; /** * {@link FindDialog} provides a text box where users can enter text that should be * searched for in the target editor/view. The buttons "Find Previous" and "Find Next" * allow users to search forwards/backwards. This dialog simply provides a front end for the user * and the actual task of searching is delegated to the {@link IFindTarget}. */ public class FindDialog extends Dialog { private Label mStatusLabel; private Button mFindNext; private Button mFindPrevious; private final IFindTarget mTarget; private Text mSearchText; private String mPreviousSearchText; private final int mDefaultButtonId; /** Id of the "Find Next" button */ public static final int FIND_NEXT_ID = IDialogConstants.CLIENT_ID; /** Id of the "Find Previous button */ public static final int FIND_PREVIOUS_ID = IDialogConstants.CLIENT_ID + 1; public FindDialog(Shell shell, IFindTarget target) { this(shell, target, FIND_PREVIOUS_ID); } /** * Construct a find dialog. * @param shell shell to use * @param target delegate to be invoked on user action * @param defaultButtonId one of {@code #FIND_NEXT_ID} or {@code #FIND_PREVIOUS_ID}. */ public FindDialog(Shell shell, IFindTarget target, int defaultButtonId) { super(shell); mTarget = target; mDefaultButtonId = defaultButtonId; setShellStyle((getShellStyle() & ~SWT.APPLICATION_MODAL) | SWT.MODELESS); setBlockOnOpen(true); } @Override protected Control createDialogArea(Composite parent) { Composite panel = new Composite(parent, SWT.NONE); panel.setLayout(new GridLayout(2, false)); panel.setLayoutData(new GridData(GridData.FILL_BOTH)); Label lblMessage = new Label(panel, SWT.NONE); lblMessage.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); lblMessage.setText("Find:"); mSearchText = new Text(panel, SWT.BORDER); mSearchText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); mSearchText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { boolean hasText = !mSearchText.getText().trim().isEmpty(); mFindNext.setEnabled(hasText); mFindPrevious.setEnabled(hasText); } }); mStatusLabel = new Label(panel, SWT.NONE); mStatusLabel.setForeground(getShell().getDisplay().getSystemColor(SWT.COLOR_DARK_RED)); GridData gd = new GridData(); gd.horizontalSpan = 2; gd.grabExcessHorizontalSpace = true; mStatusLabel.setLayoutData(gd); return panel; } @Override protected void createButtonsForButtonBar(Composite parent) { createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, false); mFindNext = createButton(parent, FIND_NEXT_ID, "Find Next", mDefaultButtonId == FIND_NEXT_ID); mFindPrevious = createButton(parent, FIND_PREVIOUS_ID, "Find Previous", mDefaultButtonId != FIND_NEXT_ID); mFindNext.setEnabled(false); mFindPrevious.setEnabled(false); } @Override protected void buttonPressed(int buttonId) { if (buttonId == IDialogConstants.CLOSE_ID) { close(); return; } if (buttonId == FIND_PREVIOUS_ID || buttonId == FIND_NEXT_ID) { if (mTarget != null) { String searchText = mSearchText.getText(); boolean newSearch = !searchText.equals(mPreviousSearchText); mPreviousSearchText = searchText; boolean searchForward = buttonId == FIND_NEXT_ID; boolean hasMatches = mTarget.findAndSelect(searchText, newSearch, searchForward); if (!hasMatches) { mStatusLabel.setText("String not found"); mStatusLabel.pack(); } else { mStatusLabel.setText(""); } } } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java0100644 0000000 0000000 00000132741 12747325007 023467 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.HeapSegment.HeapSegmentElement; import com.android.ddmlib.Log; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.labels.CategoryToolTipGenerator; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.category.CategoryItemRenderer; import org.jfree.chart.title.TextTitle; import org.jfree.data.category.CategoryDataset; import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.experimental.chart.swt.ChartComposite; import org.jfree.experimental.swt.SWTUtils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * Base class for our information panels. */ public final class HeapPanel extends BaseHeapPanel { private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$ private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$ private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$ private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$ private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$ private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$ private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$ /* args to setUpdateStatus() */ private static final int NOT_SELECTED = 0; private static final int NOT_ENABLED = 1; private static final int ENABLED = 2; /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need * Native+1 at least. We also need 2 more entries for free area and expansion area. */ private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; private static final PaletteData mMapPalette = createPalette(); private static final boolean DISPLAY_HEAP_BITMAP = false; private static final boolean DISPLAY_HILBERT_BITMAP = false; private static final int PLACEHOLDER_HILBERT_SIZE = 200; private static final int PLACEHOLDER_LINEAR_V_SIZE = 100; private static final int PLACEHOLDER_LINEAR_H_SIZE = 300; private static final int[] ZOOMS = {100, 50, 25}; private static final NumberFormat sByteFormatter = NumberFormat.getInstance(); private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance(); private static final NumberFormat sCountFormatter = NumberFormat.getInstance(); static { sByteFormatter.setMinimumFractionDigits(0); sByteFormatter.setMaximumFractionDigits(1); sLargeByteFormatter.setMinimumFractionDigits(3); sLargeByteFormatter.setMaximumFractionDigits(3); sCountFormatter.setGroupingUsed(true); } private Display mDisplay; private Composite mTop; // real top private Label mUpdateStatus; private Table mHeapSummary; private Combo mDisplayMode; //private ScrolledComposite mScrolledComposite; private Composite mDisplayBase; // base of the displays. private StackLayout mDisplayStack; private Composite mStatisticsBase; private Table mStatisticsTable; private JFreeChart mChart; private ChartComposite mChartComposite; private Button mGcButton; private DefaultCategoryDataset mAllocCountDataSet; private Composite mLinearBase; private Label mLinearHeapImage; private Composite mHilbertBase; private Label mHilbertHeapImage; private Group mLegend; private Combo mZoom; /** Image used for the hilbert display. Since we recreate a new image every time, we * keep this one around to dispose it. */ private Image mHilbertImage; private Image mLinearImage; private Composite[] mLayout; /* * Create color palette for map. Set up titles for legend. */ private static PaletteData createPalette() { RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; colors[0] = new RGB(192, 192, 192); // non-heap pixels are gray mMapLegend[0] = "(heap expansion area)"; colors[1] = new RGB(0, 0, 0); // free chunks are black mMapLegend[1] = "free"; colors[HeapSegmentElement.KIND_OBJECT + 2] = new RGB(0, 0, 255); // objects are blue mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] = "data object"; colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] = new RGB(0, 255, 0); // class objects are green mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] = "class object"; colors[HeapSegmentElement.KIND_ARRAY_1 + 2] = new RGB(255, 0, 0); // byte/bool arrays are red mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] = "1-byte array (byte[], boolean[])"; colors[HeapSegmentElement.KIND_ARRAY_2 + 2] = new RGB(255, 128, 0); // short/char arrays are orange mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] = "2-byte array (short[], char[])"; colors[HeapSegmentElement.KIND_ARRAY_4 + 2] = new RGB(255, 255, 0); // obj/int/float arrays are yellow mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] = "4-byte array (object[], int[], float[])"; colors[HeapSegmentElement.KIND_ARRAY_8 + 2] = new RGB(255, 128, 128); // long/double arrays are pink mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] = "8-byte array (long[], double[])"; colors[HeapSegmentElement.KIND_UNKNOWN + 2] = new RGB(255, 0, 255); // unknown objects are cyan mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] = "unknown object"; colors[HeapSegmentElement.KIND_NATIVE + 2] = new RGB(64, 64, 64); // native objects are dark gray mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] = "non-Java object"; return new PaletteData(colors); } /** * Sent when an existing client information changed. *

* This is sent from a non UI thread. * @param client the updated client. * @param changeMask the bit mask describing the changed properties. It can contain * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} * * @see IClientChangeListener#clientChanged(Client, int) */ @Override public void clientChanged(final Client client, int changeMask) { if (client == getCurrentClient()) { if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE || (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) { try { mTop.getDisplay().asyncExec(new Runnable() { @Override public void run() { clientSelected(); } }); } catch (SWTException e) { // display is disposed (app is quitting most likely), we do nothing. } } } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()} */ @Override public void deviceSelected() { // pass } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()}. */ @Override public void clientSelected() { if (mTop.isDisposed()) return; Client client = getCurrentClient(); Log.d("ddms", "HeapPanel: changed " + client); if (client != null) { ClientData cd = client.getClientData(); if (client.isHeapUpdateEnabled()) { mGcButton.setEnabled(true); mDisplayMode.setEnabled(true); setUpdateStatus(ENABLED); } else { setUpdateStatus(NOT_ENABLED); mGcButton.setEnabled(false); mDisplayMode.setEnabled(false); } fillSummaryTable(cd); int mode = mDisplayMode.getSelectionIndex(); if (mode == 0) { fillDetailedTable(client, false /* forceRedraw */); } else { if (DISPLAY_HEAP_BITMAP) { renderHeapData(cd, mode - 1, false /* forceRedraw */); } } } else { mGcButton.setEnabled(false); mDisplayMode.setEnabled(false); fillSummaryTable(null); fillDetailedTable(null, true); setUpdateStatus(NOT_SELECTED); } // sizes of things change frequently, so redo layout //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, // SWT.DEFAULT)); mDisplayBase.layout(); //mScrolledComposite.redraw(); } /** * Create our control(s). */ @Override protected Control createControl(Composite parent) { mDisplay = parent.getDisplay(); GridLayout gl; mTop = new Composite(parent, SWT.NONE); mTop.setLayout(new GridLayout(1, false)); mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); mUpdateStatus = new Label(mTop, SWT.NONE); setUpdateStatus(NOT_SELECTED); Composite summarySection = new Composite(mTop, SWT.NONE); summarySection.setLayout(gl = new GridLayout(2, false)); gl.marginHeight = gl.marginWidth = 0; mHeapSummary = createSummaryTable(summarySection); mGcButton = new Button(summarySection, SWT.PUSH); mGcButton.setText("Cause GC"); mGcButton.setEnabled(false); mGcButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Client client = getCurrentClient(); if (client != null) { client.executeGarbageCollector(); } } }); Composite comboSection = new Composite(mTop, SWT.NONE); gl = new GridLayout(2, false); gl.marginHeight = gl.marginWidth = 0; comboSection.setLayout(gl); Label displayLabel = new Label(comboSection, SWT.NONE); displayLabel.setText("Display: "); mDisplayMode = new Combo(comboSection, SWT.READ_ONLY); mDisplayMode.setEnabled(false); mDisplayMode.add("Stats"); if (DISPLAY_HEAP_BITMAP) { mDisplayMode.add("Linear"); if (DISPLAY_HILBERT_BITMAP) { mDisplayMode.add("Hilbert"); } } // the base of the displays. mDisplayBase = new Composite(mTop, SWT.NONE); mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH)); mDisplayStack = new StackLayout(); mDisplayBase.setLayout(mDisplayStack); // create the statistics display mStatisticsBase = new Composite(mDisplayBase, SWT.NONE); //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH)); mStatisticsBase.setLayout(gl = new GridLayout(1, false)); gl.marginHeight = gl.marginWidth = 0; mDisplayStack.topControl = mStatisticsBase; mStatisticsTable = createDetailedTable(mStatisticsBase); mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH)); createChart(); //create the linear composite mLinearBase = new Composite(mDisplayBase, SWT.NONE); //mLinearBase.setLayoutData(new GridData()); gl = new GridLayout(1, false); gl.marginHeight = gl.marginWidth = 0; mLinearBase.setLayout(gl); { mLinearHeapImage = new Label(mLinearBase, SWT.NONE); mLinearHeapImage.setLayoutData(new GridData()); mLinearHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay, PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, mDisplay.getSystemColor(SWT.COLOR_BLUE))); // create a composite to contain the bottom part (legend) Composite bottomSection = new Composite(mLinearBase, SWT.NONE); gl = new GridLayout(1, false); gl.marginHeight = gl.marginWidth = 0; bottomSection.setLayout(gl); createLegend(bottomSection); } /* mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL); mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); mScrolledComposite.setExpandHorizontal(true); mScrolledComposite.setExpandVertical(true); mScrolledComposite.setContent(mDisplayBase); */ // create the hilbert display. mHilbertBase = new Composite(mDisplayBase, SWT.NONE); //mHilbertBase.setLayoutData(new GridData()); gl = new GridLayout(2, false); gl.marginHeight = gl.marginWidth = 0; mHilbertBase.setLayout(gl); if (DISPLAY_HILBERT_BITMAP) { mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE); mHilbertHeapImage.setLayoutData(new GridData()); mHilbertHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay, PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, mDisplay.getSystemColor(SWT.COLOR_BLUE))); // create a composite to contain the right part (legend + zoom) Composite rightSection = new Composite(mHilbertBase, SWT.NONE); gl = new GridLayout(1, false); gl.marginHeight = gl.marginWidth = 0; rightSection.setLayout(gl); Composite zoomComposite = new Composite(rightSection, SWT.NONE); gl = new GridLayout(2, false); zoomComposite.setLayout(gl); Label l = new Label(zoomComposite, SWT.NONE); l.setText("Zoom:"); mZoom = new Combo(zoomComposite, SWT.READ_ONLY); for (int z : ZOOMS) { mZoom.add(String.format("%1$d%%", z)); //$NON-NLS-1$ } mZoom.select(0); mZoom.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { setLegendText(mZoom.getSelectionIndex()); Client client = getCurrentClient(); if (client != null) { renderHeapData(client.getClientData(), 1, true); mTop.pack(); } } }); createLegend(rightSection); } mHilbertBase.pack(); mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase }; mDisplayMode.select(0); mDisplayMode.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { int index = mDisplayMode.getSelectionIndex(); Client client = getCurrentClient(); if (client != null) { if (index == 0) { fillDetailedTable(client, true /* forceRedraw */); } else { renderHeapData(client.getClientData(), index-1, true /* forceRedraw */); } } mDisplayStack.topControl = mLayout[index]; //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, // SWT.DEFAULT)); mDisplayBase.layout(); //mScrolledComposite.redraw(); } }); //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, // SWT.DEFAULT)); mDisplayBase.layout(); //mScrolledComposite.redraw(); return mTop; } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mHeapSummary.setFocus(); } private Table createSummaryTable(Composite base) { Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); tab.setHeaderVisible(true); tab.setLinesVisible(true); TableColumn col; col = new TableColumn(tab, SWT.RIGHT); col.setText("ID"); col.pack(); col = new TableColumn(tab, SWT.RIGHT); col.setText("000.000WW"); //$NON-NLS-1$ col.pack(); col.setText("Heap Size"); col = new TableColumn(tab, SWT.RIGHT); col.setText("000.000WW"); //$NON-NLS-1$ col.pack(); col.setText("Allocated"); col = new TableColumn(tab, SWT.RIGHT); col.setText("000.000WW"); //$NON-NLS-1$ col.pack(); col.setText("Free"); col = new TableColumn(tab, SWT.RIGHT); col.setText("000.00%"); //$NON-NLS-1$ col.pack(); col.setText("% Used"); col = new TableColumn(tab, SWT.RIGHT); col.setText("000,000,000"); //$NON-NLS-1$ col.pack(); col.setText("# Objects"); // make sure there is always one empty item so that one table row is always displayed. TableItem item = new TableItem(tab, SWT.NONE); item.setText(""); return tab; } private Table createDetailedTable(Composite base) { IPreferenceStore store = DdmUiPreferences.getStore(); Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); tab.setHeaderVisible(true); tab.setLinesVisible(true); TableHelper.createTableColumn(tab, "Type", SWT.LEFT, "4-byte array (object[], int[], float[])", //$NON-NLS-1$ PREFS_STATS_COL_TYPE, store); TableHelper.createTableColumn(tab, "Count", SWT.RIGHT, "00,000", //$NON-NLS-1$ PREFS_STATS_COL_COUNT, store); TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT, "000.000 WW", //$NON-NLS-1$ PREFS_STATS_COL_SIZE, store); TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT, "000.000 WW", //$NON-NLS-1$ PREFS_STATS_COL_SMALLEST, store); TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT, "000.000 WW", //$NON-NLS-1$ PREFS_STATS_COL_LARGEST, store); TableHelper.createTableColumn(tab, "Median", SWT.RIGHT, "000.000 WW", //$NON-NLS-1$ PREFS_STATS_COL_MEDIAN, store); TableHelper.createTableColumn(tab, "Average", SWT.RIGHT, "000.000 WW", //$NON-NLS-1$ PREFS_STATS_COL_AVERAGE, store); tab.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Client client = getCurrentClient(); if (client != null) { int index = mStatisticsTable.getSelectionIndex(); TableItem item = mStatisticsTable.getItem(index); if (item != null) { Map> heapMap = client.getClientData().getVmHeapData().getProcessedHeapMap(); ArrayList list = heapMap.get(item.getData()); if (list != null) { showChart(list); } } } } }); return tab; } /** * Creates the chart below the statistics table */ private void createChart() { mAllocCountDataSet = new DefaultCategoryDataset(); mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet, PlotOrientation.VERTICAL, false, true, false); // get the font to make a proper title. We need to convert the swt font, // into an awt font. Font f = mStatisticsBase.getFont(); FontData[] fData = f.getFontData(); // event though on Mac OS there could be more than one fontData, we'll only use // the first one. FontData firstFontData = fData[0]; java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(), firstFontData, true /* ensureSameSize */); mChart.setTitle(new TextTitle("Allocation count per size", awtFont)); Plot plot = mChart.getPlot(); if (plot instanceof CategoryPlot) { // get the plot CategoryPlot categoryPlot = (CategoryPlot)plot; // set the domain axis to draw labels that are displayed even with many values. CategoryAxis domainAxis = categoryPlot.getDomainAxis(); domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90); CategoryItemRenderer renderer = categoryPlot.getRenderer(); renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() { @Override public String generateToolTip(CategoryDataset dataset, int row, int column) { // get the key for the size of the allocation ByteLong columnKey = (ByteLong)dataset.getColumnKey(column); String rowKey = (String)dataset.getRowKey(row); Number value = dataset.getValue(rowKey, columnKey); return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey, columnKey.getValue()); } }); } mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart, ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 3000, // max draw width. We don't want it to zoom, so we put a big number 3000, // max draw height. We don't want it to zoom, so we put a big number true, // off-screen buffer true, // properties true, // save true, // print false, // zoom true); // tooltips mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); } private static String prettyByteCount(long bytes) { double fracBytes = bytes; String units = " B"; if (fracBytes < 1024) { return sByteFormatter.format(fracBytes) + units; } else { fracBytes /= 1024; units = " KB"; } if (fracBytes >= 1024) { fracBytes /= 1024; units = " MB"; } if (fracBytes >= 1024) { fracBytes /= 1024; units = " GB"; } return sLargeByteFormatter.format(fracBytes) + units; } private static String approximateByteCount(long bytes) { double fracBytes = bytes; String units = ""; if (fracBytes >= 1024) { fracBytes /= 1024; units = "K"; } if (fracBytes >= 1024) { fracBytes /= 1024; units = "M"; } if (fracBytes >= 1024) { fracBytes /= 1024; units = "G"; } return sByteFormatter.format(fracBytes) + units; } private static String addCommasToNumber(long num) { return sCountFormatter.format(num); } private static String fractionalPercent(long num, long denom) { double val = (double)num / (double)denom; val *= 100; NumberFormat nf = NumberFormat.getInstance(); nf.setMinimumFractionDigits(2); nf.setMaximumFractionDigits(2); return nf.format(val) + "%"; } private void fillSummaryTable(ClientData cd) { if (mHeapSummary.isDisposed()) { return; } mHeapSummary.setRedraw(false); mHeapSummary.removeAll(); int numRows = 0; if (cd != null) { synchronized (cd) { Iterator iter = cd.getVmHeapIds(); while (iter.hasNext()) { numRows++; Integer id = iter.next(); ClientData.HeapInfo info = cd.getVmHeapInfo(id); if (info == null) { continue; } TableItem item = new TableItem(mHeapSummary, SWT.NONE); item.setText(0, id.toString()); item.setText(1, prettyByteCount(info.sizeInBytes)); item.setText(2, prettyByteCount(info.bytesAllocated)); item.setText(3, prettyByteCount(info.sizeInBytes - info.bytesAllocated)); item.setText(4, fractionalPercent(info.bytesAllocated, info.sizeInBytes)); item.setText(5, addCommasToNumber(info.objectsAllocated)); } } } if (numRows == 0) { // make sure there is always one empty item so that one table row is always displayed. TableItem item = new TableItem(mHeapSummary, SWT.NONE); item.setText(""); } mHeapSummary.pack(); mHeapSummary.setRedraw(true); } private void fillDetailedTable(Client client, boolean forceRedraw) { // first check if the client is invalid or heap updates are not enabled. if (client == null || client.isHeapUpdateEnabled() == false) { mStatisticsTable.removeAll(); showChart(null); return; } ClientData cd = client.getClientData(); Map> heapMap; // Atomically get and clear the heap data. synchronized (cd) { if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { // no change, we return. return; } heapMap = cd.getVmHeapData().getProcessedHeapMap(); } // we have new data, lets display it. // First, get the current selection, and its key. int index = mStatisticsTable.getSelectionIndex(); Integer selectedKey = null; if (index != -1) { selectedKey = (Integer)mStatisticsTable.getItem(index).getData(); } // disable redraws and remove all from the table. mStatisticsTable.setRedraw(false); mStatisticsTable.removeAll(); if (heapMap != null) { int selectedIndex = -1; ArrayList selectedList = null; // get the keys Set keys = heapMap.keySet(); int iter = 0; // use a manual iter int because Set doesn't have an index // based accessor. for (Integer key : keys) { ArrayList list = heapMap.get(key); // check if this is the key that is supposed to be selected if (key.equals(selectedKey)) { selectedIndex = iter; selectedList = list; } iter++; TableItem item = new TableItem(mStatisticsTable, SWT.NONE); item.setData(key); // get the type item.setText(0, mMapLegend[key]); // set the count, smallest, largest int count = list.size(); item.setText(1, addCommasToNumber(count)); if (count > 0) { item.setText(3, prettyByteCount(list.get(0).getLength())); item.setText(4, prettyByteCount(list.get(count-1).getLength())); int median = count / 2; HeapSegmentElement element = list.get(median); long size = element.getLength(); item.setText(5, prettyByteCount(size)); long totalSize = 0; for (int i = 0 ; i < count; i++) { element = list.get(i); size = element.getLength(); totalSize += size; } // set the average and total item.setText(2, prettyByteCount(totalSize)); item.setText(6, prettyByteCount(totalSize / count)); } } mStatisticsTable.setRedraw(true); if (selectedIndex != -1) { mStatisticsTable.setSelection(selectedIndex); showChart(selectedList); } else { showChart(null); } } else { mStatisticsTable.setRedraw(true); } } private static class ByteLong implements Comparable { private long mValue; private ByteLong(long value) { mValue = value; } public long getValue() { return mValue; } @Override public String toString() { return approximateByteCount(mValue); } @Override public int compareTo(ByteLong other) { if (mValue != other.mValue) { return mValue < other.mValue ? -1 : 1; } return 0; } } /** * Fills the chart with the content of the list of {@link HeapSegmentElement}. */ private void showChart(ArrayList list) { mAllocCountDataSet.clear(); if (list != null) { String rowKey = "Alloc Count"; long currentSize = -1; int currentCount = 0; for (HeapSegmentElement element : list) { if (element.getLength() != currentSize) { if (currentSize != -1) { ByteLong columnKey = new ByteLong(currentSize); mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); } currentSize = element.getLength(); currentCount = 1; } else { currentCount++; } } // add the last item if (currentSize != -1) { ByteLong columnKey = new ByteLong(currentSize); mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); } } } /* * Add a color legend to the specified table. */ private void createLegend(Composite parent) { mLegend = new Group(parent, SWT.NONE); mLegend.setText(getLegendText(0)); mLegend.setLayout(new GridLayout(2, false)); RGB[] colors = mMapPalette.colors; for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) { Image tmpImage = createColorRect(parent.getDisplay(), colors[i]); Label l = new Label(mLegend, SWT.NONE); l.setImage(tmpImage); l = new Label(mLegend, SWT.NONE); l.setText(mMapLegend[i]); } } private String getLegendText(int level) { int bytes = 8 * (100 / ZOOMS[level]); return String.format("Key (1 pixel = %1$d bytes)", bytes); } private void setLegendText(int level) { mLegend.setText(getLegendText(level)); } /* * Create a nice rectangle in the specified color. */ private Image createColorRect(Display display, RGB color) { int width = 32; int height = 16; Image img = new Image(display, width, height); GC gc = new GC(img); gc.setBackground(new Color(display, color)); gc.fillRectangle(0, 0, width, height); gc.dispose(); return img; } /* * Are updates enabled? */ private void setUpdateStatus(int status) { switch (status) { case NOT_SELECTED: mUpdateStatus.setText("Select a client to see heap updates"); break; case NOT_ENABLED: mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client"); break; case ENABLED: mUpdateStatus.setText("Heap updates will happen after " + "every GC for this client"); break; default: throw new RuntimeException(); } mUpdateStatus.pack(); } /** * Return the closest power of two greater than or equal to value. * * @param value the return value will be >= value * @return a power of two >= value. If value > 2^31, 2^31 is returned. */ //xxx use Integer.highestOneBit() or numberOfLeadingZeros(). private int nextPow2(int value) { for (int i = 31; i >= 0; --i) { if ((value & (1<>> 2) & 1) << 1 | ((i >>> 4) & 1) << 2 | ((i >>> 6) & 1) << 3 | ((i >>> 8) & 1) << 4 | ((i >>> 10) & 1) << 5 | ((i >>> 12) & 1) << 6 | ((i >>> 14) & 1) << 7 | ((i >>> 16) & 1) << 8 | ((i >>> 18) & 1) << 9 | ((i >>> 20) & 1) << 10 | ((i >>> 22) & 1) << 11 | ((i >>> 24) & 1) << 12 | ((i >>> 26) & 1) << 13 | ((i >>> 28) & 1) << 14 | ((i >>> 30) & 1) << 15; int y = ((i >>> 1) & 1) << 0 | ((i >>> 3) & 1) << 1 | ((i >>> 5) & 1) << 2 | ((i >>> 7) & 1) << 3 | ((i >>> 9) & 1) << 4 | ((i >>> 11) & 1) << 5 | ((i >>> 13) & 1) << 6 | ((i >>> 15) & 1) << 7 | ((i >>> 17) & 1) << 8 | ((i >>> 19) & 1) << 9 | ((i >>> 21) & 1) << 10 | ((i >>> 23) & 1) << 11 | ((i >>> 25) & 1) << 12 | ((i >>> 27) & 1) << 13 | ((i >>> 29) & 1) << 14 | ((i >>> 31) & 1) << 15; try { id.setPixel(x, y, pixData[i]); if (x > maxX) { maxX = x; } } catch (IllegalArgumentException ex) { System.out.println("bad pixels: i " + i + ", w " + id.width + ", h " + id.height + ", x " + x + ", y " + y); throw ex; } } return maxX; } private final static int HILBERT_DIR_N = 0; private final static int HILBERT_DIR_S = 1; private final static int HILBERT_DIR_E = 2; private final static int HILBERT_DIR_W = 3; private void hilbertWalk(ImageData id, InputStream pixData, int order, int x, int y, int dir) throws IOException { if (x >= id.width || y >= id.height) { return; } else if (order == 0) { try { int p = pixData.read(); if (p >= 0) { // flip along x=y axis; assume width == height id.setPixel(y, x, p); /* Skanky; use an otherwise-unused ImageData field * to keep track of the max x,y used. Note that x and y are inverted. */ if (y > id.x) { id.x = y; } if (x > id.y) { id.y = x; } } //xxx just give up; don't bother walking the rest of the image } catch (IllegalArgumentException ex) { System.out.println("bad pixels: order " + order + ", dir " + dir + ", w " + id.width + ", h " + id.height + ", x " + x + ", y " + y); throw ex; } } else { order--; int delta = 1 << order; int nextX = x + delta; int nextY = y + delta; switch (dir) { case HILBERT_DIR_E: hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N); hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E); hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E); hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S); break; case HILBERT_DIR_N: hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E); hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N); hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N); hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W); break; case HILBERT_DIR_S: hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W); hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S); hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S); hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E); break; case HILBERT_DIR_W: hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S); hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W); hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W); hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N); break; default: throw new RuntimeException("Unexpected Hilbert direction " + dir); } } } private Point hilbertOrderData(ImageData id, byte pixData[]) { int order = 0; for (int n = 1; n < id.width; n *= 2) { order++; } /* Skanky; use an otherwise-unused ImageData field * to keep track of maxX. */ Point p = new Point(0,0); int oldIdX = id.x; int oldIdY = id.y; id.x = id.y = 0; try { hilbertWalk(id, new ByteArrayInputStream(pixData), order, 0, 0, HILBERT_DIR_E); p.x = id.x; p.y = id.y; } catch (IOException ex) { System.err.println("Exception during hilbertWalk()"); p.x = id.height; p.y = id.width; } id.x = oldIdX; id.y = oldIdY; return p; } private ImageData createHilbertHeapImage(byte pixData[]) { int w, h; // Pick an image size that the largest of heaps will fit into. w = (int)Math.sqrt(((16 * 1024 * 1024)/8)); // Space-filling curves require a power-of-2 width. w = nextPow2(w); h = w; // Create the heap image. ImageData id = new ImageData(w, h, 8, mMapPalette); // Copy the data into the image //int maxX = zOrderData(id, pixData); Point maxP = hilbertOrderData(id, pixData); // update the max size to make it a round number once the zoom is applied int factor = 100 / ZOOMS[mZoom.getSelectionIndex()]; if (factor != 1) { int tmp = maxP.x % factor; if (tmp != 0) { maxP.x += factor - tmp; } tmp = maxP.y % factor; if (tmp != 0) { maxP.y += factor - tmp; } } if (maxP.y < id.height) { // Crop the image down to the interesting part. id = new ImageData(id.width, maxP.y, id.depth, id.palette, id.scanlinePad, id.data); } if (maxP.x < id.width) { // crop the image again. A bit trickier this time. ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette); int[] buffer = new int[maxP.x]; for (int l = 0 ; l < id.height; l++) { id.getPixels(0, l, maxP.x, buffer, 0); croppedId.setPixels(0, l, maxP.x, buffer, 0); } id = croppedId; } // apply the zoom if (factor != 1) { id = id.scaledTo(id.width / factor, id.height / factor); } return id; } /** * Convert the raw heap data to an image. We know we're running in * the UI thread, so we can issue graphics commands directly. * * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html * * @param cd The client data * @param mode The display mode. 0 = linear, 1 = hilbert. * @param forceRedraw */ private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) { Image image; byte[] pixData; // Atomically get and clear the heap data. synchronized (cd) { if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { // no change, we return. return; } pixData = getSerializedData(); } if (pixData != null) { ImageData id; if (mode == 1) { id = createHilbertHeapImage(pixData); } else { id = createLinearHeapImage(pixData, 200, mMapPalette); } image = new Image(mDisplay, id); } else { // Render a placeholder image. int width, height; if (mode == 1) { width = height = PLACEHOLDER_HILBERT_SIZE; } else { width = PLACEHOLDER_LINEAR_H_SIZE; height = PLACEHOLDER_LINEAR_V_SIZE; } image = new Image(mDisplay, width, height); GC gc = new GC(image); gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED)); gc.drawLine(0, 0, width-1, height-1); gc.dispose(); gc = null; } // set the new image if (mode == 1) { if (mHilbertImage != null) { mHilbertImage.dispose(); } mHilbertImage = image; mHilbertHeapImage.setImage(mHilbertImage); mHilbertHeapImage.pack(true); mHilbertBase.layout(); mHilbertBase.pack(true); } else { if (mLinearImage != null) { mLinearImage.dispose(); } mLinearImage = image; mLinearHeapImage.setImage(mLinearImage); mLinearHeapImage.pack(true); mLinearBase.layout(); mLinearBase.pack(true); } } @Override protected void setTableFocusListener() { addTableToFocusListener(mHeapSummary); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java0100644 0000000 0000000 00000001401 12747325007 023756 0ustar000000000 0000000 /* * 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.ddmuilib; public interface IFindTarget { boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java0100644 0000000 0000000 00000002355 12747325007 025475 0ustar000000000 0000000 /* * 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.ddmuilib; import org.eclipse.swt.dnd.Clipboard; /** * An object listening to focus change in Table objects.
* For application not relying on a RCP to provide menu changes based on focus, * this class allows to get monitor the focus change of several Table widget * and update the menu action accordingly. */ public interface ITableFocusListener { public interface IFocusedTableActivator { public void copy(Clipboard clipboard); public void selectAll(); } public void focusGained(IFocusedTableActivator activator); public void focusLost(IFocusedTableActivator activator); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java0100644 0000000 0000000 00000016007 12747325007 023777 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.Log; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import java.io.InputStream; import java.net.URL; import java.util.HashMap; /** * Class to load images stored in a jar file. * All images are loaded from /images/filename * * Because Java requires to know the jar file in which to load the image from, a class is required * when getting the instance. Instances are cached and associated to the class passed to * {@link #getLoader(Class)}. * * {@link #getDdmUiLibLoader()} use {@link ImageLoader#getClass()} as the class. This is to be used * to load images from ddmuilib. * * Loaded images are stored so that 2 calls with the same filename will return the same object. * This also means that {@link Image} object returned by the loader should never be disposed. * */ public class ImageLoader { private static final String PATH = "/images/"; //$NON-NLS-1$ private final HashMap mLoadedImages = new HashMap(); private static final HashMap, ImageLoader> mInstances = new HashMap, ImageLoader>(); private final Class mClass; /** * Private constructor, creating an instance associated with a class. * The class is used to identify which jar file the images are loaded from. */ private ImageLoader(Class theClass) { if (theClass == null) { theClass = ImageLoader.class; } mClass = theClass; } /** * Returns the {@link ImageLoader} instance to load images from ddmuilib.jar */ public static ImageLoader getDdmUiLibLoader() { return getLoader(null); } /** * Returns an {@link ImageLoader} to load images based on a given class. * * The loader will load images from the jar from which the class was loaded. using * {@link Class#getResource(String)} and {@link Class#getResourceAsStream(String)}. * * Since all images are loaded using the path /images/filename, any class from the * jar will work. However since the loader is cached and reused when the query provides the same * class instance, and since the loader will also cache the loaded images, it is recommended * to always use the same class for a given Jar file. * */ public static ImageLoader getLoader(Class theClass) { ImageLoader instance = mInstances.get(theClass); if (instance == null) { instance = new ImageLoader(theClass); mInstances.put(theClass, instance); } return instance; } /** * Disposes all images for all instances. * This should only be called when the program exits. */ public static void dispose() { for (ImageLoader loader : mInstances.values()) { loader.doDispose(); } } private synchronized void doDispose() { for (Image image : mLoadedImages.values()) { image.dispose(); } mLoadedImages.clear(); } /** * Returns an {@link ImageDescriptor} for a given filename. * * This searches for an image located at /images/filename. * * @param filename the filename of the image to load. */ public ImageDescriptor loadDescriptor(String filename) { URL url = mClass.getResource(PATH + filename); // TODO cache in a map return ImageDescriptor.createFromURL(url); } /** * Returns an {@link Image} for a given filename. * * This searches for an image located at /images/filename. * * @param filename the filename of the image to load. * @param display the Display object */ public synchronized Image loadImage(String filename, Display display) { Image img = mLoadedImages.get(filename); if (img == null) { String tmp = PATH + filename; InputStream imageStream = mClass.getResourceAsStream(tmp); if (imageStream != null) { img = new Image(display, imageStream); mLoadedImages.put(filename, img); } if (img == null) { throw new RuntimeException("Failed to load " + tmp); } } return img; } /** * Loads an image from a resource. This method used a class to locate the * resources, and then load the filename from /images inside the resources.
* Extra parameters allows for creation of a replacement image of the * loading failed. * * @param display the Display object * @param fileName the file name * @param width optional width to create replacement Image. If -1, null be * be returned if the loading fails. * @param height optional height to create replacement Image. If -1, null be * be returned if the loading fails. * @param phColor optional color to create replacement Image. If null, Blue * color will be used. * @return a new Image or null if the loading failed and the optional * replacement size was -1 */ public Image loadImage(Display display, String fileName, int width, int height, Color phColor) { Image img = loadImage(fileName, display); if (img == null) { Log.w("ddms", "Couldn't load " + fileName); // if we had the extra parameter to create replacement image then we // create and return it. if (width != -1 && height != -1) { return createPlaceHolderArt(display, width, height, phColor != null ? phColor : display .getSystemColor(SWT.COLOR_BLUE)); } // otherwise, just return null return null; } return img; } /** * Create place-holder art with the specified color. */ public static Image createPlaceHolderArt(Display display, int width, int height, Color color) { Image img = new Image(display, width, height); GC gc = new GC(img); gc.setForeground(color); gc.drawLine(0, 0, width, height); gc.drawLine(0, height - 1, width, -1); gc.dispose(); return img; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java0100644 0000000 0000000 00000015533 12747325007 023504 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; /** * Display client info in a two-column format. */ public class InfoPanel extends TablePanel { private Table mTable; private TableColumn mCol2; private static final String mLabels[] = { "DDM-aware?", "App description:", "VM version:", "Process ID:", "Supports Profiling Control:", "Supports HPROF Control:", "ABI Flavor:", "JVM Flags:", }; private static final int ENT_DDM_AWARE = 0; private static final int ENT_APP_DESCR = 1; private static final int ENT_VM_VERSION = 2; private static final int ENT_PROCESS_ID = 3; private static final int ENT_SUPPORTS_PROFILING = 4; private static final int ENT_SUPPORTS_HPROF = 5; private static final int ENT_ABI_FLAVOR = 6; private static final int ENT_JVM_FLAGS = 7; /** * Create our control(s). */ @Override protected Control createControl(Composite parent) { mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); mTable.setHeaderVisible(false); mTable.setLinesVisible(false); TableColumn col1 = new TableColumn(mTable, SWT.RIGHT); col1.setText("name"); mCol2 = new TableColumn(mTable, SWT.LEFT); mCol2.setText("PlaceHolderContentForWidth"); TableItem item; for (int i = 0; i < mLabels.length; i++) { item = new TableItem(mTable, SWT.NONE); item.setText(0, mLabels[i]); item.setText(1, "-"); } col1.pack(); mCol2.pack(); return mTable; } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mTable.setFocus(); } /** * Sent when an existing client information changed. *

* This is sent from a non UI thread. * @param client the updated client. * @param changeMask the bit mask describing the changed properties. It can contain * any of the following values: {@link Client#CHANGE_PORT}, {@link Client#CHANGE_NAME} * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} * * @see IClientChangeListener#clientChanged(Client, int) */ @Override public void clientChanged(final Client client, int changeMask) { if (client == getCurrentClient()) { if ((changeMask & Client.CHANGE_INFO) == Client.CHANGE_INFO) { if (mTable.isDisposed()) return; mTable.getDisplay().asyncExec(new Runnable() { @Override public void run() { clientSelected(); } }); } } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()} */ @Override public void deviceSelected() { // pass } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()} */ @Override public void clientSelected() { if (mTable.isDisposed()) return; Client client = getCurrentClient(); if (client == null) { for (int i = 0; i < mLabels.length; i++) { TableItem item = mTable.getItem(i); item.setText(1, "-"); } } else { TableItem item; String clientDescription, vmIdentifier, isDdmAware, pid; ClientData cd = client.getClientData(); synchronized (cd) { clientDescription = (cd.getClientDescription() != null) ? cd.getClientDescription() : "?"; vmIdentifier = (cd.getVmIdentifier() != null) ? cd.getVmIdentifier() : "?"; isDdmAware = cd.isDdmAware() ? "yes" : "no"; pid = (cd.getPid() != 0) ? String.valueOf(cd.getPid()) : "?"; } item = mTable.getItem(ENT_APP_DESCR); item.setText(1, clientDescription); item = mTable.getItem(ENT_VM_VERSION); item.setText(1, vmIdentifier); item = mTable.getItem(ENT_DDM_AWARE); item.setText(1, isDdmAware); item = mTable.getItem(ENT_PROCESS_ID); item.setText(1, pid); item = mTable.getItem(ENT_SUPPORTS_PROFILING); if (cd.hasFeature(ClientData.FEATURE_PROFILING_STREAMING)) { item.setText(1, "Yes"); } else if (cd.hasFeature(ClientData.FEATURE_PROFILING)) { item.setText(1, "Yes (Application must be able to write on the SD Card)"); } else { item.setText(1, "No"); } item = mTable.getItem(ENT_SUPPORTS_HPROF); if (cd.hasFeature(ClientData.FEATURE_HPROF_STREAMING)) { item.setText(1, "Yes"); } else if (cd.hasFeature(ClientData.FEATURE_HPROF)) { item.setText(1, "Yes (Application must be able to write on the SD Card)"); } else { item.setText(1, "No"); } item = mTable.getItem(ENT_ABI_FLAVOR); String abi = cd.getAbi(); item.setText(1, abi == null ? "" : abi); item = mTable.getItem(ENT_JVM_FLAGS); String jvmFlags = cd.getJvmFlags(); item.setText(1, jvmFlags == null ? "" : jvmFlags); } mCol2.pack(); //Log.i("ddms", "InfoPanel: changed " + client); } @Override protected void setTableFocusListener() { addTableToFocusListener(mTable); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java0100644 0000000 0000000 00000173426 12747325007 024643 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.HeapSegment.HeapSegmentElement; import com.android.ddmlib.Log; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeLibraryMapInfo; import com.android.ddmlib.NativeStackCallInfo; import com.android.ddmuilib.annotation.WorkerThread; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; /** * Panel with native heap information. */ public final class NativeHeapPanel extends BaseHeapPanel { /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need * Native+1 at least. We also need 2 more entries for free area and expansion area. */ private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; private static final PaletteData mMapPalette = createPalette(); private static final int ALLOC_DISPLAY_ALL = 0; private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1; private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2; private Display mDisplay; private Composite mBase; private Label mUpdateStatus; /** combo giving choice of what to display: all, pre-zygote, post-zygote */ private Combo mAllocDisplayCombo; private Button mFullUpdateButton; // see CreateControl() //private Button mDiffUpdateButton; private Combo mDisplayModeCombo; /** stack composite for mode (1-2) & 3 */ private Composite mTopStackComposite; private StackLayout mTopStackLayout; /** stack composite for mode 1 & 2 */ private Composite mAllocationStackComposite; private StackLayout mAllocationStackLayout; /** top level container for mode 1 & 2 */ private Composite mTableModeControl; /** top level object for the allocation mode */ private Control mAllocationModeTop; /** top level for the library mode */ private Control mLibraryModeTopControl; /** composite for page UI and total memory display */ private Composite mPageUIComposite; private Label mTotalMemoryLabel; private Label mPageLabel; private Button mPageNextButton; private Button mPagePreviousButton; private Table mAllocationTable; private Table mLibraryTable; private Table mLibraryAllocationTable; private Table mDetailTable; private Label mImage; private int mAllocDisplayMode = ALLOC_DISPLAY_ALL; /** * pointer to current stackcall thread computation in order to quit it if * required (new update requested) */ private StackCallThread mStackCallThread; /** Current Library Allocation table fill thread. killed if selection changes */ private FillTableThread mFillTableThread; /** * current client data. Used to access the malloc info when switching pages * or selecting allocation to show stack call */ private ClientData mClientData; /** * client data from a previous display. used when asking for an "update & diff" */ private ClientData mBackUpClientData; /** list of NativeAllocationInfo objects filled with the list from ClientData */ private final ArrayList mAllocations = new ArrayList(); /** list of the {@link NativeAllocationInfo} being displayed based on the selection * of {@link #mAllocDisplayCombo}. */ private final ArrayList mDisplayedAllocations = new ArrayList(); /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */ private final ArrayList mBackUpAllocations = new ArrayList(); /** back up of the total memory, used when doing an "update & diff" */ private int mBackUpTotalMemory; private int mCurrentPage = 0; private int mPageCount = 0; /** * list of allocation per Library. This is created from the list of * NativeAllocationInfo objects that is stored in the ClientData object. Since we * don't keep this list around, it is recomputed everytime the client * changes. */ private final ArrayList mLibraryAllocations = new ArrayList(); /* args to setUpdateStatus() */ private static final int NOT_SELECTED = 0; private static final int NOT_ENABLED = 1; private static final int ENABLED = 2; private static final int DISPLAY_PER_PAGE = 20; private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$ private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$ private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$ private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$ private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$ private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$ private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$ private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$ private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$ private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$ private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$ private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$ private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$ private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$ private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$ private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$ private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$ private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$ private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$ private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$ /** static formatter object to format all numbers as #,### */ private static DecimalFormat sFormatter; static { sFormatter = (DecimalFormat)NumberFormat.getInstance(); if (sFormatter == null) { sFormatter = new DecimalFormat("#,###"); } else { sFormatter.applyPattern("#,###"); } } /** * caching mechanism to avoid recomputing the backtrace for a particular * address several times. */ private HashMap mSourceCache = new HashMap(); private long mTotalSize; private Button mSaveButton; private Button mSymbolsButton; /** * thread class to convert the address call into method, file and line * number in the background. */ private class StackCallThread extends BackgroundThread { private ClientData mClientData; public StackCallThread(ClientData cd) { mClientData = cd; } public ClientData getClientData() { return mClientData; } @Override public void run() { // loop through all the NativeAllocationInfo and init them Iterator iter = mAllocations.iterator(); int total = mAllocations.size(); int count = 0; while (iter.hasNext()) { if (isQuitting()) return; NativeAllocationInfo info = iter.next(); if (info.isStackCallResolved() == false) { final List list = info.getStackCallAddresses(); final int size = list.size(); ArrayList resolvedStackCall = new ArrayList(); for (int i = 0; i < size; i++) { long addr = list.get(i); // first check if the addr has already been converted. NativeStackCallInfo source = mSourceCache.get(addr); // if not we convert it if (source == null) { source = sourceForAddr(addr); mSourceCache.put(addr, source); } resolvedStackCall.add(source); } info.setResolvedStackCall(resolvedStackCall); } // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless // we reach total, since we also do it after the loop // (only an issue in case we have a perfect number of page) count++; if ((count % DISPLAY_PER_PAGE) == 0 && count != total) { if (updateNHAllocationStackCalls(mClientData, count) == false) { // looks like the app is quitting, so we just // stopped the thread return; } } } updateNHAllocationStackCalls(mClientData, count); } private NativeStackCallInfo sourceForAddr(long addr) { NativeLibraryMapInfo library = getLibraryFor(addr); if (library != null) { Addr2Line process = Addr2Line.getProcess(library, mClientData.getAbi()); if (process != null) { // remove the base of the library address NativeStackCallInfo info = process.getAddress(addr); if (info != null) { return info; } } } return new NativeStackCallInfo(addr, library != null ? library.getLibraryName() : null, Long.toHexString(addr), ""); } private NativeLibraryMapInfo getLibraryFor(long addr) { for (NativeLibraryMapInfo info : mClientData.getMappedNativeLibraries()) { if (info.isWithinLibrary(addr)) { return info; } } Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); return null; } /** * update the Native Heap panel with the amount of allocation for which the * stack call has been computed. This is called from a non UI thread, but * will be executed in the UI thread. * * @param count the amount of allocation * @return false if the display was disposed and the update couldn't happen */ private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) { if (mDisplay.isDisposed() == false) { mDisplay.asyncExec(new Runnable() { @Override public void run() { updateAllocationStackCalls(clientData, count); } }); return true; } return false; } } private class FillTableThread extends BackgroundThread { private LibraryAllocations mLibAlloc; private int mMax; public FillTableThread(LibraryAllocations liballoc, int m) { mLibAlloc = liballoc; mMax = m; } @Override public void run() { for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) { updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10); } } /** * updates the library allocation table in the Native Heap panel. This is * called from a non UI thread, but will be executed in the UI thread. * * @param liballoc the current library allocation object being displayed * @param start start index of items that need to be displayed * @param end end index of the items that need to be displayed */ private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc, final int start, final int end) { if (mDisplay.isDisposed() == false) { mDisplay.asyncExec(new Runnable() { @Override public void run() { updateLibraryAllocationTable(libAlloc, start, end); } }); } } } /** class to aggregate allocations per library */ public static class LibraryAllocations { private String mLibrary; private final ArrayList mLibAllocations = new ArrayList(); private int mSize; private int mCount; /** construct the aggregate object for a library */ public LibraryAllocations(final String lib) { mLibrary = lib; } /** get the library name */ public String getLibrary() { return mLibrary; } /** add a NativeAllocationInfo object to this aggregate object */ public void addAllocation(NativeAllocationInfo info) { mLibAllocations.add(info); } /** get an iterator on the NativeAllocationInfo objects */ public Iterator getAllocations() { return mLibAllocations.iterator(); } /** get a NativeAllocationInfo object by index */ public NativeAllocationInfo getAllocation(int index) { return mLibAllocations.get(index); } /** returns the NativeAllocationInfo object count */ public int getAllocationSize() { return mLibAllocations.size(); } /** returns the total allocation size */ public int getSize() { return mSize; } /** returns the number of allocations */ public int getCount() { return mCount; } /** * compute the allocation count and size for allocation objects added * through addAllocation(), and sort the objects by * total allocation size. */ public void computeAllocationSizeAndCount() { mSize = 0; mCount = 0; for (NativeAllocationInfo info : mLibAllocations) { mCount += info.getAllocationCount(); mSize += info.getAllocationCount() * info.getSize(); } Collections.sort(mLibAllocations, new Comparator() { @Override public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) { return o2.getAllocationCount() * o2.getSize() - o1.getAllocationCount() * o1.getSize(); } }); } } /** * Create our control(s). */ @Override protected Control createControl(Composite parent) { mDisplay = parent.getDisplay(); mBase = new Composite(parent, SWT.NONE); GridLayout gl = new GridLayout(1, false); gl.horizontalSpacing = 0; gl.verticalSpacing = 0; mBase.setLayout(gl); mBase.setLayoutData(new GridData(GridData.FILL_BOTH)); // composite for Composite tmp = new Composite(mBase, SWT.NONE); tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); tmp.setLayout(gl = new GridLayout(2, false)); gl.marginWidth = gl.marginHeight = 0; mFullUpdateButton = new Button(tmp, SWT.NONE); mFullUpdateButton.setText("Full Update"); mFullUpdateButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mBackUpClientData = null; mDisplayModeCombo.setEnabled(false); mSaveButton.setEnabled(false); emptyTables(); // if we already have a stack call computation for this // client // we stop it if (mStackCallThread != null && mStackCallThread.getClientData() == mClientData) { mStackCallThread.quit(); mStackCallThread = null; } mLibraryAllocations.clear(); Client client = getCurrentClient(); if (client != null) { client.requestNativeHeapInformation(); } } }); mUpdateStatus = new Label(tmp, SWT.NONE); mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // top layout for the combos and oter controls on the right. Composite top_layout = new Composite(mBase, SWT.NONE); top_layout.setLayout(gl = new GridLayout(4, false)); gl.marginWidth = gl.marginHeight = 0; new Label(top_layout, SWT.NONE).setText("Show:"); mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); mAllocDisplayCombo.setLayoutData(new GridData( GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); mAllocDisplayCombo.add("All Allocations"); mAllocDisplayCombo.add("Pre-Zygote Allocations"); mAllocDisplayCombo.add("Zygote Child Allocations (Z)"); mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onAllocDisplayChange(); } }); mAllocDisplayCombo.select(0); // separator Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL); GridData gd; separator.setLayoutData(gd = new GridData( GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); gd.heightHint = 0; gd.verticalSpan = 2; mSaveButton = new Button(top_layout, SWT.PUSH); mSaveButton.setText("Save..."); mSaveButton.setEnabled(false); mSaveButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE); fileDialog.setText("Save Allocations"); fileDialog.setFileName("allocations.txt"); String fileName = fileDialog.open(); if (fileName != null) { saveAllocations(fileName); } } }); /* * TODO: either fix the diff mechanism or remove it altogether. mDiffUpdateButton = new Button(top_layout, SWT.NONE); mDiffUpdateButton.setText("Update && Diff"); mDiffUpdateButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // since this is an update and diff, we need to store the // current list // of mallocs mBackUpAllocations.clear(); mBackUpAllocations.addAll(mAllocations); mBackUpClientData = mClientData; mBackUpTotalMemory = mClientData.getTotalNativeMemory(); mDisplayModeCombo.setEnabled(false); emptyTables(); // if we already have a stack call computation for this // client // we stop it if (mStackCallThread != null && mStackCallThread.getClientData() == mClientData) { mStackCallThread.quit(); mStackCallThread = null; } mLibraryAllocations.clear(); Client client = getCurrentClient(); if (client != null) { client.requestNativeHeapInformation(); } } }); */ Label l = new Label(top_layout, SWT.NONE); l.setText("Display:"); mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY); mDisplayModeCombo.setLayoutData(new GridData( GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL)); mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" }); mDisplayModeCombo.select(0); mDisplayModeCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { switchDisplayMode(); } }); mDisplayModeCombo.setEnabled(false); mSymbolsButton = new Button(top_layout, SWT.PUSH); mSymbolsButton.setText("Load Symbols"); mSymbolsButton.setEnabled(false); // create a composite that will contains the actual content composites, // in stack mode layout. // This top level composite contains 2 other composites. // * one for both Allocations and Libraries mode // * one for flat mode (which is gone for now) mTopStackComposite = new Composite(mBase, SWT.NONE); mTopStackComposite.setLayout(mTopStackLayout = new StackLayout()); mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); // create 1st and 2nd modes createTableDisplay(mTopStackComposite); mTopStackLayout.topControl = mTableModeControl; mTopStackComposite.layout(); setUpdateStatus(NOT_SELECTED); // Work in progress // TODO add image display of native heap. //mImage = new Label(mBase, SWT.NONE); mBase.pack(); return mBase; } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { // TODO } /** * Sent when an existing client information changed. *

* This is sent from a non UI thread. * @param client the updated client. * @param changeMask the bit mask describing the changed properties. It can contain * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} * * @see IClientChangeListener#clientChanged(Client, int) */ @Override public void clientChanged(final Client client, int changeMask) { if (client == getCurrentClient()) { if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) { if (mBase.isDisposed()) return; mBase.getDisplay().asyncExec(new Runnable() { @Override public void run() { clientSelected(); } }); } } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()}. */ @Override public void deviceSelected() { // pass } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()}. */ @Override public void clientSelected() { if (mBase.isDisposed()) return; Client client = getCurrentClient(); mDisplayModeCombo.setEnabled(false); emptyTables(); Log.d("ddms", "NativeHeapPanel: changed " + client); if (client != null) { ClientData cd = client.getClientData(); mClientData = cd; // if (cd.getShowHeapUpdates()) setUpdateStatus(ENABLED); // else // setUpdateStatus(NOT_ENABLED); initAllocationDisplay(); //renderBitmap(cd); } else { mClientData = null; setUpdateStatus(NOT_SELECTED); } mBase.pack(); } /** * Update the UI with the newly compute stack calls, unless the UI switched * to a different client. * * @param cd the ClientData for which the stack call are being computed. * @param count the current count of allocations for which the stack calls * have been computed. */ @WorkerThread public void updateAllocationStackCalls(ClientData cd, int count) { // we have to check that the panel still shows the same clientdata than // the thread is computing for. if (cd == mClientData) { int total = mAllocations.size(); if (count == total) { // we're done: do something mDisplayModeCombo.setEnabled(true); mSaveButton.setEnabled(true); mStackCallThread = null; } else { // work in progress, update the progress bar. // mUiThread.setStatusLine("Computing stack call: " + count // + "/" + total); } // FIXME: attempt to only update when needed. // Because the number of pages is not related to mAllocations.size() anymore // due to pre-zygote/post-zygote display, update all the time. // At some point we should remove the pages anyway, since it's getting computed // really fast now. // if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count // || (count == total && mCurrentPage == mPageCount - 1)) { try { // get the current selection of the allocation int index = mAllocationTable.getSelectionIndex(); NativeAllocationInfo info = null; if (index != -1) { info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData(); } // empty the table emptyTables(); // fill it again fillAllocationTable(); // reselect mAllocationTable.setSelection(index); // display detail table if needed if (info != null) { fillDetailTable(info); } } catch (SWTException e) { if (mAllocationTable.isDisposed()) { // looks like the table is disposed. Let's ignore it. } else { throw e; } } } else { // old client still running. doesn't really matter. } } @Override protected void setTableFocusListener() { addTableToFocusListener(mAllocationTable); addTableToFocusListener(mLibraryTable); addTableToFocusListener(mLibraryAllocationTable); addTableToFocusListener(mDetailTable); } protected void onAllocDisplayChange() { mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex(); // create the new list updateAllocDisplayList(); updateTotalMemoryDisplay(); // reset the ui. mCurrentPage = 0; updatePageUI(); switchDisplayMode(); } private void updateAllocDisplayList() { mTotalSize = 0; mDisplayedAllocations.clear(); for (NativeAllocationInfo info : mAllocations) { if (mAllocDisplayMode == ALLOC_DISPLAY_ALL || (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) { mDisplayedAllocations.add(info); mTotalSize += info.getSize() * info.getAllocationCount(); } else { // skip this item continue; } } int count = mDisplayedAllocations.size(); mPageCount = count / DISPLAY_PER_PAGE; // need to add a page for the rest of the div if ((count % DISPLAY_PER_PAGE) > 0) { mPageCount++; } } private void updateTotalMemoryDisplay() { switch (mAllocDisplayMode) { case ALLOC_DISPLAY_ALL: mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes", sFormatter.format(mTotalSize))); break; case ALLOC_DISPLAY_PRE_ZYGOTE: mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes", sFormatter.format(mTotalSize))); break; case ALLOC_DISPLAY_POST_ZYGOTE: mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes", sFormatter.format(mTotalSize))); break; } } private void switchDisplayMode() { switch (mDisplayModeCombo.getSelectionIndex()) { case 0: {// allocations mTopStackLayout.topControl = mTableModeControl; mAllocationStackLayout.topControl = mAllocationModeTop; mAllocationStackComposite.layout(); mTopStackComposite.layout(); emptyTables(); fillAllocationTable(); } break; case 1: {// libraries mTopStackLayout.topControl = mTableModeControl; mAllocationStackLayout.topControl = mLibraryModeTopControl; mAllocationStackComposite.layout(); mTopStackComposite.layout(); emptyTables(); fillLibraryTable(); } break; } } private void initAllocationDisplay() { if (mStackCallThread != null) { mStackCallThread.quit(); } mAllocations.clear(); mAllocations.addAll(mClientData.getNativeAllocationList()); updateAllocDisplayList(); // if we have a previous clientdata and it matches the current one. we // do a diff between the new list and the old one. if (mBackUpClientData != null && mBackUpClientData == mClientData) { ArrayList add = new ArrayList(); // we go through the list of NativeAllocationInfo in the new list and check if // there's one with the same exact data (size, allocation, count and // stackcall addresses) in the old list. // if we don't find any, we add it to the "add" list for (NativeAllocationInfo mi : mAllocations) { boolean found = false; for (NativeAllocationInfo old_mi : mBackUpAllocations) { if (mi.equals(old_mi)) { found = true; break; } } if (found == false) { add.add(mi); } } // put the result in mAllocations mAllocations.clear(); mAllocations.addAll(add); // display the difference in memory usage. This is computed // calculating the memory usage of the objects in mAllocations. int count = 0; for (NativeAllocationInfo allocInfo : mAllocations) { count += allocInfo.getSize() * allocInfo.getAllocationCount(); } mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes", sFormatter.format(count))); } else { // display the full memory usage updateTotalMemoryDisplay(); //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0); } mTotalMemoryLabel.pack(); // update the page ui mDisplayModeCombo.select(0); mLibraryAllocations.clear(); // reset to first page mCurrentPage = 0; // update the label updatePageUI(); // now fill the allocation Table with the current page switchDisplayMode(); // start the thread to compute the stack calls if (mAllocations.size() > 0) { mStackCallThread = new StackCallThread(mClientData); mStackCallThread.start(); } } private void updatePageUI() { // set the label and pack to update the layout, otherwise // the label will be cut off if the new size is bigger if (mPageCount == 0) { mPageLabel.setText("0 of 0 allocations."); } else { StringBuffer buffer = new StringBuffer(); // get our starting index int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1; // end index, taking into account the last page can be half full int count = mDisplayedAllocations.size(); int end = Math.min(start + DISPLAY_PER_PAGE - 1, count); buffer.append(sFormatter.format(start)); buffer.append(" - "); buffer.append(sFormatter.format(end)); buffer.append(" of "); buffer.append(sFormatter.format(count)); buffer.append(" allocations."); mPageLabel.setText(buffer.toString()); } // handle the button enabled state. mPagePreviousButton.setEnabled(mCurrentPage > 0); // reminder: mCurrentPage starts at 0. mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1); mPageLabel.pack(); mPageUIComposite.pack(); } private void fillAllocationTable() { // get the count int count = mDisplayedAllocations.size(); // get our starting index int start = mCurrentPage * DISPLAY_PER_PAGE; // loop for DISPLAY_PER_PAGE or till we reach count int end = start + DISPLAY_PER_PAGE; for (int i = start; i < end && i < count; i++) { NativeAllocationInfo info = mDisplayedAllocations.get(i); TableItem item = null; if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) { item = new TableItem(mAllocationTable, SWT.NONE); item.setText(0, (info.isZygoteChild() ? "Z " : "") + sFormatter.format(info.getSize() * info.getAllocationCount())); item.setText(1, sFormatter.format(info.getAllocationCount())); item.setText(2, sFormatter.format(info.getSize())); } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) { item = new TableItem(mAllocationTable, SWT.NONE); item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount())); item.setText(1, sFormatter.format(info.getAllocationCount())); item.setText(2, sFormatter.format(info.getSize())); } else { // skip this item continue; } item.setData(info); NativeStackCallInfo bti = info.getRelevantStackCallInfo(); if (bti != null) { String lib = bti.getLibraryName(); String method = bti.getMethodName(); String source = bti.getSourceFile(); if (lib != null) item.setText(3, lib); if (method != null) item.setText(4, method); if (source != null) item.setText(5, source); } } } private void fillLibraryTable() { // fill the library table sortAllocationsPerLibrary(); for (LibraryAllocations liballoc : mLibraryAllocations) { if (liballoc != null) { TableItem item = new TableItem(mLibraryTable, SWT.NONE); String lib = liballoc.getLibrary(); item.setText(0, lib != null ? lib : ""); item.setText(1, sFormatter.format(liballoc.getSize())); item.setText(2, sFormatter.format(liballoc.getCount())); } } } private void fillLibraryAllocationTable() { mLibraryAllocationTable.removeAll(); mDetailTable.removeAll(); int index = mLibraryTable.getSelectionIndex(); if (index != -1) { LibraryAllocations liballoc = mLibraryAllocations.get(index); // start a thread that will fill table 10 at a time to keep the ui // responsive, but first we kill the previous one if there was one if (mFillTableThread != null) { mFillTableThread.quit(); } mFillTableThread = new FillTableThread(liballoc, liballoc.getAllocationSize()); mFillTableThread.start(); } } public void updateLibraryAllocationTable(LibraryAllocations liballoc, int start, int end) { try { if (mLibraryTable.isDisposed() == false) { int index = mLibraryTable.getSelectionIndex(); if (index != -1) { LibraryAllocations newliballoc = mLibraryAllocations.get( index); if (newliballoc == liballoc) { int count = liballoc.getAllocationSize(); for (int i = start; i < end && i < count; i++) { NativeAllocationInfo info = liballoc.getAllocation(i); TableItem item = new TableItem( mLibraryAllocationTable, SWT.NONE); item.setText(0, sFormatter.format( info.getSize() * info.getAllocationCount())); item.setText(1, sFormatter.format(info.getAllocationCount())); item.setText(2, sFormatter.format(info.getSize())); NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo(); if (stackCallInfo != null) { item.setText(3, stackCallInfo.getMethodName()); } } } else { // we should quit the thread if (mFillTableThread != null) { mFillTableThread.quit(); mFillTableThread = null; } } } } } catch (SWTException e) { Log.e("ddms", "error when updating the library allocation table"); } } private void fillDetailTable(final NativeAllocationInfo mi) { mDetailTable.removeAll(); mDetailTable.setRedraw(false); try { // populate the detail Table with the back trace List addresses = mi.getStackCallAddresses(); List resolvedStackCall = mi.getResolvedStackCall(); if (resolvedStackCall == null) { return; } for (int i = 0 ; i < resolvedStackCall.size(); i++) { if (addresses.get(i) == null || addresses.get(i).longValue() == 0) { continue; } long addr = addresses.get(i).longValue(); NativeStackCallInfo source = resolvedStackCall.get(i); TableItem item = new TableItem(mDetailTable, SWT.NONE); item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$ String libraryName = source.getLibraryName(); String methodName = source.getMethodName(); String sourceFile = source.getSourceFile(); int lineNumber = source.getLineNumber(); if (libraryName != null) item.setText(1, libraryName); if (methodName != null) item.setText(2, methodName); if (sourceFile != null) item.setText(3, sourceFile); if (lineNumber != -1) item.setText(4, Integer.toString(lineNumber)); } } finally { mDetailTable.setRedraw(true); } } /* * Are updates enabled? */ private void setUpdateStatus(int status) { switch (status) { case NOT_SELECTED: mUpdateStatus.setText("Select a client to see heap info"); mAllocDisplayCombo.setEnabled(false); mFullUpdateButton.setEnabled(false); //mDiffUpdateButton.setEnabled(false); break; case NOT_ENABLED: mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client"); mAllocDisplayCombo.setEnabled(false); mFullUpdateButton.setEnabled(false); //mDiffUpdateButton.setEnabled(false); break; case ENABLED: mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data"); mAllocDisplayCombo.setEnabled(true); mFullUpdateButton.setEnabled(true); //mDiffUpdateButton.setEnabled(true); break; default: throw new RuntimeException(); } mUpdateStatus.pack(); } /** * Create the Table display. This includes a "detail" Table in the bottom * half and 2 modes in the top half: allocation Table and * library+allocations Tables. * * @param base the top parent to create the display into */ private void createTableDisplay(Composite base) { final int minPanelWidth = 60; final IPreferenceStore prefs = DdmUiPreferences.getStore(); // top level composite for mode 1 & 2 mTableModeControl = new Composite(base, SWT.NONE); GridLayout gl = new GridLayout(1, false); gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; mTableModeControl.setLayout(gl); mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH)); mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE); mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mTotalMemoryLabel.setText("Total Memory: 0 Bytes"); // the top half of these modes is dynamic final Composite sash_composite = new Composite(mTableModeControl, SWT.NONE); sash_composite.setLayout(new FormLayout()); sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH)); // create the stacked composite mAllocationStackComposite = new Composite(sash_composite, SWT.NONE); mAllocationStackLayout = new StackLayout(); mAllocationStackComposite.setLayout(mAllocationStackLayout); mAllocationStackComposite.setLayoutData(new GridData( GridData.FILL_BOTH)); // create the top half for mode 1 createAllocationTopHalf(mAllocationStackComposite); // create the top half for mode 2 createLibraryTopHalf(mAllocationStackComposite); final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL); // bottom half of these modes is the same: detail table createDetailTable(sash_composite); // init value for stack mAllocationStackLayout.topControl = mAllocationModeTop; // form layout data FormData data = new FormData(); data.top = new FormAttachment(mTotalMemoryLabel, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); mAllocationStackComposite.setLayoutData(data); final FormData sashData = new FormData(); if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) { sashData.top = new FormAttachment(0, prefs.getInt(PREFS_ALLOCATION_SASH)); } else { sashData.top = new FormAttachment(50, 0); // 50% across } sashData.left = new FormAttachment(0, 0); sashData.right = new FormAttachment(100, 0); sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(sash, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); mDetailTable.setLayoutData(data); // allow resizes, but cap at minPanelWidth sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = sash_composite.getClientArea(); int bottom = panelRect.height - sashRect.height - minPanelWidth; e.y = Math.max(Math.min(e.y, bottom), minPanelWidth); if (e.y != sashRect.y) { sashData.top = new FormAttachment(0, e.y); prefs.setValue(PREFS_ALLOCATION_SASH, e.y); sash_composite.layout(); } } }); } private void createDetailTable(Composite base) { final IPreferenceStore prefs = DdmUiPreferences.getStore(); mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH)); mDetailTable.setHeaderVisible(true); mDetailTable.setLinesVisible(true); TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT, "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT, "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT, "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT, "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT, "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$ } private void createAllocationTopHalf(Composite b) { final IPreferenceStore prefs = DdmUiPreferences.getStore(); Composite base = new Composite(b, SWT.NONE); mAllocationModeTop = base; GridLayout gl = new GridLayout(1, false); gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; gl.verticalSpacing = 0; base.setLayout(gl); base.setLayoutData(new GridData(GridData.FILL_BOTH)); // horizontal layout for memory total and pages UI mPageUIComposite = new Composite(base, SWT.NONE); mPageUIComposite.setLayoutData(new GridData( GridData.HORIZONTAL_ALIGN_BEGINNING)); gl = new GridLayout(3, false); gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; gl.horizontalSpacing = 0; mPageUIComposite.setLayout(gl); // Page UI mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE); mPagePreviousButton.setText("<"); mPagePreviousButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mCurrentPage--; updatePageUI(); emptyTables(); fillAllocationTable(); } }); mPageNextButton = new Button(mPageUIComposite, SWT.NONE); mPageNextButton.setText(">"); mPageNextButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mCurrentPage++; updatePageUI(); emptyTables(); fillAllocationTable(); } }); mPageLabel = new Label(mPageUIComposite, SWT.NONE); mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); updatePageUI(); mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); mAllocationTable.setHeaderVisible(true); mAllocationTable.setLinesVisible(true); TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT, "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT, "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT, "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT, "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT, "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT, "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$ mAllocationTable.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // get the selection index int index = mAllocationTable.getSelectionIndex(); if (index >= 0 && index < mAllocationTable.getItemCount()) { TableItem item = mAllocationTable.getItem(index); if (item != null && item.getData() instanceof NativeAllocationInfo) { fillDetailTable((NativeAllocationInfo)item.getData()); } } } }); } private void createLibraryTopHalf(Composite base) { final int minPanelWidth = 60; final IPreferenceStore prefs = DdmUiPreferences.getStore(); // create a composite that'll contain 2 tables horizontally final Composite top = new Composite(base, SWT.NONE); mLibraryModeTopControl = top; top.setLayout(new FormLayout()); top.setLayoutData(new GridData(GridData.FILL_BOTH)); // first table: library mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH)); mLibraryTable.setHeaderVisible(true); mLibraryTable.setLinesVisible(true); TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT, "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT, "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT, "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$ mLibraryTable.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { fillLibraryAllocationTable(); } }); final Sash sash = new Sash(top, SWT.VERTICAL); // 2nd table: allocation per library mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); mLibraryAllocationTable.setHeaderVisible(true); mLibraryAllocationTable.setLinesVisible(true); TableHelper.createTableColumn(mLibraryAllocationTable, "Total", SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mLibraryAllocationTable, "Count", SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mLibraryAllocationTable, "Size", SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$ TableHelper.createTableColumn(mLibraryAllocationTable, "Method", SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$ mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // get the index of the selection in the library table int index1 = mLibraryTable.getSelectionIndex(); // get the index in the library allocation table int index2 = mLibraryAllocationTable.getSelectionIndex(); // get the MallocInfo object if (index1 != -1 && index2 != -1) { LibraryAllocations liballoc = mLibraryAllocations.get(index1); NativeAllocationInfo info = liballoc.getAllocation(index2); fillDetailTable(info); } } }); // form layout data FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(sash, 0); mLibraryTable.setLayoutData(data); final FormData sashData = new FormData(); if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) { sashData.left = new FormAttachment(0, prefs.getInt(PREFS_LIBRARY_SASH)); } else { sashData.left = new FormAttachment(50, 0); } sashData.bottom = new FormAttachment(100, 0); sashData.top = new FormAttachment(0, 0); // 50% across sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(sash, 0); data.right = new FormAttachment(100, 0); mLibraryAllocationTable.setLayoutData(data); // allow resizes, but cap at minPanelWidth sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = top.getClientArea(); int right = panelRect.width - sashRect.width - minPanelWidth; e.x = Math.max(Math.min(e.x, right), minPanelWidth); if (e.x != sashRect.x) { sashData.left = new FormAttachment(0, e.x); prefs.setValue(PREFS_LIBRARY_SASH, e.y); top.layout(); } } }); } private void emptyTables() { mAllocationTable.removeAll(); mLibraryTable.removeAll(); mLibraryAllocationTable.removeAll(); mDetailTable.removeAll(); } private void sortAllocationsPerLibrary() { if (mClientData != null) { mLibraryAllocations.clear(); // create a hash map of LibraryAllocations to access aggregate // objects already created HashMap libcache = new HashMap(); // get the allocation count int count = mDisplayedAllocations.size(); for (int i = 0; i < count; i++) { NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i); NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo(); if (stackCallInfo != null) { String libraryName = stackCallInfo.getLibraryName(); LibraryAllocations liballoc = libcache.get(libraryName); if (liballoc == null) { // didn't find a library allocation object already // created so we create one liballoc = new LibraryAllocations(libraryName); // add it to the cache libcache.put(libraryName, liballoc); // add it to the list mLibraryAllocations.add(liballoc); } // add the MallocInfo object to it. liballoc.addAllocation(allocInfo); } } // now that the list is created, we need to compute the size and // sort it by size. This will also sort the MallocInfo objects // inside each LibraryAllocation objects. for (LibraryAllocations liballoc : mLibraryAllocations) { liballoc.computeAllocationSizeAndCount(); } // now we sort it Collections.sort(mLibraryAllocations, new Comparator() { @Override public int compare(LibraryAllocations o1, LibraryAllocations o2) { return o2.getSize() - o1.getSize(); } }); } } private void renderBitmap(ClientData cd) { byte[] pixData; // Atomically get and clear the heap data. synchronized (cd) { if (serializeHeapData(cd.getVmHeapData()) == false) { // no change, we return. return; } pixData = getSerializedData(); ImageData id = createLinearHeapImage(pixData, 200, mMapPalette); Image image = new Image(mBase.getDisplay(), id); mImage.setImage(image); mImage.pack(true); } } /* * Create color palette for map. Set up titles for legend. */ private static PaletteData createPalette() { RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; colors[0] = new RGB(192, 192, 192); // non-heap pixels are gray mMapLegend[0] = "(heap expansion area)"; colors[1] = new RGB(0, 0, 0); // free chunks are black mMapLegend[1] = "free"; colors[HeapSegmentElement.KIND_OBJECT + 2] = new RGB(0, 0, 255); // objects are blue mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] = "data object"; colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] = new RGB(0, 255, 0); // class objects are green mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] = "class object"; colors[HeapSegmentElement.KIND_ARRAY_1 + 2] = new RGB(255, 0, 0); // byte/bool arrays are red mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] = "1-byte array (byte[], boolean[])"; colors[HeapSegmentElement.KIND_ARRAY_2 + 2] = new RGB(255, 128, 0); // short/char arrays are orange mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] = "2-byte array (short[], char[])"; colors[HeapSegmentElement.KIND_ARRAY_4 + 2] = new RGB(255, 255, 0); // obj/int/float arrays are yellow mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] = "4-byte array (object[], int[], float[])"; colors[HeapSegmentElement.KIND_ARRAY_8 + 2] = new RGB(255, 128, 128); // long/double arrays are pink mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] = "8-byte array (long[], double[])"; colors[HeapSegmentElement.KIND_UNKNOWN + 2] = new RGB(255, 0, 255); // unknown objects are cyan mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] = "unknown object"; colors[HeapSegmentElement.KIND_NATIVE + 2] = new RGB(64, 64, 64); // native objects are dark gray mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] = "non-Java object"; return new PaletteData(colors); } private void saveAllocations(String fileName) { try { PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); for (NativeAllocationInfo alloc : mAllocations) { out.println(alloc.toString()); } out.close(); } catch (IOException e) { Log.e("Native", e); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java0100644 0000000 0000000 00000002556 12747325007 022671 0ustar000000000 0000000 /* * 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.ddmuilib; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; /** * Base class for our information panels. */ public abstract class Panel { public final Control createPanel(Composite parent) { Control panelControl = createControl(parent); postCreation(); return panelControl; } protected abstract void postCreation(); /** * Creates a control capable of displaying some information. This is * called once, when the application is initializing, from the UI thread. */ protected abstract Control createControl(Composite parent); /** * Sets the focus to the proper control inside the panel. */ public abstract void setFocus(); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java0100644 0000000 0000000 00000026655 12747325007 025035 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.RawImage; import com.android.ddmlib.TimeoutException; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.ImageTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import java.io.File; import java.io.IOException; import java.util.Calendar; /** * Gather a screen shot from the device and save it to a file. */ public class ScreenShotDialog extends Dialog { private Label mBusyLabel; private Label mImageLabel; private Button mSave; private IDevice mDevice; private RawImage mRawImage; private Clipboard mClipboard; /** Number of 90 degree rotations applied to the current image */ private int mRotateCount = 0; /** * Create with default style. */ public ScreenShotDialog(Shell parent) { this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); mClipboard = new Clipboard(parent.getDisplay()); } /** * Create with app-defined style. */ public ScreenShotDialog(Shell parent, int style) { super(parent, style); } /** * Prepare and display the dialog. * @param device The {@link IDevice} from which to get the screenshot. */ public void open(IDevice device) { mDevice = device; Shell parent = getParent(); Shell shell = new Shell(parent, getStyle()); shell.setText("Device Screen Capture"); createContents(shell); shell.pack(); shell.open(); updateDeviceImage(shell); Display display = parent.getDisplay(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } } /* * Create the screen capture dialog contents. */ private void createContents(final Shell shell) { GridData data; final int colCount = 5; shell.setLayout(new GridLayout(colCount, true)); // "refresh" button Button refresh = new Button(shell, SWT.PUSH); refresh.setText("Refresh"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; refresh.setLayoutData(data); refresh.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { updateDeviceImage(shell); // RawImage only allows us to rotate the image 90 degrees at the time, // so to preserve the current rotation we must call getRotated() // the same number of times the user has done it manually. // TODO: improve the RawImage class. if (mRawImage != null) { for (int i = 0; i < mRotateCount; i++) { mRawImage = mRawImage.getRotated(); } updateImageDisplay(shell); } } }); // "rotate" button Button rotate = new Button(shell, SWT.PUSH); rotate.setText("Rotate"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; rotate.setLayoutData(data); rotate.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mRawImage != null) { mRotateCount = (mRotateCount + 1) % 4; mRawImage = mRawImage.getRotated(); updateImageDisplay(shell); } } }); // "save" button mSave = new Button(shell, SWT.PUSH); mSave.setText("Save"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; mSave.setLayoutData(data); mSave.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { saveImage(shell); } }); Button copy = new Button(shell, SWT.PUSH); copy.setText("Copy"); copy.setToolTipText("Copy the screenshot to the clipboard"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; copy.setLayoutData(data); copy.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { copy(); } }); // "done" button Button done = new Button(shell, SWT.PUSH); done.setText("Done"); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.widthHint = 80; done.setLayoutData(data); done.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { shell.close(); } }); // title/"capturing" label mBusyLabel = new Label(shell, SWT.NONE); mBusyLabel.setText("Preparing..."); data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); data.horizontalSpan = colCount; mBusyLabel.setLayoutData(data); // space for the image mImageLabel = new Label(shell, SWT.BORDER); data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); data.horizontalSpan = colCount; mImageLabel.setLayoutData(data); Display display = shell.getDisplay(); mImageLabel.setImage(ImageLoader.createPlaceHolderArt( display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE))); shell.setDefaultButton(done); } /** * Copies the content of {@link #mImageLabel} to the clipboard. */ private void copy() { mClipboard.setContents( new Object[] { mImageLabel.getImage().getImageData() }, new Transfer[] { ImageTransfer.getInstance() }); } /** * Captures a new image from the device, and display it. */ private void updateDeviceImage(Shell shell) { mBusyLabel.setText("Capturing..."); // no effect shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT)); mRawImage = getDeviceImage(); updateImageDisplay(shell); } /** * Updates the display with {@link #mRawImage}. * @param shell */ private void updateImageDisplay(Shell shell) { Image image; if (mRawImage == null) { Display display = shell.getDisplay(); image = ImageLoader.createPlaceHolderArt( display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); mSave.setEnabled(false); mBusyLabel.setText("Screen not available"); } else { // convert raw data to an Image. PaletteData palette = new PaletteData( mRawImage.getRedMask(), mRawImage.getGreenMask(), mRawImage.getBlueMask()); ImageData imageData = new ImageData(mRawImage.width, mRawImage.height, mRawImage.bpp, palette, 1, mRawImage.data); image = new Image(getParent().getDisplay(), imageData); mSave.setEnabled(true); mBusyLabel.setText("Captured image:"); } mImageLabel.setImage(image); mImageLabel.pack(); shell.pack(); // there's no way to restore old cursor; assume it's ARROW shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); } /** * Grabs an image from an ADB-connected device and returns it as a {@link RawImage}. */ private RawImage getDeviceImage() { try { return mDevice.getScreenshot(); } catch (IOException ioe) { Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage()); return null; } catch (TimeoutException e) { Log.w("ddms", "Unable to get frame buffer: timeout "); return null; } catch (AdbCommandRejectedException e) { Log.w("ddms", "Unable to get frame buffer: " + e.getMessage()); return null; } } /* * Prompt the user to save the image to disk. */ private void saveImage(Shell shell) { FileDialog dlg = new FileDialog(shell, SWT.SAVE); Calendar now = Calendar.getInstance(); String fileName = String.format("device-%tF-%tH%tM%tS.png", now, now, now, now); dlg.setText("Save image..."); dlg.setFileName(fileName); String lastDir = DdmUiPreferences.getStore().getString("lastImageSaveDir"); if (lastDir.length() == 0) { lastDir = DdmUiPreferences.getStore().getString("imageSaveDir"); } dlg.setFilterPath(lastDir); dlg.setFilterNames(new String[] { "PNG Files (*.png)" }); dlg.setFilterExtensions(new String[] { "*.png" //$NON-NLS-1$ }); fileName = dlg.open(); if (fileName != null) { // FileDialog.getFilterPath() does NOT always return the current // directory of the FileDialog; on the Mac it sometimes just returns // the value the dialog was initialized with. It does however return // the full path as its return value, so just pick the path from // there. if (!fileName.endsWith(".png")) { fileName = fileName + ".png"; } String saveDir = new File(fileName).getParent(); if (saveDir != null) { DdmUiPreferences.getStore().setValue("lastImageSaveDir", saveDir); } Log.d("ddms", "Saving image to " + fileName); ImageData imageData = mImageLabel.getImage().getImageData(); try { org.eclipse.swt.graphics.ImageLoader loader = new org.eclipse.swt.graphics.ImageLoader(); loader.data = new ImageData[] { imageData }; loader.save(fileName, SWT.IMAGE_PNG); } catch (SWTException e) { Log.w("ddms", "Unable to save " + fileName + ": " + e.getMessage()); } } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java0100644 0000000 0000000 00000004457 12747325007 026370 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.Client; import com.android.ddmlib.IDevice; /** * A Panel that requires {@link Device}/{@link Client} selection notifications. */ public abstract class SelectionDependentPanel extends Panel { private IDevice mCurrentDevice = null; private Client mCurrentClient = null; /** * Returns the current {@link Device}. * @return the current device or null if none are selected. */ protected final IDevice getCurrentDevice() { return mCurrentDevice; } /** * Returns the current {@link Client}. * @return the current client or null if none are selected. */ protected final Client getCurrentClient() { return mCurrentClient; } /** * Sent when a new device is selected. * @param selectedDevice the selected device. */ public final void deviceSelected(IDevice selectedDevice) { if (selectedDevice != mCurrentDevice) { mCurrentDevice = selectedDevice; deviceSelected(); } } /** * Sent when a new client is selected. * @param selectedClient the selected client. */ public final void clientSelected(Client selectedClient) { if (selectedClient != mCurrentClient) { mCurrentClient = selectedClient; clientSelected(); } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()}. */ public abstract void deviceSelected(); /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()}. */ public abstract void clientSelected(); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java0100644 0000000 0000000 00000017146 12747325007 024637 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.Client; import com.android.ddmlib.IStackTraceInfo; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Table; /** * Stack Trace Panel. *

This is not a panel in the regular sense. Instead this is just an object around the creation * and management of a Stack Trace display. *

UI creation is done through * {@link #createPanel(Composite, String, IPreferenceStore)}. * */ public final class StackTracePanel { private static ISourceRevealer sSourceRevealer; private Table mStackTraceTable; private TableViewer mStackTraceViewer; private Client mCurrentClient; /** * Content Provider to display the stack trace of a thread. * Expected input is a {@link IStackTraceInfo} object. */ private static class StackTraceContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof IStackTraceInfo) { // getElement cannot return null, so we return an empty array // if there's no stack trace StackTraceElement trace[] = ((IStackTraceInfo)inputElement).getStackTrace(); if (trace != null) { return trace; } } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } } /** * A Label Provider to use with {@link StackTraceContentProvider}. It expects the elements to be * of type {@link StackTraceElement}. */ private static class StackTraceLabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof StackTraceElement && columnIndex == 0) { StackTraceElement traceElement = (StackTraceElement) element; return " at " + traceElement.toString(); } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } /** * Classes which implement this interface provide a method that is able to reveal a method * in a source editor */ public interface ISourceRevealer { /** * Sent to reveal a particular line in a source editor * @param applicationName the name of the application running the source. * @param className the fully qualified class name * @param line the line to reveal */ public void reveal(String applicationName, String className, int line); } /** * Sets the {@link ISourceRevealer} object able to reveal source code in a source editor. * @param revealer */ public static void setSourceRevealer(ISourceRevealer revealer) { sSourceRevealer = revealer; } /** * Creates the controls for the StrackTrace display. *

This method will set the parent {@link Composite} to use a {@link GridLayout} with * 2 columns. * @param parent the parent composite. * @param prefs_stack_column * @param store */ public Table createPanel(Composite parent, String prefs_stack_column, IPreferenceStore store) { mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); mStackTraceTable.setHeaderVisible(false); mStackTraceTable.setLinesVisible(false); TableHelper.createTableColumn( mStackTraceTable, "Info", SWT.LEFT, "SomeLongClassName.method(android/somepackage/someotherpackage/somefile.java:99999)", //$NON-NLS-1$ prefs_stack_column, store); mStackTraceViewer = new TableViewer(mStackTraceTable); mStackTraceViewer.setContentProvider(new StackTraceContentProvider()); mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider()); mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() { @Override public void doubleClick(DoubleClickEvent event) { if (sSourceRevealer != null && mCurrentClient != null) { // get the selected stack trace element ISelection selection = mStackTraceViewer.getSelection(); if (selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object object = structuredSelection.getFirstElement(); if (object instanceof StackTraceElement) { StackTraceElement traceElement = (StackTraceElement)object; if (traceElement.isNativeMethod() == false) { sSourceRevealer.reveal( mCurrentClient.getClientData().getClientDescription(), traceElement.getClassName(), traceElement.getLineNumber()); } } } } } }); return mStackTraceTable; } /** * Sets the input for the {@link TableViewer}. * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of * {@link StackTraceElement} */ public void setViewerInput(IStackTraceInfo input) { mStackTraceViewer.setInput(input); mStackTraceViewer.refresh(); } /** * Sets the current client running the stack trace. * @param currentClient the {@link Client}. */ public void setCurrentClient(Client currentClient) { mCurrentClient = currentClient; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java0100644 0000000 0000000 00000006702 12747325007 025430 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.SyncException; import com.android.ddmlib.SyncService; import com.android.ddmlib.SyncService.ISyncProgressMonitor; import com.android.ddmlib.TimeoutException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.swt.widgets.Shell; import java.io.IOException; import java.lang.reflect.InvocationTargetException; /** * Helper class to run a Sync in a {@link ProgressMonitorDialog}. */ public class SyncProgressHelper { /** * a runnable class run with an {@link ISyncProgressMonitor}. */ public interface SyncRunnable { /** Runs the sync action */ void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException; /** close the {@link SyncService} */ void close(); } /** * Runs a {@link SyncRunnable} in a {@link ProgressMonitorDialog}. * @param runnable The {@link SyncRunnable} to run. * @param progressMessage the message to display in the progress dialog * @param parentShell the parent shell for the progress dialog. * * @throws InvocationTargetException * @throws InterruptedException * @throws SyncException if an error happens during the push of the package on the device. * @throws IOException * @throws TimeoutException */ public static void run(final SyncRunnable runnable, final String progressMessage, final Shell parentShell) throws InvocationTargetException, InterruptedException, SyncException, IOException, TimeoutException { final Exception[] result = new Exception[1]; new ProgressMonitorDialog(parentShell).run(true, true, new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) { try { runnable.run(new SyncProgressMonitor(monitor, progressMessage)); } catch (Exception e) { result[0] = e; } finally { runnable.close(); } } }); if (result[0] instanceof SyncException) { SyncException se = (SyncException)result[0]; if (se.wasCanceled()) { // no need to throw this return; } throw se; } // just do some casting so that the method declaration matches what's thrown. if (result[0] instanceof TimeoutException) { throw (TimeoutException)result[0]; } if (result[0] instanceof IOException) { throw (IOException)result[0]; } if (result[0] instanceof RuntimeException) { throw (RuntimeException)result[0]; } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java0100644 0000000 0000000 00000003067 12747325007 025641 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.SyncService.ISyncProgressMonitor; import org.eclipse.core.runtime.IProgressMonitor; /** * Implementation of the {@link ISyncProgressMonitor} wrapping an Eclipse {@link IProgressMonitor}. */ public class SyncProgressMonitor implements ISyncProgressMonitor { private IProgressMonitor mMonitor; private String mName; public SyncProgressMonitor(IProgressMonitor monitor, String name) { mMonitor = monitor; mName = name; } @Override public void start(int totalWork) { mMonitor.beginTask(mName, totalWork); } @Override public void stop() { mMonitor.done(); } @Override public void advance(int work) { mMonitor.worked(work); } @Override public boolean isCanceled() { return mMonitor.isCanceled(); } @Override public void startSubTask(String name) { mMonitor.subTask(name); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java0100644 0000000 0000000 00000077454 12747325007 024255 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.annotations.concurrency.GuardedBy; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.IDevice; import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.Log; import com.android.ddmlib.NullOutputReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData; import com.google.common.base.Splitter; import com.google.common.collect.Lists; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.PlotOrientation; import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.data.general.DefaultPieDataset; import org.jfree.experimental.chart.swt.ChartComposite; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Displays system information graphs obtained from a bugreport file or device. */ public class SysinfoPanel extends TablePanel { // UI components private Label mLabel; private Button mFetchButton; private Combo mDisplayMode; private DefaultPieDataset mDataset; private DefaultCategoryDataset mBarDataSet; private StackLayout mStackLayout; private Composite mChartComposite; private Composite mPieChartComposite; private Composite mStackedBarComposite; // Selects the current display: MODE_CPU, etc. private int mMode = 0; private String mGfxPackageName; private static final Object RECEIVER_LOCK = new Object(); @GuardedBy("RECEIVER_LOCK") private ShellOutputReceiver mLastOutputReceiver; private static final int MODE_CPU = 0; private static final int MODE_MEMINFO = 1; private static final int MODE_GFXINFO = 2; // argument to dumpsys; section in the bugreport holding the data private static final String DUMP_COMMAND[] = { "dumpsys cpuinfo", "cat /proc/meminfo ; procrank", "dumpsys gfxinfo", }; private static final String CAPTIONS[] = { "CPU load", "Memory usage", "Frame Render Time", }; /** Shell property that controls whether graphics profiling is enabled or not. */ private static final String PROP_GFX_PROFILING = "debug.hwui.profile"; //$NON-NLS-1$ /** * Generates the dataset to display. * * @param file The bugreport file to process. */ private void generateDataset(File file) { if (file == null) { return; } try { BufferedReader br = getBugreportReader(file); if (mMode == MODE_CPU) { readCpuDataset(br); } else if (mMode == MODE_MEMINFO) { readMeminfoDataset(br); } else if (mMode == MODE_GFXINFO) { readGfxInfoDataset(br); } br.close(); } catch (IOException e) { Log.e("DDMS", e); } } /** * Sent when a new device is selected. The new device can be accessed with * {@link #getCurrentDevice()} */ @Override public void deviceSelected() { if (getCurrentDevice() != null) { mFetchButton.setEnabled(true); loadFromDevice(); } else { mFetchButton.setEnabled(false); } } /** * Sent when a new client is selected. The new client can be accessed with * {@link #getCurrentClient()}. */ @Override public void clientSelected() { } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mDisplayMode.setFocus(); } /** * Fetches a new bugreport from the device and updates the display. * Fetching is asynchronous. See also addOutput, flush, and isCancelled. */ private void loadFromDevice() { clearDataSet(); if (mMode == MODE_GFXINFO) { boolean en = isGfxProfilingEnabled(); if (!en) { if (enableGfxProfiling()) { MessageDialog.openInformation(Display.getCurrent().getActiveShell(), "DDMS", "Graphics profiling was enabled on the device.\n" + "It may be necessary to relaunch your application to see profile information."); } else { MessageDialog.openError(Display.getCurrent().getActiveShell(), "DDMS", "Unexpected error enabling graphics profiling on device.\n"); return; } } } final String command = getDumpsysCommand(mMode); if (command == null) { return; } Thread t = new Thread(new Runnable() { @Override public void run() { try { String header = null; if (mMode == MODE_MEMINFO) { // Hack to add bugreport-style section header for meminfo header = "------ MEMORY INFO ------\n"; } IShellOutputReceiver receiver = initShellOutputBuffer(header); getCurrentDevice().executeShellCommand(command, receiver); } catch (IOException e) { Log.e("DDMS", e); } catch (TimeoutException e) { Log.e("DDMS", e); } catch (AdbCommandRejectedException e) { Log.e("DDMS", e); } catch (ShellCommandUnresponsiveException e) { Log.e("DDMS", e); } } }, "Sysinfo Output Collector"); t.start(); } private boolean isGfxProfilingEnabled() { IDevice device = getCurrentDevice(); if (device == null) { return false; } String prop; try { prop = device.getPropertySync(PROP_GFX_PROFILING); return Boolean.valueOf(prop); } catch (Exception e) { return false; } } private boolean enableGfxProfiling() { IDevice device = getCurrentDevice(); if (device == null) { return false; } try { device.executeShellCommand("setprop " + PROP_GFX_PROFILING + " true", new NullOutputReceiver()); } catch (Exception e) { return false; } return true; } private String getDumpsysCommand(int mode) { if (mode == MODE_GFXINFO) { Client c = getCurrentClient(); if (c == null) { return null; } ClientData cd = c.getClientData(); if (cd == null) { return null; } mGfxPackageName = cd.getClientDescription(); if (mGfxPackageName == null) { return null; } return "dumpsys gfxinfo " + mGfxPackageName; } else if (mode < DUMP_COMMAND.length) { return DUMP_COMMAND[mode]; } return null; } /** * Initializes temporary output file for executeShellCommand(). * * @throws IOException on file error */ IShellOutputReceiver initShellOutputBuffer(String header) throws IOException { File f = File.createTempFile("ddmsfile", ".txt"); f.deleteOnExit(); synchronized (RECEIVER_LOCK) { if (mLastOutputReceiver != null) { mLastOutputReceiver.cancel(); } mLastOutputReceiver = new ShellOutputReceiver(f, header); } return mLastOutputReceiver; } /** * Create our controls for the UI panel. */ @Override protected Control createControl(Composite parent) { Composite top = new Composite(parent, SWT.NONE); top.setLayout(new GridLayout(1, false)); top.setLayoutData(new GridData(GridData.FILL_BOTH)); Composite buttons = new Composite(top, SWT.NONE); buttons.setLayout(new RowLayout()); mDisplayMode = new Combo(buttons, SWT.PUSH); for (String mode : CAPTIONS) { mDisplayMode.add(mode); } mDisplayMode.select(mMode); mDisplayMode.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mMode = mDisplayMode.getSelectionIndex(); if (getCurrentDevice() != null) { loadFromDevice(); } } }); mFetchButton = new Button(buttons, SWT.PUSH); mFetchButton.setText("Update from Device"); mFetchButton.setEnabled(false); mFetchButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { loadFromDevice(); } }); mLabel = new Label(top, SWT.NONE); mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mChartComposite = new Composite(top, SWT.NONE); mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); mStackLayout = new StackLayout(); mChartComposite.setLayout(mStackLayout); mPieChartComposite = createPieChartComposite(mChartComposite); mStackedBarComposite = createStackedBarComposite(mChartComposite); mStackLayout.topControl = mPieChartComposite; return top; } private Composite createStackedBarComposite(Composite chartComposite) { mBarDataSet = new DefaultCategoryDataset(); JFreeChart chart = ChartFactory.createStackedBarChart("Per Frame Rendering Time", "Frame #", "Time (ms)", mBarDataSet, PlotOrientation.VERTICAL, true /* legend */, true /* tooltips */, false /* urls */); ChartComposite c = newChartComposite(chart, chartComposite); c.setLayoutData(new GridData(GridData.FILL_BOTH)); return c; } private Composite createPieChartComposite(Composite chartComposite) { mDataset = new DefaultPieDataset(); JFreeChart chart = ChartFactory.createPieChart("", mDataset, false /* legend */, true/* tooltips */, false /* urls */); ChartComposite c = newChartComposite(chart, chartComposite); c.setLayoutData(new GridData(GridData.FILL_BOTH)); return c; } private ChartComposite newChartComposite(JFreeChart chart, Composite parent) { return new ChartComposite(parent, SWT.BORDER, chart, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 3000, // max draw width. We don't want it to zoom, so we put a big number 3000, // max draw height. We don't want it to zoom, so we put a big number true, // off-screen buffer true, // properties true, // save true, // print false, // zoom true); } @Override public void clientChanged(final Client client, int changeMask) { // Don't care } /** * Helper to open a bugreport and skip to the specified section. * * @param file File to open * @return Reader to bugreport file * @throws java.io.IOException on file error */ private BufferedReader getBugreportReader(File file) throws IOException { return new BufferedReader(new FileReader(file)); } /** * Parse the time string generated by BatteryStats. * A typical new-format string is "11d 13h 45m 39s 999ms". * A typical old-format string is "12.3 sec". * @return time in ms */ private static long parseTimeMs(String s) { long total = 0; // Matches a single component e.g. "12.3 sec" or "45ms" Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)"); Matcher m = p.matcher(s); while (m.find()) { String label = m.group(2); if ("sec".equals(label)) { // Backwards compatibility with old time format total += (long) (Double.parseDouble(m.group(1)) * 1000); continue; } long value = Integer.parseInt(m.group(1)); if ("d".equals(label)) { total += value * 24 * 60 * 60 * 1000; } else if ("h".equals(label)) { total += value * 60 * 60 * 1000; } else if ("m".equals(label)) { total += value * 60 * 1000; } else if ("s".equals(label)) { total += value * 1000; } else if ("ms".equals(label)) { total += value; } } return total; } public static final class BugReportParser { public static final class DataValue { final String name; final double value; public DataValue(String n, double v) { name = n; value = v; } }; /** Components of the time it takes to draw a single frame. */ public static final class GfxProfileData { /** draw time (time spent building display lists) in ms */ final double draw; /** process time (time spent by Android's 2D renderer to execute display lists) (ms) */ final double process; /** execute time (time spent to send frame to the compositor) in ms */ final double execute; public GfxProfileData(double draw, double process, double execute) { this.draw = draw; this.process = process; this.execute = execute; } } public static List parseGfxInfo(BufferedReader br) throws IOException { Pattern headerPattern = Pattern.compile("\\s+Draw\\s+Process\\s+Execute"); String line = null; while ((line = br.readLine()) != null) { Matcher m = headerPattern.matcher(line); if (m.find()) { break; } } if (line == null) { return Collections.emptyList(); } // parse something like: " 0.85 1.10 0.61\n", 3 doubles basically Pattern dataPattern = Pattern.compile("(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)"); List data = new ArrayList(128); while ((line = br.readLine()) != null) { Matcher m = dataPattern.matcher(line); if (!m.find()) { break; } double draw = safeParseDouble(m.group(1)); double process = safeParseDouble(m.group(2)); double execute = safeParseDouble(m.group(3)); data.add(new GfxProfileData(draw, process, execute)); } return data; } /** * Processes wakelock information from bugreport. Updates mDataset with the * new data. * * @param br Reader providing the content * @throws IOException if error reading file */ public static List readWakelockDataset(BufferedReader br) throws IOException { List results = new ArrayList(); Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); double total = 0; boolean inCurrent = false; while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } if (line.startsWith("Current Battery Usage Statistics")) { inCurrent = true; } else if (inCurrent) { Matcher m = lockPattern.matcher(line); if (m.find()) { double value = parseTimeMs(m.group(2)) / 1000.; results.add(new DataValue(m.group(1), value)); total -= value; } else { m = totalPattern.matcher(line); if (m.find()) { total += parseTimeMs(m.group(1)) / 1000.; } } } } if (total > 0) { results.add(new DataValue("Unlocked", total)); } return results; } /** * Processes alarm information from bugreport. Updates mDataset with the new * data. * * @param br Reader providing the content * @throws IOException if error reading file */ public static List readAlarmDataset(BufferedReader br) throws IOException { List results = new ArrayList(); Pattern pattern = Pattern.compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } Matcher m = pattern.matcher(line); if (m.find()) { long count = Long.parseLong(m.group(1)); String name = m.group(2); results.add(new DataValue(name, count)); } } return results; } /** * Processes cpu load information from bugreport. Updates mDataset with the * new data. * * @param br Reader providing the content * @throws IOException if error reading file */ public static List readCpuDataset(BufferedReader br) throws IOException { List results = new ArrayList(); Pattern pattern1 = Pattern.compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); Pattern pattern2 = Pattern.compile("(\\S+)% (\\S+): (.+)% user . (.+)% kernel"); while (true) { String line = br.readLine(); if (line == null) { break; } line = line.trim(); if (line.startsWith("Load:")) { continue; } String name = ""; double user = 0, kernel = 0, both = 0; boolean found = false; // try pattern1 Matcher m = pattern1.matcher(line); if (m.find()) { found = true; name = m.group(1); both = safeParseLong(m.group(2)); user = safeParseLong(m.group(3)); kernel = safeParseLong(m.group(4)); } // try pattern2 m = pattern2.matcher(line); if (m.find()) { found = true; name = m.group(2); both = safeParseDouble(m.group(1)); user = safeParseDouble(m.group(3)); kernel = safeParseDouble(m.group(4)); } if (!found) { continue; } if ("TOTAL".equals(name)) { if (both < 100) { results.add(new DataValue("Idle", (100 - both))); } } else { // Try to make graphs more useful even with rounding; // log often has 0% user + 0% kernel = 1% total // We arbitrarily give extra to kernel if (user > 0) { results.add(new DataValue(name + " (user)", user)); } if (kernel > 0) { results.add(new DataValue(name + " (kernel)" , both - user)); } if (user == 0 && kernel == 0 && both > 0) { results.add(new DataValue(name, both)); } } } return results; } private static long safeParseLong(String s) { try { return Long.parseLong(s); } catch (NumberFormatException e) { return 0; } } private static double safeParseDouble(String s) { try { return Double.parseDouble(s); } catch (NumberFormatException e) { return 0; } } /** * Processes meminfo information from bugreport. Updates mDataset with the * new data. * * @param br Reader providing the content * @throws IOException if error reading file */ public static List readMeminfoDataset(BufferedReader br) throws IOException { List results = new ArrayList(); Pattern valuePattern = Pattern.compile("(\\d+) kB"); long total = 0; long other = 0; // Scan meminfo String line = null; while ((line = br.readLine()) != null) { if (line.contains("----")) { continue; } Matcher m = valuePattern.matcher(line); if (m.find()) { long kb = Long.parseLong(m.group(1)); if (line.startsWith("MemTotal")) { total = kb; } else if (line.startsWith("MemFree")) { results.add(new DataValue("Free", kb)); total -= kb; } else if (line.startsWith("Slab")) { results.add(new DataValue("Slab", kb)); total -= kb; } else if (line.startsWith("PageTables")) { results.add(new DataValue("PageTables", kb)); total -= kb; } else if (line.startsWith("Buffers") && kb > 0) { results.add(new DataValue("Buffers", kb)); total -= kb; } else if (line.startsWith("Inactive")) { results.add(new DataValue("Inactive", kb)); total -= kb; } else if (line.startsWith("MemFree")) { results.add(new DataValue("Free", kb)); total -= kb; } } else { break; } } List procRankResults = readProcRankDataset(br, line); for (DataValue procRank : procRankResults) { if (procRank.value > 2000) { // only show processes using > 2000K in memory results.add(procRank); } else { other += procRank.value; } total -= procRank.value; } if (other > 0) { results.add(new DataValue("Other", other)); } // The Pss calculation is not necessarily accurate as accounting memory to // a process is not accurate. So only if there really is unaccounted for memory do we // add it to the pie. if (total > 0) { results.add(new DataValue("Unknown", total)); } return results; } static List readProcRankDataset(BufferedReader br, String header) throws IOException { List results = new ArrayList(); if (header == null || !header.contains("PID")) { return results; } Splitter PROCRANK_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults(); List fields = Lists.newArrayList(PROCRANK_SPLITTER.split(header)); int pssIndex = fields.indexOf("Pss"); int cmdIndex = fields.indexOf("cmdline"); if (pssIndex == -1 || cmdIndex == -1) { return results; } String line; while ((line = br.readLine()) != null) { // Extract pss field from procrank output fields = Lists.newArrayList(PROCRANK_SPLITTER.split(line)); if (fields.size() < cmdIndex) { break; } String cmdline = fields.get(cmdIndex).replace("/system/bin/", ""); String pssInK = fields.get(pssIndex); if (pssInK.endsWith("K")) { pssInK = pssInK.substring(0, pssInK.length() - 1); } long pss = safeParseLong(pssInK); results.add(new DataValue(cmdline, pss)); } return results; } /** * Processes sync information from bugreport. Updates mDataset with the new * data. * * @param br Reader providing the content * @throws IOException if error reading file */ public static List readSyncDataset(BufferedReader br) throws IOException { List results = new ArrayList(); while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } if (line.startsWith(" |") && line.length() > 70) { String authority = line.substring(3, 18).trim(); String duration = line.substring(61, 70).trim(); // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime) String durParts[] = duration.split(":"); if (durParts.length == 2) { long dur = Long.parseLong(durParts[0]) * 60 + Long .parseLong(durParts[1]); results.add(new DataValue(authority, dur)); } else if (duration.length() == 3) { long dur = Long.parseLong(durParts[0]) * 3600 + Long.parseLong(durParts[1]) * 60 + Long .parseLong(durParts[2]); results.add(new DataValue(authority, dur)); } } } return results; } } private void readCpuDataset(BufferedReader br) throws IOException { updatePieDataSet(BugReportParser.readCpuDataset(br), ""); } private void readMeminfoDataset(BufferedReader br) throws IOException { updatePieDataSet(BugReportParser.readMeminfoDataset(br), "PSS in kB"); } private void readGfxInfoDataset(BufferedReader br) throws IOException { updateBarChartDataSet(BugReportParser.parseGfxInfo(br), mGfxPackageName == null ? "" : mGfxPackageName); } private void clearDataSet() { mLabel.setText(""); mDataset.clear(); mBarDataSet.clear(); } private void updatePieDataSet(final List data, final String label) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mLabel.setText(label); mStackLayout.topControl = mPieChartComposite; mChartComposite.layout(); for (BugReportParser.DataValue d : data) { mDataset.setValue(d.name, d.value); } } }); } private void updateBarChartDataSet(final List gfxProfileData, final String label) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mLabel.setText(label); mStackLayout.topControl = mStackedBarComposite; mChartComposite.layout(); for (int i = 0; i < gfxProfileData.size(); i++) { GfxProfileData d = gfxProfileData.get(i); String frameNumber = Integer.toString(i); mBarDataSet.addValue(d.draw, "Draw", frameNumber); mBarDataSet.addValue(d.process, "Process", frameNumber); mBarDataSet.addValue(d.execute, "Execute", frameNumber); } } }); } private class ShellOutputReceiver implements IShellOutputReceiver { private final OutputStream mStream; private final File mFile; private AtomicBoolean mCancelled = new AtomicBoolean(); public ShellOutputReceiver(File f, String header) { mFile = f; try { mStream = new FileOutputStream(f); } catch (FileNotFoundException e) { throw new IllegalArgumentException(e); } if (header != null) { byte[] data = header.getBytes(); addOutput(data, 0, data.length); } } @Override public void addOutput(byte[] data, int offset, int length) { try { mStream.write(data, offset, length); } catch (IOException e) { Log.e("DDMS", e); } } @Override public void flush() { try { mStream.close(); } catch (IOException e) { Log.e("DDMS", e); } if (!isCancelled()) { generateDataset(mFile); } } @Override public boolean isCancelled() { return mCancelled.get(); } public void cancel() { mCancelled.set(true); } public File getDataFile() { return mFile; } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java0100644 0000000 0000000 00000017066 12747325007 024023 0ustar000000000 0000000 /* * 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.ddmuilib; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; /** * Utility class to help using Table objects. * */ public final class TableHelper { /** * Create a TableColumn with the specified parameters. If a * PreferenceStore object and a preference entry name String * object are provided then the column will listen to change in its width * and update the preference store accordingly. * * @param parent The Table parent object * @param header The header string * @param style The column style * @param sample_text A sample text to figure out column width if preference * value is missing * @param pref_name The preference entry name for column width * @param prefs The preference store * @return The TableColumn object that was created */ public static TableColumn createTableColumn(Table parent, String header, int style, String sample_text, final String pref_name, final IPreferenceStore prefs) { // create the column TableColumn col = new TableColumn(parent, style); // if there is no pref store or the entry is missing, we use the sample // text and pack the column. // Otherwise we just read the width from the prefs and apply it. if (prefs == null || prefs.contains(pref_name) == false) { col.setText(sample_text); col.pack(); // init the prefs store with the current value if (prefs != null) { prefs.setValue(pref_name, col.getWidth()); } } else { col.setWidth(prefs.getInt(pref_name)); } // set the header col.setText(header); // if there is a pref store and a pref entry name, then we setup a // listener to catch column resize to put store the new width value. if (prefs != null && pref_name != null) { col.addControlListener(new ControlListener() { @Override public void controlMoved(ControlEvent e) { } @Override public void controlResized(ControlEvent e) { // get the new width int w = ((TableColumn)e.widget).getWidth(); // store in pref store prefs.setValue(pref_name, w); } }); } return col; } /** * Create a TreeColumn with the specified parameters. If a * PreferenceStore object and a preference entry name String * object are provided then the column will listen to change in its width * and update the preference store accordingly. * * @param parent The Table parent object * @param header The header string * @param style The column style * @param sample_text A sample text to figure out column width if preference * value is missing * @param pref_name The preference entry name for column width * @param prefs The preference store */ public static void createTreeColumn(Tree parent, String header, int style, String sample_text, final String pref_name, final IPreferenceStore prefs) { // create the column TreeColumn col = new TreeColumn(parent, style); // if there is no pref store or the entry is missing, we use the sample // text and pack the column. // Otherwise we just read the width from the prefs and apply it. if (prefs == null || prefs.contains(pref_name) == false) { col.setText(sample_text); col.pack(); // init the prefs store with the current value if (prefs != null) { prefs.setValue(pref_name, col.getWidth()); } } else { col.setWidth(prefs.getInt(pref_name)); } // set the header col.setText(header); // if there is a pref store and a pref entry name, then we setup a // listener to catch column resize to put store the new width value. if (prefs != null && pref_name != null) { col.addControlListener(new ControlListener() { @Override public void controlMoved(ControlEvent e) { } @Override public void controlResized(ControlEvent e) { // get the new width int w = ((TreeColumn)e.widget).getWidth(); // store in pref store prefs.setValue(pref_name, w); } }); } } /** * Create a TreeColumn with the specified parameters. If a * PreferenceStore object and a preference entry name String * object are provided then the column will listen to change in its width * and update the preference store accordingly. * * @param parent The Table parent object * @param header The header string * @param style The column style * @param width the width of the column if the preference value is missing * @param pref_name The preference entry name for column width * @param prefs The preference store */ public static void createTreeColumn(Tree parent, String header, int style, int width, final String pref_name, final IPreferenceStore prefs) { // create the column TreeColumn col = new TreeColumn(parent, style); // if there is no pref store or the entry is missing, we use the sample // text and pack the column. // Otherwise we just read the width from the prefs and apply it. if (prefs == null || prefs.contains(pref_name) == false) { col.setWidth(width); // init the prefs store with the current value if (prefs != null) { prefs.setValue(pref_name, width); } } else { col.setWidth(prefs.getInt(pref_name)); } // set the header col.setText(header); // if there is a pref store and a pref entry name, then we setup a // listener to catch column resize to put store the new width value. if (prefs != null && pref_name != null) { col.addControlListener(new ControlListener() { @Override public void controlMoved(ControlEvent e) { } @Override public void controlResized(ControlEvent e) { // get the new width int w = ((TreeColumn)e.widget).getWidth(); // store in pref store prefs.setValue(pref_name, w); } }); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java0100644 0000000 0000000 00000010501 12747325007 023626 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import java.util.Arrays; /** * Base class for panel containing Table that need to support copy-paste-selectAll */ public abstract class TablePanel extends ClientDisplayPanel { private ITableFocusListener mGlobalListener; /** * Sets a TableFocusListener which will be notified when one of the tables * gets or loses focus. * * @param listener */ public void setTableFocusListener(ITableFocusListener listener) { // record the global listener, to make sure table created after // this call will still be setup. mGlobalListener = listener; setTableFocusListener(); } /** * Sets up the Table of object of the panel to work with the global listener.
* Default implementation does nothing. */ protected void setTableFocusListener() { } /** * Sets up a Table object to notify the global Table Focus listener when it * gets or loses the focus. * * @param table the Table object. * @param colStart * @param colEnd */ protected final void addTableToFocusListener(final Table table, final int colStart, final int colEnd) { // create the activator for this table final IFocusedTableActivator activator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { int[] selection = table.getSelectionIndices(); // we need to sort the items to be sure. Arrays.sort(selection); // all lines must be concatenated. StringBuilder sb = new StringBuilder(); // loop on the selection and output the file. for (int i : selection) { TableItem item = table.getItem(i); for (int c = colStart ; c <= colEnd ; c++) { sb.append(item.getText(c)); sb.append('\t'); } sb.append('\n'); } // now add that to the clipboard if the string has content String data = sb.toString(); if (data != null && data.length() > 0) { clipboard.setContents( new Object[] { data }, new Transfer[] { TextTransfer.getInstance() }); } } @Override public void selectAll() { table.selectAll(); } }; // add the focus listener on the table to notify the global listener table.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { mGlobalListener.focusGained(activator); } @Override public void focusLost(FocusEvent e) { mGlobalListener.focusLost(activator); } }); } /** * Sets up a Table object to notify the global Table Focus listener when it * gets or loses the focus.
* When the copy method is invoked, all columns are put in the clipboard, separated * by tabs * * @param table the Table object. */ protected final void addTableToFocusListener(final Table table) { addTableToFocusListener(table, 0, table.getColumnCount()-1); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java0100644 0000000 0000000 00000050731 12747325007 024017 0ustar000000000 0000000 /* * 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.ddmuilib; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ThreadInfo; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Table; import java.util.Date; /** * Base class for our information panels. */ public class ThreadPanel extends TablePanel { private final static String PREFS_THREAD_COL_ID = "threadPanel.Col0"; //$NON-NLS-1$ private final static String PREFS_THREAD_COL_TID = "threadPanel.Col1"; //$NON-NLS-1$ private final static String PREFS_THREAD_COL_STATUS = "threadPanel.Col2"; //$NON-NLS-1$ private final static String PREFS_THREAD_COL_UTIME = "threadPanel.Col3"; //$NON-NLS-1$ private final static String PREFS_THREAD_COL_STIME = "threadPanel.Col4"; //$NON-NLS-1$ private final static String PREFS_THREAD_COL_NAME = "threadPanel.Col5"; //$NON-NLS-1$ private final static String PREFS_THREAD_SASH = "threadPanel.sash"; //$NON-NLS-1$ private static final String PREFS_STACK_COLUMN = "threadPanel.stack.col0"; //$NON-NLS-1$ private Display mDisplay; private Composite mBase; private Label mNotEnabled; private Label mNotSelected; private Composite mThreadBase; private Table mThreadTable; private TableViewer mThreadViewer; private Composite mStackTraceBase; private Button mRefreshStackTraceButton; private Label mStackTraceTimeLabel; private StackTracePanel mStackTracePanel; private Table mStackTraceTable; /** Indicates if a timer-based Runnable is current requesting thread updates regularly. */ private boolean mMustStopRecurringThreadUpdate = false; /** Flag to tell the recurring thread update to stop running */ private boolean mRecurringThreadUpdateRunning = false; private Object mLock = new Object(); private static final String[] THREAD_STATUS = { "Zombie", "Runnable", "TimedWait", "Monitor", "Wait", "Initializing", "Starting", "Native", "VmWait", "Suspended" }; /** * Content Provider to display the threads of a client. * Expected input is a {@link Client} object. */ private static class ThreadContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof Client) { return ((Client)inputElement).getClientData().getThreads(); } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } } /** * A Label Provider to use with {@link ThreadContentProvider}. It expects the elements to be * of type {@link ThreadInfo}. */ private static class ThreadLabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof ThreadInfo) { ThreadInfo thread = (ThreadInfo)element; switch (columnIndex) { case 0: return (thread.isDaemon() ? "*" : "") + //$NON-NLS-1$ //$NON-NLS-2$ String.valueOf(thread.getThreadId()); case 1: return String.valueOf(thread.getTid()); case 2: if (thread.getStatus() >= 0 && thread.getStatus() < THREAD_STATUS.length) return THREAD_STATUS[thread.getStatus()]; return "unknown"; case 3: return String.valueOf(thread.getUtime()); case 4: return String.valueOf(thread.getStime()); case 5: return thread.getThreadName(); } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } /** * Create our control(s). */ @Override protected Control createControl(Composite parent) { mDisplay = parent.getDisplay(); final IPreferenceStore store = DdmUiPreferences.getStore(); mBase = new Composite(parent, SWT.NONE); mBase.setLayout(new StackLayout()); // UI for thread not enabled mNotEnabled = new Label(mBase, SWT.CENTER | SWT.WRAP); mNotEnabled.setText("Thread updates not enabled for selected client\n" + "(use toolbar button to enable)"); // UI for not client selected mNotSelected = new Label(mBase, SWT.CENTER | SWT.WRAP); mNotSelected.setText("no client is selected"); // base composite for selected client with enabled thread update. mThreadBase = new Composite(mBase, SWT.NONE); mThreadBase.setLayout(new FormLayout()); // table above the sash mThreadTable = new Table(mThreadBase, SWT.MULTI | SWT.FULL_SELECTION); mThreadTable.setHeaderVisible(true); mThreadTable.setLinesVisible(true); TableHelper.createTableColumn( mThreadTable, "ID", SWT.RIGHT, "888", //$NON-NLS-1$ PREFS_THREAD_COL_ID, store); TableHelper.createTableColumn( mThreadTable, "Tid", SWT.RIGHT, "88888", //$NON-NLS-1$ PREFS_THREAD_COL_TID, store); TableHelper.createTableColumn( mThreadTable, "Status", SWT.LEFT, "timed-wait", //$NON-NLS-1$ PREFS_THREAD_COL_STATUS, store); TableHelper.createTableColumn( mThreadTable, "utime", SWT.RIGHT, "utime", //$NON-NLS-1$ PREFS_THREAD_COL_UTIME, store); TableHelper.createTableColumn( mThreadTable, "stime", SWT.RIGHT, "utime", //$NON-NLS-1$ PREFS_THREAD_COL_STIME, store); TableHelper.createTableColumn( mThreadTable, "Name", SWT.LEFT, "android.class.ReallyLongClassName.MethodName", //$NON-NLS-1$ PREFS_THREAD_COL_NAME, store); mThreadViewer = new TableViewer(mThreadTable); mThreadViewer.setContentProvider(new ThreadContentProvider()); mThreadViewer.setLabelProvider(new ThreadLabelProvider()); mThreadViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { requestThreadStackTrace(getThreadSelection(event.getSelection())); } }); mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { @Override public void doubleClick(DoubleClickEvent event) { requestThreadStackTrace(getThreadSelection(event.getSelection())); } }); // the separating sash final Sash sash = new Sash(mThreadBase, SWT.HORIZONTAL); Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); sash.setBackground(darkGray); // the UI below the sash mStackTraceBase = new Composite(mThreadBase, SWT.NONE); mStackTraceBase.setLayout(new GridLayout(2, false)); mRefreshStackTraceButton = new Button(mStackTraceBase, SWT.PUSH); mRefreshStackTraceButton.setText("Refresh"); mRefreshStackTraceButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { requestThreadStackTrace(getThreadSelection(null)); } }); mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mStackTracePanel = new StackTracePanel(); mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, PREFS_STACK_COLUMN, store); GridData gd; mStackTraceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); gd.horizontalSpan = 2; // now setup the sash. // form layout data FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); mThreadTable.setLayoutData(data); final FormData sashData = new FormData(); if (store != null && store.contains(PREFS_THREAD_SASH)) { sashData.top = new FormAttachment(0, store.getInt(PREFS_THREAD_SASH)); } else { sashData.top = new FormAttachment(50,0); // 50% across } sashData.left = new FormAttachment(0, 0); sashData.right = new FormAttachment(100, 0); sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(sash, 0); data.bottom = new FormAttachment(100, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); mStackTraceBase.setLayoutData(data); // allow resizes, but cap at minPanelWidth sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = mThreadBase.getClientArea(); int bottom = panelRect.height - sashRect.height - 100; e.y = Math.max(Math.min(e.y, bottom), 100); if (e.y != sashRect.y) { sashData.top = new FormAttachment(0, e.y); store.setValue(PREFS_THREAD_SASH, e.y); mThreadBase.layout(); } } }); ((StackLayout)mBase.getLayout()).topControl = mNotSelected; return mBase; } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mThreadTable.setFocus(); } /** * Sent when an existing client information changed. *

* This is sent from a non UI thread. * @param client the updated client. * @param changeMask the bit mask describing the changed properties. It can contain * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} * * @see IClientChangeListener#clientChanged(Client, int) */ @Override public void clientChanged(final Client client, int changeMask) { if (client == getCurrentClient()) { if ((changeMask & Client.CHANGE_THREAD_MODE) != 0 || (changeMask & Client.CHANGE_THREAD_DATA) != 0) { try { mThreadTable.getDisplay().asyncExec(new Runnable() { @Override public void run() { clientSelected(); } }); } catch (SWTException e) { // widget is disposed, we do nothing } } else if ((changeMask & Client.CHANGE_THREAD_STACKTRACE) != 0) { try { mThreadTable.getDisplay().asyncExec(new Runnable() { @Override public void run() { updateThreadStackCall(); } }); } catch (SWTException e) { // widget is disposed, we do nothing } } } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()}. */ @Override public void deviceSelected() { // pass } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()}. */ @Override public void clientSelected() { if (mThreadTable.isDisposed()) { return; } Client client = getCurrentClient(); mStackTracePanel.setCurrentClient(client); if (client != null) { if (!client.isThreadUpdateEnabled()) { ((StackLayout)mBase.getLayout()).topControl = mNotEnabled; mThreadViewer.setInput(null); // if we are currently updating the thread, stop doing it. mMustStopRecurringThreadUpdate = true; } else { ((StackLayout)mBase.getLayout()).topControl = mThreadBase; mThreadViewer.setInput(client); synchronized (mLock) { // if we're not updating we start the process if (mRecurringThreadUpdateRunning == false) { startRecurringThreadUpdate(); } else if (mMustStopRecurringThreadUpdate) { // else if there's a runnable that's still going to get called, lets // simply cancel the stop, and keep going mMustStopRecurringThreadUpdate = false; } } } } else { ((StackLayout)mBase.getLayout()).topControl = mNotSelected; mThreadViewer.setInput(null); } mBase.layout(); } private void requestThreadStackTrace(ThreadInfo selectedThread) { if (selectedThread != null) { Client client = (Client) mThreadViewer.getInput(); if (client != null) { client.requestThreadStackTrace(selectedThread.getThreadId()); } } } /** * Updates the stack call of the currently selected thread. *

* This must be called from the UI thread. */ private void updateThreadStackCall() { Client client = getCurrentClient(); if (client != null) { // get the current selection in the ThreadTable ThreadInfo selectedThread = getThreadSelection(null); if (selectedThread != null) { updateThreadStackTrace(selectedThread); } else { updateThreadStackTrace(null); } } } /** * updates the stackcall of the specified thread. If null the UI is emptied * of current data. * @param thread */ private void updateThreadStackTrace(ThreadInfo thread) { mStackTracePanel.setViewerInput(thread); if (thread != null) { mRefreshStackTraceButton.setEnabled(true); long stackcallTime = thread.getStackCallTime(); if (stackcallTime != 0) { String label = new Date(stackcallTime).toString(); mStackTraceTimeLabel.setText(label); } else { mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ } } else { mRefreshStackTraceButton.setEnabled(true); mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ } } @Override protected void setTableFocusListener() { addTableToFocusListener(mThreadTable); addTableToFocusListener(mStackTraceTable); } /** * Initiate recurring events. We use a shorter "initialWait" so we do the * first execution sooner. We don't do it immediately because we want to * give the clients a chance to get set up. */ private void startRecurringThreadUpdate() { mRecurringThreadUpdateRunning = true; int initialWait = 1000; mDisplay.timerExec(initialWait, new Runnable() { @Override public void run() { synchronized (mLock) { // lets check we still want updates. if (mMustStopRecurringThreadUpdate == false) { Client client = getCurrentClient(); if (client != null) { client.requestThreadUpdate(); mDisplay.timerExec( DdmUiPreferences.getThreadRefreshInterval() * 1000, this); } else { // we don't have a Client, which means the runnable is not // going to be called through the timer. We reset the running flag. mRecurringThreadUpdateRunning = false; } } else { // else actually stops (don't call the timerExec) and reset the flags. mRecurringThreadUpdateRunning = false; mMustStopRecurringThreadUpdate = false; } } } }); } /** * Returns the current thread selection or null if none is found. * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection * is returned, otherwise, the ISelection returned by * {@link TableViewer#getSelection()} is used. * @param selection the {@link ISelection} to use, or null */ private ThreadInfo getThreadSelection(ISelection selection) { if (selection == null) { selection = mThreadViewer.getSelection(); } if (selection instanceof IStructuredSelection) { IStructuredSelection structuredSelection = (IStructuredSelection)selection; Object object = structuredSelection.getFirstElement(); if (object instanceof ThreadInfo) { return (ThreadInfo)object; } } return null; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/0040755 0000000 0000000 00000000000 12747325007 022422 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java0100644 0000000 0000000 00000002477 12747325007 025773 0ustar000000000 0000000 /* * 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.ddmuilib.actions; /** * Common interface for basic action handling. This allows the common ui * components to access ToolItem or Action the same way. */ public interface ICommonAction { /** * Sets the enabled state of this action. * @param enabled true to enable, and * false to disable */ public void setEnabled(boolean enabled); /** * Sets the checked status of this action. * @param checked the new checked status */ public void setChecked(boolean checked); /** * Sets the {@link Runnable} that will be executed when the action is triggered. */ public void setRunnable(Runnable runnable); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java0100644 0000000 0000000 00000004361 12747325007 026160 0ustar000000000 0000000 /* * 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.ddmuilib.actions; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; /** * Wrapper around {@link ToolItem} to implement {@link ICommonAction} */ public class ToolItemAction implements ICommonAction { public ToolItem item; public ToolItemAction(ToolBar parent, int style) { item = new ToolItem(parent, style); } /** * Sets the enabled state of this action. * @param enabled true to enable, and * false to disable * @see ICommonAction#setChecked(boolean) */ @Override public void setChecked(boolean checked) { item.setSelection(checked); } /** * Sets the enabled state of this action. * @param enabled true to enable, and * false to disable * @see ICommonAction#setEnabled(boolean) */ @Override public void setEnabled(boolean enabled) { item.setEnabled(enabled); } /** * Sets the {@link Runnable} that will be executed when the action is triggered (through * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}). * @see ICommonAction#setRunnable(Runnable) */ @Override public void setRunnable(final Runnable runnable) { item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { runnable.run(); } }); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/0040755 0000000 0000000 00000000000 12747325007 023134 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java0100644 0000000 0000000 00000002141 12747325007 025477 0ustar000000000 0000000 /* * 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.ddmuilib.annotation; import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Simple utility annotation used only to mark methods that are executed on the UI thread. * This annotation's sole purpose is to help reading the source code. It has no additional effect. */ @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.SOURCE) public @interface UiThread { } ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java0100644 0000000 0000000 00000002151 12747325007 026374 0ustar000000000 0000000 /* * 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.ddmuilib.annotation; import java.lang.annotation.Target; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Simple utility annotation used only to mark methods that are not executed on the UI thread. * This annotation's sole purpose is to help reading the source code. It has no additional effect. */ @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.SOURCE) public @interface WorkerThread { } ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/0040755 0000000 0000000 00000000000 12747325007 022424 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java0100644 0000000 0000000 00000005505 12747325007 025320 0ustar000000000 0000000 /* * 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.ddmuilib.console; /** * Static Console used to ouput messages. By default outputs the message to System.out and * System.err, but can receive a IDdmConsole object which will actually do something. */ public class DdmConsole { private static IDdmConsole mConsole; /** * Prints a message to the android console. * @param message the message to print * @param forceDisplay if true, this force the console to be displayed. */ public static void printErrorToConsole(String message) { if (mConsole != null) { mConsole.printErrorToConsole(message); } else { System.err.println(message); } } /** * Prints several messages to the android console. * @param messages the messages to print * @param forceDisplay if true, this force the console to be displayed. */ public static void printErrorToConsole(String[] messages) { if (mConsole != null) { mConsole.printErrorToConsole(messages); } else { for (String message : messages) { System.err.println(message); } } } /** * Prints a message to the android console. * @param message the message to print * @param forceDisplay if true, this force the console to be displayed. */ public static void printToConsole(String message) { if (mConsole != null) { mConsole.printToConsole(message); } else { System.out.println(message); } } /** * Prints several messages to the android console. * @param messages the messages to print * @param forceDisplay if true, this force the console to be displayed. */ public static void printToConsole(String[] messages) { if (mConsole != null) { mConsole.printToConsole(messages); } else { for (String message : messages) { System.out.println(message); } } } /** * Sets a IDdmConsole to override the default behavior of the console * @param console The new IDdmConsole * **/ public static void setConsole(IDdmConsole console) { mConsole = console; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java0100644 0000000 0000000 00000002552 12747325007 025430 0ustar000000000 0000000 /* * 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.ddmuilib.console; /** * DDMS console interface. */ public interface IDdmConsole { /** * Prints a message to the android console. * @param message the message to print */ public void printErrorToConsole(String message); /** * Prints several messages to the android console. * @param messages the messages to print */ public void printErrorToConsole(String[] messages); /** * Prints a message to the android console. * @param message the message to print */ public void printToConsole(String message); /** * Prints several messages to the android console. * @param messages the messages to print */ public void printToConsole(String[] messages); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/0040755 0000000 0000000 00000000000 12747325007 022622 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java0100644 0000000 0000000 00000013436 12747325007 027736 0ustar000000000 0000000 /* * 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.ddmuilib.explorer; import com.android.ddmlib.FileListingService; import com.android.ddmlib.FileListingService.FileEntry; import com.android.ddmlib.FileListingService.IListingReceiver; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; /** * Content provider class for device Explorer. */ class DeviceContentProvider implements ITreeContentProvider { private TreeViewer mViewer; private FileListingService mFileListingService; private FileEntry mRootEntry; private IListingReceiver sListingReceiver = new IListingReceiver() { @Override public void setChildren(final FileEntry entry, FileEntry[] children) { final Tree t = mViewer.getTree(); if (t != null && t.isDisposed() == false) { Display display = t.getDisplay(); if (display.isDisposed() == false) { display.asyncExec(new Runnable() { @Override public void run() { if (t.isDisposed() == false) { // refresh the entry. mViewer.refresh(entry); // force it open, since on linux and windows // when getChildren() returns null, the node is // not considered expanded. mViewer.setExpandedState(entry, true); } } }); } } } @Override public void refreshEntry(final FileEntry entry) { final Tree t = mViewer.getTree(); if (t != null && t.isDisposed() == false) { Display display = t.getDisplay(); if (display.isDisposed() == false) { display.asyncExec(new Runnable() { @Override public void run() { if (t.isDisposed() == false) { // refresh the entry. mViewer.refresh(entry); } } }); } } } }; /** * */ public DeviceContentProvider() { } public void setListingService(FileListingService fls) { mFileListingService = fls; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object) */ @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof FileEntry) { FileEntry parentEntry = (FileEntry)parentElement; Object[] oldEntries = parentEntry.getCachedChildren(); Object[] newEntries = mFileListingService.getChildren(parentEntry, true, sListingReceiver); if (newEntries != null) { return newEntries; } else { // if null was returned, this means the cache was not valid, // and a thread was launched for ls. sListingReceiver will be // notified with the new entries. return oldEntries; } } return new Object[0]; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object) */ @Override public Object getParent(Object element) { if (element instanceof FileEntry) { FileEntry entry = (FileEntry)element; return entry.getParent(); } return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object) */ @Override public boolean hasChildren(Object element) { if (element instanceof FileEntry) { FileEntry entry = (FileEntry)element; return entry.getType() == FileListingService.TYPE_DIRECTORY; } return false; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object) */ @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof FileEntry) { FileEntry entry = (FileEntry)inputElement; if (entry.isRoot()) { return getChildren(mRootEntry); } } return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IContentProvider#dispose() */ @Override public void dispose() { } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object) */ @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { if (viewer instanceof TreeViewer) { mViewer = (TreeViewer)viewer; } if (newInput instanceof FileEntry) { mRootEntry = (FileEntry)newInput; } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java0100644 0000000 0000000 00000105611 12747325007 026406 0ustar000000000 0000000 /* * 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.ddmuilib.explorer; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.DdmConstants; import com.android.ddmlib.FileListingService; import com.android.ddmlib.FileListingService.FileEntry; import com.android.ddmlib.IDevice; import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.SyncException; import com.android.ddmlib.SyncService; import com.android.ddmlib.SyncService.ISyncProgressMonitor; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.Panel; import com.android.ddmuilib.SyncProgressHelper; import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; import com.android.ddmuilib.TableHelper; import com.android.ddmuilib.actions.ICommonAction; import com.android.ddmuilib.console.DdmConsole; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.dialogs.InputDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.ViewerDropAdapter; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.FileTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.dnd.TransferData; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.DirectoryDialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Device filesystem explorer class. */ public class DeviceExplorer extends Panel { private final static String TRACE_KEY_EXT = ".key"; // $NON-NLS-1S private final static String TRACE_DATA_EXT = ".data"; // $NON-NLS-1S private static Pattern mKeyFilePattern = Pattern.compile( "(.+)\\" + TRACE_KEY_EXT); // $NON-NLS-1S private static Pattern mDataFilePattern = Pattern.compile( "(.+)\\" + TRACE_DATA_EXT); // $NON-NLS-1S public static String COLUMN_NAME = "android.explorer.name"; //$NON-NLS-1S public static String COLUMN_SIZE = "android.explorer.size"; //$NON-NLS-1S public static String COLUMN_DATE = "android.explorer.data"; //$NON-NLS-1S public static String COLUMN_TIME = "android.explorer.time"; //$NON-NLS-1S public static String COLUMN_PERMISSIONS = "android.explorer.permissions"; // $NON-NLS-1S public static String COLUMN_INFO = "android.explorer.info"; // $NON-NLS-1S private Composite mParent; private TreeViewer mTreeViewer; private Tree mTree; private DeviceContentProvider mContentProvider; private ICommonAction mPushAction; private ICommonAction mPullAction; private ICommonAction mDeleteAction; private ICommonAction mCreateNewFolderAction; private Image mFileImage; private Image mFolderImage; private Image mPackageImage; private Image mOtherImage; private IDevice mCurrentDevice; private String mDefaultSave; public DeviceExplorer() { } /** * Sets custom images for the device explorer. If none are set then defaults are used. * This can be useful to set platform-specific explorer icons. * * This should be called before {@link #createControl(Composite)}. * * @param fileImage the icon to represent a file. * @param folderImage the icon to represent a folder. * @param packageImage the icon to represent an apk. * @param otherImage the icon to represent other types of files. */ public void setCustomImages(Image fileImage, Image folderImage, Image packageImage, Image otherImage) { mFileImage = fileImage; mFolderImage = folderImage; mPackageImage = packageImage; mOtherImage = otherImage; } /** * Sets the actions so that the device explorer can enable/disable them based on the current * selection * @param pushAction * @param pullAction * @param deleteAction * @param createNewFolderAction */ public void setActions(ICommonAction pushAction, ICommonAction pullAction, ICommonAction deleteAction, ICommonAction createNewFolderAction) { mPushAction = pushAction; mPullAction = pullAction; mDeleteAction = deleteAction; mCreateNewFolderAction = createNewFolderAction; } /** * Creates a control capable of displaying some information. This is * called once, when the application is initializing, from the UI thread. */ @Override protected Control createControl(Composite parent) { mParent = parent; parent.setLayout(new FillLayout()); ImageLoader loader = ImageLoader.getDdmUiLibLoader(); if (mFileImage == null) { mFileImage = loader.loadImage("file.png", mParent.getDisplay()); } if (mFolderImage == null) { mFolderImage = loader.loadImage("folder.png", mParent.getDisplay()); } if (mPackageImage == null) { mPackageImage = loader.loadImage("android.png", mParent.getDisplay()); } if (mOtherImage == null) { // TODO: find a default image for other. } mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL); mTree.setHeaderVisible(true); IPreferenceStore store = DdmUiPreferences.getStore(); // create columns TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$ TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT, "000000", COLUMN_SIZE, store); //$NON-NLS-1$ TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT, "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$ TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT, "20:54", COLUMN_TIME, store); //$NON-NLS-1$ TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT, "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$ TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT, "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$ // create the jface wrapper mTreeViewer = new TreeViewer(mTree); // setup data provider mContentProvider = new DeviceContentProvider(); mTreeViewer.setContentProvider(mContentProvider); mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage, mFolderImage, mPackageImage, mOtherImage)); // setup a listener for selection mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { ISelection sel = event.getSelection(); if (sel.isEmpty()) { mPullAction.setEnabled(false); mPushAction.setEnabled(false); mDeleteAction.setEnabled(false); mCreateNewFolderAction.setEnabled(false); return; } if (sel instanceof IStructuredSelection) { IStructuredSelection selection = (IStructuredSelection) sel; Object element = selection.getFirstElement(); if (element == null) return; if (element instanceof FileEntry) { mPullAction.setEnabled(true); mPushAction.setEnabled(selection.size() == 1); if (selection.size() == 1) { FileEntry entry = (FileEntry) element; setDeleteEnabledState(entry); mCreateNewFolderAction.setEnabled(entry.isDirectory()); } else { mDeleteAction.setEnabled(false); } } } } }); // add support for double click mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { @Override public void doubleClick(DoubleClickEvent event) { ISelection sel = event.getSelection(); if (sel instanceof IStructuredSelection) { IStructuredSelection selection = (IStructuredSelection) sel; if (selection.size() == 1) { FileEntry entry = (FileEntry)selection.getFirstElement(); String name = entry.getName(); FileEntry parentEntry = entry.getParent(); // can't really do anything with no parent if (parentEntry == null) { return; } // check this is a file like we want. Matcher m = mKeyFilePattern.matcher(name); if (m.matches()) { // get the name w/o the extension String baseName = m.group(1); // add the data extension String dataName = baseName + TRACE_DATA_EXT; FileEntry dataEntry = parentEntry.findChild(dataName); handleTraceDoubleClick(baseName, entry, dataEntry); } else { m = mDataFilePattern.matcher(name); if (m.matches()) { // get the name w/o the extension String baseName = m.group(1); // add the key extension String keyName = baseName + TRACE_KEY_EXT; FileEntry keyEntry = parentEntry.findChild(keyName); handleTraceDoubleClick(baseName, keyEntry, entry); } } } } } }); // setup drop listener mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, new Transfer[] { FileTransfer.getInstance() }, new ViewerDropAdapter(mTreeViewer) { @Override public boolean performDrop(Object data) { // get the item on which we dropped the item(s) FileEntry target = (FileEntry)getCurrentTarget(); // in case we drop at the same level as root if (target == null) { return false; } // if the target is not a directory, we get the parent directory if (target.isDirectory() == false) { target = target.getParent(); } if (target == null) { return false; } // get the list of files to drop String[] files = (String[])data; // do the drop pushFiles(files, target); // we need to finish with a refresh refresh(target); return true; } @Override public boolean validateDrop(Object target, int operation, TransferData transferType) { if (target == null) { return false; } // convert to the real item FileEntry targetEntry = (FileEntry)target; // if the target is not a directory, we get the parent directory if (targetEntry.isDirectory() == false) { target = targetEntry.getParent(); } if (target == null) { return false; } return true; } }); // create and start the refresh thread new Thread("Device Ls refresher") { @Override public void run() { while (true) { try { sleep(FileListingService.REFRESH_RATE); } catch (InterruptedException e) { return; } if (mTree != null && mTree.isDisposed() == false) { Display display = mTree.getDisplay(); if (display.isDisposed() == false) { display.asyncExec(new Runnable() { @Override public void run() { if (mTree.isDisposed() == false) { mTreeViewer.refresh(true); } } }); } else { return; } } else { return; } } } }.start(); return mTree; } @Override protected void postCreation() { } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mTree.setFocus(); } /** * Processes a double click on a trace file * @param baseName the base name of the 2 files. * @param keyEntry The FileEntry for the .key file. * @param dataEntry The FileEntry for the .data file. */ private void handleTraceDoubleClick(String baseName, FileEntry keyEntry, FileEntry dataEntry) { // first we need to download the files. File keyFile; File dataFile; String path; try { // create a temp file for keyFile File f = File.createTempFile(baseName, DdmConstants.DOT_TRACE); f.delete(); f.mkdir(); path = f.getAbsolutePath(); keyFile = new File(path + File.separator + keyEntry.getName()); dataFile = new File(path + File.separator + dataEntry.getName()); } catch (IOException e) { return; } // download the files try { SyncService sync = mCurrentDevice.getSyncService(); if (sync != null) { ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor(); sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor); sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor); // now that we have the file, we need to launch traceview String[] command = new String[2]; command[0] = DdmUiPreferences.getTraceview(); command[1] = path + File.separator + baseName; try { final Process p = Runtime.getRuntime().exec(command); // create a thread for the output new Thread("Traceview output") { @Override public void run() { // create a buffer to read the stderr output InputStreamReader is = new InputStreamReader(p.getErrorStream()); BufferedReader resultReader = new BufferedReader(is); // read the lines as they come. if null is returned, it's // because the process finished try { while (true) { String line = resultReader.readLine(); if (line != null) { DdmConsole.printErrorToConsole("Traceview: " + line); } else { break; } } // get the return code from the process p.waitFor(); } catch (IOException e) { } catch (InterruptedException e) { } } }.start(); } catch (IOException e) { } } } catch (IOException e) { DdmConsole.printErrorToConsole(String.format( "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); return; } catch (SyncException e) { if (e.wasCanceled() == false) { DdmConsole.printErrorToConsole(String.format( "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); return; } } catch (TimeoutException e) { DdmConsole.printErrorToConsole(String.format( "Failed to pull %1$s: timeout", keyEntry.getName())); } catch (AdbCommandRejectedException e) { DdmConsole.printErrorToConsole(String.format( "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); } } /** * Pull the current selection on the local drive. This method displays * a dialog box to let the user select where to store the file(s) and * folder(s). */ public void pullSelection() { // get the selection TreeItem[] items = mTree.getSelection(); // name of the single file pull, or null if we're pulling a directory // or more than one object. String filePullName = null; FileEntry singleEntry = null; // are we pulling a single file? if (items.length == 1) { singleEntry = (FileEntry)items[0].getData(); if (singleEntry.getType() == FileListingService.TYPE_FILE) { filePullName = singleEntry.getName(); } } // where do we save by default? String defaultPath = mDefaultSave; if (defaultPath == null) { defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ } if (filePullName != null) { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); fileDialog.setText("Get Device File"); fileDialog.setFileName(filePullName); fileDialog.setFilterPath(defaultPath); String fileName = fileDialog.open(); if (fileName != null) { mDefaultSave = fileDialog.getFilterPath(); pullFile(singleEntry, fileName); } } else { DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE); directoryDialog.setText("Get Device Files/Folders"); directoryDialog.setFilterPath(defaultPath); String directoryName = directoryDialog.open(); if (directoryName != null) { pullSelection(items, directoryName); } } } /** * Push new file(s) and folder(s) into the current selection. Current * selection must be single item. If the current selection is not a * directory, the parent directory is used. * This method displays a dialog to let the user choose file to push to * the device. */ public void pushIntoSelection() { // get the name of the object we're going to pull TreeItem[] items = mTree.getSelection(); if (items.length == 0) { return; } FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN); String fileName; dlg.setText("Put File on Device"); // There should be only one. FileEntry entry = (FileEntry)items[0].getData(); dlg.setFileName(entry.getName()); String defaultPath = mDefaultSave; if (defaultPath == null) { defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ } dlg.setFilterPath(defaultPath); fileName = dlg.open(); if (fileName != null) { mDefaultSave = dlg.getFilterPath(); // we need to figure out the remote path based on the current selection type. String remotePath; FileEntry toRefresh = entry; if (entry.isDirectory()) { remotePath = entry.getFullPath(); } else { toRefresh = entry.getParent(); remotePath = toRefresh.getFullPath(); } pushFile(fileName, remotePath); mTreeViewer.refresh(toRefresh); } } public void deleteSelection() { // get the name of the object we're going to pull TreeItem[] items = mTree.getSelection(); if (items.length != 1) { return; } FileEntry entry = (FileEntry)items[0].getData(); final FileEntry parentEntry = entry.getParent(); // create the delete command String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$ try { mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { @Override public void addOutput(byte[] data, int offset, int length) { // pass // TODO get output to display errors if any. } @Override public void flush() { mTreeViewer.refresh(parentEntry); } @Override public boolean isCancelled() { return false; } }); } catch (IOException e) { // adb failed somehow, we do nothing. We should be displaying the error from the output // of the shell command. } catch (TimeoutException e) { // adb failed somehow, we do nothing. We should be displaying the error from the output // of the shell command. } catch (AdbCommandRejectedException e) { // adb failed somehow, we do nothing. We should be displaying the error from the output // of the shell command. } catch (ShellCommandUnresponsiveException e) { // adb failed somehow, we do nothing. We should be displaying the error from the output // of the shell command. } } public void createNewFolderInSelection() { TreeItem[] items = mTree.getSelection(); if (items.length != 1) { return; } final FileEntry entry = (FileEntry) items[0].getData(); if (entry.isDirectory()) { InputDialog inputDialog = new InputDialog(mTree.getShell(), "New Folder", "Please enter the new folder name", "New Folder", new IInputValidator() { @Override public String isValid(String newText) { if ((newText != null) && (newText.length() > 0) && (newText.trim().length() > 0) && (newText.indexOf('/') == -1) && (newText.indexOf('\\') == -1)) { return null; } else { return "Invalid name"; } } }); inputDialog.open(); String value = inputDialog.getValue(); if (value != null) { // create the mkdir command String command = "mkdir " + entry.getFullEscapedPath() //$NON-NLS-1$ + FileListingService.FILE_SEPARATOR + FileEntry.escape(value); try { mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { @Override public boolean isCancelled() { return false; } @Override public void flush() { mTreeViewer.refresh(entry); } @Override public void addOutput(byte[] data, int offset, int length) { String errorMessage; if (data != null) { errorMessage = new String(data); } else { errorMessage = ""; } Status status = new Status(IStatus.ERROR, "DeviceExplorer", 0, errorMessage, null); //$NON-NLS-1$ ErrorDialog.openError(mTree.getShell(), "New Folder Error", "New Folder Error", status); } }); } catch (TimeoutException e) { // adb failed somehow, we do nothing. We should be // displaying the error from the output of the shell // command. } catch (AdbCommandRejectedException e) { // adb failed somehow, we do nothing. We should be // displaying the error from the output of the shell // command. } catch (ShellCommandUnresponsiveException e) { // adb failed somehow, we do nothing. We should be // displaying the error from the output of the shell // command. } catch (IOException e) { // adb failed somehow, we do nothing. We should be // displaying the error from the output of the shell // command. } } } } /** * Force a full refresh of the explorer. */ public void refresh() { mTreeViewer.refresh(true); } /** * Sets the new device to explorer */ public void switchDevice(final IDevice device) { if (device != mCurrentDevice) { mCurrentDevice = device; // now we change the input. but we need to do that in the // ui thread. if (mTree.isDisposed() == false) { Display d = mTree.getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { if (mTree.isDisposed() == false) { // new service if (mCurrentDevice != null) { FileListingService fls = mCurrentDevice.getFileListingService(); mContentProvider.setListingService(fls); mTreeViewer.setInput(fls.getRoot()); } } } }); } } } /** * Refresh an entry from a non ui thread. * @param entry the entry to refresh. */ private void refresh(final FileEntry entry) { Display d = mTreeViewer.getTree().getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { mTreeViewer.refresh(entry); } }); } /** * Pulls the selection from a device. * @param items the tree selection the remote file on the device * @param localDirector the local directory in which to save the files. */ private void pullSelection(TreeItem[] items, final String localDirectory) { try { final SyncService sync = mCurrentDevice.getSyncService(); if (sync != null) { // make a list of the FileEntry. ArrayList entries = new ArrayList(); for (TreeItem item : items) { Object data = item.getData(); if (data instanceof FileEntry) { entries.add((FileEntry)data); } } final FileEntry[] entryArray = entries.toArray( new FileEntry[entries.size()]); SyncProgressHelper.run(new SyncRunnable() { @Override public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { sync.pull(entryArray, localDirectory, monitor); } @Override public void close() { sync.close(); } }, "Pulling file(s) from the device", mParent.getShell()); } } catch (SyncException e) { if (e.wasCanceled() == false) { DdmConsole.printErrorToConsole(String.format( "Failed to pull selection: %1$s", e.getMessage())); } } catch (Exception e) { DdmConsole.printErrorToConsole( "Failed to pull selection"); DdmConsole.printErrorToConsole(e.getMessage()); } } /** * Pulls a file from a device. * @param remote the remote file on the device * @param local the destination filepath */ private void pullFile(final FileEntry remote, final String local) { try { final SyncService sync = mCurrentDevice.getSyncService(); if (sync != null) { SyncProgressHelper.run(new SyncRunnable() { @Override public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { sync.pullFile(remote, local, monitor); } @Override public void close() { sync.close(); } }, String.format("Pulling %1$s from the device", remote.getName()), mParent.getShell()); } } catch (SyncException e) { if (e.wasCanceled() == false) { DdmConsole.printErrorToConsole(String.format( "Failed to pull selection: %1$s", e.getMessage())); } } catch (Exception e) { DdmConsole.printErrorToConsole( "Failed to pull selection"); DdmConsole.printErrorToConsole(e.getMessage()); } } /** * Pushes several files and directory into a remote directory. * @param localFiles * @param remoteDirectory */ private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) { try { final SyncService sync = mCurrentDevice.getSyncService(); if (sync != null) { SyncProgressHelper.run(new SyncRunnable() { @Override public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { sync.push(localFiles, remoteDirectory, monitor); } @Override public void close() { sync.close(); } }, "Pushing file(s) to the device", mParent.getShell()); } } catch (SyncException e) { if (e.wasCanceled() == false) { DdmConsole.printErrorToConsole(String.format( "Failed to push selection: %1$s", e.getMessage())); } } catch (Exception e) { DdmConsole.printErrorToConsole("Failed to push the items"); DdmConsole.printErrorToConsole(e.getMessage()); } } /** * Pushes a file on a device. * @param local the local filepath of the file to push * @param remoteDirectory the remote destination directory on the device */ private void pushFile(final String local, final String remoteDirectory) { try { final SyncService sync = mCurrentDevice.getSyncService(); if (sync != null) { // get the file name String[] segs = local.split(Pattern.quote(File.separator)); String name = segs[segs.length-1]; final String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR + name; SyncProgressHelper.run(new SyncRunnable() { @Override public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { sync.pushFile(local, remoteFile, monitor); } @Override public void close() { sync.close(); } }, String.format("Pushing %1$s to the device.", name), mParent.getShell()); } } catch (SyncException e) { if (e.wasCanceled() == false) { DdmConsole.printErrorToConsole(String.format( "Failed to push selection: %1$s", e.getMessage())); } } catch (Exception e) { DdmConsole.printErrorToConsole("Failed to push the item(s)."); DdmConsole.printErrorToConsole(e.getMessage()); } } /** * Sets the enabled state based on a FileEntry properties * @param element The selected FileEntry */ protected void setDeleteEnabledState(FileEntry element) { mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java0100644 0000000 0000000 00000011576 12747325007 027026 0ustar000000000 0000000 /* * 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.ddmuilib.explorer; import com.android.ddmlib.FileListingService; import com.android.ddmlib.FileListingService.FileEntry; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.swt.graphics.Image; /** * Label provider for the FileEntry. */ class FileLabelProvider implements ILabelProvider, ITableLabelProvider { private Image mFileImage; private Image mFolderImage; private Image mPackageImage; private Image mOtherImage; /** * Creates Label provider with custom images. * @param fileImage the Image to represent a file * @param folderImage the Image to represent a folder * @param packageImage the Image to represent a .apk file. If null, * fileImage is used instead. * @param otherImage the Image to represent all other entry type. */ public FileLabelProvider(Image fileImage, Image folderImage, Image packageImage, Image otherImage) { mFileImage = fileImage; mFolderImage = folderImage; mOtherImage = otherImage; if (packageImage != null) { mPackageImage = packageImage; } else { mPackageImage = fileImage; } } /** * Creates a label provider with default images. * */ public FileLabelProvider() { } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object) */ @Override public Image getImage(Object element) { return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object) */ @Override public String getText(Object element) { return null; } @Override public Image getColumnImage(Object element, int columnIndex) { if (columnIndex == 0) { if (element instanceof FileEntry) { FileEntry entry = (FileEntry)element; switch (entry.getType()) { case FileListingService.TYPE_FILE: case FileListingService.TYPE_LINK: // get the name and extension if (entry.isApplicationPackage()) { return mPackageImage; } return mFileImage; case FileListingService.TYPE_DIRECTORY: case FileListingService.TYPE_DIRECTORY_LINK: return mFolderImage; } } // default case return a different image. return mOtherImage; } return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof FileEntry) { FileEntry entry = (FileEntry)element; switch (columnIndex) { case 0: return entry.getName(); case 1: return entry.getSize(); case 2: return entry.getDate(); case 3: return entry.getTime(); case 4: return entry.getPermissions(); case 5: return entry.getInfo(); } } return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener) */ @Override public void addListener(ILabelProviderListener listener) { // we don't need listeners. } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose() */ @Override public void dispose() { } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String) */ @Override public boolean isLabelProperty(Object element, String property) { return false; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener) */ @Override public void removeListener(ILabelProviderListener listener) { // we don't need listeners } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/0040755 0000000 0000000 00000000000 12747325007 022377 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java0100644 0000000 0000000 00000015140 12747325007 026210 0ustar000000000 0000000 /* * 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.ddmuilib.handler; import com.android.ddmlib.ClientData.IHprofDumpHandler; import com.android.ddmlib.ClientData.IMethodProfilingHandler; import com.android.ddmlib.SyncException; import com.android.ddmlib.SyncService; import com.android.ddmlib.SyncService.ISyncProgressMonitor; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.SyncProgressHelper; import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Shell; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; /** * Base handler class for handler dealing with files located on a device. * * @see IHprofDumpHandler * @see IMethodProfilingHandler */ public abstract class BaseFileHandler { protected final Shell mParentShell; public BaseFileHandler(Shell parentShell) { mParentShell = parentShell; } protected abstract String getDialogTitle(); /** * Prompts the user for a save location and pulls the remote files into this location. *

This must be called from the UI Thread. * @param sync the {@link SyncService} to use to pull the file from the device * @param localFileName The default local name * @param remoteFilePath The name of the file to pull off of the device * @param title The title of the File Save dialog. * @return The result of the pull as a {@link SyncResult} object, or null if the sync * didn't happen (canceled by the user). * @throws InvocationTargetException * @throws InterruptedException * @throws SyncException if an error happens during the push of the package on the device. * @throws IOException */ protected void promptAndPull(final SyncService sync, String localFileName, final String remoteFilePath, String title) throws InvocationTargetException, InterruptedException, SyncException, TimeoutException, IOException { FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE); fileDialog.setText(title); fileDialog.setFileName(localFileName); final String localFilePath = fileDialog.open(); if (localFilePath != null) { SyncProgressHelper.run(new SyncRunnable() { @Override public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { sync.pullFile(remoteFilePath, localFilePath, monitor); } @Override public void close() { sync.close(); } }, String.format("Pulling %1$s from the device", remoteFilePath), mParentShell); } } /** * Prompts the user for a save location and copies a temp file into it. *

This must be called from the UI Thread. * @param localFileName The default local name * @param tempFilePath The name of the temp file to copy. * @param title The title of the File Save dialog. * @return true if success, false on error or cancel. */ protected boolean promptAndSave(String localFileName, byte[] data, String title) { FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE); fileDialog.setText(title); fileDialog.setFileName(localFileName); String localFilePath = fileDialog.open(); if (localFilePath != null) { try { saveFile(data, new File(localFilePath)); return true; } catch (IOException e) { String errorMsg = e.getMessage(); displayErrorInUiThread( "Failed to save file '%1$s'%2$s", localFilePath, errorMsg != null ? ":\n" + errorMsg : "."); } } return false; } /** * Display an error message. *

This will call about to {@link Display} to run this in an async {@link Runnable} in the * UI Thread. This is safe to be called from a non-UI Thread. * @param format the string to display * @param args the string arguments */ protected void displayErrorInUiThread(final String format, final Object... args) { mParentShell.getDisplay().asyncExec(new Runnable() { @Override public void run() { MessageDialog.openError(mParentShell, getDialogTitle(), String.format(format, args)); } }); } /** * Display an error message. * This must be called from the UI Thread. * @param format the string to display * @param args the string arguments */ protected void displayErrorFromUiThread(final String format, final Object... args) { MessageDialog.openError(mParentShell, getDialogTitle(), String.format(format, args)); } /** * Saves a given data into a temp file and returns its corresponding {@link File} object. * @param data the data to save * @return the File into which the data was written or null if it failed. * @throws IOException */ protected File saveTempFile(byte[] data, String extension) throws IOException { File f = File.createTempFile("ddms", extension); saveFile(data, f); return f; } /** * Saves some data into a given File. * @param data the data to save * @param output the file into the data is saved. * @throws IOException */ protected void saveFile(byte[] data, File output) throws IOException { FileOutputStream fos = null; try { fos = new FileOutputStream(output); fos.write(data); } finally { if (fos != null) { fos.close(); } } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java0100644 0000000 0000000 00000016471 12747325007 027640 0ustar000000000 0000000 /* * 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.ddmuilib.handler; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData.IMethodProfilingHandler; import com.android.ddmlib.DdmConstants; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.SyncException; import com.android.ddmlib.SyncService; import com.android.ddmlib.SyncService.ISyncProgressMonitor; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.SyncProgressHelper; import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; import com.android.ddmuilib.console.DdmConsole; import org.eclipse.swt.widgets.Shell; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; /** * Handler for Method tracing. * This will pull the trace file into a temp file and launch traceview. */ public class MethodProfilingHandler extends BaseFileHandler implements IMethodProfilingHandler { public MethodProfilingHandler(Shell parentShell) { super(parentShell); } @Override protected String getDialogTitle() { return "Method Profiling Error"; } @Override public void onStartFailure(final Client client, final String message) { displayErrorInUiThread( "Unable to create Method Profiling file for application '%1$s'\n\n%2$s" + "Check logcat for more information.", client.getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); } @Override public void onEndFailure(final Client client, final String message) { displayErrorInUiThread( "Unable to finish Method Profiling for application '%1$s'\n\n%2$s" + "Check logcat for more information.", client.getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); } @Override public void onSuccess(final String remoteFilePath, final Client client) { mParentShell.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (remoteFilePath == null) { displayErrorFromUiThread( "Unable to download trace file: unknown file name.\n" + "This can happen if you disconnected the device while recording the trace."); return; } final IDevice device = client.getDevice(); try { // get the sync service to pull the HPROF file final SyncService sync = client.getDevice().getSyncService(); if (sync != null) { pullAndOpen(sync, remoteFilePath); } else { displayErrorFromUiThread( "Unable to download trace file from device '%1$s'.", device.getSerialNumber()); } } catch (Exception e) { displayErrorFromUiThread("Unable to download trace file from device '%1$s'.", device.getSerialNumber()); } } }); } @Override public void onSuccess(byte[] data, final Client client) { try { File tempFile = saveTempFile(data, DdmConstants.DOT_TRACE); open(tempFile.getAbsolutePath()); } catch (IOException e) { String errorMsg = e.getMessage(); displayErrorInUiThread( "Failed to save trace data into temp file%1$s", errorMsg != null ? ":\n" + errorMsg : "."); } } /** * pulls and open a file. This is run from the UI thread. */ private void pullAndOpen(final SyncService sync, final String remoteFilePath) throws InvocationTargetException, InterruptedException, IOException { // get a temp file File temp = File.createTempFile("android", DdmConstants.DOT_TRACE); //$NON-NLS-1$ final String tempPath = temp.getAbsolutePath(); // pull the file try { SyncProgressHelper.run(new SyncRunnable() { @Override public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { sync.pullFile(remoteFilePath, tempPath, monitor); } @Override public void close() { sync.close(); } }, String.format("Pulling %1$s from the device", remoteFilePath), mParentShell); // open the temp file in traceview open(tempPath); } catch (SyncException e) { if (e.wasCanceled() == false) { displayErrorFromUiThread("Unable to download trace file:\n\n%1$s", e.getMessage()); } } catch (TimeoutException e) { displayErrorFromUiThread("Unable to download trace file:\n\ntimeout"); } } protected void open(String tempPath) { // now that we have the file, we need to launch traceview String[] command = new String[2]; command[0] = DdmUiPreferences.getTraceview(); command[1] = tempPath; try { final Process p = Runtime.getRuntime().exec(command); // create a thread for the output new Thread("Traceview output") { @Override public void run() { // create a buffer to read the stderr output InputStreamReader is = new InputStreamReader(p.getErrorStream()); BufferedReader resultReader = new BufferedReader(is); // read the lines as they come. if null is returned, it's // because the process finished try { while (true) { String line = resultReader.readLine(); if (line != null) { DdmConsole.printErrorToConsole("Traceview: " + line); } else { break; } } // get the return code from the process p.waitFor(); } catch (Exception e) { Log.e("traceview", e); } } }.start(); } catch (IOException e) { Log.e("traceview", e); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/0040755 0000000 0000000 00000000000 12747325007 021677 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeDiffAllocationInfo.java0100644 0000000 0000000 00000004135 12747325007 027403 0ustar000000000 0000000 /* * Copyright (C) 2013 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeStackCallInfo; import java.util.List; /** * {@link NativeDiffAllocationInfo} stores the difference in the allocation * counts between two allocations with the same stack trace. * * Since only the allocation counts are different, it delegates all other functionality to * one of the allocations and just maintains the allocation count. */ public class NativeDiffAllocationInfo extends NativeAllocationInfo { private final NativeAllocationInfo info; public NativeDiffAllocationInfo(NativeAllocationInfo cur, NativeAllocationInfo prev) { super(cur.getSize(), getNewAllocations(cur, prev)); info = cur; } private static int getNewAllocations(NativeAllocationInfo n1, NativeAllocationInfo n2) { return n1.getAllocationCount() - n2.getAllocationCount(); } @Override public boolean isStackCallResolved() { return info.isStackCallResolved(); } @Override public List getStackCallAddresses() { return info.getStackCallAddresses(); } @Override public synchronized List getResolvedStackCall() { return info.getResolvedStackCall(); } @Override public synchronized NativeStackCallInfo getRelevantStackCallInfo() { return info.getRelevantStackCallInfo(); } @Override public boolean isZygoteChild() { return info.isZygoteChild(); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java0100644 0000000 0000000 00000020066 12747325007 027103 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeStackCallInfo; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.operation.IRunnableWithProgress; import java.io.IOException; import java.io.LineNumberReader; import java.io.Reader; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.InputMismatchException; import java.util.List; import java.util.Scanner; import java.util.regex.Pattern; public class NativeHeapDataImporter implements IRunnableWithProgress { private final LineNumberReader mReader; private int mStartLineNumber; private int mEndLineNumber; private NativeHeapSnapshot mSnapshot; public NativeHeapDataImporter(Reader stream) { mReader = new LineNumberReader(stream); mReader.setLineNumber(1); // start numbering at 1 } @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { monitor.beginTask("Importing Heap Data", IProgressMonitor.UNKNOWN); List allocations = new ArrayList(); try { while (true) { String line; StringBuilder sb = new StringBuilder(); // read in a sequence of lines corresponding to a single NativeAllocationInfo mStartLineNumber = mReader.getLineNumber(); while ((line = mReader.readLine()) != null) { if (line.trim().length() == 0) { // each block of allocations end with an empty line break; } sb.append(line); sb.append('\n'); } mEndLineNumber = mReader.getLineNumber(); // parse those lines into a NativeAllocationInfo object String allocationBlock = sb.toString(); if (allocationBlock.trim().length() > 0) { allocations.add(getNativeAllocation(allocationBlock)); } if (line == null) { // EOF break; } } } catch (Exception e) { if (e.getMessage() == null) { e = new RuntimeException(genericErrorMessage("Unexpected Parse error")); } throw new InvocationTargetException(e); } finally { try { mReader.close(); } catch (IOException e) { // we can ignore this exception } monitor.done(); } mSnapshot = new NativeHeapSnapshot(allocations); } /** Parse a single native allocation dump. This is the complement of * {@link NativeAllocationInfo#toString()}. * * An allocation is of the following form: * Allocations: 1 * Size: 344748 * Total Size: 344748 * BeginStackTrace: * 40069bd8 /lib/libc_malloc_leak.so --- get_backtrace --- /libc/bionic/malloc_leak.c:258 * 40069dd8 /lib/libc_malloc_leak.so --- leak_calloc --- /libc/bionic/malloc_leak.c:576 * 40069bd8 /lib/libc_malloc_leak.so --- 40069bd8 --- * 40069dd8 /lib/libc_malloc_leak.so --- 40069dd8 --- * EndStackTrace * Note that in the above stack trace, the last two lines are examples where the address * was not resolved. * * @param block a string of lines corresponding to a single {@code NativeAllocationInfo} * @return parse the input and return the corresponding {@link NativeAllocationInfo} * @throws InputMismatchException if there are any parse errors */ private NativeAllocationInfo getNativeAllocation(String block) { Scanner sc = new Scanner(block); try { String kw = sc.next(); if (!NativeAllocationInfo.ALLOCATIONS_KW.equals(kw)) { throw new InputMismatchException( expectedKeywordErrorMessage(NativeAllocationInfo.ALLOCATIONS_KW, kw)); } int allocations = sc.nextInt(); kw = sc.next(); if (!NativeAllocationInfo.SIZE_KW.equals(kw)) { throw new InputMismatchException( expectedKeywordErrorMessage(NativeAllocationInfo.SIZE_KW, kw)); } int size = sc.nextInt(); kw = sc.next(); if (!NativeAllocationInfo.TOTAL_SIZE_KW.equals(kw)) { throw new InputMismatchException( expectedKeywordErrorMessage(NativeAllocationInfo.TOTAL_SIZE_KW, kw)); } int totalSize = sc.nextInt(); if (totalSize != size * allocations) { throw new InputMismatchException( genericErrorMessage("Total Size does not match size * # of allocations")); } NativeAllocationInfo info = new NativeAllocationInfo(size, allocations); kw = sc.next(); if (!NativeAllocationInfo.BEGIN_STACKTRACE_KW.equals(kw)) { throw new InputMismatchException( expectedKeywordErrorMessage(NativeAllocationInfo.BEGIN_STACKTRACE_KW, kw)); } List stackInfo = new ArrayList(); Pattern endTracePattern = Pattern.compile(NativeAllocationInfo.END_STACKTRACE_KW); while (true) { long address = sc.nextLong(16); info.addStackCallAddress(address); String library = sc.next(); sc.next(); // ignore "---" String method = scanTillSeparator(sc, "---"); String filename = ""; if (!isUnresolved(method, address)) { filename = sc.next(); } stackInfo.add(new NativeStackCallInfo(address, library, method, filename)); if (sc.hasNext(endTracePattern)) { break; } } info.setResolvedStackCall(stackInfo); return info; } finally { sc.close(); } } private String scanTillSeparator(Scanner sc, String separator) { StringBuilder sb = new StringBuilder(); while (true) { String token = sc.next(); if (token.equals(separator)) { break; } sb.append(token); // We do not know the exact delimiter that was skipped over, but we know // that there was atleast 1 whitespace. Add a single whitespace character // to account for this. sb.append(' '); } return sb.toString().trim(); } private boolean isUnresolved(String method, long address) { // a method is unresolved if it is just the hex representation of the address return Long.toString(address, 16).equals(method); } private String genericErrorMessage(String message) { return String.format("%1$s between lines %2$d and %3$d", message, mStartLineNumber, mEndLineNumber); } private String expectedKeywordErrorMessage(String expected, String actual) { return String.format("Expected keyword '%1$s', saw '%2$s' between lines %3$d to %4$d.", expected, actual, mStartLineNumber, mEndLineNumber); } public NativeHeapSnapshot getImportedSnapshot() { return mSnapshot; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java0100644 0000000 0000000 00000010337 12747325007 027100 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Models a heap snapshot that is the difference between two snapshots. */ public class NativeHeapDiffSnapshot extends NativeHeapSnapshot { private long mCommonAllocationsTotalMemory; public NativeHeapDiffSnapshot(NativeHeapSnapshot newSnapshot, NativeHeapSnapshot oldSnapshot) { // The diff snapshots behaves like a snapshot that only contains the new allocations // not present in the old snapshot super(getNewAllocations(newSnapshot, oldSnapshot)); Set commonAllocations = new HashSet(oldSnapshot.getAllocations()); commonAllocations.retainAll(newSnapshot.getAllocations()); // Memory common between the old and new snapshots mCommonAllocationsTotalMemory = getTotalMemory(commonAllocations); } private static List getNewAllocations(NativeHeapSnapshot newSnapshot, NativeHeapSnapshot oldSnapshot) { Set allocations = new HashSet(newSnapshot.getAllocations()); // compute new allocations allocations.removeAll(oldSnapshot.getAllocations()); // Account for allocations with the same stack trace that were // present in the older set of allocations. // e.g. A particular stack trace might have had 3 allocations in snapshot 1, // and 2 more in snapshot 2. We only want to show the new allocations (just the 2 from // snapshot 2). However, the way the allocations are stored, in snapshot 2, we'll see // 5 allocations at the stack trace. We need to subtract out the 3 from the first allocation Set onlyInPrevious = new HashSet(oldSnapshot.getAllocations()); Set newAllocations = Sets.newHashSetWithExpectedSize(allocations.size()); onlyInPrevious.removeAll(newSnapshot.getAllocations()); for (NativeAllocationInfo current : allocations) { NativeAllocationInfo old = getOldAllocationWithSameStack(current, onlyInPrevious); if (old == null) { newAllocations.add(current); } else if (current.getAllocationCount() > old.getAllocationCount()) { newAllocations.add(new NativeDiffAllocationInfo(current, old)); } } return new ArrayList(newAllocations); } private static NativeAllocationInfo getOldAllocationWithSameStack( NativeAllocationInfo info, Set allocations) { for (NativeAllocationInfo a : allocations) { if (info.getSize() == a.getSize() && info.stackEquals(a)) { return a; } } return null; } @Override public String getFormattedMemorySize() { // for a diff snapshot, we report the following string for display: // xxx bytes new allocation + yyy bytes retained from previous allocation // = zzz bytes total long newAllocations = getTotalSize(); return String.format("%s bytes new + %s bytes retained = %s bytes total", formatMemorySize(newAllocations), formatMemorySize(mCommonAllocationsTotalMemory), formatMemorySize(newAllocations + mCommonAllocationsTotalMemory)); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java0100644 0000000 0000000 00000007424 12747325007 027245 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeStackCallInfo; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.graphics.Image; /** * A Label Provider for the Native Heap TreeViewer in {@link NativeHeapPanel}. */ public class NativeHeapLabelProvider extends LabelProvider implements ITableLabelProvider { private long mTotalSize; @Override public Image getColumnImage(Object arg0, int arg1) { return null; } @Override public String getColumnText(Object element, int index) { if (element instanceof NativeAllocationInfo) { return getColumnTextForNativeAllocation((NativeAllocationInfo) element, index); } if (element instanceof NativeLibraryAllocationInfo) { return getColumnTextForNativeLibrary((NativeLibraryAllocationInfo) element, index); } return null; } private String getColumnTextForNativeAllocation(NativeAllocationInfo info, int index) { NativeStackCallInfo stackInfo = info.getRelevantStackCallInfo(); switch (index) { case 0: return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getLibraryName(); case 1: return Integer.toString(info.getSize() * info.getAllocationCount()); case 2: return getPercentageString(info.getSize() * info.getAllocationCount(), mTotalSize); case 3: String prefix = ""; if (!info.isZygoteChild()) { prefix = "Z "; } return prefix + Integer.toString(info.getAllocationCount()); case 4: return Integer.toString(info.getSize()); case 5: return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getMethodName(); default: return null; } } private String getColumnTextForNativeLibrary(NativeLibraryAllocationInfo info, int index) { switch (index) { case 0: return info.getLibraryName(); case 1: return Long.toString(info.getTotalSize()); case 2: return getPercentageString(info.getTotalSize(), mTotalSize); default: return null; } } private String getPercentageString(long size, long total) { if (total == 0) { return ""; } return String.format("%.1f%%", (float)(size * 100)/(float)total); } private String stackResolutionStatus(NativeAllocationInfo info) { if (info.isStackCallResolved()) { return "?"; // resolved and unknown } else { return "Resolving..."; // still resolving... } } /** * Set the total size of the heap dump for use in percentage calculations. * This value should be set whenever the input to the tree changes so that the percentages * are computed correctly. */ public void setTotalSize(long totalSize) { mTotalSize = totalSize; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java0100644 0000000 0000000 00000123600 12747325007 025545 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.Client; import com.android.ddmlib.Log; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeLibraryMapInfo; import com.android.ddmlib.NativeStackCallInfo; import com.android.ddmuilib.Addr2Line; import com.android.ddmuilib.BaseHeapPanel; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.TableHelper; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Sash; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** Panel to display native heap information. */ public class NativeHeapPanel extends BaseHeapPanel { private static final boolean USE_OLD_RESOLVER; static { String useOldResolver = System.getenv("ANDROID_DDMS_OLD_SYMRESOLVER"); USE_OLD_RESOLVER = useOldResolver != null && useOldResolver.equalsIgnoreCase("true"); } private final int MAX_DISPLAYED_ERROR_ITEMS = 5; private static final String TOOLTIP_EXPORT_DATA = "Export Heap Data"; private static final String TOOLTIP_ZYGOTE_ALLOCATIONS = "Show Zygote Allocations"; private static final String TOOLTIP_DIFFS_ONLY = "Only show new allocations not present in previous snapshot"; private static final String TOOLTIP_GROUPBY = "Group allocations by library."; private static final String EXPORT_DATA_IMAGE = "save.png"; private static final String ZYGOTE_IMAGE = "zygote.png"; private static final String DIFFS_ONLY_IMAGE = "diff.png"; private static final String GROUPBY_IMAGE = "groupby.png"; private static final String SNAPSHOT_HEAP_BUTTON_TEXT = "Snapshot Current Native Heap Usage"; private static final String LOAD_HEAP_DATA_BUTTON_TEXT = "Import Heap Data"; private static final String SYMBOL_SEARCH_PATH_LABEL_TEXT = "Symbol Search Path:"; private static final String SYMBOL_SEARCH_PATH_TEXT_MESSAGE = "List of colon separated paths to search for symbol debug information. See tooltip for examples."; private static final String SYMBOL_SEARCH_PATH_TOOLTIP_TEXT = "Colon separated paths that contain unstripped libraries with debug symbols.\n" + "e.g.: /out/target/product/generic/symbols/system/lib:/path/to/my/app/obj/local/armeabi"; private static final String PREFS_SHOW_DIFFS_ONLY = "nativeheap.show.diffs.only"; private static final String PREFS_SHOW_ZYGOTE_ALLOCATIONS = "nativeheap.show.zygote"; private static final String PREFS_GROUP_BY_LIBRARY = "nativeheap.grouby.library"; private static final String PREFS_SYMBOL_SEARCH_PATH = "nativeheap.search.path"; private static final String PREFS_SASH_HEIGHT_PERCENT = "nativeheap.sash.percent"; private static final String PREFS_LAST_IMPORTED_HEAPPATH = "nativeheap.last.import.path"; private IPreferenceStore mPrefStore; private List mNativeHeapSnapshots; // Maintain the differences between a snapshot and its predecessor. // mDiffSnapshots[i] = mNativeHeapSnapshots[i] - mNativeHeapSnapshots[i-1] // The zeroth entry is null since there is no predecessor. // The list is filled lazily on demand. private List mDiffSnapshots; private Map> mImportedSnapshotsPerPid; private Button mSnapshotHeapButton; private Button mLoadHeapDataButton; private Text mSymbolSearchPathText; private Combo mSnapshotIndexCombo; private Label mMemoryAllocatedText; private TreeViewer mDetailsTreeViewer; private TreeViewer mStackTraceTreeViewer; private NativeHeapProviderByAllocations mContentProviderByAllocations; private NativeHeapProviderByLibrary mContentProviderByLibrary; private NativeHeapLabelProvider mDetailsTreeLabelProvider; private ToolItem mGroupByButton; private ToolItem mDiffsOnlyButton; private ToolItem mShowZygoteAllocationsButton; private ToolItem mExportHeapDataButton; public NativeHeapPanel(IPreferenceStore prefStore) { mPrefStore = prefStore; mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75); mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, ""); mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false); mPrefStore.setDefault(PREFS_SHOW_ZYGOTE_ALLOCATIONS, true); mPrefStore.setDefault(PREFS_SHOW_DIFFS_ONLY, false); mNativeHeapSnapshots = new ArrayList(); mDiffSnapshots = new ArrayList(); mImportedSnapshotsPerPid = new HashMap>(); } /** {@inheritDoc} */ @Override public void clientChanged(final Client client, int changeMask) { if (client != getCurrentClient()) { return; } if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) != Client.CHANGE_NATIVE_HEAP_DATA) { return; } List allocations = client.getClientData().getNativeAllocationList(); if (allocations.size() == 0) { return; } // We need to clone this list since getClientData().getNativeAllocationList() clobbers // the list on future updates final List nativeAllocations = shallowCloneList(allocations); addNativeHeapSnapshot(new NativeHeapSnapshot(nativeAllocations)); updateDisplay(); // Attempt to resolve symbols in a separate thread. // The UI should be refreshed once the symbols have been resolved. if (USE_OLD_RESOLVER) { Thread t = new Thread(new SymbolResolverTask(nativeAllocations, client.getClientData().getMappedNativeLibraries())); t.setName("Address to Symbol Resolver"); t.start(); } else { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { resolveSymbols(); mDetailsTreeViewer.refresh(); mStackTraceTreeViewer.refresh(); } public void resolveSymbols() { Shell shell = Display.getDefault().getActiveShell(); ProgressMonitorDialog d = new ProgressMonitorDialog(shell); NativeSymbolResolverTask resolver = new NativeSymbolResolverTask( nativeAllocations, client.getClientData().getMappedNativeLibraries(), mSymbolSearchPathText.getText(), client.getClientData().getAbi()); try { d.run(true, true, resolver); } catch (InvocationTargetException e) { MessageDialog.openError(shell, "Error Resolving Symbols", e.getCause().getMessage()); return; } catch (InterruptedException e) { return; } MessageDialog.openInformation(shell, "Symbol Resolution Status", getResolutionStatusMessage(resolver)); } }); } } private String getResolutionStatusMessage(NativeSymbolResolverTask resolver) { StringBuilder sb = new StringBuilder(); sb.append("Symbol Resolution Complete.\n\n"); // show addresses that were not mapped Set unmappedAddresses = resolver.getUnmappedAddresses(); if (unmappedAddresses.size() > 0) { sb.append(String.format("Unmapped addresses (%d): ", unmappedAddresses.size())); sb.append(getSampleForDisplay(unmappedAddresses)); sb.append('\n'); } // show libraries that were not present on disk Set notFoundLibraries = resolver.getNotFoundLibraries(); if (notFoundLibraries.size() > 0) { sb.append(String.format("Libraries not found on disk (%d): ", notFoundLibraries.size())); sb.append(getSampleForDisplay(notFoundLibraries)); sb.append('\n'); } // show addresses that were mapped but not resolved Set unresolvableAddresses = resolver.getUnresolvableAddresses(); if (unresolvableAddresses.size() > 0) { sb.append(String.format("Unresolved addresses (%d): ", unresolvableAddresses.size())); sb.append(getSampleForDisplay(unresolvableAddresses)); sb.append('\n'); } if (resolver.getAddr2LineErrorMessage() != null) { sb.append("Error launching addr2line: "); sb.append(resolver.getAddr2LineErrorMessage()); } return sb.toString(); } /** * Get the string representation for a collection of items. * If there are more items than {@link #MAX_DISPLAYED_ERROR_ITEMS}, then only the first * {@link #MAX_DISPLAYED_ERROR_ITEMS} items are taken into account, * and an ellipsis is added at the end. */ private String getSampleForDisplay(Collection items) { StringBuilder sb = new StringBuilder(); int c = 1; Iterator it = items.iterator(); while (it.hasNext()) { Object item = it.next(); if (item instanceof Long) { sb.append(String.format("0x%x", item)); } else { sb.append(item); } if (c == MAX_DISPLAYED_ERROR_ITEMS && it.hasNext()) { sb.append(", ..."); break; } else if (it.hasNext()) { sb.append(", "); } c++; } return sb.toString(); } private void addNativeHeapSnapshot(NativeHeapSnapshot snapshot) { mNativeHeapSnapshots.add(snapshot); // The diff snapshots are filled in lazily on demand. // But the list needs to be the same size as mNativeHeapSnapshots, so we add a null. mDiffSnapshots.add(null); } private List shallowCloneList(List allocations) { List clonedList = new ArrayList(allocations.size()); for (NativeAllocationInfo i : allocations) { clonedList.add(i); } return clonedList; } @Override public void deviceSelected() { // pass } @Override public void clientSelected() { Client c = getCurrentClient(); if (c == null) { // if there is no client selected, then we disable the buttons but leave the // display as is so that whatever snapshots are displayed continue to stay // visible to the user. mSnapshotHeapButton.setEnabled(false); mLoadHeapDataButton.setEnabled(false); return; } mNativeHeapSnapshots = new ArrayList(); mDiffSnapshots = new ArrayList(); mSnapshotHeapButton.setEnabled(true); mLoadHeapDataButton.setEnabled(true); List importedSnapshots = mImportedSnapshotsPerPid.get( c.getClientData().getPid()); if (importedSnapshots != null) { for (NativeHeapSnapshot n : importedSnapshots) { addNativeHeapSnapshot(n); } } List allocations = c.getClientData().getNativeAllocationList(); allocations = shallowCloneList(allocations); if (allocations.size() > 0) { addNativeHeapSnapshot(new NativeHeapSnapshot(allocations)); } updateDisplay(); } private void updateDisplay() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { updateSnapshotIndexCombo(); updateToolbars(); int lastSnapshotIndex = mNativeHeapSnapshots.size() - 1; displaySnapshot(lastSnapshotIndex); displayStackTraceForSelection(); } }); } private void displaySelectedSnapshot() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { int idx = mSnapshotIndexCombo.getSelectionIndex(); displaySnapshot(idx); } }); } private void displaySnapshot(int index) { if (index < 0 || mNativeHeapSnapshots.size() == 0) { mDetailsTreeViewer.setInput(null); mMemoryAllocatedText.setText(""); return; } assert index < mNativeHeapSnapshots.size() : "Invalid snapshot index"; NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(index); if (mDiffsOnlyButton.getSelection() && index > 0) { snapshot = getDiffSnapshot(index); } mMemoryAllocatedText.setText(snapshot.getFormattedMemorySize()); mMemoryAllocatedText.pack(); mDetailsTreeLabelProvider.setTotalSize(snapshot.getTotalSize()); mDetailsTreeViewer.setInput(snapshot); mDetailsTreeViewer.refresh(); } /** Obtain the diff of snapshot[index] & snapshot[index-1] */ private NativeHeapSnapshot getDiffSnapshot(int index) { // if it was already computed, simply return that NativeHeapSnapshot diffSnapshot = mDiffSnapshots.get(index); if (diffSnapshot != null) { return diffSnapshot; } // compute the diff NativeHeapSnapshot cur = mNativeHeapSnapshots.get(index); NativeHeapSnapshot prev = mNativeHeapSnapshots.get(index - 1); diffSnapshot = new NativeHeapDiffSnapshot(cur, prev); // cache for future use mDiffSnapshots.set(index, diffSnapshot); return diffSnapshot; } private void updateDisplayGrouping() { boolean groupByLibrary = mGroupByButton.getSelection(); mPrefStore.setValue(PREFS_GROUP_BY_LIBRARY, groupByLibrary); if (groupByLibrary) { mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); } else { mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); } } private void updateDisplayForZygotes() { boolean displayZygoteMemory = mShowZygoteAllocationsButton.getSelection(); mPrefStore.setValue(PREFS_SHOW_ZYGOTE_ALLOCATIONS, displayZygoteMemory); // inform the content providers of the zygote display setting mContentProviderByLibrary.displayZygoteMemory(displayZygoteMemory); mContentProviderByAllocations.displayZygoteMemory(displayZygoteMemory); // refresh the UI mDetailsTreeViewer.refresh(); } private void updateSnapshotIndexCombo() { List items = new ArrayList(); int numSnapshots = mNativeHeapSnapshots.size(); for (int i = 0; i < numSnapshots; i++) { // offset indices by 1 so that users see index starting at 1 rather than 0 items.add("Snapshot " + (i + 1)); } mSnapshotIndexCombo.setItems(items.toArray(new String[items.size()])); if (numSnapshots > 0) { mSnapshotIndexCombo.setEnabled(true); mSnapshotIndexCombo.select(numSnapshots - 1); } else { mSnapshotIndexCombo.setEnabled(false); } } private void updateToolbars() { int numSnapshots = mNativeHeapSnapshots.size(); mExportHeapDataButton.setEnabled(numSnapshots > 0); } @Override protected Control createControl(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(1, false)); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createControlsSection(c); createDetailsSection(c); // Initialize widget state based on whether a client // is selected or not. clientSelected(); return c; } private void createControlsSection(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(3, false)); c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); createGetHeapDataSection(c); Label l = new Label(c, SWT.SEPARATOR | SWT.VERTICAL); l.setLayoutData(new GridData(GridData.FILL_VERTICAL)); createDisplaySection(c); } private void createGetHeapDataSection(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(1, false)); createTakeHeapSnapshotButton(c); Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); createLoadHeapDataButton(c); } private void createTakeHeapSnapshotButton(Composite parent) { mSnapshotHeapButton = new Button(parent, SWT.BORDER | SWT.PUSH); mSnapshotHeapButton.setText(SNAPSHOT_HEAP_BUTTON_TEXT); mSnapshotHeapButton.setLayoutData(new GridData()); // disable by default, enabled only when a client is selected mSnapshotHeapButton.setEnabled(false); mSnapshotHeapButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent evt) { snapshotHeap(); } }); } private void snapshotHeap() { Client c = getCurrentClient(); assert c != null : "Snapshot Heap could not have been enabled w/o a selected client."; // send an async request c.requestNativeHeapInformation(); } private void createLoadHeapDataButton(Composite parent) { mLoadHeapDataButton = new Button(parent, SWT.BORDER | SWT.PUSH); mLoadHeapDataButton.setText(LOAD_HEAP_DATA_BUTTON_TEXT); mLoadHeapDataButton.setLayoutData(new GridData()); // disable by default, enabled only when a client is selected mLoadHeapDataButton.setEnabled(false); mLoadHeapDataButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent evt) { loadHeapDataFromFile(); } }); } private void loadHeapDataFromFile() { // pop up a file dialog and get the file to load final String path = getHeapDumpToImport(); if (path == null) { return; } Reader reader = null; try { reader = new FileReader(path); } catch (FileNotFoundException e) { // cannot occur since user input was via a FileDialog } Shell shell = Display.getDefault().getActiveShell(); ProgressMonitorDialog d = new ProgressMonitorDialog(shell); NativeHeapDataImporter importer = new NativeHeapDataImporter(reader); try { d.run(true, true, importer); } catch (InvocationTargetException e) { // exception while parsing, display error to user and then return MessageDialog.openError(shell, "Error Importing Heap Data", e.getCause().getMessage()); return; } catch (InterruptedException e) { // operation cancelled by user, simply return return; } NativeHeapSnapshot snapshot = importer.getImportedSnapshot(); addToImportedSnapshots(snapshot); // save imported snapshot for future use addNativeHeapSnapshot(snapshot); // add to currently displayed snapshots as well updateDisplay(); } private void addToImportedSnapshots(NativeHeapSnapshot snapshot) { Client c = getCurrentClient(); if (c == null) { return; } Integer pid = c.getClientData().getPid(); List importedSnapshots = mImportedSnapshotsPerPid.get(pid); if (importedSnapshots == null) { importedSnapshots = new ArrayList(); } importedSnapshots.add(snapshot); mImportedSnapshotsPerPid.put(pid, importedSnapshots); } private String getHeapDumpToImport() { FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), SWT.OPEN); fileDialog.setText("Import Heap Dump"); fileDialog.setFilterExtensions(new String[] {"*.txt"}); fileDialog.setFilterPath(mPrefStore.getString(PREFS_LAST_IMPORTED_HEAPPATH)); String selectedFile = fileDialog.open(); if (selectedFile != null) { // save the path to restore in future dialog open mPrefStore.setValue(PREFS_LAST_IMPORTED_HEAPPATH, new File(selectedFile).getParent()); } return selectedFile; } private void createDisplaySection(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(2, false)); c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // Create: Display: __________________ createLabel(c, "Display:"); mSnapshotIndexCombo = new Combo(c, SWT.READ_ONLY); mSnapshotIndexCombo.setItems(new String[] {"No heap snapshots available."}); mSnapshotIndexCombo.setEnabled(false); mSnapshotIndexCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { displaySelectedSnapshot(); } }); // Create: Memory Allocated (bytes): _________________ createLabel(c, "Memory Allocated:"); mMemoryAllocatedText = new Label(c, SWT.NONE); GridData gd = new GridData(); gd.widthHint = 100; mMemoryAllocatedText.setLayoutData(gd); // Create: Search Path: __________________ createLabel(c, SYMBOL_SEARCH_PATH_LABEL_TEXT); mSymbolSearchPathText = new Text(c, SWT.BORDER); mSymbolSearchPathText.setMessage(SYMBOL_SEARCH_PATH_TEXT_MESSAGE); mSymbolSearchPathText.setToolTipText(SYMBOL_SEARCH_PATH_TOOLTIP_TEXT); mSymbolSearchPathText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { String path = mSymbolSearchPathText.getText(); updateSearchPath(path); mPrefStore.setValue(PREFS_SYMBOL_SEARCH_PATH, path); } }); mSymbolSearchPathText.setText(mPrefStore.getString(PREFS_SYMBOL_SEARCH_PATH)); mSymbolSearchPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); } private void updateSearchPath(String path) { Addr2Line.setSearchPath(path); } private void createLabel(Composite parent, String text) { Label l = new Label(parent, SWT.NONE); l.setText(text); GridData gd = new GridData(); gd.horizontalAlignment = SWT.RIGHT; l.setLayoutData(gd); } /** * Create the details section displaying the details table and the stack trace * corresponding to the selection. * * The details is laid out like so: * Details Toolbar * Details Table * ------------sash--- * Stack Trace Label * Stack Trace Text * There is a sash in between the two sections, and we need to save/restore the sash * preferences. Using FormLayout seems like the easiest solution here, but the layout * code looks ugly as a result. */ private void createDetailsSection(Composite parent) { final Composite c = new Composite(parent, SWT.NONE); c.setLayout(new FormLayout()); c.setLayoutData(new GridData(GridData.FILL_BOTH)); ToolBar detailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER); initializeDetailsToolBar(detailsToolBar); Tree detailsTree = new Tree(c, SWT.VIRTUAL | SWT.BORDER | SWT.MULTI); initializeDetailsTree(detailsTree); final Sash sash = new Sash(c, SWT.HORIZONTAL | SWT.BORDER); Label stackTraceLabel = new Label(c, SWT.NONE); stackTraceLabel.setText("Stack Trace:"); Tree stackTraceTree = new Tree(c, SWT.BORDER | SWT.MULTI); initializeStackTraceTree(stackTraceTree); // layout the widgets created above FormData data = new FormData(); data.top = new FormAttachment(0, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); detailsToolBar.setLayoutData(data); data = new FormData(); data.top = new FormAttachment(detailsToolBar, 0); data.bottom = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); detailsTree.setLayoutData(data); final FormData sashData = new FormData(); sashData.top = new FormAttachment(mPrefStore.getInt(PREFS_SASH_HEIGHT_PERCENT), 0); sashData.left = new FormAttachment(0, 0); sashData.right = new FormAttachment(100, 0); sash.setLayoutData(sashData); data = new FormData(); data.top = new FormAttachment(sash, 0); data.left = new FormAttachment(0, 0); data.right = new FormAttachment(100, 0); stackTraceLabel.setLayoutData(data); data = new FormData(); data.top = new FormAttachment(stackTraceLabel, 0); data.left = new FormAttachment(0, 0); data.bottom = new FormAttachment(100, 0); data.right = new FormAttachment(100, 0); stackTraceTree.setLayoutData(data); sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { Rectangle sashRect = sash.getBounds(); Rectangle panelRect = c.getClientArea(); int sashPercent = sashRect.y * 100 / panelRect.height; mPrefStore.setValue(PREFS_SASH_HEIGHT_PERCENT, sashPercent); sashData.top = new FormAttachment(0, e.y); c.layout(); } }); } private void initializeDetailsToolBar(ToolBar toolbar) { mGroupByButton = new ToolItem(toolbar, SWT.CHECK); mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE, toolbar.getDisplay())); mGroupByButton.setToolTipText(TOOLTIP_GROUPBY); mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)); mGroupByButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { updateDisplayGrouping(); } }); mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK); mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE, toolbar.getDisplay())); mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY); mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY)); mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { // simply refresh the display, as the display logic takes care of // the current state of the diffs only checkbox. int idx = mSnapshotIndexCombo.getSelectionIndex(); displaySnapshot(idx); } }); mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK); mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( ZYGOTE_IMAGE, toolbar.getDisplay())); mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS); mShowZygoteAllocationsButton.setSelection( mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS)); mShowZygoteAllocationsButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { updateDisplayForZygotes(); } }); mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH); mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( EXPORT_DATA_IMAGE, toolbar.getDisplay())); mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA); mExportHeapDataButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { exportSnapshot(); } }); } /** Export currently displayed snapshot to a file */ private void exportSnapshot() { int idx = mSnapshotIndexCombo.getSelectionIndex(); String snapshotName = mSnapshotIndexCombo.getItem(idx); FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), SWT.SAVE); fileDialog.setText("Save " + snapshotName); fileDialog.setFileName("allocations.txt"); final String fileName = fileDialog.open(); if (fileName == null) { return; } final NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(idx); Thread t = new Thread(new Runnable() { @Override public void run() { PrintWriter out; try { out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); } catch (IOException e) { displayErrorMessage(e.getMessage()); return; } for (NativeAllocationInfo alloc : snapshot.getAllocations()) { out.println(alloc.toString()); } out.close(); } private void displayErrorMessage(final String message) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { MessageDialog.openError(Display.getDefault().getActiveShell(), "Failed to export heap data", message); } }); } }); t.setName("Saving Heap Data to File..."); t.start(); } private void initializeDetailsTree(Tree tree) { tree.setHeaderVisible(true); tree.setLinesVisible(true); List properties = Arrays.asList("Library", "Total", "Percentage", "Count", "Size", "Method"); List sampleValues = Arrays.asList("/path/in/device/to/system/library.so", "123456789", " 100%", "123456789", "123456789", "PossiblyLongDemangledMethodName"); // right align numeric values List swtFlags = Arrays.asList(SWT.LEFT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.LEFT); for (int i = 0; i < properties.size(); i++) { String p = properties.get(i); String v = sampleValues.get(i); int flags = swtFlags.get(i); TableHelper.createTreeColumn(tree, p, flags, v, getPref("details", p), mPrefStore); } mDetailsTreeViewer = new TreeViewer(tree); mDetailsTreeViewer.setUseHashlookup(true); boolean displayZygotes = mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS); mContentProviderByAllocations = new NativeHeapProviderByAllocations(mDetailsTreeViewer, displayZygotes); mContentProviderByLibrary = new NativeHeapProviderByLibrary(mDetailsTreeViewer, displayZygotes); if (mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)) { mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); } else { mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); } mDetailsTreeLabelProvider = new NativeHeapLabelProvider(); mDetailsTreeViewer.setLabelProvider(mDetailsTreeLabelProvider); mDetailsTreeViewer.setInput(null); tree.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { displayStackTraceForSelection(); } }); } private void initializeStackTraceTree(Tree tree) { tree.setHeaderVisible(true); tree.setLinesVisible(true); List properties = Arrays.asList("Address", "Library", "Method", "File", "Line"); List sampleValues = Arrays.asList("0x1234_5678", "/path/in/device/to/system/library.so", "PossiblyLongDemangledMethodName", "/android/out/prefix/in/home/directory/to/path/in/device/to/system/library.so", "2000"); for (int i = 0; i < properties.size(); i++) { String p = properties.get(i); String v = sampleValues.get(i); TableHelper.createTreeColumn(tree, p, SWT.LEFT, v, getPref("stack", p), mPrefStore); } mStackTraceTreeViewer = new TreeViewer(tree); mStackTraceTreeViewer.setContentProvider(new NativeStackContentProvider()); mStackTraceTreeViewer.setLabelProvider(new NativeStackLabelProvider()); mStackTraceTreeViewer.setInput(null); } private void displayStackTraceForSelection() { TreeItem []items = mDetailsTreeViewer.getTree().getSelection(); if (items.length == 0) { mStackTraceTreeViewer.setInput(null); return; } Object data = items[0].getData(); if (!(data instanceof NativeAllocationInfo)) { mStackTraceTreeViewer.setInput(null); return; } NativeAllocationInfo info = (NativeAllocationInfo) data; if (info.isStackCallResolved()) { mStackTraceTreeViewer.setInput(info.getResolvedStackCall()); } else { mStackTraceTreeViewer.setInput(info.getStackCallAddresses()); } } private String getPref(String prefix, String s) { return "nativeheap.tree." + prefix + "." + s; } @Override public void setFocus() { } private ITableFocusListener mTableFocusListener; @Override public void setTableFocusListener(ITableFocusListener listener) { mTableFocusListener = listener; final Tree heapSitesTree = mDetailsTreeViewer.getTree(); final IFocusedTableActivator heapSitesActivator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { TreeItem[] items = heapSitesTree.getSelection(); copyToClipboard(items, clipboard); } @Override public void selectAll() { heapSitesTree.selectAll(); } }; heapSitesTree.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent arg0) { mTableFocusListener.focusLost(heapSitesActivator); } @Override public void focusGained(FocusEvent arg0) { mTableFocusListener.focusGained(heapSitesActivator); } }); final Tree stackTraceTree = mStackTraceTreeViewer.getTree(); final IFocusedTableActivator stackTraceActivator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { TreeItem[] items = stackTraceTree.getSelection(); copyToClipboard(items, clipboard); } @Override public void selectAll() { stackTraceTree.selectAll(); } }; stackTraceTree.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent arg0) { mTableFocusListener.focusLost(stackTraceActivator); } @Override public void focusGained(FocusEvent arg0) { mTableFocusListener.focusGained(stackTraceActivator); } }); } private void copyToClipboard(TreeItem[] items, Clipboard clipboard) { StringBuilder sb = new StringBuilder(); for (TreeItem item : items) { Object data = item.getData(); if (data != null) { sb.append(data.toString()); sb.append('\n'); } } String content = sb.toString(); if (content.length() > 0) { clipboard.setContents( new Object[] {sb.toString()}, new Transfer[] {TextTransfer.getInstance()} ); } } private class SymbolResolverTask implements Runnable { private List mCallSites; private List mMappedLibraries; private Map mResolvedSymbolCache; public SymbolResolverTask(List callSites, List mappedLibraries) { mCallSites = callSites; mMappedLibraries = mappedLibraries; mResolvedSymbolCache = new HashMap(); } @Override public void run() { for (NativeAllocationInfo callSite : mCallSites) { if (callSite.isStackCallResolved()) { continue; } List addresses = callSite.getStackCallAddresses(); List resolvedStackInfo = new ArrayList(addresses.size()); for (Long address : addresses) { NativeStackCallInfo info = mResolvedSymbolCache.get(address); if (info != null) { resolvedStackInfo.add(info); } else { info = resolveAddress(address); resolvedStackInfo.add(info); mResolvedSymbolCache.put(address, info); } } callSite.setResolvedStackCall(resolvedStackInfo); } Display.getDefault().asyncExec(new Runnable() { @Override public void run() { mDetailsTreeViewer.refresh(); mStackTraceTreeViewer.refresh(); } }); } private NativeStackCallInfo resolveAddress(long addr) { NativeLibraryMapInfo library = getLibraryFor(addr); if (library != null) { Client c = getCurrentClient(); Addr2Line process = Addr2Line.getProcess(library, c.getClientData().getAbi()); if (process != null) { NativeStackCallInfo info = process.getAddress(addr); if (info != null) { return info; } } } return new NativeStackCallInfo(addr, library != null ? library.getLibraryName() : null, Long.toHexString(addr), ""); } private NativeLibraryMapInfo getLibraryFor(long addr) { for (NativeLibraryMapInfo info : mMappedLibraries) { if (info.isWithinLibrary(addr)) { return info; } } Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); return null; } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java0100644 0000000 0000000 00000005225 12747325007 030766 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import org.eclipse.jface.viewers.ILazyTreeContentProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import java.util.List; /** * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}. * It expects a {@link NativeHeapSnapshot} as input, and provides the list of allocations * in the heap dump as content to the UI. */ public final class NativeHeapProviderByAllocations implements ILazyTreeContentProvider { private TreeViewer mViewer; private boolean mDisplayZygoteMemory; private NativeHeapSnapshot mNativeHeapDump; public NativeHeapProviderByAllocations(TreeViewer viewer, boolean displayZygotes) { mViewer = viewer; mDisplayZygoteMemory = displayZygotes; } @Override public void dispose() { } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { mNativeHeapDump = (NativeHeapSnapshot) newInput; } @Override public Object getParent(Object arg0) { return null; } @Override public void updateChildCount(Object element, int currentChildCount) { int childCount = 0; if (element == mNativeHeapDump) { // root element childCount = getAllocations().size(); } mViewer.setChildCount(element, childCount); } @Override public void updateElement(Object parent, int index) { Object item = null; if (parent == mNativeHeapDump) { // root element item = getAllocations().get(index); } mViewer.replace(parent, index, item); mViewer.setChildCount(item, 0); } public void displayZygoteMemory(boolean en) { mDisplayZygoteMemory = en; } private List getAllocations() { if (mDisplayZygoteMemory) { return mNativeHeapDump.getAllocations(); } else { return mNativeHeapDump.getNonZygoteAllocations(); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java0100644 0000000 0000000 00000006254 12747325007 030125 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import org.eclipse.jface.viewers.ILazyTreeContentProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import java.util.List; /** * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}. * It expects input of type {@link NativeHeapSnapshot}, and provides heap allocations * grouped by library to the UI. */ public class NativeHeapProviderByLibrary implements ILazyTreeContentProvider { private TreeViewer mViewer; private boolean mDisplayZygoteMemory; public NativeHeapProviderByLibrary(TreeViewer viewer, boolean displayZygotes) { mViewer = viewer; mDisplayZygoteMemory = displayZygotes; } @Override public void dispose() { } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } @Override public Object getParent(Object element) { return null; } @Override public void updateChildCount(Object element, int currentChildCount) { int childCount = 0; if (element instanceof NativeHeapSnapshot) { NativeHeapSnapshot snapshot = (NativeHeapSnapshot) element; childCount = getLibraryAllocations(snapshot).size(); } else if (element instanceof NativeLibraryAllocationInfo) { NativeLibraryAllocationInfo info = (NativeLibraryAllocationInfo) element; childCount = info.getAllocations().size(); } mViewer.setChildCount(element, childCount); } @Override public void updateElement(Object parent, int index) { Object item = null; int childCount = 0; if (parent instanceof NativeHeapSnapshot) { // root element NativeHeapSnapshot snapshot = (NativeHeapSnapshot) parent; item = getLibraryAllocations(snapshot).get(index); childCount = ((NativeLibraryAllocationInfo) item).getAllocations().size(); } else if (parent instanceof NativeLibraryAllocationInfo) { item = ((NativeLibraryAllocationInfo) parent).getAllocations().get(index); } mViewer.replace(parent, index, item); mViewer.setChildCount(item, childCount); } public void displayZygoteMemory(boolean en) { mDisplayZygoteMemory = en; } private List getLibraryAllocations(NativeHeapSnapshot snapshot) { if (mDisplayZygoteMemory) { return snapshot.getAllocationsByLibrary(); } else { return snapshot.getNonZygoteAllocationsByLibrary(); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java0100644 0000000 0000000 00000010430 12747325007 026301 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * A Native Heap Snapshot models a single heap dump. * * It primarily consists of a list of {@link NativeAllocationInfo} objects. From this list, * other objects of interest to the UI are computed and cached for future use. */ public class NativeHeapSnapshot { private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); private List mHeapAllocations; private List mHeapAllocationsByLibrary; private List mNonZygoteHeapAllocations; private List mNonZygoteHeapAllocationsByLibrary; private long mTotalSize; public NativeHeapSnapshot(List heapAllocations) { mHeapAllocations = heapAllocations; // precompute the total size as this is always needed. mTotalSize = getTotalMemory(heapAllocations); } protected long getTotalMemory(Collection heapSnapshot) { long total = 0; for (NativeAllocationInfo info : heapSnapshot) { total += info.getAllocationCount() * info.getSize(); } return total; } public List getAllocations() { return mHeapAllocations; } public List getAllocationsByLibrary() { if (mHeapAllocationsByLibrary != null) { return mHeapAllocationsByLibrary; } List heapAllocations = NativeLibraryAllocationInfo.constructFrom(mHeapAllocations); // cache for future uses only if it is fully resolved. if (isFullyResolved(heapAllocations)) { mHeapAllocationsByLibrary = heapAllocations; } return heapAllocations; } private boolean isFullyResolved(List heapAllocations) { for (NativeLibraryAllocationInfo info : heapAllocations) { if (info.getLibraryName().equals(NativeLibraryAllocationInfo.UNRESOLVED_LIBRARY_NAME)) { return false; } } return true; } public long getTotalSize() { return mTotalSize; } public String getFormattedMemorySize() { return String.format("%s bytes", formatMemorySize(getTotalSize())); } protected String formatMemorySize(long memSize) { return NUMBER_FORMATTER.format(memSize); } public List getNonZygoteAllocations() { if (mNonZygoteHeapAllocations != null) { return mNonZygoteHeapAllocations; } // filter out all zygote allocations mNonZygoteHeapAllocations = new ArrayList(); for (NativeAllocationInfo info : mHeapAllocations) { if (info.isZygoteChild()) { mNonZygoteHeapAllocations.add(info); } } return mNonZygoteHeapAllocations; } public List getNonZygoteAllocationsByLibrary() { if (mNonZygoteHeapAllocationsByLibrary != null) { return mNonZygoteHeapAllocationsByLibrary; } List heapAllocations = NativeLibraryAllocationInfo.constructFrom(getNonZygoteAllocations()); // cache for future uses only if it is fully resolved. if (isFullyResolved(heapAllocations)) { mNonZygoteHeapAllocationsByLibrary = heapAllocations; } return heapAllocations; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java0100644 0000000 0000000 00000011532 12747325007 030136 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeStackCallInfo; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A heap dump representation where each call site is associated with its source library. */ public final class NativeLibraryAllocationInfo { /** Library name to use when grouping before symbol resolution is complete. */ public static final String UNRESOLVED_LIBRARY_NAME = "Resolving.."; /** Any call site that cannot be resolved to a specific library goes under this name. */ private static final String UNKNOWN_LIBRARY_NAME = "unknown"; private final String mLibraryName; private final List mHeapAllocations; private int mTotalSize; private NativeLibraryAllocationInfo(String libraryName) { mLibraryName = libraryName; mHeapAllocations = new ArrayList(); } private void addAllocation(NativeAllocationInfo info) { mHeapAllocations.add(info); } private void updateTotalSize() { mTotalSize = 0; for (NativeAllocationInfo i : mHeapAllocations) { mTotalSize += i.getAllocationCount() * i.getSize(); } } public String getLibraryName() { return mLibraryName; } public long getTotalSize() { return mTotalSize; } public List getAllocations() { return mHeapAllocations; } /** * Factory method to create a list of {@link NativeLibraryAllocationInfo} objects, * given the list of {@link NativeAllocationInfo} objects. * * If the {@link NativeAllocationInfo} objects do not have their symbols resolved, * then they are grouped under the library {@link #UNRESOLVED_LIBRARY_NAME}. If they do * have their symbols resolved, but map to an unknown library, then they are grouped under * the library {@link #UNKNOWN_LIBRARY_NAME}. */ public static List constructFrom( List allocations) { if (allocations == null) { return null; } Map allocationsByLibrary = new HashMap(); // go through each native allocation and assign it to the appropriate library for (NativeAllocationInfo info : allocations) { String libName = UNRESOLVED_LIBRARY_NAME; if (info.isStackCallResolved()) { NativeStackCallInfo relevantStackCall = info.getRelevantStackCallInfo(); if (relevantStackCall != null) { libName = relevantStackCall.getLibraryName(); } else { libName = UNKNOWN_LIBRARY_NAME; } } addtoLibrary(allocationsByLibrary, libName, info); } List libraryAllocations = new ArrayList(allocationsByLibrary.values()); // now update some summary statistics for each library for (NativeLibraryAllocationInfo l : libraryAllocations) { l.updateTotalSize(); } // finally, sort by total size Collections.sort(libraryAllocations, new Comparator() { @Override public int compare(NativeLibraryAllocationInfo o1, NativeLibraryAllocationInfo o2) { return (int) (o2.getTotalSize() - o1.getTotalSize()); } }); return libraryAllocations; } private static void addtoLibrary(Map libraryAllocations, String libName, NativeAllocationInfo info) { NativeLibraryAllocationInfo libAllocationInfo = libraryAllocations.get(libName); if (libAllocationInfo == null) { libAllocationInfo = new NativeLibraryAllocationInfo(libName); libraryAllocations.put(libName, libAllocationInfo); } libAllocationInfo.addAllocation(info); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java0100644 0000000 0000000 00000002732 12747325007 030025 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; import java.util.List; public class NativeStackContentProvider implements ITreeContentProvider { @Override public Object[] getElements(Object arg0) { return getChildren(arg0); } @Override public void dispose() { } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof List) { return ((List) parentElement).toArray(); } return null; } @Override public Object getParent(Object element) { return null; } @Override public boolean hasChildren(Object element) { return false; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java0100644 0000000 0000000 00000004271 12747325007 027432 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeStackCallInfo; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.graphics.Image; public class NativeStackLabelProvider extends LabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object arg0, int arg1) { return null; } @Override public String getColumnText(Object element, int index) { if (element instanceof NativeStackCallInfo) { return getResolvedStackTraceColumnText((NativeStackCallInfo) element, index); } if (element instanceof Long) { // if the addresses have not been resolved, then just display the // addresses alone return getStackAddressColumnText((Long) element, index); } return null; } public String getResolvedStackTraceColumnText(NativeStackCallInfo info, int index) { switch (index) { case 0: return String.format("0x%08x", info.getAddress()); case 1: return info.getLibraryName(); case 2: return info.getMethodName(); case 3: return info.getSourceFile(); case 4: int l = info.getLineNumber(); return l == -1 ? "" : Integer.toString(l); } return null; } private String getStackAddressColumnText(Long address, int index) { if (index == 0) { return String.format("0x%08x", address); } return null; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java0100644 0000000 0000000 00000037542 12747325007 027533 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeLibraryMapInfo; import com.android.ddmlib.NativeStackCallInfo; import com.android.ddmuilib.DdmUiPreferences; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.operation.IRunnableWithProgress; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.io.OutputStreamWriter; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; /** * A symbol resolver task that can resolve a set of addresses to their corresponding * source method name + file name:line number. * * It first identifies the library that contains the address, and then runs addr2line on * the library to get the symbol name + source location. */ public class NativeSymbolResolverTask implements IRunnableWithProgress { private static final String ADDR2LINE; private static final String ADDR2LINE64; private static final String DEFAULT_SYMBOLS_FOLDER; private static final int ELF_CLASS32 = 1; private static final int ELF_CLASS64 = 2; private static final int ELF_DATA2LSB = 1; private static final int ELF_PT_LOAD = 1; static { String addr2lineEnv = System.getenv("ANDROID_ADDR2LINE"); String addr2line64Env = System.getenv("ANDROID_ADDR2LINE64"); ADDR2LINE = addr2lineEnv != null ? addr2lineEnv : DdmUiPreferences.getAddr2Line(); ADDR2LINE64 = addr2line64Env != null ? addr2line64Env : DdmUiPreferences.getAddr2Line64(); String symbols = System.getenv("ANDROID_SYMBOLS"); DEFAULT_SYMBOLS_FOLDER = symbols != null ? symbols : DdmUiPreferences.getSymbolDirectory(); } private List mCallSites; private List mMappedLibraries; private List mSymbolSearchFolders; /** All unresolved addresses from all the callsites. */ private SortedSet mUnresolvedAddresses; /** Set of all addresses that could were not resolved at the end of the resolution process. */ private Set mUnresolvableAddresses; /** Map of library -> [unresolved addresses mapping to this library]. */ private Map> mUnresolvedAddressesPerLibrary; /** Addresses that could not be mapped to a library, should be mostly empty. */ private Set mUnmappedAddresses; /** Cache of the resolution for every unresolved address. */ private Map mAddressResolution; /** List of libraries that were not located on disk. */ private Set mNotFoundLibraries; private String mAddr2LineErrorMessage = null; /** The addr2line command to use to resolve addresses. */ private String mAddr2LineCmd; public NativeSymbolResolverTask(List callSites, List mappedLibraries, @NonNull String symbolSearchPath, @Nullable String abi) { mCallSites = callSites; mMappedLibraries = mappedLibraries; mSymbolSearchFolders = new ArrayList(); mSymbolSearchFolders.add(DEFAULT_SYMBOLS_FOLDER); mSymbolSearchFolders.addAll(Arrays.asList(symbolSearchPath.split(":"))); mUnresolvedAddresses = new TreeSet(); mUnresolvableAddresses = new HashSet(); mUnresolvedAddressesPerLibrary = new HashMap>(); mUnmappedAddresses = new HashSet(); mAddressResolution = new HashMap(); mNotFoundLibraries = new HashSet(); if (abi == null || abi.startsWith("32")) { mAddr2LineCmd = ADDR2LINE; } else { mAddr2LineCmd = ADDR2LINE64; } } @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { monitor.beginTask("Resolving symbols", IProgressMonitor.UNKNOWN); collectAllUnresolvedAddresses(); checkCancellation(monitor); mapUnresolvedAddressesToLibrary(); checkCancellation(monitor); resolveLibraryAddresses(monitor); checkCancellation(monitor); resolveCallSites(mCallSites); monitor.done(); } private void collectAllUnresolvedAddresses() { for (NativeAllocationInfo callSite : mCallSites) { mUnresolvedAddresses.addAll(callSite.getStackCallAddresses()); } } private void mapUnresolvedAddressesToLibrary() { Set mappedAddresses = new HashSet(); for (NativeLibraryMapInfo lib : mMappedLibraries) { SortedSet addressesInLibrary = mUnresolvedAddresses.subSet(lib.getStartAddress(), lib.getEndAddress() + 1); if (addressesInLibrary.size() > 0) { mUnresolvedAddressesPerLibrary.put(lib, addressesInLibrary); mappedAddresses.addAll(addressesInLibrary); } } // unmapped addresses = unresolved addresses - mapped addresses mUnmappedAddresses.addAll(mUnresolvedAddresses); mUnmappedAddresses.removeAll(mappedAddresses); } private void resolveLibraryAddresses(IProgressMonitor monitor) throws InterruptedException { for (NativeLibraryMapInfo lib : mUnresolvedAddressesPerLibrary.keySet()) { String libPath = getLibraryLocation(lib); Set addressesToResolve = mUnresolvedAddressesPerLibrary.get(lib); if (libPath == null) { mNotFoundLibraries.add(lib.getLibraryName()); markAddressesNotResolvable(addressesToResolve, lib); } else { monitor.subTask(String.format("Resolving addresses mapped to %s.", libPath)); resolveAddresses(lib, libPath, addressesToResolve); } checkCancellation(monitor); } } private long unsigned(byte value, long shift) { return ((long) value & 0xFF) << shift; } private short elfGetHalfWord(RandomAccessFile file, long offset) throws IOException { byte[] buf = new byte[2]; file.seek(offset); file.readFully(buf, 0, 2); return (short) (unsigned(buf[0], 0) | unsigned(buf[1], 8)); } private int elfGetWord(RandomAccessFile file, long offset) throws IOException { byte[] buf = new byte[4]; file.seek(offset); file.readFully(buf, 0, 4); return (int) (unsigned(buf[0], 0) | unsigned(buf[1], 8) | unsigned(buf[2], 16) | unsigned(buf[3], 24)); } private long elfGetDoubleWord(RandomAccessFile file, long offset) throws IOException { byte[] buf = new byte[8]; file.seek(offset); file.readFully(buf, 0, 8); return unsigned(buf[0], 0) | unsigned(buf[1], 8) | unsigned(buf[2], 16) | unsigned(buf[3], 24) | unsigned(buf[4], 32) | unsigned(buf[5], 40) | unsigned(buf[6], 48) | unsigned(buf[7], 56); } private long getLoadBase(String libPath) { RandomAccessFile file; try { file = new RandomAccessFile(libPath, "r"); } catch (FileNotFoundException e) { return 0; } byte[] buffer = new byte[8]; try { file.readFully(buffer, 0, 6); } catch (IOException e) { return 0; } if (buffer[0] != 0x7f || buffer[1] != 'E' || buffer[2] != 'L' || buffer[3] != 'F' || buffer[5] != ELF_DATA2LSB) { return 0; } boolean elf32; long elfPhdrSize; long ePhnumOffset; long ePhoffOffset; long pTypeOffset; long pOffsetOffset; long pVaddrOffset; if (buffer[4] == ELF_CLASS32) { // ELFCLASS32 elf32 = true; elfPhdrSize = 32; ePhnumOffset = 44; ePhoffOffset = 28; pTypeOffset = 0; pOffsetOffset = 4; pVaddrOffset = 8; } else if (buffer[4] == ELF_CLASS64) { // ELFCLASS64 elf32 = false; elfPhdrSize = 56; ePhnumOffset = 56; ePhoffOffset = 32; pTypeOffset = 0; pOffsetOffset = 8; pVaddrOffset = 16; } else { // Unknown class type. return 0; } try { int ePhnum = elfGetHalfWord(file, ePhnumOffset); long offset; if (elf32) { offset = elfGetWord(file, ePhoffOffset); } else { offset = elfGetDoubleWord(file, ePhoffOffset); } for (int i = 0; i < ePhnum; i++) { int pType = elfGetWord(file, offset + pTypeOffset); long pOffset; if (elf32) { pOffset = elfGetWord(file, offset + pOffsetOffset); } else { pOffset = elfGetDoubleWord(file, offset + pOffsetOffset); } // Assume all offsets are zero. if (pType == ELF_PT_LOAD && pOffset == 0) { long pVaddr; if (elf32) { pVaddr = elfGetWord(file, offset + pVaddrOffset); } else { pVaddr = elfGetDoubleWord(file, offset + pVaddrOffset); } return pVaddr; } offset += elfPhdrSize; } } catch (IOException e) { return 0; } return 0; } private void resolveAddresses(NativeLibraryMapInfo lib, String libPath, Set addressesToResolve) { Process addr2line; try { addr2line = new ProcessBuilder(mAddr2LineCmd, "-C", // demangle "-f", // display function names in addition to file:number "-e", libPath).start(); } catch (IOException e) { // Since the library path is known to be valid, the only reason for an exception // is that addr2line was not found. We just save the message in this case. mAddr2LineErrorMessage = e.getMessage(); markAddressesNotResolvable(addressesToResolve, lib); return; } BufferedReader resultReader = new BufferedReader(new InputStreamReader( addr2line.getInputStream())); BufferedWriter addressWriter = new BufferedWriter(new OutputStreamWriter( addr2line.getOutputStream())); long libStartAddress = isExecutable(lib) ? 0 : lib.getStartAddress(); long libLoadBase = isExecutable(lib) ? 0 : getLoadBase(libPath); try { for (Long addr : addressesToResolve) { long offset = addr - libStartAddress + libLoadBase; addressWriter.write(Long.toHexString(offset)); addressWriter.newLine(); addressWriter.flush(); String method = resultReader.readLine(); String sourceFile = resultReader.readLine(); mAddressResolution.put(addr, new NativeStackCallInfo(addr, lib.getLibraryName(), method, sourceFile)); } } catch (IOException e) { // if there is any error, then mark the addresses not already resolved // as unresolvable. for (Long addr : addressesToResolve) { if (mAddressResolution.get(addr) == null) { markAddressNotResolvable(lib, addr); } } } try { resultReader.close(); addressWriter.close(); } catch (IOException e) { // we can ignore these exceptions } addr2line.destroy(); } private boolean isExecutable(NativeLibraryMapInfo object) { // TODO: Use a tool like readelf or nm to determine whether this object is a library // or an executable. // For now, we'll just assume that any object present in the bin folder is an executable. String devicePath = object.getLibraryName(); return devicePath.contains("/bin/"); } private void markAddressesNotResolvable(Set addressesToResolve, NativeLibraryMapInfo lib) { for (Long addr : addressesToResolve) { markAddressNotResolvable(lib, addr); } } private void markAddressNotResolvable(NativeLibraryMapInfo lib, Long addr) { mAddressResolution.put(addr, new NativeStackCallInfo(addr, lib.getLibraryName(), Long.toHexString(addr), "")); mUnresolvableAddresses.add(addr); } /** * Locate on local disk the debug library w/ symbols corresponding to the * library on the device. It searches for this library in the symbol path. * @return absolute path if found, null otherwise */ private String getLibraryLocation(NativeLibraryMapInfo lib) { String pathOnDevice = lib.getLibraryName(); String libName = new File(pathOnDevice).getName(); for (String p : mSymbolSearchFolders) { // try appending the full path on device String fullPath = p + File.separator + pathOnDevice; if (new File(fullPath).exists()) { return fullPath; } // try appending basename(library) fullPath = p + File.separator + libName; if (new File(fullPath).exists()) { return fullPath; } } return null; } private void resolveCallSites(List callSites) { for (NativeAllocationInfo callSite : callSites) { List stackInfo = new ArrayList(); for (Long addr : callSite.getStackCallAddresses()) { NativeStackCallInfo info = mAddressResolution.get(addr); if (info != null) { stackInfo.add(info); } } callSite.setResolvedStackCall(stackInfo); } } private void checkCancellation(IProgressMonitor monitor) throws InterruptedException { if (monitor.isCanceled()) { throw new InterruptedException(); } } public String getAddr2LineErrorMessage() { return mAddr2LineErrorMessage; } public Set getUnmappedAddresses() { return mUnmappedAddresses; } public Set getUnresolvableAddresses() { return mUnresolvableAddresses; } public Set getNotFoundLibraries() { return mNotFoundLibraries; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/0040755 0000000 0000000 00000000000 12747325007 022572 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java0100644 0000000 0000000 00000022102 12747325007 027242 0ustar000000000 0000000 /* * 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.ddmuilib.location; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Text; import java.text.DecimalFormat; import java.text.ParseException; /** * Encapsulation of controls handling a location coordinate in decimal and sexagesimal. *

This handle the conversion between both modes automatically by using a {@link ModifyListener} * on all the {@link Text} widgets. *

To get/set the coordinate, use {@link #setValue(double)} and {@link #getValue()} (preceded by * a call to {@link #isValueValid()}) */ public final class CoordinateControls { private double mValue; private boolean mValueValidity = false; private Text mDecimalText; private Text mSexagesimalDegreeText; private Text mSexagesimalMinuteText; private Text mSexagesimalSecondText; private final DecimalFormat mDecimalFormat = new DecimalFormat(); /** Internal flag to prevent {@link ModifyEvent} to be sent when {@link Text#setText(String)} * is called. This is an int instead of a boolean to act as a counter. */ private int mManualTextChange = 0; /** * ModifyListener for the 3 {@link Text} controls of the sexagesimal mode. */ private ModifyListener mSexagesimalListener = new ModifyListener() { @Override public void modifyText(ModifyEvent event) { if (mManualTextChange > 0) { return; } try { mValue = getValueFromSexagesimalControls(); setValueIntoDecimalControl(mValue); mValueValidity = true; } catch (ParseException e) { // wrong format empty the decimal controls. mValueValidity = false; resetDecimalControls(); } } }; /** * Creates the {@link Text} control for the decimal display of the coordinate. *

The control is expected to be placed in a Composite using a {@link GridLayout}. * @param parent The {@link Composite} parent of the control. */ public void createDecimalText(Composite parent) { mDecimalText = createTextControl(parent, "-199.999999", new ModifyListener() { @Override public void modifyText(ModifyEvent event) { if (mManualTextChange > 0) { return; } try { mValue = mDecimalFormat.parse(mDecimalText.getText()).doubleValue(); setValueIntoSexagesimalControl(mValue); mValueValidity = true; } catch (ParseException e) { // wrong format empty the sexagesimal controls. mValueValidity = false; resetSexagesimalControls(); } } }); } /** * Creates the {@link Text} control for the "degree" display of the coordinate in sexagesimal * mode. *

The control is expected to be placed in a Composite using a {@link GridLayout}. * @param parent The {@link Composite} parent of the control. */ public void createSexagesimalDegreeText(Composite parent) { mSexagesimalDegreeText = createTextControl(parent, "-199", mSexagesimalListener); //$NON-NLS-1$ } /** * Creates the {@link Text} control for the "minute" display of the coordinate in sexagesimal * mode. *

The control is expected to be placed in a Composite using a {@link GridLayout}. * @param parent The {@link Composite} parent of the control. */ public void createSexagesimalMinuteText(Composite parent) { mSexagesimalMinuteText = createTextControl(parent, "99", mSexagesimalListener); //$NON-NLS-1$ } /** * Creates the {@link Text} control for the "second" display of the coordinate in sexagesimal * mode. *

The control is expected to be placed in a Composite using a {@link GridLayout}. * @param parent The {@link Composite} parent of the control. */ public void createSexagesimalSecondText(Composite parent) { mSexagesimalSecondText = createTextControl(parent, "99.999", mSexagesimalListener); //$NON-NLS-1$ } /** * Sets the coordinate into the {@link Text} controls. * @param value the coordinate value to set. */ public void setValue(double value) { mValue = value; mValueValidity = true; setValueIntoDecimalControl(value); setValueIntoSexagesimalControl(value); } /** * Returns whether the value in the control(s) is valid. */ public boolean isValueValid() { return mValueValidity; } /** * Returns the current value set in the control(s). *

This value can be erroneous, and a check with {@link #isValueValid()} should be performed * before any call to this method. */ public double getValue() { return mValue; } /** * Enables or disables all the {@link Text} controls. * @param enabled the enabled state. */ public void setEnabled(boolean enabled) { mDecimalText.setEnabled(enabled); mSexagesimalDegreeText.setEnabled(enabled); mSexagesimalMinuteText.setEnabled(enabled); mSexagesimalSecondText.setEnabled(enabled); } private void resetDecimalControls() { mManualTextChange++; mDecimalText.setText(""); //$NON-NLS-1$ mManualTextChange--; } private void resetSexagesimalControls() { mManualTextChange++; mSexagesimalDegreeText.setText(""); //$NON-NLS-1$ mSexagesimalMinuteText.setText(""); //$NON-NLS-1$ mSexagesimalSecondText.setText(""); //$NON-NLS-1$ mManualTextChange--; } /** * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener} * @param parent the parent {@link Composite}. * @param defaultString the default string to be used to compute the {@link Text} control * size hint. * @param listener the {@link ModifyListener} to be called when the {@link Text} control is * modified. */ private Text createTextControl(Composite parent, String defaultString, ModifyListener listener) { // create the control Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE); // add the standard listener to it. text.addModifyListener(listener); // compute its size/ mManualTextChange++; text.setText(defaultString); text.pack(); Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT); text.setText(""); //$NON-NLS-1$ mManualTextChange--; GridData gridData = new GridData(); gridData.widthHint = size.x; text.setLayoutData(gridData); return text; } private double getValueFromSexagesimalControls() throws ParseException { double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue(); double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue(); double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue(); boolean isPositive = (degrees >= 0.); degrees = Math.abs(degrees); double value = degrees + minutes / 60. + seconds / 3600.; return isPositive ? value : - value; } private void setValueIntoDecimalControl(double value) { mManualTextChange++; mDecimalText.setText(String.format("%.6f", value)); mManualTextChange--; } private void setValueIntoSexagesimalControl(double value) { // get the sign and make the number positive no matter what. boolean isPositive = (value >= 0.); value = Math.abs(value); // get the degree double degrees = Math.floor(value); // get the minutes double minutes = Math.floor((value - degrees) * 60.); // get the seconds. double seconds = (value - degrees) * 3600. - minutes * 60.; mManualTextChange++; mSexagesimalDegreeText.setText( Integer.toString(isPositive ? (int)degrees : (int)- degrees)); mSexagesimalMinuteText.setText(Integer.toString((int)minutes)); mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$ mManualTextChange--; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java0100644 0000000 0000000 00000032206 12747325007 025350 0ustar000000000 0000000 /* * 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.ddmuilib.location; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; /** * A very basic GPX parser to meet the need of the emulator control panel. *

* It parses basic waypoint information, and tracks (merging segments). */ public class GpxParser { private final static String NS_GPX = "http://www.topografix.com/GPX/1/1"; //$NON-NLS-1$ private final static String NODE_WAYPOINT = "wpt"; //$NON-NLS-1$ private final static String NODE_TRACK = "trk"; //$NON-NLS-1$ private final static String NODE_TRACK_SEGMENT = "trkseg"; //$NON-NLS-1$ private final static String NODE_TRACK_POINT = "trkpt"; //$NON-NLS-1$ private final static String NODE_NAME = "name"; //$NON-NLS-1$ private final static String NODE_TIME = "time"; //$NON-NLS-1$ private final static String NODE_ELEVATION = "ele"; //$NON-NLS-1$ private final static String NODE_DESCRIPTION = "desc"; //$NON-NLS-1$ private final static String ATTR_LONGITUDE = "lon"; //$NON-NLS-1$ private final static String ATTR_LATITUDE = "lat"; //$NON-NLS-1$ private static SAXParserFactory sParserFactory; static { sParserFactory = SAXParserFactory.newInstance(); sParserFactory.setNamespaceAware(true); } private String mFileName; private GpxHandler mHandler; /** Pattern to parse time with optional sub-second precision, and optional * Z indicating the time is in UTC. */ private final static Pattern ISO8601_TIME = Pattern.compile("(\\d{4})-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:(\\.\\d+))?(Z)?"); //$NON-NLS-1$ /** * Handler for the SAX parser. */ private static class GpxHandler extends DefaultHandler { // --------- parsed data --------- List mWayPoints; List mTrackList; // --------- state for parsing --------- Track mCurrentTrack; TrackPoint mCurrentTrackPoint; WayPoint mCurrentWayPoint; final StringBuilder mStringAccumulator = new StringBuilder(); boolean mSuccess = true; @Override public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { // we only care about the standard GPX nodes. try { if (NS_GPX.equals(uri)) { if (NODE_WAYPOINT.equals(localName)) { if (mWayPoints == null) { mWayPoints = new ArrayList(); } mWayPoints.add(mCurrentWayPoint = new WayPoint()); handleLocation(mCurrentWayPoint, attributes); } else if (NODE_TRACK.equals(localName)) { if (mTrackList == null) { mTrackList = new ArrayList(); } mTrackList.add(mCurrentTrack = new Track()); } else if (NODE_TRACK_SEGMENT.equals(localName)) { // for now we do nothing here. This will merge all the segments into // a single TrackPoint list in the Track. } else if (NODE_TRACK_POINT.equals(localName)) { if (mCurrentTrack != null) { mCurrentTrack.addPoint(mCurrentTrackPoint = new TrackPoint()); handleLocation(mCurrentTrackPoint, attributes); } } } } finally { // no matter the node, we empty the StringBuilder accumulator when we start // a new node. mStringAccumulator.setLength(0); } } /** * Processes new characters for the node content. The characters are simply stored, * and will be processed when {@link #endElement(String, String, String)} is called. */ @Override public void characters(char[] ch, int start, int length) throws SAXException { mStringAccumulator.append(ch, start, length); } @Override public void endElement(String uri, String localName, String name) throws SAXException { if (NS_GPX.equals(uri)) { if (NODE_WAYPOINT.equals(localName)) { mCurrentWayPoint = null; } else if (NODE_TRACK.equals(localName)) { mCurrentTrack = null; } else if (NODE_TRACK_POINT.equals(localName)) { mCurrentTrackPoint = null; } else if (NODE_NAME.equals(localName)) { if (mCurrentTrack != null) { mCurrentTrack.setName(mStringAccumulator.toString()); } else if (mCurrentWayPoint != null) { mCurrentWayPoint.setName(mStringAccumulator.toString()); } } else if (NODE_TIME.equals(localName)) { if (mCurrentTrackPoint != null) { mCurrentTrackPoint.setTime(computeTime(mStringAccumulator.toString())); } } else if (NODE_ELEVATION.equals(localName)) { if (mCurrentTrackPoint != null) { mCurrentTrackPoint.setElevation( Double.parseDouble(mStringAccumulator.toString())); } else if (mCurrentWayPoint != null) { mCurrentWayPoint.setElevation( Double.parseDouble(mStringAccumulator.toString())); } } else if (NODE_DESCRIPTION.equals(localName)) { if (mCurrentWayPoint != null) { mCurrentWayPoint.setDescription(mStringAccumulator.toString()); } } } } @Override public void error(SAXParseException e) throws SAXException { mSuccess = false; } @Override public void fatalError(SAXParseException e) throws SAXException { mSuccess = false; } /** * Converts the string description of the time into milliseconds since epoch. * @param timeString the string data. * @return date in milliseconds. */ private long computeTime(String timeString) { // Time looks like: 2008-04-05T19:24:50Z Matcher m = ISO8601_TIME.matcher(timeString); if (m.matches()) { // get the various elements and reconstruct time as a long. try { int year = Integer.parseInt(m.group(1)); int month = Integer.parseInt(m.group(2)); int date = Integer.parseInt(m.group(3)); int hourOfDay = Integer.parseInt(m.group(4)); int minute = Integer.parseInt(m.group(5)); int second = Integer.parseInt(m.group(6)); // handle the optional parameters. int milliseconds = 0; String subSecondGroup = m.group(7); if (subSecondGroup != null) { milliseconds = (int)(1000 * Double.parseDouble(subSecondGroup)); } boolean utcTime = m.group(8) != null; // now we convert into milliseconds since epoch. Calendar c; if (utcTime) { c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$ } else { c = Calendar.getInstance(); } c.set(year, month, date, hourOfDay, minute, second); return c.getTimeInMillis() + milliseconds; } catch (NumberFormatException e) { // format is invalid, we'll return -1 below. } } // invalid time! return -1; } /** * Handles the location attributes and store them into a {@link LocationPoint}. * @param locationNode the {@link LocationPoint} to receive the location data. * @param attributes the attributes from the XML node. */ private void handleLocation(LocationPoint locationNode, Attributes attributes) { try { double longitude = Double.parseDouble(attributes.getValue(ATTR_LONGITUDE)); double latitude = Double.parseDouble(attributes.getValue(ATTR_LATITUDE)); locationNode.setLocation(longitude, latitude); } catch (NumberFormatException e) { // wrong data, do nothing. } } WayPoint[] getWayPoints() { if (mWayPoints != null) { return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); } return null; } Track[] getTracks() { if (mTrackList != null) { return mTrackList.toArray(new Track[mTrackList.size()]); } return null; } boolean getSuccess() { return mSuccess; } } /** * A GPS track. *

A track is composed of a list of {@link TrackPoint} and optional name and comment. */ public final static class Track { private String mName; private String mComment; private List mPoints = new ArrayList(); void setName(String name) { mName = name; } public String getName() { return mName; } void setComment(String comment) { mComment = comment; } public String getComment() { return mComment; } void addPoint(TrackPoint trackPoint) { mPoints.add(trackPoint); } public TrackPoint[] getPoints() { return mPoints.toArray(new TrackPoint[mPoints.size()]); } public long getFirstPointTime() { if (mPoints.size() > 0) { return mPoints.get(0).getTime(); } return -1; } public long getLastPointTime() { if (mPoints.size() > 0) { return mPoints.get(mPoints.size()-1).getTime(); } return -1; } public int getPointCount() { return mPoints.size(); } } /** * Creates a new GPX parser for a file specified by its full path. * @param fileName The full path of the GPX file to parse. */ public GpxParser(String fileName) { mFileName = fileName; } /** * Parses the GPX file. * @return true if success. */ public boolean parse() { try { SAXParser parser = sParserFactory.newSAXParser(); mHandler = new GpxHandler(); parser.parse(new InputSource(new FileReader(mFileName)), mHandler); return mHandler.getSuccess(); } catch (ParserConfigurationException e) { } catch (SAXException e) { } catch (IOException e) { } finally { } return false; } /** * Returns the parsed {@link WayPoint} objects, or null if none were found (or * if the parsing failed. */ public WayPoint[] getWayPoints() { if (mHandler != null) { return mHandler.getWayPoints(); } return null; } /** * Returns the parsed {@link Track} objects, or null if none were found (or * if the parsing failed. */ public Track[] getTracks() { if (mHandler != null) { return mHandler.getTracks(); } return null; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java0100644 0000000 0000000 00000015645 12747325007 025345 0ustar000000000 0000000 /* * 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.ddmuilib.location; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; /** * A very basic KML parser to meet the need of the emulator control panel. *

* It parses basic Placemark information. */ public class KmlParser { private final static String NS_KML_2 = "http://earth.google.com/kml/2."; //$NON-NLS-1$ private final static String NODE_PLACEMARK = "Placemark"; //$NON-NLS-1$ private final static String NODE_NAME = "name"; //$NON-NLS-1$ private final static String NODE_COORDINATES = "coordinates"; //$NON-NLS-1$ private final static Pattern sLocationPattern = Pattern.compile("([^,]+),([^,]+)(?:,([^,]+))?"); private static SAXParserFactory sParserFactory; static { sParserFactory = SAXParserFactory.newInstance(); sParserFactory.setNamespaceAware(true); } private String mFileName; private KmlHandler mHandler; /** * Handler for the SAX parser. */ private static class KmlHandler extends DefaultHandler { // --------- parsed data --------- List mWayPoints; // --------- state for parsing --------- WayPoint mCurrentWayPoint; final StringBuilder mStringAccumulator = new StringBuilder(); boolean mSuccess = true; @Override public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { // we only care about the standard GPX nodes. try { if (uri.startsWith(NS_KML_2)) { if (NODE_PLACEMARK.equals(localName)) { if (mWayPoints == null) { mWayPoints = new ArrayList(); } mWayPoints.add(mCurrentWayPoint = new WayPoint()); } } } finally { // no matter the node, we empty the StringBuilder accumulator when we start // a new node. mStringAccumulator.setLength(0); } } /** * Processes new characters for the node content. The characters are simply stored, * and will be processed when {@link #endElement(String, String, String)} is called. */ @Override public void characters(char[] ch, int start, int length) throws SAXException { mStringAccumulator.append(ch, start, length); } @Override public void endElement(String uri, String localName, String name) throws SAXException { if (uri.startsWith(NS_KML_2)) { if (NODE_PLACEMARK.equals(localName)) { mCurrentWayPoint = null; } else if (NODE_NAME.equals(localName)) { if (mCurrentWayPoint != null) { mCurrentWayPoint.setName(mStringAccumulator.toString()); } } else if (NODE_COORDINATES.equals(localName)) { if (mCurrentWayPoint != null) { parseLocation(mCurrentWayPoint, mStringAccumulator.toString()); } } } } @Override public void error(SAXParseException e) throws SAXException { mSuccess = false; } @Override public void fatalError(SAXParseException e) throws SAXException { mSuccess = false; } /** * Parses the location string and store the information into a {@link LocationPoint}. * @param locationNode the {@link LocationPoint} to receive the location data. * @param location The string containing the location info. */ private void parseLocation(LocationPoint locationNode, String location) { Matcher m = sLocationPattern.matcher(location); if (m.matches()) { try { double longitude = Double.parseDouble(m.group(1)); double latitude = Double.parseDouble(m.group(2)); locationNode.setLocation(longitude, latitude); if (m.groupCount() == 3) { // looks like we have elevation data. locationNode.setElevation(Double.parseDouble(m.group(3))); } } catch (NumberFormatException e) { // wrong data, do nothing. } } } WayPoint[] getWayPoints() { if (mWayPoints != null) { return mWayPoints.toArray(new WayPoint[mWayPoints.size()]); } return null; } boolean getSuccess() { return mSuccess; } } /** * Creates a new GPX parser for a file specified by its full path. * @param fileName The full path of the GPX file to parse. */ public KmlParser(String fileName) { mFileName = fileName; } /** * Parses the GPX file. * @return true if success. */ public boolean parse() { try { SAXParser parser = sParserFactory.newSAXParser(); mHandler = new KmlHandler(); parser.parse(new InputSource(new FileReader(mFileName)), mHandler); return mHandler.getSuccess(); } catch (ParserConfigurationException e) { } catch (SAXException e) { } catch (IOException e) { } finally { } return false; } /** * Returns the parsed {@link WayPoint} objects, or null if none were found (or * if the parsing failed. */ public WayPoint[] getWayPoints() { if (mHandler != null) { return mHandler.getWayPoints(); } return null; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java0100644 0000000 0000000 00000002635 12747325007 026222 0ustar000000000 0000000 /* * 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.ddmuilib.location; /** * Base class for Location aware points. */ class LocationPoint { private double mLongitude; private double mLatitude; private boolean mHasElevation = false; private double mElevation; final void setLocation(double longitude, double latitude) { mLongitude = longitude; mLatitude = latitude; } public final double getLongitude() { return mLongitude; } public final double getLatitude() { return mLatitude; } final void setElevation(double elevation) { mElevation = elevation; mHasElevation = true; } public final boolean hasElevation() { return mHasElevation; } public final double getElevation() { return mElevation; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java0100644 0000000 0000000 00000002624 12747325007 027550 0ustar000000000 0000000 /* * 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.ddmuilib.location; import com.android.ddmuilib.location.GpxParser.Track; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.Viewer; /** * Content provider to display {@link Track} objects in a Table. *

The expected type for the input is {@link Track}[]. */ public class TrackContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof Track[]) { return (Track[])inputElement; } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java0100644 0000000 0000000 00000004730 12747325007 027155 0ustar000000000 0000000 /* * 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.ddmuilib.location; import com.android.ddmuilib.location.GpxParser.Track; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Table; import java.util.Date; /** * Label Provider for {@link Table} objects displaying {@link Track} objects. */ public class TrackLabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof Track) { Track track = (Track)element; switch (columnIndex) { case 0: return track.getName(); case 1: return Integer.toString(track.getPointCount()); case 2: long time = track.getFirstPointTime(); if (time != -1) { return new Date(time).toString(); } break; case 3: time = track.getLastPointTime(); if (time != -1) { return new Date(time).toString(); } break; case 4: return track.getComment(); } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java0100644 0000000 0000000 00000001645 12747325007 025516 0ustar000000000 0000000 /* * 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.ddmuilib.location; /** * A Track Point. *

A track point is a point in time and space. */ public class TrackPoint extends LocationPoint { private long mTime; void setTime(long time) { mTime = time; } public long getTime() { return mTime; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java0100644 0000000 0000000 00000002235 12747325007 025206 0ustar000000000 0000000 /* * 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.ddmuilib.location; /** * A GPS/KML way point. *

A waypoint is a user specified location, with a name and an optional description. */ public final class WayPoint extends LocationPoint { private String mName; private String mDescription; void setName(String name) { mName = name; } public String getName() { return mName; } void setDescription(String description) { mDescription = description; } public String getDescription() { return mDescription; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java0100644 0000000 0000000 00000002554 12747325007 030260 0ustar000000000 0000000 /* * 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.ddmuilib.location; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.Viewer; /** * Content provider to display {@link WayPoint} objects in a Table. *

The expected type for the input is {@link WayPoint}[]. */ public class WayPointContentProvider implements IStructuredContentProvider { @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof WayPoint[]) { return (WayPoint[])inputElement; } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java0100644 0000000 0000000 00000004475 12747325007 027671 0ustar000000000 0000000 /* * 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.ddmuilib.location; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Table; /** * Label Provider for {@link Table} objects displaying {@link WayPoint} objects. */ public class WayPointLabelProvider implements ITableLabelProvider { @Override public Image getColumnImage(Object element, int columnIndex) { return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof WayPoint) { WayPoint wayPoint = (WayPoint)element; switch (columnIndex) { case 0: return wayPoint.getName(); case 1: return String.format("%.6f", wayPoint.getLongitude()); case 2: return String.format("%.6f", wayPoint.getLatitude()); case 3: if (wayPoint.hasElevation()) { return String.format("%.1f", wayPoint.getElevation()); } else { return "-"; } case 4: return wayPoint.getDescription(); } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/0040755 0000000 0000000 00000000000 12747325007 021543 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/0040755 0000000 0000000 00000000000 12747325007 022664 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java0100644 0000000 0000000 00000005441 12747325007 027163 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; public class BugReportImporter { private final static String TAG_HEADER = "------ EVENT LOG TAGS ------"; private final static String LOG_HEADER = "------ EVENT LOG ------"; private final static String HEADER_TAG = "------"; private String[] mTags; private String[] mLog; public BugReportImporter(String filePath) throws FileNotFoundException { BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(filePath))); try { String line; while ((line = reader.readLine()) != null) { if (TAG_HEADER.equals(line)) { readTags(reader); return; } } } catch (IOException e) { } finally { if (reader != null) { try { reader.close(); } catch (IOException ignore) { } } } } public String[] getTags() { return mTags; } public String[] getLog() { return mLog; } private void readTags(BufferedReader reader) throws IOException { String line; ArrayList content = new ArrayList(); while ((line = reader.readLine()) != null) { if (LOG_HEADER.equals(line)) { mTags = content.toArray(new String[content.size()]); readLog(reader); return; } else { content.add(line); } } } private void readLog(BufferedReader reader) throws IOException { String line; ArrayList content = new ArrayList(); while ((line = reader.readLine()) != null) { if (line.startsWith(HEADER_TAG) == false) { content.add(line); } else { break; } } mLog = content.toArray(new String[content.size()]); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java0100644 0000000 0000000 00000003162 12747325007 027254 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import java.util.ArrayList; public class DisplayFilteredLog extends DisplayLog { public DisplayFilteredLog(String name) { super(name); } /** * Adds event to the display. */ @Override void newEvent(EventContainer event, EventLogParser logParser) { ArrayList valueDescriptors = new ArrayList(); ArrayList occurrenceDescriptors = new ArrayList(); if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); } } /** * Gets display type * * @return display type as an integer */ @Override int getDisplayType() { return DISPLAY_TYPE_FILTERED_LOG; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java0100644 0000000 0000000 00000037510 12747325007 026121 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.EventValueDescription; import com.android.ddmlib.log.InvalidTypeException; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; import org.jfree.chart.renderer.xy.XYAreaRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.time.Millisecond; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; public class DisplayGraph extends EventDisplay { public DisplayGraph(String name) { super(name); } /** * Resets the display. */ @Override void resetUI() { Collection datasets = mValueTypeDataSetMap.values(); for (TimeSeriesCollection dataset : datasets) { dataset.removeAllSeries(); } if (mOccurrenceDataSet != null) { mOccurrenceDataSet.removeAllSeries(); } mValueDescriptorSeriesMap.clear(); mOcurrenceDescriptorSeriesMap.clear(); } /** * Creates the UI for the event display. * @param parent the parent composite. * @param logParser the current log parser. * @return the created control (which may have children). */ @Override public Control createComposite(final Composite parent, EventLogParser logParser, final ILogColumnListener listener) { String title = getChartTitle(logParser); return createCompositeChart(parent, logParser, title); } /** * Adds event to the display. */ @Override void newEvent(EventContainer event, EventLogParser logParser) { ArrayList valueDescriptors = new ArrayList(); ArrayList occurrenceDescriptors = new ArrayList(); if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); } } /** * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from * the two lists. *

This method is only called when at least one of the descriptor list is non empty. * @param event * @param logParser * @param valueDescriptors * @param occurrenceDescriptors */ private void updateChart(EventContainer event, EventLogParser logParser, ArrayList valueDescriptors, ArrayList occurrenceDescriptors) { Map tagMap = logParser.getTagMap(); Millisecond millisecondTime = null; long msec = -1; // If the event container is a cpu container (tag == 2721), and there is no descriptor // for the total CPU load, then we do accumulate all the values. boolean accumulateValues = false; double accumulatedValue = 0; if (event.mTag == 2721) { accumulateValues = true; for (ValueDisplayDescriptor descriptor : valueDescriptors) { accumulateValues &= (descriptor.valueIndex != 0); } } for (ValueDisplayDescriptor descriptor : valueDescriptors) { try { // get the hashmap for this descriptor HashMap map = mValueDescriptorSeriesMap.get(descriptor); // if it's not there yet, we create it. if (map == null) { map = new HashMap(); mValueDescriptorSeriesMap.put(descriptor, map); } // get the TimeSeries for this pid TimeSeries timeSeries = map.get(event.pid); // if it doesn't exist yet, we create it if (timeSeries == null) { // get the series name String seriesFullName = null; String seriesLabel = getSeriesLabel(event, descriptor); switch (mValueDescriptorCheck) { case EVENT_CHECK_SAME_TAG: seriesFullName = String.format("%1$s / %2$s", seriesLabel, descriptor.valueName); break; case EVENT_CHECK_SAME_VALUE: seriesFullName = String.format("%1$s", seriesLabel); break; default: seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, tagMap.get(descriptor.eventTag), descriptor.valueName); break; } // get the data set for this ValueType TimeSeriesCollection dataset = getValueDataset( logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] .getValueType(), accumulateValues); // create the series timeSeries = new TimeSeries(seriesFullName, Millisecond.class); if (mMaximumChartItemAge != -1) { timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); } dataset.addSeries(timeSeries); // add it to the map. map.put(event.pid, timeSeries); } // update the timeSeries. // get the value from the event double value = event.getValueAsDouble(descriptor.valueIndex); // accumulate the values if needed. if (accumulateValues) { accumulatedValue += value; value = accumulatedValue; } // get the time if (millisecondTime == null) { msec = (long)event.sec * 1000L + (event.nsec / 1000000L); millisecondTime = new Millisecond(new Date(msec)); } // add the value to the time series timeSeries.addOrUpdate(millisecondTime, value); } catch (InvalidTypeException e) { // just ignore this descriptor if there's a type mismatch } } for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { try { // get the hashmap for this descriptor HashMap map = mOcurrenceDescriptorSeriesMap.get(descriptor); // if it's not there yet, we create it. if (map == null) { map = new HashMap(); mOcurrenceDescriptorSeriesMap.put(descriptor, map); } // get the TimeSeries for this pid TimeSeries timeSeries = map.get(event.pid); // if it doesn't exist yet, we create it. if (timeSeries == null) { String seriesLabel = getSeriesLabel(event, descriptor); String seriesFullName = String.format("[%1$s:%2$s]", tagMap.get(descriptor.eventTag), seriesLabel); timeSeries = new TimeSeries(seriesFullName, Millisecond.class); if (mMaximumChartItemAge != -1) { timeSeries.setMaximumItemAge(mMaximumChartItemAge); } getOccurrenceDataSet().addSeries(timeSeries); map.put(event.pid, timeSeries); } // update the series // get the time if (millisecondTime == null) { msec = (long)event.sec * 1000L + (event.nsec / 1000000L); millisecondTime = new Millisecond(new Date(msec)); } // add the value to the time series timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused } catch (InvalidTypeException e) { // just ignore this descriptor if there's a type mismatch } } // go through all the series and remove old values. if (msec != -1 && mMaximumChartItemAge != -1) { Collection> pidMapValues = mValueDescriptorSeriesMap.values(); for (HashMap pidMapValue : pidMapValues) { Collection seriesCollection = pidMapValue.values(); for (TimeSeries timeSeries : seriesCollection) { timeSeries.removeAgedItems(msec, true); } } pidMapValues = mOcurrenceDescriptorSeriesMap.values(); for (HashMap pidMapValue : pidMapValues) { Collection seriesCollection = pidMapValue.values(); for (TimeSeries timeSeries : seriesCollection) { timeSeries.removeAgedItems(msec, true); } } } } /** * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}. * If the data set is not yet created, it is first allocated and set up into the * {@link org.jfree.chart.JFreeChart} object. * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set. * @param accumulateValues */ private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) { TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); if (dataset == null) { // create the data set and store it in the map dataset = new TimeSeriesCollection(); mValueTypeDataSetMap.put(type, dataset); // create the renderer and configure it depending on the ValueType AbstractXYItemRenderer renderer; if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) { renderer = new XYAreaRenderer(); } else { XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT); renderer = r; } // set both the dataset and the renderer in the plot object. XYPlot xyPlot = mChart.getXYPlot(); xyPlot.setDataset(mDataSetCount, dataset); xyPlot.setRenderer(mDataSetCount, renderer); // put a new axis label, and configure it. NumberAxis axis = new NumberAxis(type.toString()); if (type == EventValueDescription.ValueType.PERCENT) { // force percent range to be (0,100) fixed. axis.setAutoRange(false); axis.setRange(0., 100.); } // for the index, we ignore the occurrence dataset int count = mDataSetCount; if (mOccurrenceDataSet != null) { count--; } xyPlot.setRangeAxis(count, axis); if ((count % 2) == 0) { xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); } else { xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); } // now we link the dataset and the axis xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); mDataSetCount++; } return dataset; } /** * Return the series label for this event. This only contains the pid information. * @param event the {@link EventContainer} * @param descriptor the {@link OccurrenceDisplayDescriptor} * @return the series label. * @throws InvalidTypeException */ private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) throws InvalidTypeException { if (descriptor.seriesValueIndex != -1) { if (descriptor.includePid == false) { return event.getValueAsString(descriptor.seriesValueIndex); } else { return String.format("%1$s (%2$d)", event.getValueAsString(descriptor.seriesValueIndex), event.pid); } } return Integer.toString(event.pid); } /** * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object. */ private TimeSeriesCollection getOccurrenceDataSet() { if (mOccurrenceDataSet == null) { mOccurrenceDataSet = new TimeSeriesCollection(); XYPlot xyPlot = mChart.getXYPlot(); xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); OccurrenceRenderer renderer = new OccurrenceRenderer(); renderer.setBaseShapesVisible(false); xyPlot.setRenderer(mDataSetCount, renderer); mDataSetCount++; } return mOccurrenceDataSet; } /** * Gets display type * * @return display type as an integer */ @Override int getDisplayType() { return DISPLAY_TYPE_GRAPH; } /** * Sets the current {@link EventLogParser} object. */ @Override protected void setNewLogParser(EventLogParser logParser) { if (mChart != null) { mChart.setTitle(getChartTitle(logParser)); } } /** * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. * * @param logParser the logParser. * @return the chart title. */ private String getChartTitle(EventLogParser logParser) { if (mValueDescriptors.size() > 0) { String chartDesc = null; switch (mValueDescriptorCheck) { case EVENT_CHECK_SAME_TAG: if (logParser != null) { chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); } break; case EVENT_CHECK_SAME_VALUE: if (logParser != null) { chartDesc = String.format("%1$s / %2$s", logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), mValueDescriptors.get(0).valueName); } break; } if (chartDesc != null) { return String.format("%1$s - %2$s", mName, chartDesc); } } return mName; } }ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java0100644 0000000 0000000 00000033406 12747325007 025601 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.EventValueDescription; import com.android.ddmlib.log.InvalidTypeException; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.TableHelper; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import java.util.ArrayList; import java.util.Calendar; public class DisplayLog extends EventDisplay { public DisplayLog(String name) { super(name); } private final static String PREFS_COL_DATE = "EventLogPanel.log.Col1"; //$NON-NLS-1$ private final static String PREFS_COL_PID = "EventLogPanel.log.Col2"; //$NON-NLS-1$ private final static String PREFS_COL_EVENTTAG = "EventLogPanel.log.Col3"; //$NON-NLS-1$ private final static String PREFS_COL_VALUENAME = "EventLogPanel.log.Col4"; //$NON-NLS-1$ private final static String PREFS_COL_VALUE = "EventLogPanel.log.Col5"; //$NON-NLS-1$ private final static String PREFS_COL_TYPE = "EventLogPanel.log.Col6"; //$NON-NLS-1$ /** * Resets the display. */ @Override void resetUI() { mLogTable.removeAll(); } /** * Adds event to the display. */ @Override void newEvent(EventContainer event, EventLogParser logParser) { addToLog(event, logParser); } /** * Creates the UI for the event display. * * @param parent the parent composite. * @param logParser the current log parser. * @return the created control (which may have children). */ @Override Control createComposite(Composite parent, EventLogParser logParser, ILogColumnListener listener) { return createLogUI(parent, listener); } /** * Adds an {@link EventContainer} to the log. * * @param event the event. * @param logParser the log parser. */ private void addToLog(EventContainer event, EventLogParser logParser) { ScrollBar bar = mLogTable.getVerticalBar(); boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); // get the date. Calendar c = Calendar.getInstance(); long msec = event.sec * 1000L; c.setTimeInMillis(msec); // convert the time into a string String date = String.format("%1$tF %1$tT", c); String eventName = logParser.getTagMap().get(event.mTag); String pidName = Integer.toString(event.pid); // get the value description EventValueDescription[] valueDescription = logParser.getEventInfoMap().get(event.mTag); if (valueDescription != null) { for (int i = 0; i < valueDescription.length; i++) { EventValueDescription description = valueDescription[i]; try { String value = event.getValueAsString(i); logValue(date, pidName, eventName, description.getName(), value, description.getEventValueType(), description.getValueType()); } catch (InvalidTypeException e) { logValue(date, pidName, eventName, description.getName(), e.getMessage(), description.getEventValueType(), description.getValueType()); } } // scroll if needed, by showing the last item if (scroll) { int itemCount = mLogTable.getItemCount(); if (itemCount > 0) { mLogTable.showItem(mLogTable.getItem(itemCount - 1)); } } } } /** * Adds an {@link EventContainer} to the log. Only add the values/occurrences defined by * the list of descriptors. If an event is configured to be displayed by value and occurrence, * only the values are displayed (as they mark an event occurrence anyway). *

This method is only called when at least one of the descriptor list is non empty. * * @param event * @param logParser * @param valueDescriptors * @param occurrenceDescriptors */ protected void addToLog(EventContainer event, EventLogParser logParser, ArrayList valueDescriptors, ArrayList occurrenceDescriptors) { ScrollBar bar = mLogTable.getVerticalBar(); boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); // get the date. Calendar c = Calendar.getInstance(); long msec = event.sec * 1000L; c.setTimeInMillis(msec); // convert the time into a string String date = String.format("%1$tF %1$tT", c); String eventName = logParser.getTagMap().get(event.mTag); String pidName = Integer.toString(event.pid); if (valueDescriptors.size() > 0) { for (ValueDisplayDescriptor descriptor : valueDescriptors) { logDescriptor(event, descriptor, date, pidName, eventName, logParser); } } else { // we display the event. Since the StringBuilder contains the header (date, event name, // pid) at this point, there isn't anything else to display. } // scroll if needed, by showing the last item if (scroll) { int itemCount = mLogTable.getItemCount(); if (itemCount > 0) { mLogTable.showItem(mLogTable.getItem(itemCount - 1)); } } } /** * Logs a value in the ui. * * @param date * @param pid * @param event * @param valueName * @param value * @param eventValueType * @param valueType */ private void logValue(String date, String pid, String event, String valueName, String value, EventContainer.EventValueType eventValueType, EventValueDescription.ValueType valueType) { TableItem item = new TableItem(mLogTable, SWT.NONE); item.setText(0, date); item.setText(1, pid); item.setText(2, event); item.setText(3, valueName); item.setText(4, value); String type; if (valueType != EventValueDescription.ValueType.NOT_APPLICABLE) { type = String.format("%1$s, %2$s", eventValueType.toString(), valueType.toString()); } else { type = eventValueType.toString(); } item.setText(5, type); } /** * Logs a value from an {@link EventContainer} as defined by the {@link ValueDisplayDescriptor}. * * @param event the EventContainer * @param descriptor the ValueDisplayDescriptor defining which value to display. * @param date the date of the event in a string. * @param pidName * @param eventName * @param logParser */ private void logDescriptor(EventContainer event, ValueDisplayDescriptor descriptor, String date, String pidName, String eventName, EventLogParser logParser) { String value; try { value = event.getValueAsString(descriptor.valueIndex); } catch (InvalidTypeException e) { value = e.getMessage(); } EventValueDescription[] values = logParser.getEventInfoMap().get(event.mTag); EventValueDescription valueDescription = values[descriptor.valueIndex]; logValue(date, pidName, eventName, descriptor.valueName, value, valueDescription.getEventValueType(), valueDescription.getValueType()); } /** * Creates the UI for a log display. * * @param parent the parent {@link Composite} * @param listener the {@link ILogColumnListener} to notify on column resize events. * @return the top Composite of the UI. */ private Control createLogUI(Composite parent, final ILogColumnListener listener) { Composite mainComp = new Composite(parent, SWT.NONE); GridLayout gl; mainComp.setLayout(gl = new GridLayout(1, false)); gl.marginHeight = gl.marginWidth = 0; mainComp.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mLogTable = null; } }); Label l = new Label(mainComp, SWT.CENTER); l.setText(mName); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mLogTable = new Table(mainComp, SWT.MULTI | SWT.FULL_SELECTION | SWT.V_SCROLL | SWT.BORDER); mLogTable.setLayoutData(new GridData(GridData.FILL_BOTH)); IPreferenceStore store = DdmUiPreferences.getStore(); TableColumn col = TableHelper.createTableColumn( mLogTable, "Time", SWT.LEFT, "0000-00-00 00:00:00", PREFS_COL_DATE, store); //$NON-NLS-1$ col.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Object source = e.getSource(); if (source instanceof TableColumn) { listener.columnResized(0, (TableColumn) source); } } }); col = TableHelper.createTableColumn( mLogTable, "pid", SWT.LEFT, "0000", PREFS_COL_PID, store); //$NON-NLS-1$ col.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Object source = e.getSource(); if (source instanceof TableColumn) { listener.columnResized(1, (TableColumn) source); } } }); col = TableHelper.createTableColumn( mLogTable, "Event", SWT.LEFT, "abcdejghijklmno", PREFS_COL_EVENTTAG, store); //$NON-NLS-1$ col.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Object source = e.getSource(); if (source instanceof TableColumn) { listener.columnResized(2, (TableColumn) source); } } }); col = TableHelper.createTableColumn( mLogTable, "Name", SWT.LEFT, "Process Name", PREFS_COL_VALUENAME, store); //$NON-NLS-1$ col.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Object source = e.getSource(); if (source instanceof TableColumn) { listener.columnResized(3, (TableColumn) source); } } }); col = TableHelper.createTableColumn( mLogTable, "Value", SWT.LEFT, "0000000", PREFS_COL_VALUE, store); //$NON-NLS-1$ col.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Object source = e.getSource(); if (source instanceof TableColumn) { listener.columnResized(4, (TableColumn) source); } } }); col = TableHelper.createTableColumn( mLogTable, "Type", SWT.LEFT, "long, seconds", PREFS_COL_TYPE, store); //$NON-NLS-1$ col.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { Object source = e.getSource(); if (source instanceof TableColumn) { listener.columnResized(5, (TableColumn) source); } } }); mLogTable.setHeaderVisible(true); mLogTable.setLinesVisible(true); return mainComp; } /** * Resizes the index-th column of the log {@link Table} (if applicable). *

* This does nothing if the Table object is null (because the display * type does not use a column) or if the index-th column is in fact the originating * column passed as argument. * * @param index the index of the column to resize * @param sourceColumn the original column that was resize, and on which we need to sync the * index-th column width. */ @Override void resizeColumn(int index, TableColumn sourceColumn) { if (mLogTable != null) { TableColumn col = mLogTable.getColumn(index); if (col != sourceColumn) { col.setWidth(sourceColumn.getWidth()); } } } /** * Gets display type * * @return display type as an integer */ @Override int getDisplayType() { return DISPLAY_TYPE_LOG_ALL; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java0100644 0000000 0000000 00000030132 12747325007 025765 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.InvalidTypeException; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.jfree.chart.labels.CustomXYToolTipGenerator; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYBarRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.time.FixedMillisecond; import org.jfree.data.time.SimpleTimePeriod; import org.jfree.data.time.TimePeriodValues; import org.jfree.data.time.TimePeriodValuesCollection; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.util.ShapeUtilities; import java.awt.Color; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.regex.Pattern; public class DisplaySync extends SyncCommon { // Information to graph for each authority private TimePeriodValues mDatasetsSync[]; private List mTooltipsSync[]; private CustomXYToolTipGenerator mTooltipGenerators[]; private TimeSeries mDatasetsSyncTickle[]; // Dataset of error events to graph private TimeSeries mDatasetError; public DisplaySync(String name) { super(name); } /** * Creates the UI for the event display. * @param parent the parent composite. * @param logParser the current log parser. * @return the created control (which may have children). */ @Override public Control createComposite(final Composite parent, EventLogParser logParser, final ILogColumnListener listener) { Control composite = createCompositeChart(parent, logParser, "Sync Status"); resetUI(); return composite; } /** * Resets the display. */ @Override void resetUI() { super.resetUI(); XYPlot xyPlot = mChart.getXYPlot(); XYBarRenderer br = new XYBarRenderer(); mDatasetsSync = new TimePeriodValues[NUM_AUTHS]; @SuppressWarnings("unchecked") List mTooltipsSyncTmp[] = new List[NUM_AUTHS]; mTooltipsSync = mTooltipsSyncTmp; mTooltipGenerators = new CustomXYToolTipGenerator[NUM_AUTHS]; TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); xyPlot.setDataset(tpvc); xyPlot.setRenderer(0, br); XYLineAndShapeRenderer ls = new XYLineAndShapeRenderer(); ls.setBaseLinesVisible(false); mDatasetsSyncTickle = new TimeSeries[NUM_AUTHS]; TimeSeriesCollection tsc = new TimeSeriesCollection(); xyPlot.setDataset(1, tsc); xyPlot.setRenderer(1, ls); mDatasetError = new TimeSeries("Errors", FixedMillisecond.class); xyPlot.setDataset(2, new TimeSeriesCollection(mDatasetError)); XYLineAndShapeRenderer errls = new XYLineAndShapeRenderer(); errls.setBaseLinesVisible(false); errls.setSeriesPaint(0, Color.RED); xyPlot.setRenderer(2, errls); for (int i = 0; i < NUM_AUTHS; i++) { br.setSeriesPaint(i, AUTH_COLORS[i]); ls.setSeriesPaint(i, AUTH_COLORS[i]); mDatasetsSync[i] = new TimePeriodValues(AUTH_NAMES[i]); tpvc.addSeries(mDatasetsSync[i]); mTooltipsSync[i] = new ArrayList(); mTooltipGenerators[i] = new CustomXYToolTipGenerator(); br.setSeriesToolTipGenerator(i, mTooltipGenerators[i]); mTooltipGenerators[i].addToolTipSeries(mTooltipsSync[i]); mDatasetsSyncTickle[i] = new TimeSeries(AUTH_NAMES[i] + " tickle", FixedMillisecond.class); tsc.addSeries(mDatasetsSyncTickle[i]); ls.setSeriesShape(i, ShapeUtilities.createUpTriangle(2.5f)); } } /** * Updates the display with a new event. * * @param event The event * @param logParser The parser providing the event. */ @Override void newEvent(EventContainer event, EventLogParser logParser) { super.newEvent(event, logParser); // Handle sync operation try { if (event.mTag == EVENT_TICKLE) { int auth = getAuth(event.getValueAsString(0)); if (auth >= 0) { long msec = event.sec * 1000L + (event.nsec / 1000000L); mDatasetsSyncTickle[auth].addOrUpdate(new FixedMillisecond(msec), -1); } } } catch (InvalidTypeException e) { } } /** * Generate the height for an event. * Height is somewhat arbitrarily the count of "things" that happened * during the sync. * When network traffic measurements are available, code should be modified * to use that instead. * @param details The details string associated with the event * @return The height in arbirary units (0-100) */ private int getHeightFromDetails(String details) { if (details == null) { return 1; // Arbitrary } int total = 0; String parts[] = details.split("[a-zA-Z]"); for (String part : parts) { if ("".equals(part)) continue; total += Integer.parseInt(part); } if (total == 0) { total = 1; } return total; } /** * Generates the tooltips text for an event. * This method decodes the cryptic details string. * @param auth The authority associated with the event * @param details The details string * @param eventSource server, poll, etc. * @return The text to display in the tooltips */ private String getTextFromDetails(int auth, String details, int eventSource) { StringBuffer sb = new StringBuffer(); sb.append(AUTH_NAMES[auth]).append(": \n"); Scanner scanner = new Scanner(details); Pattern charPat = Pattern.compile("[a-zA-Z]"); Pattern numPat = Pattern.compile("[0-9]+"); while (scanner.hasNext()) { String key = scanner.findInLine(charPat); int val = Integer.parseInt(scanner.findInLine(numPat)); if (auth == GMAIL && "M".equals(key)) { sb.append("messages from server: ").append(val).append("\n"); } else if (auth == GMAIL && "L".equals(key)) { sb.append("labels from server: ").append(val).append("\n"); } else if (auth == GMAIL && "C".equals(key)) { sb.append("check conversation requests from server: ").append(val).append("\n"); } else if (auth == GMAIL && "A".equals(key)) { sb.append("attachments from server: ").append(val).append("\n"); } else if (auth == GMAIL && "U".equals(key)) { sb.append("op updates from server: ").append(val).append("\n"); } else if (auth == GMAIL && "u".equals(key)) { sb.append("op updates to server: ").append(val).append("\n"); } else if (auth == GMAIL && "S".equals(key)) { sb.append("send/receive cycles: ").append(val).append("\n"); } else if ("Q".equals(key)) { sb.append("queries to server: ").append(val).append("\n"); } else if ("E".equals(key)) { sb.append("entries from server: ").append(val).append("\n"); } else if ("u".equals(key)) { sb.append("updates from client: ").append(val).append("\n"); } else if ("i".equals(key)) { sb.append("inserts from client: ").append(val).append("\n"); } else if ("d".equals(key)) { sb.append("deletes from client: ").append(val).append("\n"); } else if ("f".equals(key)) { sb.append("full sync requested\n"); } else if ("r".equals(key)) { sb.append("partial sync unavailable\n"); } else if ("X".equals(key)) { sb.append("hard error\n"); } else if ("e".equals(key)) { sb.append("number of parse exceptions: ").append(val).append("\n"); } else if ("c".equals(key)) { sb.append("number of conflicts: ").append(val).append("\n"); } else if ("a".equals(key)) { sb.append("number of auth exceptions: ").append(val).append("\n"); } else if ("D".equals(key)) { sb.append("too many deletions\n"); } else if ("R".equals(key)) { sb.append("too many retries: ").append(val).append("\n"); } else if ("b".equals(key)) { sb.append("database error\n"); } else if ("x".equals(key)) { sb.append("soft error\n"); } else if ("l".equals(key)) { sb.append("sync already in progress\n"); } else if ("I".equals(key)) { sb.append("io exception\n"); } else if (auth == CONTACTS && "g".equals(key)) { sb.append("aggregation query: ").append(val).append("\n"); } else if (auth == CONTACTS && "G".equals(key)) { sb.append("aggregation merge: ").append(val).append("\n"); } else if (auth == CONTACTS && "n".equals(key)) { sb.append("num entries: ").append(val).append("\n"); } else if (auth == CONTACTS && "p".equals(key)) { sb.append("photos uploaded from server: ").append(val).append("\n"); } else if (auth == CONTACTS && "P".equals(key)) { sb.append("photos downloaded from server: ").append(val).append("\n"); } else if (auth == CALENDAR && "F".equals(key)) { sb.append("server refresh\n"); } else if (auth == CALENDAR && "s".equals(key)) { sb.append("server diffs fetched\n"); } else { sb.append(key).append("=").append(val); } } if (eventSource == 0) { sb.append("(server)"); } else if (eventSource == 1) { sb.append("(local)"); } else if (eventSource == 2) { sb.append("(poll)"); } else if (eventSource == 3) { sb.append("(user)"); } scanner.close(); return sb.toString(); } /** * Callback to process a sync event. */ @Override void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, String details, boolean newEvent, int syncSource) { if (!newEvent) { // Details arrived for a previous sync event // Remove event before reinserting. int lastItem = mDatasetsSync[auth].getItemCount(); mDatasetsSync[auth].delete(lastItem-1, lastItem-1); mTooltipsSync[auth].remove(lastItem-1); } double height = getHeightFromDetails(details); height = height / (stopTime - startTime + 1) * 10000; if (height > 30) { height = 30; } mDatasetsSync[auth].add(new SimpleTimePeriod(startTime, stopTime), height); mTooltipsSync[auth].add(getTextFromDetails(auth, details, syncSource)); mTooltipGenerators[auth].addToolTipSeries(mTooltipsSync[auth]); if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { long msec = event.sec * 1000L + (event.nsec / 1000000L); mDatasetError.addOrUpdate(new FixedMillisecond(msec), -1); } } /** * Gets display type * * @return display type as an integer */ @Override int getDisplayType() { return DISPLAY_TYPE_SYNC; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java0100644 0000000 0000000 00000014642 12747325007 027653 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; import org.jfree.chart.renderer.xy.XYBarRenderer; import org.jfree.data.time.RegularTimePeriod; import org.jfree.data.time.SimpleTimePeriod; import org.jfree.data.time.TimePeriodValues; import org.jfree.data.time.TimePeriodValuesCollection; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; public class DisplaySyncHistogram extends SyncCommon { Map mTimePeriodMap[]; // Information to graph for each authority private TimePeriodValues mDatasetsSyncHist[]; public DisplaySyncHistogram(String name) { super(name); } /** * Creates the UI for the event display. * @param parent the parent composite. * @param logParser the current log parser. * @return the created control (which may have children). */ @Override public Control createComposite(final Composite parent, EventLogParser logParser, final ILogColumnListener listener) { Control composite = createCompositeChart(parent, logParser, "Sync Histogram"); resetUI(); return composite; } /** * Resets the display. */ @Override void resetUI() { super.resetUI(); XYPlot xyPlot = mChart.getXYPlot(); AbstractXYItemRenderer br = new XYBarRenderer(); mDatasetsSyncHist = new TimePeriodValues[NUM_AUTHS+1]; @SuppressWarnings("unchecked") Map mTimePeriodMapTmp[] = new HashMap[NUM_AUTHS + 1]; mTimePeriodMap = mTimePeriodMapTmp; TimePeriodValuesCollection tpvc = new TimePeriodValuesCollection(); xyPlot.setDataset(tpvc); xyPlot.setRenderer(br); for (int i = 0; i < NUM_AUTHS + 1; i++) { br.setSeriesPaint(i, AUTH_COLORS[i]); mDatasetsSyncHist[i] = new TimePeriodValues(AUTH_NAMES[i]); tpvc.addSeries(mDatasetsSyncHist[i]); mTimePeriodMap[i] = new HashMap(); } } /** * Callback to process a sync event. * * @param event The sync event * @param startTime Start time (ms) of events * @param stopTime Stop time (ms) of events * @param details Details associated with the event. * @param newEvent True if this event is a new sync event. False if this event * @param syncSource */ @Override void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, String details, boolean newEvent, int syncSource) { if (newEvent) { if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { auth = ERRORS; } double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour addHistEvent(0, auth, delta); } else { // sync_details arrived for an event that has already been graphed. if (details.indexOf('x') >= 0 || details.indexOf('X') >= 0) { // Item turns out to be in error, so transfer time from old auth to error. double delta = (stopTime - startTime) * 100. / 1000 / 3600; // Percent of hour addHistEvent(0, auth, -delta); addHistEvent(0, ERRORS, delta); } } } /** * Helper to add an event to the data series. * Also updates error series if appropriate (x or X in details). * @param stopTime Time event ends * @param auth Sync authority * @param value Value to graph for event */ private void addHistEvent(long stopTime, int auth, double value) { SimpleTimePeriod hour = getTimePeriod(stopTime, mHistWidth); // Loop over all datasets to do the stacking. for (int i = auth; i <= ERRORS; i++) { addToPeriod(mDatasetsSyncHist, i, hour, value); } } private void addToPeriod(TimePeriodValues tpv[], int auth, SimpleTimePeriod period, double value) { int index; if (mTimePeriodMap[auth].containsKey(period)) { index = mTimePeriodMap[auth].get(period); double oldValue = tpv[auth].getValue(index).doubleValue(); tpv[auth].update(index, oldValue + value); } else { index = tpv[auth].getItemCount(); mTimePeriodMap[auth].put(period, index); tpv[auth].add(period, value); } } /** * Creates a multiple-hour time period for the histogram. * @param time Time in milliseconds. * @param numHoursWide: should divide into a day. * @return SimpleTimePeriod covering the number of hours and containing time. */ private SimpleTimePeriod getTimePeriod(long time, long numHoursWide) { Date date = new Date(time); TimeZone zone = RegularTimePeriod.DEFAULT_TIME_ZONE; Calendar calendar = Calendar.getInstance(zone); calendar.setTime(date); long hoursOfYear = calendar.get(Calendar.HOUR_OF_DAY) + calendar.get(Calendar.DAY_OF_YEAR) * 24; int year = calendar.get(Calendar.YEAR); hoursOfYear = (hoursOfYear / numHoursWide) * numHoursWide; calendar.clear(); calendar.set(year, 0, 1, 0, 0); // Jan 1 long start = calendar.getTimeInMillis() + hoursOfYear * 3600 * 1000; return new SimpleTimePeriod(start, start + numHoursWide * 3600 * 1000); } /** * Gets display type * * @return display type as an integer */ @Override int getDisplayType() { return DISPLAY_TYPE_SYNC_HIST; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java0100644 0000000 0000000 00000021101 12747325007 026576 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.InvalidTypeException; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.jfree.chart.labels.CustomXYToolTipGenerator; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYBarRenderer; import org.jfree.data.time.SimpleTimePeriod; import org.jfree.data.time.TimePeriodValues; import org.jfree.data.time.TimePeriodValuesCollection; import java.awt.Color; import java.util.ArrayList; import java.util.List; public class DisplaySyncPerf extends SyncCommon { CustomXYToolTipGenerator mTooltipGenerator; List mTooltips[]; // The series number for each graphed item. // sync authorities are 0-3 private static final int DB_QUERY = 4; private static final int DB_WRITE = 5; private static final int HTTP_NETWORK = 6; private static final int HTTP_PROCESSING = 7; private static final int NUM_SERIES = (HTTP_PROCESSING + 1); private static final String SERIES_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", "DB Query", "DB Write", "HTTP Response", "HTTP Processing",}; private static final Color SERIES_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, Color.ORANGE, Color.RED, Color.CYAN, Color.PINK, Color.DARK_GRAY}; private static final double SERIES_YCOORD[] = {0, 0, 0, 0, 1, 1, 2, 2}; // Values from data/etc/event-log-tags private static final int EVENT_DB_OPERATION = 52000; private static final int EVENT_HTTP_STATS = 52001; // op types for EVENT_DB_OPERATION final int EVENT_DB_QUERY = 0; final int EVENT_DB_WRITE = 1; // Information to graph for each authority private TimePeriodValues mDatasets[]; /** * TimePeriodValuesCollection that supports Y intervals. This allows the * creation of "floating" bars, rather than bars rooted to the axis. */ class YIntervalTimePeriodValuesCollection extends TimePeriodValuesCollection { /** default serial UID */ private static final long serialVersionUID = 1L; private double yheight; /** * Constructs a collection of bars with a fixed Y height. * * @param yheight The height of the bars. */ YIntervalTimePeriodValuesCollection(double yheight) { this.yheight = yheight; } /** * Returns ending Y value that is a fixed amount greater than the starting value. * * @param series the series (zero-based index). * @param item the item (zero-based index). * @return The ending Y value for the specified series and item. */ @Override public Number getEndY(int series, int item) { return getY(series, item).doubleValue() + yheight; } } /** * Constructs a graph of network and database stats. * * @param name The name of this graph in the graph list. */ public DisplaySyncPerf(String name) { super(name); } /** * Creates the UI for the event display. * * @param parent the parent composite. * @param logParser the current log parser. * @return the created control (which may have children). */ @Override public Control createComposite(final Composite parent, EventLogParser logParser, final ILogColumnListener listener) { Control composite = createCompositeChart(parent, logParser, "Sync Performance"); resetUI(); return composite; } /** * Resets the display. */ @Override void resetUI() { super.resetUI(); XYPlot xyPlot = mChart.getXYPlot(); xyPlot.getRangeAxis().setVisible(false); mTooltipGenerator = new CustomXYToolTipGenerator(); @SuppressWarnings("unchecked") List[] mTooltipsTmp = new List[NUM_SERIES]; mTooltips = mTooltipsTmp; XYBarRenderer br = new XYBarRenderer(); br.setUseYInterval(true); mDatasets = new TimePeriodValues[NUM_SERIES]; TimePeriodValuesCollection tpvc = new YIntervalTimePeriodValuesCollection(1); xyPlot.setDataset(tpvc); xyPlot.setRenderer(br); for (int i = 0; i < NUM_SERIES; i++) { br.setSeriesPaint(i, SERIES_COLORS[i]); mDatasets[i] = new TimePeriodValues(SERIES_NAMES[i]); tpvc.addSeries(mDatasets[i]); mTooltips[i] = new ArrayList(); mTooltipGenerator.addToolTipSeries(mTooltips[i]); br.setSeriesToolTipGenerator(i, mTooltipGenerator); } } /** * Updates the display with a new event. * * @param event The event * @param logParser The parser providing the event. */ @Override void newEvent(EventContainer event, EventLogParser logParser) { super.newEvent(event, logParser); // Handle sync operation try { if (event.mTag == EVENT_DB_OPERATION) { // 52000 db_operation (name|3),(op_type|1|5),(time|2|3) String tip = event.getValueAsString(0); long endTime = event.sec * 1000L + (event.nsec / 1000000L); int opType = Integer.parseInt(event.getValueAsString(1)); long duration = Long.parseLong(event.getValueAsString(2)); if (opType == EVENT_DB_QUERY) { mDatasets[DB_QUERY].add(new SimpleTimePeriod(endTime - duration, endTime), SERIES_YCOORD[DB_QUERY]); mTooltips[DB_QUERY].add(tip); } else if (opType == EVENT_DB_WRITE) { mDatasets[DB_WRITE].add(new SimpleTimePeriod(endTime - duration, endTime), SERIES_YCOORD[DB_WRITE]); mTooltips[DB_WRITE].add(tip); } } else if (event.mTag == EVENT_HTTP_STATS) { // 52001 http_stats (useragent|3),(response|2|3),(processing|2|3),(tx|1|2),(rx|1|2) String tip = event.getValueAsString(0) + ", tx:" + event.getValueAsString(3) + ", rx: " + event.getValueAsString(4); long endTime = event.sec * 1000L + (event.nsec / 1000000L); long netEndTime = endTime - Long.parseLong(event.getValueAsString(2)); long netStartTime = netEndTime - Long.parseLong(event.getValueAsString(1)); mDatasets[HTTP_NETWORK].add(new SimpleTimePeriod(netStartTime, netEndTime), SERIES_YCOORD[HTTP_NETWORK]); mDatasets[HTTP_PROCESSING].add(new SimpleTimePeriod(netEndTime, endTime), SERIES_YCOORD[HTTP_PROCESSING]); mTooltips[HTTP_NETWORK].add(tip); mTooltips[HTTP_PROCESSING].add(tip); } } catch (NumberFormatException e) { // This can happen when parsing events from froyo+ where the event with id 52000 // as a completely different format. For now, skip this event if this happens. } catch (InvalidTypeException e) { } } /** * Callback from super.newEvent to process a sync event. * * @param event The sync event * @param startTime Start time (ms) of events * @param stopTime Stop time (ms) of events * @param details Details associated with the event. * @param newEvent True if this event is a new sync event. False if this event * @param syncSource */ @Override void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, String details, boolean newEvent, int syncSource) { if (newEvent) { mDatasets[auth].add(new SimpleTimePeriod(startTime, stopTime), SERIES_YCOORD[auth]); } } /** * Gets display type * * @return display type as an integer */ @Override int getDisplayType() { return DISPLAY_TYPE_SYNC_PERF; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java0100644 0000000 0000000 00000102215 12747325007 026134 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.Log; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventContainer.CompareMethod; import com.android.ddmlib.log.EventContainer.EventValueType; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.EventValueDescription.ValueType; import com.android.ddmlib.log.InvalidTypeException; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.event.ChartChangeEvent; import org.jfree.chart.event.ChartChangeEventType; import org.jfree.chart.event.ChartChangeListener; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.title.TextTitle; import org.jfree.data.time.Millisecond; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.experimental.chart.swt.ChartComposite; import org.jfree.experimental.swt.SWTUtils; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Set; import java.util.regex.Pattern; /** * Represents a custom display of one or more events. */ abstract class EventDisplay { private final static String DISPLAY_DATA_STORAGE_SEPARATOR = ":"; //$NON-NLS-1$ private final static String PID_STORAGE_SEPARATOR = ","; //$NON-NLS-1$ private final static String DESCRIPTOR_STORAGE_SEPARATOR = "$"; //$NON-NLS-1$ private final static String DESCRIPTOR_DATA_STORAGE_SEPARATOR = "!"; //$NON-NLS-1$ private final static String FILTER_VALUE_NULL = ""; //$NON-NLS-1$ public final static int DISPLAY_TYPE_LOG_ALL = 0; public final static int DISPLAY_TYPE_FILTERED_LOG = 1; public final static int DISPLAY_TYPE_GRAPH = 2; public final static int DISPLAY_TYPE_SYNC = 3; public final static int DISPLAY_TYPE_SYNC_HIST = 4; public final static int DISPLAY_TYPE_SYNC_PERF = 5; private final static int EVENT_CHECK_FAILED = 0; protected final static int EVENT_CHECK_SAME_TAG = 1; protected final static int EVENT_CHECK_SAME_VALUE = 2; /** * Creates the appropriate EventDisplay subclass. * * @param type the type of display (DISPLAY_TYPE_LOG_ALL, etc) * @param name the name of the display * @return the created object */ public static EventDisplay eventDisplayFactory(int type, String name) { switch (type) { case DISPLAY_TYPE_LOG_ALL: return new DisplayLog(name); case DISPLAY_TYPE_FILTERED_LOG: return new DisplayFilteredLog(name); case DISPLAY_TYPE_SYNC: return new DisplaySync(name); case DISPLAY_TYPE_SYNC_HIST: return new DisplaySyncHistogram(name); case DISPLAY_TYPE_GRAPH: return new DisplayGraph(name); case DISPLAY_TYPE_SYNC_PERF: return new DisplaySyncPerf(name); default: throw new InvalidParameterException("Unknown Display Type " + type); //$NON-NLS-1$ } } /** * Adds event to the display. * @param event The event * @param logParser The log parser. */ abstract void newEvent(EventContainer event, EventLogParser logParser); /** * Resets the display. */ abstract void resetUI(); /** * Gets display type * * @return display type as an integer */ abstract int getDisplayType(); /** * Creates the UI for the event display. * * @param parent the parent composite. * @param logParser the current log parser. * @return the created control (which may have children). */ abstract Control createComposite(final Composite parent, EventLogParser logParser, final ILogColumnListener listener); interface ILogColumnListener { void columnResized(int index, TableColumn sourceColumn); } /** * Describes an event to be displayed. */ static class OccurrenceDisplayDescriptor { int eventTag = -1; int seriesValueIndex = -1; boolean includePid = false; int filterValueIndex = -1; CompareMethod filterCompareMethod = CompareMethod.EQUAL_TO; Object filterValue = null; OccurrenceDisplayDescriptor() { } OccurrenceDisplayDescriptor(OccurrenceDisplayDescriptor descriptor) { replaceWith(descriptor); } OccurrenceDisplayDescriptor(int eventTag) { this.eventTag = eventTag; } OccurrenceDisplayDescriptor(int eventTag, int seriesValueIndex) { this.eventTag = eventTag; this.seriesValueIndex = seriesValueIndex; } void replaceWith(OccurrenceDisplayDescriptor descriptor) { eventTag = descriptor.eventTag; seriesValueIndex = descriptor.seriesValueIndex; includePid = descriptor.includePid; filterValueIndex = descriptor.filterValueIndex; filterCompareMethod = descriptor.filterCompareMethod; filterValue = descriptor.filterValue; } /** * Loads the descriptor parameter from a storage string. The storage string must have * been generated with {@link #getStorageString()}. * * @param storageString the storage string */ final void loadFrom(String storageString) { String[] values = storageString.split(Pattern.quote(DESCRIPTOR_DATA_STORAGE_SEPARATOR)); loadFrom(values, 0); } /** * Loads the parameters from an array of strings. * * @param storageStrings the strings representing each parameter. * @param index the starting index in the array of strings. * @return the new index in the array. */ protected int loadFrom(String[] storageStrings, int index) { eventTag = Integer.parseInt(storageStrings[index++]); seriesValueIndex = Integer.parseInt(storageStrings[index++]); includePid = Boolean.parseBoolean(storageStrings[index++]); filterValueIndex = Integer.parseInt(storageStrings[index++]); try { filterCompareMethod = CompareMethod.valueOf(storageStrings[index++]); } catch (IllegalArgumentException e) { // if the name does not match any known CompareMethod, we init it to the default one filterCompareMethod = CompareMethod.EQUAL_TO; } String value = storageStrings[index++]; if (filterValueIndex != -1 && FILTER_VALUE_NULL.equals(value) == false) { filterValue = EventValueType.getObjectFromStorageString(value); } return index; } /** * Returns the storage string for the receiver. */ String getStorageString() { StringBuilder sb = new StringBuilder(); sb.append(eventTag); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); sb.append(seriesValueIndex); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); sb.append(Boolean.toString(includePid)); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); sb.append(filterValueIndex); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); sb.append(filterCompareMethod.name()); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); if (filterValue != null) { String value = EventValueType.getStorageString(filterValue); if (value != null) { sb.append(value); } else { sb.append(FILTER_VALUE_NULL); } } else { sb.append(FILTER_VALUE_NULL); } return sb.toString(); } } /** * Describes an event value to be displayed. */ static final class ValueDisplayDescriptor extends OccurrenceDisplayDescriptor { String valueName; int valueIndex = -1; ValueDisplayDescriptor() { super(); } ValueDisplayDescriptor(ValueDisplayDescriptor descriptor) { super(); replaceWith(descriptor); } ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex) { super(eventTag); this.valueName = valueName; this.valueIndex = valueIndex; } ValueDisplayDescriptor(int eventTag, String valueName, int valueIndex, int seriesValueIndex) { super(eventTag, seriesValueIndex); this.valueName = valueName; this.valueIndex = valueIndex; } @Override void replaceWith(OccurrenceDisplayDescriptor descriptor) { super.replaceWith(descriptor); if (descriptor instanceof ValueDisplayDescriptor) { ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor) descriptor; valueName = valueDescriptor.valueName; valueIndex = valueDescriptor.valueIndex; } } /** * Loads the parameters from an array of strings. * * @param storageStrings the strings representing each parameter. * @param index the starting index in the array of strings. * @return the new index in the array. */ @Override protected int loadFrom(String[] storageStrings, int index) { index = super.loadFrom(storageStrings, index); valueName = storageStrings[index++]; valueIndex = Integer.parseInt(storageStrings[index++]); return index; } /** * Returns the storage string for the receiver. */ @Override String getStorageString() { String superStorage = super.getStorageString(); StringBuilder sb = new StringBuilder(); sb.append(superStorage); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); sb.append(valueName); sb.append(DESCRIPTOR_DATA_STORAGE_SEPARATOR); sb.append(valueIndex); return sb.toString(); } } /* ================== * Event Display parameters. * ================== */ protected String mName; private boolean mPidFiltering = false; private ArrayList mPidFilterList = null; protected final ArrayList mValueDescriptors = new ArrayList(); private final ArrayList mOccurrenceDescriptors = new ArrayList(); /* ================== * Event Display members for display purpose. * ================== */ // chart objects /** * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) */ protected final HashMap> mValueDescriptorSeriesMap = new HashMap>(); /** * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) */ protected final HashMap> mOcurrenceDescriptorSeriesMap = new HashMap>(); /** * This is a map of (ValueType, dataset) */ protected final HashMap mValueTypeDataSetMap = new HashMap(); protected JFreeChart mChart; protected TimeSeriesCollection mOccurrenceDataSet; protected int mDataSetCount; private ChartComposite mChartComposite; protected long mMaximumChartItemAge = -1; protected long mHistWidth = 1; // log objects. protected Table mLogTable; /* ================== * Misc data. * ================== */ protected int mValueDescriptorCheck = EVENT_CHECK_FAILED; EventDisplay(String name) { mName = name; } static EventDisplay clone(EventDisplay from) { EventDisplay ed = eventDisplayFactory(from.getDisplayType(), from.getName()); ed.mName = from.mName; ed.mPidFiltering = from.mPidFiltering; ed.mMaximumChartItemAge = from.mMaximumChartItemAge; ed.mHistWidth = from.mHistWidth; if (from.mPidFilterList != null) { ed.mPidFilterList = new ArrayList(); ed.mPidFilterList.addAll(from.mPidFilterList); } for (ValueDisplayDescriptor desc : from.mValueDescriptors) { ed.mValueDescriptors.add(new ValueDisplayDescriptor(desc)); } ed.mValueDescriptorCheck = from.mValueDescriptorCheck; for (OccurrenceDisplayDescriptor desc : from.mOccurrenceDescriptors) { ed.mOccurrenceDescriptors.add(new OccurrenceDisplayDescriptor(desc)); } return ed; } /** * Returns the parameters of the receiver as a single String for storage. */ String getStorageString() { StringBuilder sb = new StringBuilder(); sb.append(mName); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(getDisplayType()); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(Boolean.toString(mPidFiltering)); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(getPidStorageString()); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(getDescriptorStorageString(mValueDescriptors)); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(getDescriptorStorageString(mOccurrenceDescriptors)); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(mMaximumChartItemAge); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); sb.append(mHistWidth); sb.append(DISPLAY_DATA_STORAGE_SEPARATOR); return sb.toString(); } void setName(String name) { mName = name; } String getName() { return mName; } void setPidFiltering(boolean filterByPid) { mPidFiltering = filterByPid; } boolean getPidFiltering() { return mPidFiltering; } void setPidFilterList(ArrayList pids) { if (mPidFiltering == false) { throw new InvalidParameterException(); } mPidFilterList = pids; } ArrayList getPidFilterList() { return mPidFilterList; } void addPidFiler(int pid) { if (mPidFiltering == false) { throw new InvalidParameterException(); } if (mPidFilterList == null) { mPidFilterList = new ArrayList(); } mPidFilterList.add(pid); } /** * Returns an iterator to the list of {@link ValueDisplayDescriptor}. */ Iterator getValueDescriptors() { return mValueDescriptors.iterator(); } /** * Update checks on the descriptors. Must be called whenever a descriptor is modified outside * of this class. */ void updateValueDescriptorCheck() { mValueDescriptorCheck = checkDescriptors(); } /** * Returns an iterator to the list of {@link OccurrenceDisplayDescriptor}. */ Iterator getOccurrenceDescriptors() { return mOccurrenceDescriptors.iterator(); } /** * Adds a descriptor. This can be a {@link OccurrenceDisplayDescriptor} or a * {@link ValueDisplayDescriptor}. * * @param descriptor the descriptor to be added. */ void addDescriptor(OccurrenceDisplayDescriptor descriptor) { if (descriptor instanceof ValueDisplayDescriptor) { mValueDescriptors.add((ValueDisplayDescriptor) descriptor); mValueDescriptorCheck = checkDescriptors(); } else { mOccurrenceDescriptors.add(descriptor); } } /** * Returns a descriptor by index and class (extending {@link OccurrenceDisplayDescriptor}). * * @param descriptorClass the class of the descriptor to return. * @param index the index of the descriptor to return. * @return either a {@link OccurrenceDisplayDescriptor} or a {@link ValueDisplayDescriptor} * or null if descriptorClass is another class. */ OccurrenceDisplayDescriptor getDescriptor( Class descriptorClass, int index) { if (descriptorClass == OccurrenceDisplayDescriptor.class) { return mOccurrenceDescriptors.get(index); } else if (descriptorClass == ValueDisplayDescriptor.class) { return mValueDescriptors.get(index); } return null; } /** * Removes a descriptor based on its class and index. * * @param descriptorClass the class of the descriptor. * @param index the index of the descriptor to be removed. */ void removeDescriptor(Class descriptorClass, int index) { if (descriptorClass == OccurrenceDisplayDescriptor.class) { mOccurrenceDescriptors.remove(index); } else if (descriptorClass == ValueDisplayDescriptor.class) { mValueDescriptors.remove(index); mValueDescriptorCheck = checkDescriptors(); } } Control createCompositeChart(final Composite parent, EventLogParser logParser, String title) { mChart = ChartFactory.createTimeSeriesChart( null, null /* timeAxisLabel */, null /* valueAxisLabel */, null, /* dataset. set below */ true /* legend */, false /* tooltips */, false /* urls */); // get the font to make a proper title. We need to convert the swt font, // into an awt font. Font f = parent.getFont(); FontData[] fData = f.getFontData(); // event though on Mac OS there could be more than one fontData, we'll only use // the first one. FontData firstFontData = fData[0]; java.awt.Font awtFont = SWTUtils.toAwtFont(parent.getDisplay(), firstFontData, true /* ensureSameSize */); mChart.setTitle(new TextTitle(title, awtFont)); final XYPlot xyPlot = mChart.getXYPlot(); xyPlot.setRangeCrosshairVisible(true); xyPlot.setRangeCrosshairLockedOnData(true); xyPlot.setDomainCrosshairVisible(true); xyPlot.setDomainCrosshairLockedOnData(true); mChart.addChangeListener(new ChartChangeListener() { @Override public void chartChanged(ChartChangeEvent event) { ChartChangeEventType type = event.getType(); if (type == ChartChangeEventType.GENERAL) { // because the value we need (rangeCrosshair and domainCrosshair) are // updated on the draw, but the notification happens before the draw, // we process the click in a future runnable! parent.getDisplay().asyncExec(new Runnable() { @Override public void run() { processClick(xyPlot); } }); } } }); mChartComposite = new ChartComposite(parent, SWT.BORDER, mChart, ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 3000, // max draw width. We don't want it to zoom, so we put a big number 3000, // max draw height. We don't want it to zoom, so we put a big number true, // off-screen buffer true, // properties true, // save true, // print true, // zoom true); // tooltips mChartComposite.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mValueTypeDataSetMap.clear(); mDataSetCount = 0; mOccurrenceDataSet = null; mChart = null; mChartComposite = null; mValueDescriptorSeriesMap.clear(); mOcurrenceDescriptorSeriesMap.clear(); } }); return mChartComposite; } private void processClick(XYPlot xyPlot) { double rangeValue = xyPlot.getRangeCrosshairValue(); if (rangeValue != 0) { double domainValue = xyPlot.getDomainCrosshairValue(); Millisecond msec = new Millisecond(new Date((long) domainValue)); // look for values in the dataset that contains data at this TimePeriod Set descKeys = mValueDescriptorSeriesMap.keySet(); for (ValueDisplayDescriptor descKey : descKeys) { HashMap map = mValueDescriptorSeriesMap.get(descKey); Set pidKeys = map.keySet(); for (Integer pidKey : pidKeys) { TimeSeries series = map.get(pidKey); Number value = series.getValue(msec); if (value != null) { // found a match. lets check against the actual value. if (value.doubleValue() == rangeValue) { return; } } } } } } /** * Resizes the index-th column of the log {@link Table} (if applicable). * Subclasses can override if necessary. *

* This does nothing if the Table object is null (because the display * type does not use a column) or if the index-th column is in fact the originating * column passed as argument. * * @param index the index of the column to resize * @param sourceColumn the original column that was resize, and on which we need to sync the * index-th column width. */ void resizeColumn(int index, TableColumn sourceColumn) { } /** * Sets the current {@link EventLogParser} object. * Subclasses can override if necessary. */ protected void setNewLogParser(EventLogParser logParser) { } /** * Prepares the {@link EventDisplay} for a multi event display. */ void startMultiEventDisplay() { if (mLogTable != null) { mLogTable.setRedraw(false); } } /** * Finalizes the {@link EventDisplay} after a multi event display. */ void endMultiEventDisplay() { if (mLogTable != null) { mLogTable.setRedraw(true); } } /** * Returns the {@link Table} object used to display events, if any. * * @return a Table object or null. */ Table getTable() { return mLogTable; } /** * Loads a new {@link EventDisplay} from a storage string. The string must have been created * with {@link #getStorageString()}. * * @param storageString the storage string * @return a new {@link EventDisplay} or null if the load failed. */ static EventDisplay load(String storageString) { if (storageString.length() > 0) { // the storage string is separated by ':' String[] values = storageString.split(Pattern.quote(DISPLAY_DATA_STORAGE_SEPARATOR)); try { int index = 0; String name = values[index++]; int displayType = Integer.parseInt(values[index++]); boolean pidFiltering = Boolean.parseBoolean(values[index++]); EventDisplay ed = eventDisplayFactory(displayType, name); ed.setPidFiltering(pidFiltering); // because empty sections are removed by String.split(), we have to check // the index for those. if (index < values.length) { ed.loadPidFilters(values[index++]); } if (index < values.length) { ed.loadValueDescriptors(values[index++]); } if (index < values.length) { ed.loadOccurrenceDescriptors(values[index++]); } ed.updateValueDescriptorCheck(); if (index < values.length) { ed.mMaximumChartItemAge = Long.parseLong(values[index++]); } if (index < values.length) { ed.mHistWidth = Long.parseLong(values[index++]); } return ed; } catch (RuntimeException re) { // we'll return null below. Log.e("ddms", re); } } return null; } private String getPidStorageString() { if (mPidFilterList != null) { StringBuilder sb = new StringBuilder(); boolean first = true; for (Integer i : mPidFilterList) { if (first == false) { sb.append(PID_STORAGE_SEPARATOR); } else { first = false; } sb.append(i); } return sb.toString(); } return ""; //$NON-NLS-1$ } private void loadPidFilters(String storageString) { if (storageString.length() > 0) { String[] values = storageString.split(Pattern.quote(PID_STORAGE_SEPARATOR)); for (String value : values) { if (mPidFilterList == null) { mPidFilterList = new ArrayList(); } mPidFilterList.add(Integer.parseInt(value)); } } } private String getDescriptorStorageString( ArrayList descriptorList) { StringBuilder sb = new StringBuilder(); boolean first = true; for (OccurrenceDisplayDescriptor descriptor : descriptorList) { if (first == false) { sb.append(DESCRIPTOR_STORAGE_SEPARATOR); } else { first = false; } sb.append(descriptor.getStorageString()); } return sb.toString(); } private void loadOccurrenceDescriptors(String storageString) { if (storageString.length() == 0) { return; } String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); for (String value : values) { OccurrenceDisplayDescriptor desc = new OccurrenceDisplayDescriptor(); desc.loadFrom(value); mOccurrenceDescriptors.add(desc); } } private void loadValueDescriptors(String storageString) { if (storageString.length() == 0) { return; } String[] values = storageString.split(Pattern.quote(DESCRIPTOR_STORAGE_SEPARATOR)); for (String value : values) { ValueDisplayDescriptor desc = new ValueDisplayDescriptor(); desc.loadFrom(value); mValueDescriptors.add(desc); } } /** * Fills a list with {@link OccurrenceDisplayDescriptor} (or a subclass of it) from another * list if they are configured to display the {@link EventContainer} * * @param event the event container * @param fullList the list with all the descriptors. * @param outList the list to fill. */ @SuppressWarnings("unchecked") private void getDescriptors(EventContainer event, ArrayList fullList, ArrayList outList) { for (OccurrenceDisplayDescriptor descriptor : fullList) { try { // first check the event tag. if (descriptor.eventTag == event.mTag) { // now check if we have a filter on a value if (descriptor.filterValueIndex == -1 || event.testValue(descriptor.filterValueIndex, descriptor.filterValue, descriptor.filterCompareMethod)) { outList.add(descriptor); } } } catch (InvalidTypeException ite) { // if the filter for the descriptor was incorrect, we ignore the descriptor. } catch (ArrayIndexOutOfBoundsException aioobe) { // if the index was wrong (the event content may have changed since we setup the // display), we do nothing but log the error Log.e("Event Log", String.format( "ArrayIndexOutOfBoundsException occured when checking %1$d-th value of event %2$d", //$NON-NLS-1$ descriptor.filterValueIndex, descriptor.eventTag)); } } } /** * Filters the {@link com.android.ddmlib.log.EventContainer}, and fills two list of {@link com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor} * and {@link com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor} configured to display the event. * * @param event * @param valueDescriptors * @param occurrenceDescriptors * @return true if the event should be displayed. */ protected boolean filterEvent(EventContainer event, ArrayList valueDescriptors, ArrayList occurrenceDescriptors) { // test the pid first (if needed) if (mPidFiltering && mPidFilterList != null) { boolean found = false; for (int pid : mPidFilterList) { if (pid == event.pid) { found = true; break; } } if (found == false) { return false; } } // now get the list of matching descriptors getDescriptors(event, mValueDescriptors, valueDescriptors); getDescriptors(event, mOccurrenceDescriptors, occurrenceDescriptors); // and return whether there is at least one match in either list. return (valueDescriptors.size() > 0 || occurrenceDescriptors.size() > 0); } /** * Checks all the {@link ValueDisplayDescriptor} for similarity. * If all the event values are from the same tag, the method will return EVENT_CHECK_SAME_TAG. * If all the event/value are the same, the method will return EVENT_CHECK_SAME_VALUE * * @return flag as described above */ private int checkDescriptors() { if (mValueDescriptors.size() < 2) { return EVENT_CHECK_SAME_VALUE; } int tag = -1; int index = -1; for (ValueDisplayDescriptor display : mValueDescriptors) { if (tag == -1) { tag = display.eventTag; index = display.valueIndex; } else { if (tag != display.eventTag) { return EVENT_CHECK_FAILED; } else { if (index != -1) { if (index != display.valueIndex) { index = -1; } } } } } if (index == -1) { return EVENT_CHECK_SAME_TAG; } return EVENT_CHECK_SAME_VALUE; } /** * Resets the time limit on the chart to be infinite. */ void resetChartTimeLimit() { mMaximumChartItemAge = -1; } /** * Sets the time limit on the charts. * * @param timeLimit the time limit in seconds. */ void setChartTimeLimit(long timeLimit) { mMaximumChartItemAge = timeLimit; } long getChartTimeLimit() { return mMaximumChartItemAge; } /** * m * Resets the histogram width */ void resetHistWidth() { mHistWidth = 1; } /** * Sets the histogram width * * @param histWidth the width in hours */ void setHistWidth(long histWidth) { mHistWidth = histWidth; } long getHistWidth() { return mHistWidth; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java0100644 0000000 0000000 00000112537 12747325007 027520 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.EventValueDescription; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.List; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; class EventDisplayOptions extends Dialog { private static final int DLG_WIDTH = 700; private static final int DLG_HEIGHT = 700; private Shell mParent; private Shell mShell; private boolean mEditStatus = false; private final ArrayList mDisplayList = new ArrayList(); /* LEFT LIST */ private List mEventDisplayList; private Button mEventDisplayNewButton; private Button mEventDisplayDeleteButton; private Button mEventDisplayUpButton; private Button mEventDisplayDownButton; private Text mDisplayWidthText; private Text mDisplayHeightText; /* WIDGETS ON THE RIGHT */ private Text mDisplayNameText; private Combo mDisplayTypeCombo; private Group mChartOptions; private Group mHistOptions; private Button mPidFilterCheckBox; private Text mPidText; /** Map with (event-tag, event name) */ private Map mEventTagMap; /** Map with (event-tag, array of value info for the event) */ private Map mEventDescriptionMap; /** list of current pids */ private ArrayList mPidList; private EventLogParser mLogParser; private Group mInfoGroup; private static class SelectionWidgets { private List mList; private Button mNewButton; private Button mEditButton; private Button mDeleteButton; private void setEnabled(boolean enable) { mList.setEnabled(enable); mNewButton.setEnabled(enable); mEditButton.setEnabled(enable); mDeleteButton.setEnabled(enable); } } private SelectionWidgets mValueSelection; private SelectionWidgets mOccurrenceSelection; /** flag to temporarly disable processing of {@link Text} changes, so that * {@link Text#setText(String)} can be called safely. */ private boolean mProcessTextChanges = true; private Text mTimeLimitText; private Text mHistWidthText; EventDisplayOptions(Shell parent) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); } /** * Opens the display option dialog, to edit the {@link EventDisplay} objects provided in the * list. * @param logParser * @param displayList * @param eventList * @return true if the list of {@link EventDisplay} objects was updated. */ boolean open(EventLogParser logParser, ArrayList displayList, ArrayList eventList) { mLogParser = logParser; if (logParser != null) { // we need 2 things from the parser. // the event tag / event name map mEventTagMap = logParser.getTagMap(); // the event info map mEventDescriptionMap = logParser.getEventInfoMap(); } // make a copy of the EventDisplay list since we'll use working copies. duplicateEventDisplay(displayList); // build a list of pid from the list of events. buildPidList(eventList); createUI(); if (mParent == null || mShell == null) { return false; } // Set the dialog size. mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); Rectangle r = mParent.getBounds(); // get the center new top left. int cx = r.x + r.width/2; int x = cx - DLG_WIDTH / 2; int cy = r.y + r.height/2; int y = cy - DLG_HEIGHT / 2; mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); mShell.layout(); // actually open the dialog mShell.open(); // event loop until the dialog is closed. Display display = mParent.getDisplay(); while (!mShell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } return mEditStatus; } ArrayList getEventDisplays() { return mDisplayList; } private void createUI() { mParent = getParent(); mShell = new Shell(mParent, getStyle()); mShell.setText("Event Display Configuration"); mShell.setLayout(new GridLayout(1, true)); final Composite topPanel = new Composite(mShell, SWT.NONE); topPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); topPanel.setLayout(new GridLayout(2, false)); // create the tree on the left and the controls on the right. Composite leftPanel = new Composite(topPanel, SWT.NONE); Composite rightPanel = new Composite(topPanel, SWT.NONE); createLeftPanel(leftPanel); createRightPanel(rightPanel); mShell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { event.doit = true; } }); Label separator = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Composite bottomButtons = new Composite(mShell, SWT.NONE); bottomButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); GridLayout gl; bottomButtons.setLayout(gl = new GridLayout(2, true)); gl.marginHeight = gl.marginWidth = 0; Button okButton = new Button(bottomButtons, SWT.PUSH); okButton.setText("OK"); okButton.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { mShell.close(); } }); Button cancelButton = new Button(bottomButtons, SWT.PUSH); cancelButton.setText("Cancel"); cancelButton.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { // cancel the modification flag. mEditStatus = false; // and close mShell.close(); } }); enable(false); // fill the list with the current display fillEventDisplayList(); } private void createLeftPanel(Composite leftPanel) { final IPreferenceStore store = DdmUiPreferences.getStore(); GridLayout gl; leftPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); leftPanel.setLayout(gl = new GridLayout(1, false)); gl.verticalSpacing = 1; mEventDisplayList = new List(leftPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL | SWT.FULL_SELECTION); mEventDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH)); mEventDisplayList.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { handleEventDisplaySelection(); } }); Composite bottomControls = new Composite(leftPanel, SWT.NONE); bottomControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); bottomControls.setLayout(gl = new GridLayout(5, false)); gl.marginHeight = gl.marginWidth = 0; gl.verticalSpacing = 0; gl.horizontalSpacing = 0; ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); mEventDisplayNewButton.setImage(loader.loadImage("add.png", //$NON-NLS-1$ leftPanel.getDisplay())); mEventDisplayNewButton.setToolTipText("Adds a new event display"); mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { createNewEventDisplay(); } }); mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); mEventDisplayDeleteButton.setImage(loader.loadImage("delete.png", //$NON-NLS-1$ leftPanel.getDisplay())); mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display"); mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { deleteEventDisplay(); } }); mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); mEventDisplayUpButton.setImage(loader.loadImage("up.png", //$NON-NLS-1$ leftPanel.getDisplay())); mEventDisplayUpButton.setToolTipText("Moves the selected event display up"); mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // get current selection. int selection = mEventDisplayList.getSelectionIndex(); if (selection > 0) { // update the list of EventDisplay. EventDisplay display = mDisplayList.remove(selection); mDisplayList.add(selection - 1, display); // update the list widget mEventDisplayList.remove(selection); mEventDisplayList.add(display.getName(), selection - 1); // update the selection and reset the ui. mEventDisplayList.select(selection - 1); handleEventDisplaySelection(); mEventDisplayList.showSelection(); setModified(); } } }); mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); mEventDisplayDownButton.setImage(loader.loadImage("down.png", //$NON-NLS-1$ leftPanel.getDisplay())); mEventDisplayDownButton.setToolTipText("Moves the selected event display down"); mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // get current selection. int selection = mEventDisplayList.getSelectionIndex(); if (selection != -1 && selection < mEventDisplayList.getItemCount() - 1) { // update the list of EventDisplay. EventDisplay display = mDisplayList.remove(selection); mDisplayList.add(selection + 1, display); // update the list widget mEventDisplayList.remove(selection); mEventDisplayList.add(display.getName(), selection + 1); // update the selection and reset the ui. mEventDisplayList.select(selection + 1); handleEventDisplaySelection(); mEventDisplayList.showSelection(); setModified(); } } }); Group sizeGroup = new Group(leftPanel, SWT.NONE); sizeGroup.setText("Display Size:"); sizeGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); sizeGroup.setLayout(new GridLayout(2, false)); Label l = new Label(sizeGroup, SWT.NONE); l.setText("Width:"); mDisplayWidthText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); mDisplayWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mDisplayWidthText.setText(Integer.toString( store.getInt(EventLogPanel.PREFS_DISPLAY_WIDTH))); mDisplayWidthText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { String text = mDisplayWidthText.getText().trim(); try { store.setValue(EventLogPanel.PREFS_DISPLAY_WIDTH, Integer.parseInt(text)); setModified(); } catch (NumberFormatException nfe) { // do something? } } }); l = new Label(sizeGroup, SWT.NONE); l.setText("Height:"); mDisplayHeightText = new Text(sizeGroup, SWT.LEFT | SWT.SINGLE | SWT.BORDER); mDisplayHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mDisplayHeightText.setText(Integer.toString( store.getInt(EventLogPanel.PREFS_DISPLAY_HEIGHT))); mDisplayHeightText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { String text = mDisplayHeightText.getText().trim(); try { store.setValue(EventLogPanel.PREFS_DISPLAY_HEIGHT, Integer.parseInt(text)); setModified(); } catch (NumberFormatException nfe) { // do something? } } }); } private void createRightPanel(Composite rightPanel) { rightPanel.setLayout(new GridLayout(1, true)); rightPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); mInfoGroup = new Group(rightPanel, SWT.NONE); mInfoGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mInfoGroup.setLayout(new GridLayout(2, false)); Label nameLabel = new Label(mInfoGroup, SWT.LEFT); nameLabel.setText("Name:"); mDisplayNameText = new Text(mInfoGroup, SWT.BORDER | SWT.LEFT | SWT.SINGLE); mDisplayNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mDisplayNameText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { if (mProcessTextChanges) { EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { eventDisplay.setName(mDisplayNameText.getText()); int index = mEventDisplayList.getSelectionIndex(); mEventDisplayList.remove(index); mEventDisplayList.add(eventDisplay.getName(), index); mEventDisplayList.select(index); handleEventDisplaySelection(); setModified(); } } } }); Label displayLabel = new Label(mInfoGroup, SWT.LEFT); displayLabel.setText("Type:"); mDisplayTypeCombo = new Combo(mInfoGroup, SWT.READ_ONLY | SWT.DROP_DOWN); mDisplayTypeCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // add the combo values. This must match the values EventDisplay.DISPLAY_TYPE_* mDisplayTypeCombo.add("Log All"); mDisplayTypeCombo.add("Filtered Log"); mDisplayTypeCombo.add("Graph"); mDisplayTypeCombo.add("Sync"); mDisplayTypeCombo.add("Sync Histogram"); mDisplayTypeCombo.add("Sync Performance"); mDisplayTypeCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null && eventDisplay.getDisplayType() != mDisplayTypeCombo.getSelectionIndex()) { /* Replace the EventDisplay object with a different subclass */ setModified(); String name = eventDisplay.getName(); EventDisplay newEventDisplay = EventDisplay.eventDisplayFactory(mDisplayTypeCombo.getSelectionIndex(), name); setCurrentEventDisplay(newEventDisplay); fillUiWith(newEventDisplay); } } }); mChartOptions = new Group(mInfoGroup, SWT.NONE); mChartOptions.setText("Chart Options"); GridData gd; mChartOptions.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; mChartOptions.setLayout(new GridLayout(2, false)); Label l = new Label(mChartOptions, SWT.NONE); l.setText("Time Limit (seconds):"); mTimeLimitText = new Text(mChartOptions, SWT.BORDER); mTimeLimitText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mTimeLimitText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { String text = mTimeLimitText.getText().trim(); EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { try { if (text.length() == 0) { eventDisplay.resetChartTimeLimit(); } else { eventDisplay.setChartTimeLimit(Long.parseLong(text)); } } catch (NumberFormatException nfe) { eventDisplay.resetChartTimeLimit(); } finally { setModified(); } } } }); mHistOptions = new Group(mInfoGroup, SWT.NONE); mHistOptions.setText("Histogram Options"); GridData gdh; mHistOptions.setLayoutData(gdh = new GridData(GridData.FILL_HORIZONTAL)); gdh.horizontalSpan = 2; mHistOptions.setLayout(new GridLayout(2, false)); Label lh = new Label(mHistOptions, SWT.NONE); lh.setText("Histogram width (hours):"); mHistWidthText = new Text(mHistOptions, SWT.BORDER); mHistWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mHistWidthText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { String text = mHistWidthText.getText().trim(); EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { try { if (text.length() == 0) { eventDisplay.resetHistWidth(); } else { eventDisplay.setHistWidth(Long.parseLong(text)); } } catch (NumberFormatException nfe) { eventDisplay.resetHistWidth(); } finally { setModified(); } } } }); mPidFilterCheckBox = new Button(mInfoGroup, SWT.CHECK); mPidFilterCheckBox.setText("Enable filtering by pid"); mPidFilterCheckBox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); gd.horizontalSpan = 2; mPidFilterCheckBox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { eventDisplay.setPidFiltering(mPidFilterCheckBox.getSelection()); mPidText.setEnabled(mPidFilterCheckBox.getSelection()); setModified(); } } }); Label pidLabel = new Label(mInfoGroup, SWT.NONE); pidLabel.setText("Pid Filter:"); pidLabel.setToolTipText("Enter all pids, separated by commas"); mPidText = new Text(mInfoGroup, SWT.BORDER); mPidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mPidText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { if (mProcessTextChanges) { EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null && eventDisplay.getPidFiltering()) { String pidText = mPidText.getText().trim(); String[] pids = pidText.split("\\s*,\\s*"); //$NON-NLS-1$ ArrayList list = new ArrayList(); for (String pid : pids) { try { list.add(Integer.valueOf(pid)); } catch (NumberFormatException nfe) { // just ignore non valid pid } } eventDisplay.setPidFilterList(list); setModified(); } } } }); /* ------------------ * EVENT VALUE/OCCURRENCE SELECTION * ------------------ */ mValueSelection = createEventSelection(rightPanel, ValueDisplayDescriptor.class, "Event Value Display"); mOccurrenceSelection = createEventSelection(rightPanel, OccurrenceDisplayDescriptor.class, "Event Occurrence Display"); } private SelectionWidgets createEventSelection(Composite rightPanel, final Class descriptorClass, String groupMessage) { Group eventSelectionPanel = new Group(rightPanel, SWT.NONE); eventSelectionPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); GridLayout gl; eventSelectionPanel.setLayout(gl = new GridLayout(2, false)); gl.marginHeight = gl.marginWidth = 0; eventSelectionPanel.setText(groupMessage); final SelectionWidgets widgets = new SelectionWidgets(); widgets.mList = new List(eventSelectionPanel, SWT.BORDER | SWT.SINGLE | SWT.V_SCROLL); widgets.mList.setLayoutData(new GridData(GridData.FILL_BOTH)); widgets.mList.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { int index = widgets.mList.getSelectionIndex(); if (index != -1) { widgets.mDeleteButton.setEnabled(true); widgets.mEditButton.setEnabled(true); } else { widgets.mDeleteButton.setEnabled(false); widgets.mEditButton.setEnabled(false); } } }); Composite rightControls = new Composite(eventSelectionPanel, SWT.NONE); rightControls.setLayoutData(new GridData(GridData.FILL_VERTICAL)); rightControls.setLayout(gl = new GridLayout(1, false)); gl.marginHeight = gl.marginWidth = 0; gl.verticalSpacing = 0; gl.horizontalSpacing = 0; widgets.mNewButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); widgets.mNewButton.setText("New..."); widgets.mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); widgets.mNewButton.setEnabled(false); widgets.mNewButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // current event try { EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { EventValueSelector dialog = new EventValueSelector(mShell); if (dialog.open(descriptorClass, mLogParser)) { eventDisplay.addDescriptor(dialog.getDescriptor()); fillUiWith(eventDisplay); setModified(); } } } catch (Exception e1) { e1.printStackTrace(); } } }); widgets.mEditButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); widgets.mEditButton.setText("Edit..."); widgets.mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); widgets.mEditButton.setEnabled(false); widgets.mEditButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // current event EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { // get the current descriptor index int index = widgets.mList.getSelectionIndex(); if (index != -1) { // get the descriptor itself OccurrenceDisplayDescriptor descriptor = eventDisplay.getDescriptor( descriptorClass, index); // open the edit dialog. EventValueSelector dialog = new EventValueSelector(mShell); if (dialog.open(descriptor, mLogParser)) { descriptor.replaceWith(dialog.getDescriptor()); eventDisplay.updateValueDescriptorCheck(); fillUiWith(eventDisplay); // reselect the item since fillUiWith remove the selection. widgets.mList.select(index); widgets.mList.notifyListeners(SWT.Selection, null); setModified(); } } } } }); widgets.mDeleteButton = new Button(rightControls, SWT.PUSH | SWT.FLAT); widgets.mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); widgets.mDeleteButton.setText("Delete"); widgets.mDeleteButton.setEnabled(false); widgets.mDeleteButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // current event EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { // get the current descriptor index int index = widgets.mList.getSelectionIndex(); if (index != -1) { eventDisplay.removeDescriptor(descriptorClass, index); fillUiWith(eventDisplay); setModified(); } } } }); return widgets; } private void duplicateEventDisplay(ArrayList displayList) { for (EventDisplay eventDisplay : displayList) { mDisplayList.add(EventDisplay.clone(eventDisplay)); } } private void buildPidList(ArrayList eventList) { mPidList = new ArrayList(); for (EventContainer event : eventList) { if (mPidList.indexOf(event.pid) == -1) { mPidList.add(event.pid); } } } private void setModified() { mEditStatus = true; } private void enable(boolean status) { mEventDisplayDeleteButton.setEnabled(status); // enable up/down int selection = mEventDisplayList.getSelectionIndex(); int count = mEventDisplayList.getItemCount(); mEventDisplayUpButton.setEnabled(status && selection > 0); mEventDisplayDownButton.setEnabled(status && selection != -1 && selection < count - 1); mDisplayNameText.setEnabled(status); mDisplayTypeCombo.setEnabled(status); mPidFilterCheckBox.setEnabled(status); mValueSelection.setEnabled(status); mOccurrenceSelection.setEnabled(status); mValueSelection.mNewButton.setEnabled(status); mOccurrenceSelection.mNewButton.setEnabled(status); if (status == false) { mPidText.setEnabled(false); } } private void fillEventDisplayList() { for (EventDisplay eventDisplay : mDisplayList) { mEventDisplayList.add(eventDisplay.getName()); } } private void createNewEventDisplay() { int count = mDisplayList.size(); String name = String.format("display %1$d", count + 1); EventDisplay eventDisplay = EventDisplay.eventDisplayFactory(0 /* type*/, name); mDisplayList.add(eventDisplay); mEventDisplayList.add(name); mEventDisplayList.select(count); handleEventDisplaySelection(); mEventDisplayList.showSelection(); setModified(); } private void deleteEventDisplay() { int selection = mEventDisplayList.getSelectionIndex(); if (selection != -1) { mDisplayList.remove(selection); mEventDisplayList.remove(selection); if (mDisplayList.size() < selection) { selection--; } mEventDisplayList.select(selection); handleEventDisplaySelection(); setModified(); } } private EventDisplay getCurrentEventDisplay() { int selection = mEventDisplayList.getSelectionIndex(); if (selection != -1) { return mDisplayList.get(selection); } return null; } private void setCurrentEventDisplay(EventDisplay eventDisplay) { int selection = mEventDisplayList.getSelectionIndex(); if (selection != -1) { mDisplayList.set(selection, eventDisplay); } } private void handleEventDisplaySelection() { EventDisplay eventDisplay = getCurrentEventDisplay(); if (eventDisplay != null) { // enable the UI enable(true); // and fill it fillUiWith(eventDisplay); } else { // disable the UI enable(false); // and empty it. emptyUi(); } } private void emptyUi() { mDisplayNameText.setText(""); mDisplayTypeCombo.clearSelection(); mValueSelection.mList.removeAll(); mOccurrenceSelection.mList.removeAll(); } private void fillUiWith(EventDisplay eventDisplay) { mProcessTextChanges = false; mDisplayNameText.setText(eventDisplay.getName()); int displayMode = eventDisplay.getDisplayType(); mDisplayTypeCombo.select(displayMode); if (displayMode == EventDisplay.DISPLAY_TYPE_GRAPH) { GridData gd = (GridData) mChartOptions.getLayoutData(); gd.exclude = false; mChartOptions.setVisible(!gd.exclude); long limit = eventDisplay.getChartTimeLimit(); if (limit != -1) { mTimeLimitText.setText(Long.toString(limit)); } else { mTimeLimitText.setText(""); //$NON-NLS-1$ } } else { GridData gd = (GridData) mChartOptions.getLayoutData(); gd.exclude = true; mChartOptions.setVisible(!gd.exclude); mTimeLimitText.setText(""); //$NON-NLS-1$ } if (displayMode == EventDisplay.DISPLAY_TYPE_SYNC_HIST) { GridData gd = (GridData) mHistOptions.getLayoutData(); gd.exclude = false; mHistOptions.setVisible(!gd.exclude); long limit = eventDisplay.getHistWidth(); if (limit != -1) { mHistWidthText.setText(Long.toString(limit)); } else { mHistWidthText.setText(""); //$NON-NLS-1$ } } else { GridData gd = (GridData) mHistOptions.getLayoutData(); gd.exclude = true; mHistOptions.setVisible(!gd.exclude); mHistWidthText.setText(""); //$NON-NLS-1$ } mInfoGroup.layout(true); mShell.layout(true); mShell.pack(); if (eventDisplay.getPidFiltering()) { mPidFilterCheckBox.setSelection(true); mPidText.setEnabled(true); // build the pid list. ArrayList list = eventDisplay.getPidFilterList(); if (list != null) { StringBuilder sb = new StringBuilder(); int count = list.size(); for (int i = 0 ; i < count ; i++) { sb.append(list.get(i)); if (i < count - 1) { sb.append(", ");//$NON-NLS-1$ } } mPidText.setText(sb.toString()); } else { mPidText.setText(""); //$NON-NLS-1$ } } else { mPidFilterCheckBox.setSelection(false); mPidText.setEnabled(false); mPidText.setText(""); //$NON-NLS-1$ } mProcessTextChanges = true; mValueSelection.mList.removeAll(); mOccurrenceSelection.mList.removeAll(); if (eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_FILTERED_LOG || eventDisplay.getDisplayType() == EventDisplay.DISPLAY_TYPE_GRAPH) { mOccurrenceSelection.setEnabled(true); mValueSelection.setEnabled(true); Iterator valueIterator = eventDisplay.getValueDescriptors(); while (valueIterator.hasNext()) { ValueDisplayDescriptor descriptor = valueIterator.next(); mValueSelection.mList.add(String.format("%1$s: %2$s [%3$s]%4$s", mEventTagMap.get(descriptor.eventTag), descriptor.valueName, getSeriesLabelDescription(descriptor), getFilterDescription(descriptor))); } Iterator occurrenceIterator = eventDisplay.getOccurrenceDescriptors(); while (occurrenceIterator.hasNext()) { OccurrenceDisplayDescriptor descriptor = occurrenceIterator.next(); mOccurrenceSelection.mList.add(String.format("%1$s [%2$s]%3$s", mEventTagMap.get(descriptor.eventTag), getSeriesLabelDescription(descriptor), getFilterDescription(descriptor))); } mValueSelection.mList.notifyListeners(SWT.Selection, null); mOccurrenceSelection.mList.notifyListeners(SWT.Selection, null); } else { mOccurrenceSelection.setEnabled(false); mValueSelection.setEnabled(false); } } /** * Returns a String describing what is used as the series label * @param descriptor the descriptor of the display. */ private String getSeriesLabelDescription(OccurrenceDisplayDescriptor descriptor) { if (descriptor.seriesValueIndex != -1) { if (descriptor.includePid) { return String.format("%1$s + pid", mEventDescriptionMap.get( descriptor.eventTag)[descriptor.seriesValueIndex].getName()); } else { return mEventDescriptionMap.get(descriptor.eventTag)[descriptor.seriesValueIndex] .getName(); } } return "pid"; } private String getFilterDescription(OccurrenceDisplayDescriptor descriptor) { if (descriptor.filterValueIndex != -1) { return String.format(" [%1$s %2$s %3$s]", mEventDescriptionMap.get( descriptor.eventTag)[descriptor.filterValueIndex].getName(), descriptor.filterCompareMethod.testString(), descriptor.filterValue != null ? descriptor.filterValue.toString() : "?"); //$NON-NLS-1$ } return ""; //$NON-NLS-1$ } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java0100644 0000000 0000000 00000005441 12747325007 026775 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.Log; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; /** * Imports a textual event log. Gets tags from build path. */ public class EventLogImporter { private String[] mTags; private String[] mLog; public EventLogImporter(String filePath) throws FileNotFoundException { String top = System.getenv("ANDROID_BUILD_TOP"); if (top == null) { throw new FileNotFoundException(); } final String tagFile = top + "/system/core/logcat/event-log-tags"; BufferedReader tagReader = new BufferedReader( new InputStreamReader(new FileInputStream(tagFile))); BufferedReader eventReader = new BufferedReader( new InputStreamReader(new FileInputStream(filePath))); try { readTags(tagReader); readLog(eventReader); } catch (IOException e) { } finally { if (tagReader != null) { try { tagReader.close(); } catch (IOException ignore) { } } if (eventReader != null) { try { eventReader.close(); } catch (IOException ignore) { } } } } public String[] getTags() { return mTags; } public String[] getLog() { return mLog; } private void readTags(BufferedReader reader) throws IOException { String line; ArrayList content = new ArrayList(); while ((line = reader.readLine()) != null) { content.add(line); } mTags = content.toArray(new String[content.size()]); } private void readLog(BufferedReader reader) throws IOException { String line; ArrayList content = new ArrayList(); while ((line = reader.readLine()) != null) { content.add(line); } mLog = content.toArray(new String[content.size()]); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java0100644 0000000 0000000 00000074431 12747325007 026240 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.Client; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.LogReceiver; import com.android.ddmlib.log.LogReceiver.ILogListener; import com.android.ddmlib.log.LogReceiver.LogEntry; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.TablePanel; import com.android.ddmuilib.actions.ICommonAction; import com.android.ddmuilib.annotation.UiThread; import com.android.ddmuilib.annotation.WorkerThread; import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.RowData; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.NumberFormat; import java.util.ArrayList; import java.util.regex.Pattern; /** * Event log viewer */ public class EventLogPanel extends TablePanel implements ILogListener, ILogColumnListener { private final static String TAG_FILE_EXT = ".tag"; //$NON-NLS-1$ private final static String PREFS_EVENT_DISPLAY = "EventLogPanel.eventDisplay"; //$NON-NLS-1$ private final static String EVENT_DISPLAY_STORAGE_SEPARATOR = "|"; //$NON-NLS-1$ static final String PREFS_DISPLAY_WIDTH = "EventLogPanel.width"; //$NON-NLS-1$ static final String PREFS_DISPLAY_HEIGHT = "EventLogPanel.height"; //$NON-NLS-1$ private final static int DEFAULT_DISPLAY_WIDTH = 500; private final static int DEFAULT_DISPLAY_HEIGHT = 400; private IDevice mCurrentLoggedDevice; private String mCurrentLogFile; private LogReceiver mCurrentLogReceiver; private EventLogParser mCurrentEventLogParser; private Object mLock = new Object(); /** list of all the events. */ private final ArrayList mEvents = new ArrayList(); /** list of all the new events, that have yet to be displayed by the ui */ private final ArrayList mNewEvents = new ArrayList(); /** indicates a pending ui thread display */ private boolean mPendingDisplay = false; /** list of all the custom event displays */ private final ArrayList mEventDisplays = new ArrayList(); private final NumberFormat mFormatter = NumberFormat.getInstance(); private Composite mParent; private ScrolledComposite mBottomParentPanel; private Composite mBottomPanel; private ICommonAction mOptionsAction; private ICommonAction mClearAction; private ICommonAction mSaveAction; private ICommonAction mLoadAction; private ICommonAction mImportAction; /** file containing the current log raw data. */ private File mTempFile = null; public EventLogPanel() { super(); mFormatter.setGroupingUsed(true); } /** * Sets the external actions. *

This method sets up the {@link ICommonAction} objects to execute the proper code * when triggered by using {@link ICommonAction#setRunnable(Runnable)}. *

It will also make sure they are enabled only when possible. * @param optionsAction * @param clearAction * @param saveAction * @param loadAction * @param importAction */ public void setActions(ICommonAction optionsAction, ICommonAction clearAction, ICommonAction saveAction, ICommonAction loadAction, ICommonAction importAction) { mOptionsAction = optionsAction; mOptionsAction.setRunnable(new Runnable() { @Override public void run() { openOptionPanel(); } }); mClearAction = clearAction; mClearAction.setRunnable(new Runnable() { @Override public void run() { clearLog(); } }); mSaveAction = saveAction; mSaveAction.setRunnable(new Runnable() { @Override public void run() { try { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); fileDialog.setText("Save Event Log"); fileDialog.setFileName("event.log"); String fileName = fileDialog.open(); if (fileName != null) { saveLog(fileName); } } catch (IOException e1) { } } }); mLoadAction = loadAction; mLoadAction.setRunnable(new Runnable() { @Override public void run() { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); fileDialog.setText("Load Event Log"); String fileName = fileDialog.open(); if (fileName != null) { loadLog(fileName); } } }); mImportAction = importAction; mImportAction.setRunnable(new Runnable() { @Override public void run() { FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.OPEN); fileDialog.setText("Import Bug Report"); String fileName = fileDialog.open(); if (fileName != null) { importBugReport(fileName); } } }); mOptionsAction.setEnabled(false); mClearAction.setEnabled(false); mSaveAction.setEnabled(false); } /** * Opens the option panel. *

* This must be called from the UI thread */ @UiThread public void openOptionPanel() { try { EventDisplayOptions dialog = new EventDisplayOptions(mParent.getShell()); if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) { synchronized (mLock) { // get the new EventDisplay list mEventDisplays.clear(); mEventDisplays.addAll(dialog.getEventDisplays()); // since the list of EventDisplay changed, we store it. saveEventDisplays(); rebuildUi(); } } } catch (SWTException e) { Log.e("EventLog", e); //$NON-NLS-1$ } } /** * Clears the log. *

* This must be called from the UI thread */ public void clearLog() { try { synchronized (mLock) { mEvents.clear(); mNewEvents.clear(); mPendingDisplay = false; for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.resetUI(); } } } catch (SWTException e) { Log.e("EventLog", e); //$NON-NLS-1$ } } /** * Saves the content of the event log into a file. The log is saved in the same * binary format than on the device. * @param filePath * @throws IOException */ public void saveLog(String filePath) throws IOException { if (mCurrentLoggedDevice != null && mCurrentEventLogParser != null) { File destFile = new File(filePath); destFile.createNewFile(); FileInputStream fis = new FileInputStream(mTempFile); FileOutputStream fos = new FileOutputStream(destFile); byte[] buffer = new byte[1024]; int count; while ((count = fis.read(buffer)) != -1) { fos.write(buffer, 0, count); } fos.close(); fis.close(); // now we save the tag file filePath = filePath + TAG_FILE_EXT; mCurrentEventLogParser.saveTags(filePath); } } /** * Loads a binary event log (if has associated .tag file) or * otherwise loads a textual event log. * @param filePath Event log path (and base of potential tag file) */ public void loadLog(String filePath) { if ((new File(filePath + TAG_FILE_EXT)).exists()) { startEventLogFromFiles(filePath); } else { try { EventLogImporter importer = new EventLogImporter(filePath); String[] tags = importer.getTags(); String[] log = importer.getLog(); startEventLogFromContent(tags, log); } catch (FileNotFoundException e) { // If this fails, display the error message from startEventLogFromFiles, // and pretend we never tried EventLogImporter Log.logAndDisplay(Log.LogLevel.ERROR, "EventLog", String.format("Failure to read %1$s", filePath + TAG_FILE_EXT)); } } } public void importBugReport(String filePath) { try { BugReportImporter importer = new BugReportImporter(filePath); String[] tags = importer.getTags(); String[] log = importer.getLog(); startEventLogFromContent(tags, log); } catch (FileNotFoundException e) { Log.logAndDisplay(LogLevel.ERROR, "Import", "Unable to import bug report: " + e.getMessage()); } } /* (non-Javadoc) * @see com.android.ddmuilib.SelectionDependentPanel#clientSelected() */ @Override public void clientSelected() { // pass } /* (non-Javadoc) * @see com.android.ddmuilib.SelectionDependentPanel#deviceSelected() */ @Override public void deviceSelected() { startEventLog(getCurrentDevice()); } /* * (non-Javadoc) * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int) */ @Override public void clientChanged(Client client, int changeMask) { // pass } /* (non-Javadoc) * @see com.android.ddmuilib.Panel#createControl(org.eclipse.swt.widgets.Composite) */ @Override protected Control createControl(Composite parent) { mParent = parent; mParent.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { synchronized (mLock) { if (mCurrentLogReceiver != null) { mCurrentLogReceiver.cancel(); mCurrentLogReceiver = null; mCurrentEventLogParser = null; mCurrentLoggedDevice = null; mEventDisplays.clear(); mEvents.clear(); } } } }); final IPreferenceStore store = DdmUiPreferences.getStore(); // init some store stuff store.setDefault(PREFS_DISPLAY_WIDTH, DEFAULT_DISPLAY_WIDTH); store.setDefault(PREFS_DISPLAY_HEIGHT, DEFAULT_DISPLAY_HEIGHT); mBottomParentPanel = new ScrolledComposite(parent, SWT.V_SCROLL); mBottomParentPanel.setLayoutData(new GridData(GridData.FILL_BOTH)); mBottomParentPanel.setExpandHorizontal(true); mBottomParentPanel.setExpandVertical(true); mBottomParentPanel.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { if (mBottomPanel != null) { Rectangle r = mBottomParentPanel.getClientArea(); mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, SWT.DEFAULT)); } } }); prepareDisplayUi(); // load the EventDisplay from storage. loadEventDisplays(); // create the ui createDisplayUi(); return mBottomParentPanel; } /* (non-Javadoc) * @see com.android.ddmuilib.Panel#postCreation() */ @Override protected void postCreation() { // pass } /* (non-Javadoc) * @see com.android.ddmuilib.Panel#setFocus() */ @Override public void setFocus() { mBottomParentPanel.setFocus(); } /** * Starts a new logcat and set mCurrentLogCat as the current receiver. * @param device the device to connect logcat to. */ private void startEventLog(final IDevice device) { if (device == mCurrentLoggedDevice) { return; } // if we have a logcat already running if (mCurrentLogReceiver != null) { stopEventLog(false); } mCurrentLoggedDevice = null; mCurrentLogFile = null; if (device != null) { // create a new output receiver mCurrentLogReceiver = new LogReceiver(this); // start the logcat in a different thread new Thread("EventLog") { //$NON-NLS-1$ @Override public void run() { while (device.isOnline() == false && mCurrentLogReceiver != null && mCurrentLogReceiver.isCancelled() == false) { try { sleep(2000); } catch (InterruptedException e) { return; } } if (mCurrentLogReceiver == null || mCurrentLogReceiver.isCancelled()) { // logcat was stopped/cancelled before the device became ready. return; } try { mCurrentLoggedDevice = device; synchronized (mLock) { mCurrentEventLogParser = new EventLogParser(); mCurrentEventLogParser.init(device); } // update the event display with the new parser. updateEventDisplays(); // prepare the temp file that will contain the raw data mTempFile = File.createTempFile("android-event-", ".log"); device.runEventLogService(mCurrentLogReceiver); } catch (Exception e) { Log.e("EventLog", e); } finally { } } }.start(); } } private void startEventLogFromFiles(final String fileName) { // if we have a logcat already running if (mCurrentLogReceiver != null) { stopEventLog(false); } mCurrentLoggedDevice = null; mCurrentLogFile = null; // create a new output receiver mCurrentLogReceiver = new LogReceiver(this); mSaveAction.setEnabled(false); // start the logcat in a different thread new Thread("EventLog") { //$NON-NLS-1$ @Override public void run() { try { mCurrentLogFile = fileName; synchronized (mLock) { mCurrentEventLogParser = new EventLogParser(); if (mCurrentEventLogParser.init(fileName + TAG_FILE_EXT) == false) { mCurrentEventLogParser = null; Log.logAndDisplay(LogLevel.ERROR, "EventLog", String.format("Failure to read %1$s", fileName + TAG_FILE_EXT)); return; } } // update the event display with the new parser. updateEventDisplays(); runLocalEventLogService(fileName, mCurrentLogReceiver); } catch (Exception e) { Log.e("EventLog", e); } finally { } } }.start(); } private void startEventLogFromContent(final String[] tags, final String[] log) { // if we have a logcat already running if (mCurrentLogReceiver != null) { stopEventLog(false); } mCurrentLoggedDevice = null; mCurrentLogFile = null; // create a new output receiver mCurrentLogReceiver = new LogReceiver(this); mSaveAction.setEnabled(false); // start the logcat in a different thread new Thread("EventLog") { //$NON-NLS-1$ @Override public void run() { try { synchronized (mLock) { mCurrentEventLogParser = new EventLogParser(); if (mCurrentEventLogParser.init(tags) == false) { mCurrentEventLogParser = null; return; } } // update the event display with the new parser. updateEventDisplays(); runLocalEventLogService(log, mCurrentLogReceiver); } catch (Exception e) { Log.e("EventLog", e); } finally { } } }.start(); } public void stopEventLog(boolean inUiThread) { if (mCurrentLogReceiver != null) { mCurrentLogReceiver.cancel(); // when the thread finishes, no one will reference that object // and it'll be destroyed synchronized (mLock) { mCurrentLogReceiver = null; mCurrentEventLogParser = null; mCurrentLoggedDevice = null; mEvents.clear(); mNewEvents.clear(); mPendingDisplay = false; } resetUI(inUiThread); } if (mTempFile != null) { mTempFile.delete(); mTempFile = null; } } private void resetUI(boolean inUiThread) { mEvents.clear(); // the ui is static we just empty it. if (inUiThread) { resetUiFromUiThread(); } else { try { Display d = mBottomParentPanel.getDisplay(); // run sync as we need to update right now. d.syncExec(new Runnable() { @Override public void run() { if (mBottomParentPanel.isDisposed() == false) { resetUiFromUiThread(); } } }); } catch (SWTException e) { // display is disposed, we're quitting. Do nothing. } } } private void resetUiFromUiThread() { synchronized (mLock) { for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.resetUI(); } } mOptionsAction.setEnabled(false); mClearAction.setEnabled(false); mSaveAction.setEnabled(false); } private void prepareDisplayUi() { mBottomPanel = new Composite(mBottomParentPanel, SWT.NONE); mBottomParentPanel.setContent(mBottomPanel); } private void createDisplayUi() { RowLayout rowLayout = new RowLayout(); rowLayout.wrap = true; rowLayout.pack = false; rowLayout.justify = true; rowLayout.fill = true; rowLayout.type = SWT.HORIZONTAL; mBottomPanel.setLayout(rowLayout); IPreferenceStore store = DdmUiPreferences.getStore(); int displayWidth = store.getInt(PREFS_DISPLAY_WIDTH); int displayHeight = store.getInt(PREFS_DISPLAY_HEIGHT); for (EventDisplay eventDisplay : mEventDisplays) { Control c = eventDisplay.createComposite(mBottomPanel, mCurrentEventLogParser, this); if (c != null) { RowData rd = new RowData(); rd.height = displayHeight; rd.width = displayWidth; c.setLayoutData(rd); } Table table = eventDisplay.getTable(); if (table != null) { addTableToFocusListener(table); } } mBottomPanel.layout(); mBottomParentPanel.setMinSize(mBottomPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT)); mBottomParentPanel.layout(); } /** * Rebuild the display ui. */ @UiThread private void rebuildUi() { synchronized (mLock) { // we need to rebuild the ui. First we get rid of it. mBottomPanel.dispose(); mBottomPanel = null; prepareDisplayUi(); createDisplayUi(); // and fill it boolean start_event = false; synchronized (mNewEvents) { mNewEvents.addAll(0, mEvents); if (mPendingDisplay == false) { mPendingDisplay = true; start_event = true; } } if (start_event) { scheduleUIEventHandler(); } Rectangle r = mBottomParentPanel.getClientArea(); mBottomParentPanel.setMinSize(mBottomPanel.computeSize(r.width, SWT.DEFAULT)); } } /** * Processes a new {@link LogEntry} by parsing it with {@link EventLogParser} and displaying it. * @param entry The new log entry * @see LogReceiver.ILogListener#newEntry(LogEntry) */ @Override @WorkerThread public void newEntry(LogEntry entry) { synchronized (mLock) { if (mCurrentEventLogParser != null) { EventContainer event = mCurrentEventLogParser.parse(entry); if (event != null) { handleNewEvent(event); } } } } @WorkerThread private void handleNewEvent(EventContainer event) { // add the event to the generic list mEvents.add(event); // add to the list of events that needs to be displayed, and trigger a // new display if needed. boolean start_event = false; synchronized (mNewEvents) { mNewEvents.add(event); if (mPendingDisplay == false) { mPendingDisplay = true; start_event = true; } } if (start_event == false) { // we're done return; } scheduleUIEventHandler(); } /** * Schedules the UI thread to execute a {@link Runnable} calling {@link #displayNewEvents()}. */ private void scheduleUIEventHandler() { try { Display d = mBottomParentPanel.getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { if (mBottomParentPanel.isDisposed() == false) { if (mCurrentEventLogParser != null) { displayNewEvents(); } } } }); } catch (SWTException e) { // if the ui is disposed, do nothing } } /** * Processes raw data coming from the log service. * @see LogReceiver.ILogListener#newData(byte[], int, int) */ @Override public void newData(byte[] data, int offset, int length) { if (mTempFile != null) { try { FileOutputStream fos = new FileOutputStream(mTempFile, true /* append */); fos.write(data, offset, length); fos.close(); } catch (FileNotFoundException e) { } catch (IOException e) { } } } @UiThread private void displayNewEvents() { // never display more than 1,000 events in this loop. We can't do too much in the UI thread. int count = 0; // prepare the displays for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.startMultiEventDisplay(); } // display the new events EventContainer event = null; boolean need_to_reloop = false; do { // get the next event to display. synchronized (mNewEvents) { if (mNewEvents.size() > 0) { if (count > 200) { // there are still events to be displayed, but we don't want to hog the // UI thread for too long, so we stop this runnable, but launch a new // one to keep going. need_to_reloop = true; event = null; } else { event = mNewEvents.remove(0); count++; } } else { // we're done. event = null; mPendingDisplay = false; } } if (event != null) { // notify the event display for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.newEvent(event, mCurrentEventLogParser); } } } while (event != null); // we're done displaying events. for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.endMultiEventDisplay(); } // if needed, ask the UI thread to re-run this method. if (need_to_reloop) { scheduleUIEventHandler(); } } /** * Loads the {@link EventDisplay}s from the preference store. */ private void loadEventDisplays() { IPreferenceStore store = DdmUiPreferences.getStore(); String storage = store.getString(PREFS_EVENT_DISPLAY); if (storage.length() > 0) { String[] values = storage.split(Pattern.quote(EVENT_DISPLAY_STORAGE_SEPARATOR)); for (String value : values) { EventDisplay eventDisplay = EventDisplay.load(value); if (eventDisplay != null) { mEventDisplays.add(eventDisplay); } } } } /** * Saves the {@link EventDisplay}s into the {@link DdmUiPreferences} store. */ private void saveEventDisplays() { IPreferenceStore store = DdmUiPreferences.getStore(); boolean first = true; StringBuilder sb = new StringBuilder(); for (EventDisplay eventDisplay : mEventDisplays) { String storage = eventDisplay.getStorageString(); if (storage != null) { if (first == false) { sb.append(EVENT_DISPLAY_STORAGE_SEPARATOR); } else { first = false; } sb.append(storage); } } store.setValue(PREFS_EVENT_DISPLAY, sb.toString()); } /** * Updates the {@link EventDisplay} with the new {@link EventLogParser}. *

* This will run asynchronously in the UI thread. */ @WorkerThread private void updateEventDisplays() { try { Display d = mBottomParentPanel.getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { if (mBottomParentPanel.isDisposed() == false) { for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.setNewLogParser(mCurrentEventLogParser); } mOptionsAction.setEnabled(true); mClearAction.setEnabled(true); if (mCurrentLogFile == null) { mSaveAction.setEnabled(true); } else { mSaveAction.setEnabled(false); } } } }); } catch (SWTException e) { // display is disposed: do nothing. } } @Override @UiThread public void columnResized(int index, TableColumn sourceColumn) { for (EventDisplay eventDisplay : mEventDisplays) { eventDisplay.resizeColumn(index, sourceColumn); } } /** * Runs an event log service out of a local file. * @param fileName the full file name of the local file containing the event log. * @param logReceiver the receiver that will handle the log * @throws IOException */ @WorkerThread private void runLocalEventLogService(String fileName, LogReceiver logReceiver) throws IOException { byte[] buffer = new byte[256]; FileInputStream fis = new FileInputStream(fileName); try { int count; while ((count = fis.read(buffer)) != -1) { logReceiver.parseNewData(buffer, 0, count); } } finally { fis.close(); } } @WorkerThread private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) { synchronized (mLock) { for (String line : log) { EventContainer event = mCurrentEventLogParser.parse(line); if (event != null) { handleNewEvent(event); } } } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java0100644 0000000 0000000 00000054734 12747325007 027320 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer.CompareMethod; import com.android.ddmlib.log.EventContainer.EventValueType; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.EventValueDescription; import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.util.ArrayList; import java.util.Map; import java.util.Set; final class EventValueSelector extends Dialog { private static final int DLG_WIDTH = 400; private static final int DLG_HEIGHT = 300; private Shell mParent; private Shell mShell; private boolean mEditStatus; private Combo mEventCombo; private Combo mValueCombo; private Combo mSeriesCombo; private Button mDisplayPidCheckBox; private Combo mFilterCombo; private Combo mFilterMethodCombo; private Text mFilterValue; private Button mOkButton; private EventLogParser mLogParser; private OccurrenceDisplayDescriptor mDescriptor; /** list of event integer in the order of the combo. */ private Integer[] mEventTags; /** list of indices in the {@link EventValueDescription} array of the current event * that are of type string. This lets us get back the {@link EventValueDescription} from the * index in the Series {@link Combo}. */ private final ArrayList mSeriesIndices = new ArrayList(); public EventValueSelector(Shell parent) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); } /** * Opens the display option dialog to edit a new descriptor. * @param decriptorClass the class of the object to instantiate. Must extend * {@link OccurrenceDisplayDescriptor} * @param logParser * @return true if the object is to be created, false if the creation was canceled. */ boolean open(Class descriptorClass, EventLogParser logParser) { try { OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance(); setModified(); return open(descriptor, logParser); } catch (InstantiationException e) { return false; } catch (IllegalAccessException e) { return false; } } /** * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or * a {@link ValueDisplayDescriptor} object. * @param descriptor The descriptor to edit. * @return true if the object was modified. */ boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) { // make a copy of the descriptor as we'll use a working copy. if (descriptor instanceof ValueDisplayDescriptor) { mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor); } else if (descriptor instanceof OccurrenceDisplayDescriptor) { mDescriptor = new OccurrenceDisplayDescriptor(descriptor); } else { return false; } mLogParser = logParser; createUI(); if (mParent == null || mShell == null) { return false; } loadValueDescriptor(); checkValidity(); // Set the dialog size. try { mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); Rectangle r = mParent.getBounds(); // get the center new top left. int cx = r.x + r.width/2; int x = cx - DLG_WIDTH / 2; int cy = r.y + r.height/2; int y = cy - DLG_HEIGHT / 2; mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); } catch (Exception e) { e.printStackTrace(); } mShell.layout(); // actually open the dialog mShell.open(); // event loop until the dialog is closed. Display display = mParent.getDisplay(); while (!mShell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } return mEditStatus; } OccurrenceDisplayDescriptor getDescriptor() { return mDescriptor; } private void createUI() { GridData gd; mParent = getParent(); mShell = new Shell(mParent, getStyle()); mShell.setText("Event Display Configuration"); mShell.setLayout(new GridLayout(2, false)); Label l = new Label(mShell, SWT.NONE); l.setText("Event:"); mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // the event tag / event name map Map eventTagMap = mLogParser.getTagMap(); Map eventInfoMap = mLogParser.getEventInfoMap(); Set keys = eventTagMap.keySet(); ArrayList list = new ArrayList(); for (Integer i : keys) { if (eventInfoMap.get(i) != null) { String eventName = eventTagMap.get(i); mEventCombo.add(eventName); list.add(i); } } mEventTags = list.toArray(new Integer[list.size()]); mEventCombo.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { handleEventComboSelection(); setModified(); } }); l = new Label(mShell, SWT.NONE); l.setText("Value:"); mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mValueCombo.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { handleValueComboSelection(); setModified(); } }); l = new Label(mShell, SWT.NONE); l.setText("Series Name:"); mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mSeriesCombo.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { handleSeriesComboSelection(); setModified(); } }); // empty comp new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData()); gd.heightHint = gd.widthHint = 0; mDisplayPidCheckBox = new Button(mShell, SWT.CHECK); mDisplayPidCheckBox.setText("Also Show pid"); mDisplayPidCheckBox.setEnabled(false); mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { mDescriptor.includePid = mDisplayPidCheckBox.getSelection(); setModified(); } }); l = new Label(mShell, SWT.NONE); l.setText("Filter By:"); mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mFilterCombo.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { handleFilterComboSelection(); setModified(); } }); l = new Label(mShell, SWT.NONE); l.setText("Filter Method:"); mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); for (CompareMethod method : CompareMethod.values()) { mFilterMethodCombo.add(method.toString()); } mFilterMethodCombo.select(0); mFilterMethodCombo.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { handleFilterMethodComboSelection(); setModified(); } }); l = new Label(mShell, SWT.NONE); l.setText("Filter Value:"); mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE); mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mFilterValue.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { if (mDescriptor.filterValueIndex != -1) { // get the current selection in the event combo int index = mEventCombo.getSelectionIndex(); if (index != -1) { // match it to an event int eventTag = mEventTags[index]; mDescriptor.eventTag = eventTag; // get the EventValueDescription for this tag EventValueDescription valueDesc = mLogParser.getEventInfoMap() .get(eventTag)[mDescriptor.filterValueIndex]; // let the EventValueDescription convert the String value into an object // of the proper type. mDescriptor.filterValue = valueDesc.getObjectFromString( mFilterValue.getText().trim()); setModified(); } } } }); // add a separator spanning the 2 columns l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); gd = new GridData(GridData.FILL_HORIZONTAL); gd.horizontalSpan = 2; l.setLayoutData(gd); // add a composite to hold the ok/cancel button, no matter what the columns size are. Composite buttonComp = new Composite(mShell, SWT.NONE); gd = new GridData(GridData.FILL_HORIZONTAL); gd.horizontalSpan = 2; buttonComp.setLayoutData(gd); GridLayout gl; buttonComp.setLayout(gl = new GridLayout(6, true)); gl.marginHeight = gl.marginWidth = 0; Composite padding = new Composite(mShell, SWT.NONE); padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mOkButton = new Button(buttonComp, SWT.PUSH); mOkButton.setText("OK"); mOkButton.setLayoutData(new GridData(GridData.CENTER)); mOkButton.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { mShell.close(); } }); padding = new Composite(mShell, SWT.NONE); padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); padding = new Composite(mShell, SWT.NONE); padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Button cancelButton = new Button(buttonComp, SWT.PUSH); cancelButton.setText("Cancel"); cancelButton.setLayoutData(new GridData(GridData.CENTER)); cancelButton.addSelectionListener(new SelectionAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) */ @Override public void widgetSelected(SelectionEvent e) { // cancel the edit mEditStatus = false; mShell.close(); } }); padding = new Composite(mShell, SWT.NONE); padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mShell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { event.doit = true; } }); } private void setModified() { mEditStatus = true; } private void handleEventComboSelection() { // get the current selection in the event combo int index = mEventCombo.getSelectionIndex(); if (index != -1) { // match it to an event int eventTag = mEventTags[index]; mDescriptor.eventTag = eventTag; // get the EventValueDescription for this tag EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); // fill the combo for the values mValueCombo.removeAll(); if (values != null) { if (mDescriptor instanceof ValueDisplayDescriptor) { ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; mValueCombo.setEnabled(true); for (EventValueDescription value : values) { mValueCombo.add(value.toString()); } if (valueDescriptor.valueIndex != -1) { mValueCombo.select(valueDescriptor.valueIndex); } else { mValueCombo.clearSelection(); } } else { mValueCombo.setEnabled(false); } // fill the axis combo mSeriesCombo.removeAll(); mSeriesCombo.setEnabled(false); mSeriesIndices.clear(); int axisIndex = 0; int selectionIndex = -1; for (EventValueDescription value : values) { if (value.getEventValueType() == EventValueType.STRING) { mSeriesCombo.add(value.getName()); mSeriesCombo.setEnabled(true); mSeriesIndices.add(axisIndex); if (mDescriptor.seriesValueIndex != -1 && mDescriptor.seriesValueIndex == axisIndex) { selectionIndex = axisIndex; } } axisIndex++; } if (mSeriesCombo.isEnabled()) { mSeriesCombo.add("default (pid)", 0 /* index */); mSeriesIndices.add(0 /* index */, -1 /* value */); // +1 because we added another item at index 0 mSeriesCombo.select(selectionIndex + 1); if (selectionIndex >= 0) { mDisplayPidCheckBox.setSelection(mDescriptor.includePid); mDisplayPidCheckBox.setEnabled(true); } else { mDisplayPidCheckBox.setEnabled(false); mDisplayPidCheckBox.setSelection(false); } } else { mDisplayPidCheckBox.setSelection(false); mDisplayPidCheckBox.setEnabled(false); } // fill the filter combo mFilterCombo.setEnabled(true); mFilterCombo.removeAll(); mFilterCombo.add("(no filter)"); for (EventValueDescription value : values) { mFilterCombo.add(value.toString()); } // select the current filter mFilterCombo.select(mDescriptor.filterValueIndex + 1); mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod)); // fill the current filter value if (mDescriptor.filterValueIndex != -1) { EventValueDescription valueInfo = values[mDescriptor.filterValueIndex]; if (valueInfo.checkForType(mDescriptor.filterValue)) { mFilterValue.setText(mDescriptor.filterValue.toString()); } else { mFilterValue.setText(""); } } else { mFilterValue.setText(""); } } else { disableSubCombos(); } } else { disableSubCombos(); } checkValidity(); } /** * */ private void disableSubCombos() { mValueCombo.removeAll(); mValueCombo.clearSelection(); mValueCombo.setEnabled(false); mSeriesCombo.removeAll(); mSeriesCombo.clearSelection(); mSeriesCombo.setEnabled(false); mDisplayPidCheckBox.setEnabled(false); mDisplayPidCheckBox.setSelection(false); mFilterCombo.removeAll(); mFilterCombo.clearSelection(); mFilterCombo.setEnabled(false); mFilterValue.setEnabled(false); mFilterValue.setText(""); mFilterMethodCombo.setEnabled(false); } private void handleValueComboSelection() { ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; // get the current selection in the value combo int index = mValueCombo.getSelectionIndex(); valueDescriptor.valueIndex = index; // for now set the built-in name // get the current selection in the event combo int eventIndex = mEventCombo.getSelectionIndex(); // match it to an event int eventTag = mEventTags[eventIndex]; // get the EventValueDescription for this tag EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); valueDescriptor.valueName = values[index].getName(); checkValidity(); } private void handleSeriesComboSelection() { // get the current selection in the axis combo int index = mSeriesCombo.getSelectionIndex(); // get the actual value index from the list. int valueIndex = mSeriesIndices.get(index); mDescriptor.seriesValueIndex = valueIndex; if (index > 0) { mDisplayPidCheckBox.setEnabled(true); mDisplayPidCheckBox.setSelection(mDescriptor.includePid); } else { mDisplayPidCheckBox.setSelection(false); mDisplayPidCheckBox.setEnabled(false); } } private void handleFilterComboSelection() { // get the current selection in the axis combo int index = mFilterCombo.getSelectionIndex(); // decrement index by 1 since the item 0 means // no filter (index = -1), and the rest is offset by 1 index--; mDescriptor.filterValueIndex = index; if (index != -1) { mFilterValue.setEnabled(true); mFilterMethodCombo.setEnabled(true); if (mDescriptor.filterValue instanceof String) { mFilterValue.setText((String)mDescriptor.filterValue); } } else { mFilterValue.setText(""); mFilterValue.setEnabled(false); mFilterMethodCombo.setEnabled(false); } } private void handleFilterMethodComboSelection() { // get the current selection in the axis combo int index = mFilterMethodCombo.getSelectionIndex(); CompareMethod method = CompareMethod.values()[index]; mDescriptor.filterCompareMethod = method; } /** * Returns the index of the filter method * @param filterCompareMethod the {@link CompareMethod} enum. */ private int getFilterMethodIndex(CompareMethod filterCompareMethod) { CompareMethod[] values = CompareMethod.values(); for (int i = 0 ; i < values.length ; i++) { if (values[i] == filterCompareMethod) { return i; } } return -1; } private void loadValueDescriptor() { // get the index from the eventTag. int eventIndex = 0; int comboIndex = -1; for (int i : mEventTags) { if (i == mDescriptor.eventTag) { comboIndex = eventIndex; break; } eventIndex++; } if (comboIndex == -1) { mEventCombo.clearSelection(); } else { mEventCombo.select(comboIndex); } // get the event from the descriptor handleEventComboSelection(); } private void checkValidity() { mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 && (((mDescriptor instanceof ValueDisplayDescriptor) == false) || mValueCombo.getSelectionIndex() != -1)); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java0100644 0000000 0000000 00000006511 12747325007 027306 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.CrosshairState; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.PlotRenderingInfo; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRendererState; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.data.xy.XYDataset; import org.jfree.ui.RectangleEdge; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Stroke; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; /** * Custom renderer to render event occurrence. This rendered ignores the y value, and simply * draws a line from min to max at the time of the item. */ public class OccurrenceRenderer extends XYLineAndShapeRenderer { private static final long serialVersionUID = 1L; @Override public void drawItem(Graphics2D g2, XYItemRendererState state, Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, int series, int item, CrosshairState crosshairState, int pass) { TimeSeriesCollection timeDataSet = (TimeSeriesCollection)dataset; // get the x value for the series/item. double x = timeDataSet.getX(series, item).doubleValue(); // get the min/max of the range axis double yMin = rangeAxis.getLowerBound(); double yMax = rangeAxis.getUpperBound(); RectangleEdge domainEdge = plot.getDomainAxisEdge(); RectangleEdge rangeEdge = plot.getRangeAxisEdge(); // convert the coordinates to java2d. double x2D = domainAxis.valueToJava2D(x, dataArea, domainEdge); double yMin2D = rangeAxis.valueToJava2D(yMin, dataArea, rangeEdge); double yMax2D = rangeAxis.valueToJava2D(yMax, dataArea, rangeEdge); // get the paint information for the series/item Paint p = getItemPaint(series, item); Stroke s = getItemStroke(series, item); Line2D line = null; PlotOrientation orientation = plot.getOrientation(); if (orientation == PlotOrientation.HORIZONTAL) { line = new Line2D.Double(yMin2D, x2D, yMax2D, x2D); } else if (orientation == PlotOrientation.VERTICAL) { line = new Line2D.Double(x2D, yMin2D, x2D, yMax2D); } g2.setPaint(p); g2.setStroke(s); g2.draw(line); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java0100644 0000000 0000000 00000016243 12747325007 025617 0ustar000000000 0000000 /* * 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.ddmuilib.log.event; import com.android.ddmlib.log.EventContainer; import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.InvalidTypeException; import java.awt.Color; abstract public class SyncCommon extends EventDisplay { // State information while processing the event stream private int mLastState; // 0 if event started, 1 if event stopped private long mLastStartTime; // ms private long mLastStopTime; //ms private String mLastDetails; private int mLastSyncSource; // poll, server, user, etc. // Some common variables for sync display. These define the sync backends //and how they should be displayed. protected static final int CALENDAR = 0; protected static final int GMAIL = 1; protected static final int FEEDS = 2; protected static final int CONTACTS = 3; protected static final int ERRORS = 4; protected static final int NUM_AUTHS = (CONTACTS + 1); protected static final String AUTH_NAMES[] = {"Calendar", "Gmail", "Feeds", "Contacts", "Errors"}; protected static final Color AUTH_COLORS[] = {Color.MAGENTA, Color.GREEN, Color.BLUE, Color.ORANGE, Color.RED}; // Values from data/etc/event-log-tags final int EVENT_SYNC = 2720; final int EVENT_TICKLE = 2742; final int EVENT_SYNC_DETAILS = 2743; final int EVENT_CONTACTS_AGGREGATION = 2747; protected SyncCommon(String name) { super(name); } /** * Resets the display. */ @Override void resetUI() { mLastStartTime = 0; mLastStopTime = 0; mLastState = -1; mLastSyncSource = -1; mLastDetails = ""; } /** * Updates the display with a new event. This is the main entry point for * each event. This method has the logic to tie together the start event, * stop event, and details event into one graph item. The combined sync event * is handed to the subclass via processSycnEvent. Note that the details * can happen before or after the stop event. * * @param event The event * @param logParser The parser providing the event. */ @Override void newEvent(EventContainer event, EventLogParser logParser) { try { if (event.mTag == EVENT_SYNC) { int state = Integer.parseInt(event.getValueAsString(1)); if (state == 0) { // start mLastStartTime = (long) event.sec * 1000L + (event.nsec / 1000000L); mLastState = 0; mLastSyncSource = Integer.parseInt(event.getValueAsString(2)); mLastDetails = ""; } else if (state == 1) { // stop if (mLastState == 0) { mLastStopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); if (mLastStartTime == 0) { // Log starts with a stop event mLastStartTime = mLastStopTime; } int auth = getAuth(event.getValueAsString(0)); processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, true, mLastSyncSource); mLastState = 1; } } } else if (event.mTag == EVENT_SYNC_DETAILS) { mLastDetails = event.getValueAsString(3); if (mLastState != 0) { // Not inside event long updateTime = (long) event.sec * 1000L + (event.nsec / 1000000L); if (updateTime - mLastStopTime <= 250) { // Got details within 250ms after event, so delete and re-insert // Details later than 250ms (arbitrary) are discarded as probably // unrelated. int auth = getAuth(event.getValueAsString(0)); processSyncEvent(event, auth, mLastStartTime, mLastStopTime, mLastDetails, false, mLastSyncSource); } } } else if (event.mTag == EVENT_CONTACTS_AGGREGATION) { long stopTime = (long) event.sec * 1000L + (event.nsec / 1000000L); long startTime = stopTime - Long.parseLong(event.getValueAsString(0)); String details; int count = Integer.parseInt(event.getValueAsString(1)); if (count < 0) { details = "g" + (-count); } else { details = "G" + count; } processSyncEvent(event, CONTACTS, startTime, stopTime, details, true /* newEvent */, mLastSyncSource); } } catch (InvalidTypeException e) { } } /** * Callback hook for subclass to process a sync event. newEvent has the logic * to combine start and stop events and passes a processed event to the * subclass. * * @param event The sync event * @param auth The sync authority * @param startTime Start time (ms) of events * @param stopTime Stop time (ms) of events * @param details Details associated with the event. * @param newEvent True if this event is a new sync event. False if this event * @param syncSource Poll, user, server, etc. */ abstract void processSyncEvent(EventContainer event, int auth, long startTime, long stopTime, String details, boolean newEvent, int syncSource); /** * Converts authority name to auth number. * * @param authname "calendar", etc. * @return number series number associated with the authority */ protected int getAuth(String authname) throws InvalidTypeException { if ("calendar".equals(authname) || "cl".equals(authname) || "com.android.calendar".equals(authname)) { return CALENDAR; } else if ("contacts".equals(authname) || "cp".equals(authname) || "com.android.contacts".equals(authname)) { return CONTACTS; } else if ("subscribedfeeds".equals(authname)) { return FEEDS; } else if ("gmail-ls".equals(authname) || "mail".equals(authname)) { return GMAIL; } else if ("gmail-live".equals(authname)) { return GMAIL; } else if ("unknown".equals(authname)) { return -1; // Unknown tickles; discard } else { throw new InvalidTypeException("Unknown authname " + authname); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/0040755 0000000 0000000 00000000000 12747325007 022233 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java0100644 0000000 0000000 00000030652 12747325007 026254 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmuilib.ImageLoader; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; /** * Small dialog box to edit a static port number. */ public class EditFilterDialog extends Dialog { private static final int DLG_WIDTH = 400; private static final int DLG_HEIGHT = 260; private static final String IMAGE_WARNING = "warning.png"; //$NON-NLS-1$ private static final String IMAGE_EMPTY = "empty.png"; //$NON-NLS-1$ private Shell mParent; private Shell mShell; private boolean mOk = false; /** * Filter being edited or created */ private LogFilter mFilter; private String mName; private String mTag; private String mPid; /** Log level as an index of the drop-down combo * @see getLogLevel * @see getComboIndex */ private int mLogLevel; private Button mOkButton; private Label mNameWarning; private Label mTagWarning; private Label mPidWarning; public EditFilterDialog(Shell parent) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); } public EditFilterDialog(Shell shell, LogFilter filter) { this(shell); mFilter = filter; } /** * Opens the dialog. The method will return when the user closes the dialog * somehow. * * @return true if ok was pressed, false if cancelled. */ public boolean open() { createUI(); if (mParent == null || mShell == null) { return false; } mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); Rectangle r = mParent.getBounds(); // get the center new top left. int cx = r.x + r.width/2; int x = cx - DLG_WIDTH / 2; int cy = r.y + r.height/2; int y = cy - DLG_HEIGHT / 2; mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); mShell.open(); Display display = mParent.getDisplay(); while (!mShell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } // we're quitting with OK. // Lets update the filter if needed if (mOk) { // if it was a "Create filter" action we need to create it first. if (mFilter == null) { mFilter = new LogFilter(mName); } // setup the filter mFilter.setTagMode(mTag); if (mPid != null && mPid.length() > 0) { mFilter.setPidMode(Integer.parseInt(mPid)); } else { mFilter.setPidMode(-1); } mFilter.setLogLevel(getLogLevel(mLogLevel)); } return mOk; } public LogFilter getFilter() { return mFilter; } private void createUI() { mParent = getParent(); mShell = new Shell(mParent, getStyle()); mShell.setText("Log Filter"); mShell.setLayout(new GridLayout(1, false)); mShell.addListener(SWT.Close, new Listener() { @Override public void handleEvent(Event event) { } }); // top part with the filter name Composite nameComposite = new Composite(mShell, SWT.NONE); nameComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); nameComposite.setLayout(new GridLayout(3, false)); Label l = new Label(nameComposite, SWT.NONE); l.setText("Filter Name:"); final Text filterNameText = new Text(nameComposite, SWT.SINGLE | SWT.BORDER); if (mFilter != null) { mName = mFilter.getName(); if (mName != null) { filterNameText.setText(mName); } } filterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); filterNameText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mName = filterNameText.getText().trim(); validate(); } }); mNameWarning = new Label(nameComposite, SWT.NONE); mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, mShell.getDisplay())); // separator l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // center part with the filter parameters Composite main = new Composite(mShell, SWT.NONE); main.setLayoutData(new GridData(GridData.FILL_BOTH)); main.setLayout(new GridLayout(3, false)); l = new Label(main, SWT.NONE); l.setText("by Log Tag:"); final Text tagText = new Text(main, SWT.SINGLE | SWT.BORDER); if (mFilter != null) { mTag = mFilter.getTagFilter(); if (mTag != null) { tagText.setText(mTag); } } tagText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); tagText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mTag = tagText.getText().trim(); validate(); } }); mTagWarning = new Label(main, SWT.NONE); mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, mShell.getDisplay())); l = new Label(main, SWT.NONE); l.setText("by pid:"); final Text pidText = new Text(main, SWT.SINGLE | SWT.BORDER); if (mFilter != null) { if (mFilter.getPidFilter() != -1) { mPid = Integer.toString(mFilter.getPidFilter()); } else { mPid = ""; } pidText.setText(mPid); } pidText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); pidText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { mPid = pidText.getText().trim(); validate(); } }); mPidWarning = new Label(main, SWT.NONE); mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, mShell.getDisplay())); l = new Label(main, SWT.NONE); l.setText("by Log level:"); final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY); GridData gd = new GridData(GridData.FILL_HORIZONTAL); gd.horizontalSpan = 2; logCombo.setLayoutData(gd); // add the labels logCombo.add(""); logCombo.add("Error"); logCombo.add("Warning"); logCombo.add("Info"); logCombo.add("Debug"); logCombo.add("Verbose"); if (mFilter != null) { mLogLevel = getComboIndex(mFilter.getLogLevel()); logCombo.select(mLogLevel); } else { logCombo.select(0); } logCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // get the selection mLogLevel = logCombo.getSelectionIndex(); validate(); } }); // separator l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // bottom part with the ok/cancel Composite bottomComp = new Composite(mShell, SWT.NONE); bottomComp .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); bottomComp.setLayout(new GridLayout(2, true)); mOkButton = new Button(bottomComp, SWT.NONE); mOkButton.setText("OK"); mOkButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mOk = true; mShell.close(); } }); mOkButton.setEnabled(false); mShell.setDefaultButton(mOkButton); Button cancelButton = new Button(bottomComp, SWT.NONE); cancelButton.setText("Cancel"); cancelButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mShell.close(); } }); validate(); } /** * Returns the log level from a combo index. * @param index the Combo index * @return a log level valid for the Log class. */ protected int getLogLevel(int index) { if (index == 0) { return -1; } return 7 - index; } /** * Returns the index in the combo that matches the log level * @param logLevel The Log level. * @return the combo index */ private int getComboIndex(int logLevel) { if (logLevel == -1) { return 0; } return 7 - logLevel; } /** * Validates the content of the 2 text fields and enable/disable "ok", while * setting up the warning/error message. */ private void validate() { boolean result = true; // then we check it only contains digits. if (mPid != null) { if (mPid.matches("[0-9]*") == false) { //$NON-NLS-1$ mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( IMAGE_WARNING, mShell.getDisplay())); mPidWarning.setToolTipText("PID must be a number"); //$NON-NLS-1$ result = false; } else { mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( IMAGE_EMPTY, mShell.getDisplay())); mPidWarning.setToolTipText(null); } } // then we check it not contains character | or : if (mTag != null) { if (mTag.matches(".*[:|].*") == true) { //$NON-NLS-1$ mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( IMAGE_WARNING, mShell.getDisplay())); mTagWarning.setToolTipText("Tag cannot contain | or :"); //$NON-NLS-1$ result = false; } else { mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( IMAGE_EMPTY, mShell.getDisplay())); mTagWarning.setToolTipText(null); } } // then we check it not contains character | or : if (mName != null && mName.length() > 0) { if (mName.matches(".*[:|].*") == true) { //$NON-NLS-1$ mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( IMAGE_WARNING, mShell.getDisplay())); mNameWarning.setToolTipText("Name cannot contain | or :"); //$NON-NLS-1$ result = false; } else { mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( IMAGE_EMPTY, mShell.getDisplay())); mNameWarning.setToolTipText(null); } } else { result = false; } mOkButton.setEnabled(result); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java0100644 0000000 0000000 00000002266 12747325007 030331 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.logcat.LogCatMessage; import java.util.List; /** * Listeners interested in changes in the logcat buffer should implement this interface. */ public interface ILogCatBufferChangeListener { /** * Called when the logcat buffer changes. * @param addedMessages list of messages that were added to the logcat buffer * @param deletedMessages list of messages that were removed from the logcat buffer */ void bufferChanged(List addedMessages, List deletedMessages); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java0100644 0000000 0000000 00000001634 12747325007 031242 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.logcat.LogCatMessage; /** * Classes interested in listening to user selection of logcat * messages should implement this interface. */ public interface ILogCatMessageSelectionListener { void messageDoubleClicked(LogCatMessage m); } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java0100644 0000000 0000000 00000002645 12747325007 030467 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.logcat.LogCatFilter; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.Viewer; import java.util.List; /** * A JFace content provider for logcat filter list, used in {@link LogCatPanel}. */ public final class LogCatFilterContentProvider implements IStructuredContentProvider { @Override public void dispose() { } @Override public void inputChanged(Viewer arg0, Object arg1, Object arg2) { } /** * Obtain the list of filters currently in use. * @param model list of {@link LogCatFilter}'s * @return array of {@link LogCatFilter} objects, or null. */ @Override public Object[] getElements(Object model) { return ((List) model).toArray(); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java0100644 0000000 0000000 00000004744 12747325007 026215 0ustar000000000 0000000 /* * Copyright (C) 2013 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.ddmuilib.logcat; import com.android.ddmlib.logcat.LogCatFilter; import com.android.ddmlib.logcat.LogCatMessage; import java.util.List; public class LogCatFilterData { private final LogCatFilter mFilter; /** Indicates the number of messages that match this filter, but have not * yet been read by the user. This is really metadata about this filter * necessary for the UI. If we ever end up needing to store more metadata, * then it is probably better to move it out into a separate class. */ private int mUnreadCount; /** Indicates that this filter is transient, and should not be persisted * across Eclipse sessions. */ private boolean mTransient; public LogCatFilterData(LogCatFilter f) { mFilter = f; // By default, all filters are persistent. Transient filters should explicitly // mark it so by calling setTransient. mTransient = false; } /** * Update the unread count based on new messages received. The unread count * is incremented by the count of messages in the received list that will be * accepted by this filter. * @param newMessages list of new messages. */ public void updateUnreadCount(List newMessages) { for (LogCatMessage m : newMessages) { if (mFilter.matches(m)) { mUnreadCount++; } } } /** * Reset count of unread messages. */ public void resetUnreadCount() { mUnreadCount = 0; } /** * Get current value for the unread message counter. */ public int getUnreadCount() { return mUnreadCount; } /** Make this filter transient: It will not be persisted across sessions. */ public void setTransient() { mTransient = true; } public boolean isTransient() { return mTransient; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java0100644 0000000 0000000 00000004014 12747325007 030064 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.logcat.LogCatFilter; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.graphics.Image; import java.util.Map; /** * A JFace label provider for the LogCat filters. It expects elements of type * {@link LogCatFilter}. */ public final class LogCatFilterLabelProvider extends LabelProvider implements ITableLabelProvider { private Map mFilterData; public LogCatFilterLabelProvider(Map filterData) { mFilterData = filterData; } @Override public Image getColumnImage(Object arg0, int arg1) { return null; } /** * Implements {@link ITableLabelProvider#getColumnText(Object, int)}. * @param element an instance of {@link LogCatFilter} * @param index index of the column * @return text to use in the column */ @Override public String getColumnText(Object element, int index) { if (!(element instanceof LogCatFilter)) { return null; } LogCatFilter f = (LogCatFilter) element; LogCatFilterData fd = mFilterData.get(f); if (fd != null && fd.getUnreadCount() > 0) { return String.format("%s (%d)", f.getName(), fd.getUnreadCount()); } else { return f.getName(); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java0100644 0000000 0000000 00000025101 12747325007 030252 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.Log.LogLevel; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.TitleAreaDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * Dialog used to create or edit settings for a logcat filter. */ public final class LogCatFilterSettingsDialog extends TitleAreaDialog { private static final String TITLE = "Logcat Message Filter Settings"; private static final String DEFAULT_MESSAGE = "Filter logcat messages by the source's tag, pid or minimum log level.\n" + "Empty fields will match all messages."; private String mFilterName; private String mTag; private String mText; private String mPid; private String mAppName; private String mLogLevel; private Text mFilterNameText; private Text mTagFilterText; private Text mTextFilterText; private Text mPidFilterText; private Text mAppNameFilterText; private Combo mLogLevelCombo; private Button mOkButton; /** * Construct the filter settings dialog with default values for all fields. * @param parentShell . */ public LogCatFilterSettingsDialog(Shell parentShell) { super(parentShell); setDefaults("", "", "", "", "", LogLevel.VERBOSE); } /** * Set the default values to show when the dialog is opened. * @param filterName name for the filter. * @param tag value for filter by tag * @param text value for filter by text * @param pid value for filter by pid * @param appName value for filter by app name * @param level value for filter by log level */ public void setDefaults(String filterName, String tag, String text, String pid, String appName, LogLevel level) { mFilterName = filterName; mTag = tag; mText = text; mPid = pid; mAppName = appName; mLogLevel = level.getStringValue(); } @Override protected Control createDialogArea(Composite shell) { setTitle(TITLE); setMessage(DEFAULT_MESSAGE); Composite parent = (Composite) super.createDialogArea(shell); Composite c = new Composite(parent, SWT.BORDER); c.setLayout(new GridLayout(2, false)); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createLabel(c, "Filter Name:"); mFilterNameText = new Text(c, SWT.BORDER); mFilterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mFilterNameText.setText(mFilterName); createSeparator(c); createLabel(c, "by Log Tag:"); mTagFilterText = new Text(c, SWT.BORDER); mTagFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mTagFilterText.setText(mTag); createLabel(c, "by Log Message:"); mTextFilterText = new Text(c, SWT.BORDER); mTextFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mTextFilterText.setText(mText); createLabel(c, "by PID:"); mPidFilterText = new Text(c, SWT.BORDER); mPidFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mPidFilterText.setText(mPid); createLabel(c, "by Application Name:"); mAppNameFilterText = new Text(c, SWT.BORDER); mAppNameFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mAppNameFilterText.setText(mAppName); createLabel(c, "by Log Level:"); mLogLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN); mLogLevelCombo.setItems(getLogLevels().toArray(new String[0])); mLogLevelCombo.select(getLogLevels().indexOf(mLogLevel)); /* call validateDialog() whenever user modifies any text field */ ModifyListener m = new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { DialogStatus status = validateDialog(); mOkButton.setEnabled(status.valid); setErrorMessage(status.message); } }; mFilterNameText.addModifyListener(m); mTagFilterText.addModifyListener(m); mTextFilterText.addModifyListener(m); mPidFilterText.addModifyListener(m); mAppNameFilterText.addModifyListener(m); return c; } @Override protected void createButtonsForButtonBar(Composite parent) { super.createButtonsForButtonBar(parent); mOkButton = getButton(IDialogConstants.OK_ID); DialogStatus status = validateDialog(); mOkButton.setEnabled(status.valid); } /** * A tuple that specifies whether the current state of the inputs * on the dialog is valid or not. If it is not valid, the message * field stores the reason why it isn't. */ private static final class DialogStatus { final boolean valid; final String message; private DialogStatus(boolean isValid, String errMessage) { valid = isValid; message = errMessage; } } private DialogStatus validateDialog() { /* check that there is some name for the filter */ if (mFilterNameText.getText().trim().equals("")) { return new DialogStatus(false, "Please provide a name for this filter."); } /* if a pid is provided, it should be a +ve integer */ String pidText = mPidFilterText.getText().trim(); if (pidText.trim().length() > 0) { int pid = 0; try { pid = Integer.parseInt(pidText); } catch (NumberFormatException e) { return new DialogStatus(false, "PID should be a positive integer."); } if (pid < 0) { return new DialogStatus(false, "PID should be a positive integer."); } } /* tag field must use a valid regex pattern */ String tagText = mTagFilterText.getText().trim(); if (tagText.trim().length() > 0) { try { Pattern.compile(tagText); } catch (PatternSyntaxException e) { return new DialogStatus(false, "Invalid regex used in tag field: " + e.getMessage()); } } /* text field must use a valid regex pattern */ String messageText = mTextFilterText.getText().trim(); if (messageText.trim().length() > 0) { try { Pattern.compile(messageText); } catch (PatternSyntaxException e) { return new DialogStatus(false, "Invalid regex used in text field: " + e.getMessage()); } } /* app name field must use a valid regex pattern */ String appNameText = mAppNameFilterText.getText().trim(); if (appNameText.trim().length() > 0) { try { Pattern.compile(appNameText); } catch (PatternSyntaxException e) { return new DialogStatus(false, "Invalid regex used in application name field: " + e.getMessage()); } } return new DialogStatus(true, null); } private void createSeparator(Composite c) { Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL); GridData gd = new GridData(GridData.FILL_HORIZONTAL); gd.horizontalSpan = 2; l.setLayoutData(gd); } private void createLabel(Composite c, String text) { Label l = new Label(c, SWT.NONE); l.setText(text); GridData gd = new GridData(); gd.horizontalAlignment = SWT.RIGHT; l.setLayoutData(gd); } @Override protected void okPressed() { /* save values from the widgets before the shell is closed. */ mFilterName = mFilterNameText.getText(); mTag = mTagFilterText.getText(); mText = mTextFilterText.getText(); mLogLevel = mLogLevelCombo.getText(); mPid = mPidFilterText.getText(); mAppName = mAppNameFilterText.getText(); super.okPressed(); } /** * Obtain the name for this filter. * @return user provided filter name, maybe empty. */ public String getFilterName() { return mFilterName; } /** * Obtain the tag regex to filter by. * @return user provided tag regex, maybe empty. */ public String getTag() { return mTag; } /** * Obtain the text regex to filter by. * @return user provided tag regex, maybe empty. */ public String getText() { return mText; } /** * Obtain user provided PID to filter by. * @return user provided pid, maybe empty. */ public String getPid() { return mPid; } /** * Obtain user provided application name to filter by. * @return user provided app name regex, maybe empty */ public String getAppName() { return mAppName; } /** * Obtain log level to filter by. * @return log level string. */ public String getLogLevel() { return mLogLevel; } /** * Obtain the string representation of all supported log levels. * @return an array of strings, each representing a certain log level. */ public static List getLogLevels() { List logLevels = new ArrayList(); for (LogLevel l : LogLevel.values()) { logLevels.add(l.getStringValue()); } return logLevels; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java0100644 0000000 0000000 00000017233 12747325007 031173 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.logcat.LogCatFilter; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Class to help save/restore user created filters. * * Users can create multiple filters in the logcat view. These filters could have regexes * in their settings. All of the user created filters are saved into a single Eclipse * preference. This class helps in generating the string to be saved given a list of * {@link LogCatFilter}'s, and also does the reverse of creating the list of filters * given the encoded string. */ public final class LogCatFilterSettingsSerializer { private static final char SINGLE_QUOTE = '\''; private static final char ESCAPE_CHAR = '\\'; private static final String ATTR_DELIM = ", "; private static final String KW_DELIM = ": "; private static final String KW_NAME = "name"; private static final String KW_TAG = "tag"; private static final String KW_TEXT = "text"; private static final String KW_PID = "pid"; private static final String KW_APP = "app"; private static final String KW_LOGLEVEL = "level"; /** * Encode the settings from a list of {@link LogCatFilter}'s into a string for saving to * the preference store. See * {@link LogCatFilterSettingsSerializer#decodeFromPreferenceString(String)} for the * reverse operation. * @param filters list of filters to save. * @param filterData mapping from filter to per filter UI data * @return an encoded string that can be saved in Eclipse preference store. The encoded string * is of a list of key:'value' pairs. */ public String encodeToPreferenceString(List filters, Map filterData) { StringBuffer sb = new StringBuffer(); for (LogCatFilter f : filters) { LogCatFilterData fd = filterData.get(f); if (fd != null && fd.isTransient()) { // do not persist transient filters continue; } sb.append(KW_NAME); sb.append(KW_DELIM); sb.append(quoteString(f.getName())); sb.append(ATTR_DELIM); sb.append(KW_TAG); sb.append(KW_DELIM); sb.append(quoteString(f.getTag())); sb.append(ATTR_DELIM); sb.append(KW_TEXT); sb.append(KW_DELIM); sb.append(quoteString(f.getText())); sb.append(ATTR_DELIM); sb.append(KW_PID); sb.append(KW_DELIM); sb.append(quoteString(f.getPid())); sb.append(ATTR_DELIM); sb.append(KW_APP); sb.append(KW_DELIM); sb.append(quoteString(f.getAppName())); sb.append(ATTR_DELIM); sb.append(KW_LOGLEVEL); sb.append(KW_DELIM); sb.append(quoteString(f.getLogLevel().getStringValue())); sb.append(ATTR_DELIM); } return sb.toString(); } /** * Decode an encoded string representing the settings of a list of logcat * filters into a list of {@link LogCatFilter}'s. * @param pref encoded preference string * @return a list of {@link LogCatFilter} */ public List decodeFromPreferenceString(String pref) { List fs = new ArrayList(); /* first split the string into a list of key, value pairs */ List kv = getKeyValues(pref); if (kv.size() == 0) { return fs; } /* construct filter settings from the key value pairs */ int index = 0; while (index < kv.size()) { String name = ""; String tag = ""; String pid = ""; String app = ""; String text = ""; LogLevel level = LogLevel.VERBOSE; assert kv.get(index).equals(KW_NAME); name = kv.get(index + 1); index += 2; while (index < kv.size() && !kv.get(index).equals(KW_NAME)) { String key = kv.get(index); String value = kv.get(index + 1); index += 2; if (key.equals(KW_TAG)) { tag = value; } else if (key.equals(KW_TEXT)) { text = value; } else if (key.equals(KW_PID)) { pid = value; } else if (key.equals(KW_APP)) { app = value; } else if (key.equals(KW_LOGLEVEL)) { level = LogLevel.getByString(value); } } fs.add(new LogCatFilter(name, tag, text, pid, app, level)); } return fs; } private List getKeyValues(String pref) { List kv = new ArrayList(); int index = 0; while (index < pref.length()) { String kw = getKeyword(pref.substring(index)); if (kw == null) { break; } index += kw.length() + KW_DELIM.length(); String value = getNextString(pref.substring(index)); index += value.length() + ATTR_DELIM.length(); value = unquoteString(value); kv.add(kw); kv.add(value); } return kv; } /** * Enclose a string in quotes, escaping all the quotes within the string. */ private String quoteString(String s) { return SINGLE_QUOTE + s.replace(Character.toString(SINGLE_QUOTE), "\\'") + SINGLE_QUOTE; } /** * Recover original string from its escaped version created using * {@link LogCatFilterSettingsSerializer#quoteString(String)}. */ private String unquoteString(String s) { s = s.substring(1, s.length() - 1); /* remove start and end QUOTES */ return s.replace("\\'", Character.toString(SINGLE_QUOTE)); } private String getKeyword(String pref) { int kwlen = pref.indexOf(KW_DELIM); if (kwlen == -1) { return null; } return pref.substring(0, kwlen); } /** * Get the next quoted string from the input stream of characters. */ private String getNextString(String s) { assert s.charAt(0) == SINGLE_QUOTE; StringBuffer sb = new StringBuffer(); int index = 0; while (index < s.length()) { sb.append(s.charAt(index)); if (index > 0 && s.charAt(index) == SINGLE_QUOTE // current char is a single quote && s.charAt(index - 1) != ESCAPE_CHAR) { // prev char wasn't a backslash /* break if an unescaped SINGLE QUOTE (end of string) is seen */ break; } index++; } return sb.toString(); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java0100644 0000000 0000000 00000007261 12747325007 026413 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.logcat.LogCatMessage; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; /** * Container for a list of log messages. The list of messages are * maintained in a circular buffer (FIFO). */ public final class LogCatMessageList { /** Preference key for size of the FIFO. */ public static final String MAX_MESSAGES_PREFKEY = "logcat.messagelist.max.size"; /** Default value for max # of messages. */ public static final int MAX_MESSAGES_DEFAULT = 5000; private int mFifoSize; private BlockingQueue mQ; /** * Construct an empty message list. * @param maxMessages capacity of the circular buffer */ public LogCatMessageList(int maxMessages) { mFifoSize = maxMessages; mQ = new ArrayBlockingQueue(mFifoSize); } /** * Resize the message list. * @param n new size for the list */ public synchronized void resize(int n) { mFifoSize = n; if (mFifoSize > mQ.size()) { /* if resizing to a bigger fifo, we can copy over all elements from the current mQ */ mQ = new ArrayBlockingQueue(mFifoSize, true, mQ); } else { /* for a smaller fifo, copy over the last n entries */ LogCatMessage[] curMessages = mQ.toArray(new LogCatMessage[mQ.size()]); mQ = new ArrayBlockingQueue(mFifoSize); for (int i = curMessages.length - mFifoSize; i < curMessages.length; i++) { mQ.offer(curMessages[i]); } } } /** * Append a message to the list. If the list is full, the first * message will be popped off of it. * @param m log to be inserted */ public synchronized void appendMessages(final List messages) { ensureSpace(messages.size()); for (LogCatMessage m: messages) { mQ.offer(m); } } /** * Ensure that there is sufficient space for given number of messages. * @return list of messages that were deleted to create additional space. */ public synchronized List ensureSpace(int messageCount) { List l = new ArrayList(messageCount); while (mQ.remainingCapacity() < messageCount) { l.add(mQ.poll()); } return l; } /** * Returns the number of additional elements that this queue can * ideally (in the absence of memory or resource constraints) * accept without blocking. * @return the remaining capacity */ public synchronized int remainingCapacity() { return mQ.remainingCapacity(); } /** Clear all messages in the list. */ public synchronized void clear() { mQ.clear(); } /** Obtain a copy of the message list. */ public synchronized List getAllMessages() { return new ArrayList(mQ); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java0100644 0000000 0000000 00000170077 12747325007 025240 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.DdmConstants; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.logcat.LogCatFilter; import com.android.ddmlib.logcat.LogCatMessage; import com.android.ddmuilib.AbstractBufferFindTarget; import com.android.ddmuilib.FindDialog; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.SelectionDependentPanel; import com.android.ddmuilib.TableHelper; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceConverter; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * LogCatPanel displays a table listing the logcat messages. */ public final class LogCatPanel extends SelectionDependentPanel implements ILogCatBufferChangeListener { /** Preference key to use for storing list of logcat filters. */ public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list"; /** Preference key to use for storing font settings. */ public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font"; /** Preference key to use for deciding whether to automatically en/disable scroll lock. */ public static final String AUTO_SCROLL_LOCK_PREFKEY = "logcat.view.auto-scroll-lock"; // Preference keys for message colors based on severity level private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color."; public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$ public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$ public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$ public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$ public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$ public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$ // Use a monospace font family private static final String FONT_FAMILY = DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New"; // Use the default system font size private static final FontData DEFAULT_LOGCAT_FONTDATA; static { int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight(); DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL); } private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize."; private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters"; /** Default message to show in the message search field. */ private static final String DEFAULT_SEARCH_MESSAGE = "Search for messages. Accepts Java regexes. " + "Prefix with pid:, app:, tag: or text: to limit scope."; /** Tooltip to show in the message search field. */ private static final String DEFAULT_SEARCH_TOOLTIP = "Example search patterns:\n" + " sqlite (search for sqlite in text field)\n" + " app:browser (search for messages generated by the browser application)"; private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$ private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$ private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$ private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$ private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$ private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$ private static final String IMAGE_SCROLL_LOCK = "scroll_lock.png"; //$NON-NLS-1$ private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85}; private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100}; /** Index of the default filter in the saved filters column. */ private static final int DEFAULT_FILTER_INDEX = 0; /* Text colors for the filter box */ private static final Color VALID_FILTER_REGEX_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_BLACK); private static final Color INVALID_FILTER_REGEX_COLOR = Display.getDefault().getSystemColor(SWT.COLOR_RED); private LogCatReceiver mReceiver; private IPreferenceStore mPrefStore; private List mLogCatFilters; private Map mLogCatFilterData; private int mCurrentSelectedFilterIndex; private ToolItem mNewFilterToolItem; private ToolItem mDeleteFilterToolItem; private ToolItem mEditFilterToolItem; private TableViewer mFiltersTableViewer; private Combo mLiveFilterLevelCombo; private Text mLiveFilterText; private List mCurrentFilters = Collections.emptyList(); private Table mTable; private boolean mShouldScrollToLatestLog = true; private ToolItem mScrollLockCheckBox; private boolean mAutoScrollLock; // Lock under which the vertical scroll bar listener should be added private final Object mScrollBarSelectionListenerLock = new Object(); private SelectionListener mScrollBarSelectionListener; private boolean mScrollBarListenerSet = false; private String mLogFileExportFolder; private Font mFont; private int mWrapWidthInChars; private Color mVerboseColor; private Color mDebugColor; private Color mInfoColor; private Color mWarnColor; private Color mErrorColor; private Color mAssertColor; private SashForm mSash; // messages added since last refresh, synchronized on mLogBuffer private List mLogBuffer; // # of messages deleted since last refresh, synchronized on mLogBuffer private int mDeletedLogCount; /** * Construct a logcat panel. * @param prefStore preference store where UI preferences will be saved */ public LogCatPanel(IPreferenceStore prefStore) { mPrefStore = prefStore; mLogBuffer = new ArrayList(LogCatMessageList.MAX_MESSAGES_DEFAULT); initializeFilters(); setupDefaultPreferences(); initializePreferenceUpdateListeners(); mFont = getFontFromPrefStore(); loadMessageColorPreferences(); mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY); } private void loadMessageColorPreferences() { if (mVerboseColor != null) { disposeMessageColors(); } mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY); mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY); mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY); mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY); mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY); mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY); } private void initializeFilters() { mLogCatFilters = new ArrayList(); mLogCatFilterData = new ConcurrentHashMap(); /* add default filter matching all messages */ String tag = ""; String text = ""; String pid = ""; String app = ""; LogCatFilter defaultFilter = new LogCatFilter("All messages (no filters)", tag, text, pid, app, LogLevel.VERBOSE); mLogCatFilters.add(defaultFilter); mLogCatFilterData.put(defaultFilter, new LogCatFilterData(defaultFilter)); /* restore saved filters from prefStore */ List savedFilters = getSavedFilters(); for (LogCatFilter f: savedFilters) { mLogCatFilters.add(f); mLogCatFilterData.put(f, new LogCatFilterData(f)); } } private void setupDefaultPreferences() { PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY, DEFAULT_LOGCAT_FONTDATA); mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY, LogCatMessageList.MAX_MESSAGES_DEFAULT); mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true); mPrefStore.setDefault(AUTO_SCROLL_LOCK_PREFKEY, true); /* Default Colors for different log levels. */ PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY, new RGB(0, 0, 0)); PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY, new RGB(0, 0, 127)); PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY, new RGB(0, 127, 0)); PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY, new RGB(255, 127, 0)); PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY, new RGB(255, 0, 0)); PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY, new RGB(255, 0, 0)); } private void initializePreferenceUpdateListeners() { mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { String changedProperty = event.getProperty(); if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) { if (mFont != null) { mFont.dispose(); } mFont = getFontFromPrefStore(); recomputeWrapWidth(); Display.getDefault().syncExec(new Runnable() { @Override public void run() { for (TableItem it: mTable.getItems()) { it.setFont(mFont); } } }); } else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) { loadMessageColorPreferences(); Display.getDefault().syncExec(new Runnable() { @Override public void run() { Color c = mVerboseColor; for (TableItem it: mTable.getItems()) { Object data = it.getData(); if (data instanceof LogCatMessage) { c = getForegroundColor((LogCatMessage) data); } it.setForeground(c); } } }); } else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) { mReceiver.resizeFifo(mPrefStore.getInt( LogCatMessageList.MAX_MESSAGES_PREFKEY)); reloadLogBuffer(); } else if (changedProperty.equals(AUTO_SCROLL_LOCK_PREFKEY)) { mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY); } } }); } private void saveFilterPreferences() { LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); /* save all filter settings except the first one which is the default */ String e = serializer.encodeToPreferenceString( mLogCatFilters.subList(1, mLogCatFilters.size()), mLogCatFilterData); mPrefStore.setValue(LOGCAT_FILTERS_LIST, e); } private List getSavedFilters() { LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); String e = mPrefStore.getString(LOGCAT_FILTERS_LIST); return serializer.decodeFromPreferenceString(e); } @Override public void deviceSelected() { IDevice device = getCurrentDevice(); if (device == null) { // If the device is not working properly, getCurrentDevice() could return null. // In such a case, we don't launch logcat, nor switch the display. return; } if (mReceiver != null) { // Don't need to listen to new logcat messages from previous device anymore. mReceiver.removeMessageReceivedEventListener(this); // When switching between devices, existing filter match count should be reset. for (LogCatFilter f : mLogCatFilters) { LogCatFilterData fd = mLogCatFilterData.get(f); fd.resetUnreadCount(); } } mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore); mReceiver.addMessageReceivedEventListener(this); reloadLogBuffer(); // Always scroll to last line whenever the selected device changes. // Run this in a separate async thread to give the table some time to update after the // setInput above. Display.getDefault().asyncExec(new Runnable() { @Override public void run() { scrollToLatestLog(); } }); } @Override public void clientSelected() { } @Override protected void postCreation() { } @Override protected Control createControl(Composite parent) { GridLayout layout = new GridLayout(1, false); parent.setLayout(layout); createViews(parent); setupDefaults(); return null; } private void createViews(Composite parent) { mSash = createSash(parent); createListOfFilters(mSash); createLogTableView(mSash); boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY); updateFiltersColumn(showFilters); } private SashForm createSash(Composite parent) { SashForm sash = new SashForm(parent, SWT.HORIZONTAL); sash.setLayoutData(new GridData(GridData.FILL_BOTH)); return sash; } private void createListOfFilters(SashForm sash) { Composite c = new Composite(sash, SWT.BORDER); GridLayout layout = new GridLayout(2, false); c.setLayout(layout); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createFiltersToolbar(c); createFiltersTable(c); } private void createFiltersToolbar(Composite parent) { Label l = new Label(parent, SWT.NONE); l.setText("Saved Filters"); GridData gd = new GridData(); gd.horizontalAlignment = SWT.LEFT; l.setLayoutData(gd); ToolBar t = new ToolBar(parent, SWT.FLAT); gd = new GridData(); gd.horizontalAlignment = SWT.RIGHT; t.setLayoutData(gd); /* new filter */ mNewFilterToolItem = new ToolItem(t, SWT.PUSH); mNewFilterToolItem.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay())); mNewFilterToolItem.setToolTipText("Add a new logcat filter"); mNewFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { addNewFilter(); } }); /* delete filter */ mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH); mDeleteFilterToolItem.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay())); mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter"); mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { deleteSelectedFilter(); } }); /* edit filter */ mEditFilterToolItem = new ToolItem(t, SWT.PUSH); mEditFilterToolItem.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay())); mEditFilterToolItem.setToolTipText("Edit selected logcat filter"); mEditFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { editSelectedFilter(); } }); } private void addNewFilter(String defaultTag, String defaultText, String defaultPid, String defaultAppName, LogLevel defaultLevel) { LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog( Display.getCurrent().getActiveShell()); d.setDefaults("", defaultTag, defaultText, defaultPid, defaultAppName, defaultLevel); if (d.open() != Window.OK) { return; } LogCatFilter f = new LogCatFilter(d.getFilterName().trim(), d.getTag().trim(), d.getText().trim(), d.getPid().trim(), d.getAppName().trim(), LogLevel.getByString(d.getLogLevel())); mLogCatFilters.add(f); mLogCatFilterData.put(f, new LogCatFilterData(f)); mFiltersTableViewer.refresh(); /* select the newly added entry */ int idx = mLogCatFilters.size() - 1; mFiltersTableViewer.getTable().setSelection(idx); filterSelectionChanged(); saveFilterPreferences(); } private void addNewFilter() { addNewFilter("", "", "", "", LogLevel.VERBOSE); } private void deleteSelectedFilter() { int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); if (selectedIndex <= 0) { /* return if no selected filter, or the default filter was selected (0th). */ return; } LogCatFilter f = mLogCatFilters.get(selectedIndex); mLogCatFilters.remove(selectedIndex); mLogCatFilterData.remove(f); mFiltersTableViewer.refresh(); mFiltersTableViewer.getTable().setSelection(selectedIndex - 1); filterSelectionChanged(); saveFilterPreferences(); } private void editSelectedFilter() { int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); if (selectedIndex < 0) { return; } LogCatFilter curFilter = mLogCatFilters.get(selectedIndex); LogCatFilterSettingsDialog dialog = new LogCatFilterSettingsDialog( Display.getCurrent().getActiveShell()); dialog.setDefaults(curFilter.getName(), curFilter.getTag(), curFilter.getText(), curFilter.getPid(), curFilter.getAppName(), curFilter.getLogLevel()); if (dialog.open() != Window.OK) { return; } LogCatFilter f = new LogCatFilter(dialog.getFilterName(), dialog.getTag(), dialog.getText(), dialog.getPid(), dialog.getAppName(), LogLevel.getByString(dialog.getLogLevel())); mLogCatFilters.set(selectedIndex, f); mFiltersTableViewer.refresh(); mFiltersTableViewer.getTable().setSelection(selectedIndex); filterSelectionChanged(); saveFilterPreferences(); } /** * Select the transient filter for the specified application. If no such filter * exists, then create one and then select that. This method should be called from * the UI thread. * @param appName application name to filter by */ public void selectTransientAppFilter(String appName) { assert mTable.getDisplay().getThread() == Thread.currentThread(); LogCatFilter f = findTransientAppFilter(appName); if (f == null) { f = createTransientAppFilter(appName); mLogCatFilters.add(f); LogCatFilterData fd = new LogCatFilterData(f); fd.setTransient(); mLogCatFilterData.put(f, fd); } selectFilterAt(mLogCatFilters.indexOf(f)); } private LogCatFilter findTransientAppFilter(String appName) { for (LogCatFilter f : mLogCatFilters) { LogCatFilterData fd = mLogCatFilterData.get(f); if (fd != null && fd.isTransient() && f.getAppName().equals(appName)) { return f; } } return null; } private LogCatFilter createTransientAppFilter(String appName) { LogCatFilter f = new LogCatFilter(appName + " (Session Filter)", "", "", "", appName, LogLevel.VERBOSE); return f; } private void selectFilterAt(final int index) { mFiltersTableViewer.refresh(); if (index != mFiltersTableViewer.getTable().getSelectionIndex()) { mFiltersTableViewer.getTable().setSelection(index); filterSelectionChanged(); } } private void createFiltersTable(Composite parent) { final Table table = new Table(parent, SWT.FULL_SELECTION); GridData gd = new GridData(GridData.FILL_BOTH); gd.horizontalSpan = 2; table.setLayoutData(gd); mFiltersTableViewer = new TableViewer(table); mFiltersTableViewer.setContentProvider(new LogCatFilterContentProvider()); mFiltersTableViewer.setLabelProvider(new LogCatFilterLabelProvider(mLogCatFilterData)); mFiltersTableViewer.setInput(mLogCatFilters); mFiltersTableViewer.getTable().addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { filterSelectionChanged(); } @Override public void widgetDefaultSelected(SelectionEvent arg0) { editSelectedFilter(); } }); } private void createLogTableView(SashForm sash) { Composite c = new Composite(sash, SWT.NONE); c.setLayout(new GridLayout()); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createLiveFilters(c); createLogcatViewTable(c); } /** Create the search bar at the top of the logcat messages table. */ private void createLiveFilters(Composite parent) { Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(3, false)); c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mLiveFilterText = new Text(c, SWT.BORDER | SWT.SEARCH); mLiveFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mLiveFilterText.setMessage(DEFAULT_SEARCH_MESSAGE); mLiveFilterText.setToolTipText(DEFAULT_SEARCH_TOOLTIP); mLiveFilterText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent arg0) { updateFilterTextColor(); updateAppliedFilters(); } }); mLiveFilterLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN); mLiveFilterLevelCombo.setItems( LogCatFilterSettingsDialog.getLogLevels().toArray(new String[0])); mLiveFilterLevelCombo.select(0); mLiveFilterLevelCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { updateAppliedFilters(); } }); ToolBar toolBar = new ToolBar(c, SWT.FLAT); ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH); saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE, toolBar.getDisplay())); saveToLog.setToolTipText("Export Selected Items To Text File.."); saveToLog.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { saveLogToFile(); } }); ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH); clearLog.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay())); clearLog.setToolTipText("Clear Log"); clearLog.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { if (mReceiver != null) { mReceiver.clearMessages(); refreshLogCatTable(); resetUnreadCountForAllFilters(); // the filters view is not cleared unless the filters are re-applied. updateAppliedFilters(); } } }); final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK); showFiltersColumn.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS, toolBar.getDisplay())); showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY)); showFiltersColumn.setToolTipText("Display Saved Filters View"); showFiltersColumn.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean showFilters = showFiltersColumn.getSelection(); mPrefStore.setValue(DISPLAY_FILTERS_COLUMN_PREFKEY, showFilters); updateFiltersColumn(showFilters); } }); mScrollLockCheckBox = new ToolItem(toolBar, SWT.CHECK); mScrollLockCheckBox.setImage( ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SCROLL_LOCK, toolBar.getDisplay())); mScrollLockCheckBox.setSelection(true); mScrollLockCheckBox.setToolTipText("Scroll Lock"); mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean scrollLock = mScrollLockCheckBox.getSelection(); setScrollToLatestLog(scrollLock); } }); } /** Sets the foreground color of filter text based on whether the regex is valid. */ private void updateFilterTextColor() { String text = mLiveFilterText.getText(); Color c; try { Pattern.compile(text.trim()); c = VALID_FILTER_REGEX_COLOR; } catch (PatternSyntaxException e) { c = INVALID_FILTER_REGEX_COLOR; } mLiveFilterText.setForeground(c); } private void updateFiltersColumn(boolean showFilters) { if (showFilters) { mSash.setWeights(WEIGHTS_SHOW_FILTERS); } else { mSash.setWeights(WEIGHTS_LOGCAT_ONLY); } } /** * Save logcat messages selected in the table to a file. */ private void saveLogToFile() { /* show dialog box and get target file name */ final String fName = getLogFileTargetLocation(); if (fName == null) { return; } /* obtain list of selected messages */ final List selectedMessages = getSelectedLogCatMessages(); /* save messages to file in a different (non UI) thread */ Thread t = new Thread(new Runnable() { @Override public void run() { BufferedWriter w = null; try { w = new BufferedWriter(new FileWriter(fName)); for (LogCatMessage m : selectedMessages) { w.append(m.toString()); w.newLine(); } } catch (final IOException e) { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { MessageDialog.openError(Display.getCurrent().getActiveShell(), "Unable to export selection to file.", "Unexpected error while saving selected messages to file: " + e.getMessage()); } }); } finally { if (w != null) { try { w.close(); } catch (IOException e) { // ignore } } } } }); t.setName("Saving selected items to logfile.."); t.start(); } /** * Display a {@link FileDialog} to the user and obtain the location for the log file. * @return path to target file, null if user canceled the dialog */ private String getLogFileTargetLocation() { FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE); fd.setText("Save Log.."); fd.setFileName("log.txt"); if (mLogFileExportFolder == null) { mLogFileExportFolder = System.getProperty("user.home"); } fd.setFilterPath(mLogFileExportFolder); fd.setFilterNames(new String[] { "Text Files (*.txt)" }); fd.setFilterExtensions(new String[] { "*.txt" }); String fName = fd.open(); if (fName != null) { mLogFileExportFolder = fd.getFilterPath(); /* save path to restore on future calls */ } return fName; } private List getSelectedLogCatMessages() { int[] indices = mTable.getSelectionIndices(); Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */ List selectedMessages = new ArrayList(indices.length); for (int i : indices) { Object data = mTable.getItem(i).getData(); if (data instanceof LogCatMessage) { selectedMessages.add((LogCatMessage) data); } } return selectedMessages; } private List applyCurrentFilters(List msgList) { List filteredItems = new ArrayList(msgList.size()); for (LogCatMessage msg: msgList) { if (isMessageAccepted(msg, mCurrentFilters)) { filteredItems.add(msg); } } return filteredItems; } private boolean isMessageAccepted(LogCatMessage msg, List filters) { for (LogCatFilter f : filters) { if (!f.matches(msg)) { // not accepted by this filter return false; } } // accepted by all filters return true; } private void createLogcatViewTable(Composite parent) { mTable = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI); mTable.setLayoutData(new GridData(GridData.FILL_BOTH)); mTable.getHorizontalBar().setVisible(true); /** Columns to show in the table. */ String[] properties = { "Level", "Time", "PID", "TID", "Application", "Tag", "Text", }; /** The sampleText for each column is used to determine the default widths * for each column. The contents do not matter, only their lengths are needed. */ String[] sampleText = { " ", " 00-00 00:00:00.0000 ", " 0000", " 0000", " com.android.launcher", " SampleTagText", " Log Message field should be pretty long by default. As long as possible for correct display on Mac.", }; for (int i = 0; i < properties.length; i++) { TableHelper.createTableColumn(mTable, properties[i], /* Column title */ SWT.LEFT, /* Column Style */ sampleText[i], /* String to compute default col width */ getColPreferenceKey(properties[i]), /* Preference Store key for this column */ mPrefStore); } // don't zebra stripe the table: When the buffer is full, and scroll lock is on, having // zebra striping means that the background could keep changing depending on the number // of new messages added to the bottom of the log. mTable.setLinesVisible(false); mTable.setHeaderVisible(true); // Set the row height to be sufficient enough to display the current font. // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So // we explicitly set it to be sure. mTable.addListener(SWT.MeasureItem, new Listener() { @Override public void handleEvent(Event event) { event.height = event.gc.getFontMetrics().getHeight(); } }); // Update the label provider whenever the text column's width changes TableColumn textColumn = mTable.getColumn(properties.length - 1); textColumn.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent event) { recomputeWrapWidth(); } }); addRightClickMenu(mTable); initDoubleClickListener(); recomputeWrapWidth(); mTable.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent arg0) { dispose(); } }); final ScrollBar vbar = mTable.getVerticalBar(); mScrollBarSelectionListener = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (!mAutoScrollLock) { return; } // thumb + selection < max => bar is not at the bottom. // We subtract an arbitrary amount (thumbSize/2) from this difference to allow // for cases like half a line being displayed at the end from affecting this // calculation. The thumbSize/2 number seems to work experimentally across // Linux/Mac & Windows, but might possibly need tweaking. int diff = vbar.getThumb() + vbar.getSelection() - vbar.getMaximum(); boolean isAtBottom = Math.abs(diff) < vbar.getThumb() / 2; if (isAtBottom != mShouldScrollToLatestLog) { setScrollToLatestLog(isAtBottom); mScrollLockCheckBox.setSelection(isAtBottom); } } }; startScrollBarMonitor(vbar); // Explicitly set the values to use for the scroll bar. In particular, we want these values // to have a high enough accuracy that even small movements of the scroll bar have an // effect on the selection. The auto scroll lock detection assumes that the scroll bar is // at the bottom iff selection + thumb == max. final int MAX = 10000; final int THUMB = 10; vbar.setValues(MAX - THUMB, // selection 0, // min MAX, // max THUMB, // thumb 1, // increment THUMB); // page increment } private void startScrollBarMonitor(ScrollBar vbar) { synchronized (mScrollBarSelectionListenerLock) { if (!mScrollBarListenerSet) { mScrollBarListenerSet = true; vbar.addSelectionListener(mScrollBarSelectionListener); } } } private void stopScrollBarMonitor(ScrollBar vbar) { synchronized (mScrollBarSelectionListenerLock) { if (mScrollBarListenerSet) { mScrollBarListenerSet = false; vbar.removeSelectionListener(mScrollBarSelectionListener); } } } /** Setup menu to be displayed when right clicking a log message. */ private void addRightClickMenu(final Table table) { // This action will pop up a create filter dialog pre-populated with current selection final Action filterAction = new Action("Filter similar messages...") { @Override public void run() { List selectedMessages = getSelectedLogCatMessages(); if (selectedMessages.size() == 0) { addNewFilter(); } else { LogCatMessage m = selectedMessages.get(0); addNewFilter(m.getTag(), m.getMessage(), Integer.toString(m.getPid()), m.getAppName(), m.getLogLevel()); } } }; final Action findAction = new Action("Find...") { @Override public void run() { showFindDialog(); }; }; final MenuManager mgr = new MenuManager(); mgr.add(filterAction); mgr.add(findAction); final Menu menu = mgr.createContextMenu(table); table.addListener(SWT.MenuDetect, new Listener() { @Override public void handleEvent(Event event) { Point pt = table.getDisplay().map(null, table, new Point(event.x, event.y)); Rectangle clientArea = table.getClientArea(); // The click location is in the header if it is between // clientArea.y and clientArea.y + header height boolean header = pt.y > clientArea.y && pt.y < (clientArea.y + table.getHeaderHeight()); // Show the menu only if it is not inside the header table.setMenu(header ? null : menu); } }); } public void recomputeWrapWidth() { if (mTable == null || mTable.isDisposed()) { return; } // get width of the last column (log message) TableColumn tc = mTable.getColumn(mTable.getColumnCount() - 1); int colWidth = tc.getWidth(); // get font width GC gc = new GC(tc.getParent()); gc.setFont(mFont); int avgCharWidth = gc.getFontMetrics().getAverageCharWidth(); gc.dispose(); int MIN_CHARS_PER_LINE = 50; // show atleast these many chars per line mWrapWidthInChars = Math.max(colWidth/avgCharWidth, MIN_CHARS_PER_LINE); int OFFSET_AT_END_OF_LINE = 10; // leave some space at the end of the line mWrapWidthInChars -= OFFSET_AT_END_OF_LINE; } private void setScrollToLatestLog(boolean scroll) { mShouldScrollToLatestLog = scroll; if (scroll) { scrollToLatestLog(); } } private String getColPreferenceKey(String field) { return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field; } private Font getFontFromPrefStore() { FontData fd = PreferenceConverter.getFontData(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY); return new Font(Display.getDefault(), fd); } private Color getColorFromPrefStore(String key) { RGB rgb = PreferenceConverter.getColor(mPrefStore, key); return new Color(Display.getDefault(), rgb); } private void setupDefaults() { int defaultFilterIndex = 0; mFiltersTableViewer.getTable().setSelection(defaultFilterIndex); filterSelectionChanged(); } /** * Perform all necessary updates whenever a filter is selected (by user or programmatically). */ private void filterSelectionChanged() { int idx = mFiltersTableViewer.getTable().getSelectionIndex(); if (idx == -1) { /* One of the filters should always be selected. * On Linux, there is no way to deselect an item. * On Mac, clicking inside the list view, but not an any item will result * in all items being deselected. In such a case, we simply reselect the * first entry. */ idx = 0; mFiltersTableViewer.getTable().setSelection(idx); } mCurrentSelectedFilterIndex = idx; resetUnreadCountForAllFilters(); updateFiltersToolBar(); updateAppliedFilters(); } private void resetUnreadCountForAllFilters() { for (LogCatFilterData fd: mLogCatFilterData.values()) { fd.resetUnreadCount(); } refreshFiltersTable(); } private void updateFiltersToolBar() { /* The default filter at index 0 can neither be edited, nor removed. */ boolean en = mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX; mEditFilterToolItem.setEnabled(en); mDeleteFilterToolItem.setEnabled(en); } private void updateAppliedFilters() { mCurrentFilters = getFiltersToApply(); reloadLogBuffer(); } private List getFiltersToApply() { /* list of filters to apply = saved filter + live filters */ List filters = new ArrayList(); if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) { filters.add(getSelectedSavedFilter()); } filters.addAll(getCurrentLiveFilters()); return filters; } private List getCurrentLiveFilters() { return LogCatFilter.fromString( mLiveFilterText.getText(), /* current query */ LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */ } private LogCatFilter getSelectedSavedFilter() { return mLogCatFilters.get(mCurrentSelectedFilterIndex); } @Override public void setFocus() { } @Override public void bufferChanged(List addedMessages, List deletedMessages) { updateUnreadCount(addedMessages); refreshFiltersTable(); synchronized (mLogBuffer) { addedMessages = applyCurrentFilters(addedMessages); deletedMessages = applyCurrentFilters(deletedMessages); mLogBuffer.addAll(addedMessages); mDeletedLogCount += deletedMessages.size(); } refreshLogCatTable(); } private void reloadLogBuffer() { mTable.removeAll(); synchronized (mLogBuffer) { mLogBuffer.clear(); mDeletedLogCount = 0; } if (mReceiver == null || mReceiver.getMessages() == null) { return; } List addedMessages = mReceiver.getMessages().getAllMessages(); List deletedMessages = Collections.emptyList(); bufferChanged(addedMessages, deletedMessages); } /** * When new messages are received, and they match a saved filter, update * the unread count associated with that filter. * @param receivedMessages list of new messages received */ private void updateUnreadCount(List receivedMessages) { for (int i = 0; i < mLogCatFilters.size(); i++) { if (i == mCurrentSelectedFilterIndex) { /* no need to update unread count for currently selected filter */ continue; } LogCatFilter f = mLogCatFilters.get(i); LogCatFilterData fd = mLogCatFilterData.get(f); fd.updateUnreadCount(receivedMessages); } } private void refreshFiltersTable() { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (mFiltersTableViewer.getTable().isDisposed()) { return; } mFiltersTableViewer.refresh(); } }); } /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */ private LogCatTableRefresherTask mCurrentRefresher; /** * Refresh the logcat table asynchronously from the UI thread. * This method adds a new async refresh only if there are no pending refreshes for the table. * Doing so eliminates redundant refresh threads from being queued up to be run on the * display thread. */ private void refreshLogCatTable() { synchronized (this) { if (mCurrentRefresher == null) { mCurrentRefresher = new LogCatTableRefresherTask(); Display.getDefault().asyncExec(mCurrentRefresher); } } } /** * The {@link LogCatTableRefresherTask} takes care of refreshing the table with the * new log messages that have been received. Since the log behaves like a circular buffer, * the first step is to remove items from the top of the table (if necessary). This step * is complicated by the fact that a single log message may span multiple rows if the message * was wrapped. Once the deleted items are removed, the new messages are added to the bottom * of the table. If scroll lock is enabled, the item that was original visible is made visible * again, if not, the last item is made visible. */ private class LogCatTableRefresherTask implements Runnable { @Override public void run() { if (mTable.isDisposed()) { return; } synchronized (LogCatPanel.this) { mCurrentRefresher = null; } // Current topIndex so that it can be restored if scroll locked. int topIndex = mTable.getTopIndex(); mTable.setRedraw(false); // the scroll bar should only listen to user generated scroll events, not the // scroll events that happen due to the addition of logs stopScrollBarMonitor(mTable.getVerticalBar()); // Obtain the list of new messages, and the number of deleted messages. List newMessages; int deletedMessageCount; synchronized (mLogBuffer) { newMessages = new ArrayList(mLogBuffer); mLogBuffer.clear(); deletedMessageCount = mDeletedLogCount; mDeletedLogCount = 0; mFindTarget.scrollBy(deletedMessageCount); } int originalItemCount = mTable.getItemCount(); // Remove entries from the start of the table if they were removed in the log buffer // This is complicated by the fact that a single message may span multiple TableItems // if it was word-wrapped. deletedMessageCount -= removeFromTable(mTable, deletedMessageCount); // Compute number of table items that were deleted from the table. int deletedItemCount = originalItemCount - mTable.getItemCount(); // If there are more messages to delete (after deleting messages from the table), // then delete them from the start of the newly added messages list if (deletedMessageCount > 0) { assert deletedMessageCount < newMessages.size(); for (int i = 0; i < deletedMessageCount; i++) { newMessages.remove(0); } } // Add the remaining messages to the table. for (LogCatMessage m: newMessages) { List wrappedMessageList = wrapMessage(m.getMessage(), mWrapWidthInChars); Color c = getForegroundColor(m); for (int i = 0; i < wrappedMessageList.size(); i++) { TableItem item = new TableItem(mTable, SWT.NONE); if (i == 0) { // Only set the message data in the first item. This allows code that // examines the table item data (such as copy selection) to distinguish // between real messages versus lines that are really just wrapped // content from the previous message. item.setData(m); item.setText(new String[] { Character.toString(m.getLogLevel().getPriorityLetter()), m.getTimestamp().toString(), Integer.toString(m.getPid()), Integer.toString(m.getTid()), m.getAppName(), m.getTag(), wrappedMessageList.get(i) }); } else { item.setText(new String[] { "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ wrappedMessageList.get(i) }); } item.setForeground(c); item.setFont(mFont); } } if (mShouldScrollToLatestLog) { scrollToLatestLog(); } else { // If scroll locked, show the same item that was original visible in the table. int index = Math.max(topIndex - deletedItemCount, 0); mTable.setTopIndex(index); } mTable.setRedraw(true); // re-enable listening to scroll bar events, but do so in a separate thread to make // sure that the current task (LogCatRefresherTask) has completed first Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (!mTable.isDisposed()) { startScrollBarMonitor(mTable.getVerticalBar()); } } }); } /** * Removes given number of messages from the table, starting at the top of the table. * Note that the number of messages deleted is not equal to the number of rows * deleted since a single message could span multiple rows. This method first calculates * the number of rows that correspond to the number of messages to delete, and then * removes all those rows. * @param table table from which messages should be removed * @param msgCount number of messages to be removed * @return number of messages that were actually removed */ private int removeFromTable(Table table, int msgCount) { int deletedMessageCount = 0; // # of messages that have been deleted int lastItemToDelete = 0; // index of the last item that should be deleted while (deletedMessageCount < msgCount && lastItemToDelete < table.getItemCount()) { // only rows that begin a message have their item data set TableItem item = table.getItem(lastItemToDelete); if (item.getData() != null) { deletedMessageCount++; } lastItemToDelete++; } // If there are any table items left over at the end that are wrapped over from the // previous message, mark them for deletion as well. if (lastItemToDelete < table.getItemCount() && table.getItem(lastItemToDelete).getData() == null) { lastItemToDelete++; } table.remove(0, lastItemToDelete - 1); return deletedMessageCount; } } /** Scroll to the last line. */ private void scrollToLatestLog() { if (!mTable.isDisposed()) { mTable.setTopIndex(mTable.getItemCount() - 1); } } /** * Splits the message into multiple lines if the message length exceeds given width. * If the message was split, then a wrap character \u23ce is appended to the end of all * lines but the last one. */ private List wrapMessage(String msg, int wrapWidth) { if (msg.length() < wrapWidth) { return Collections.singletonList(msg); } List wrappedMessages = new ArrayList(); int offset = 0; int len = msg.length(); while (len > 0) { int copylen = Math.min(wrapWidth, len); String s = msg.substring(offset, offset + copylen); offset += copylen; len -= copylen; if (len > 0) { // if there are more lines following, then append a wrap marker s += " \u23ce"; //$NON-NLS-1$ } wrappedMessages.add(s); } return wrappedMessages; } private Color getForegroundColor(LogCatMessage m) { LogLevel l = m.getLogLevel(); if (l.equals(LogLevel.VERBOSE)) { return mVerboseColor; } else if (l.equals(LogLevel.INFO)) { return mInfoColor; } else if (l.equals(LogLevel.DEBUG)) { return mDebugColor; } else if (l.equals(LogLevel.ERROR)) { return mErrorColor; } else if (l.equals(LogLevel.WARN)) { return mWarnColor; } else if (l.equals(LogLevel.ASSERT)) { return mAssertColor; } return mVerboseColor; } private List mMessageSelectionListeners; private void initDoubleClickListener() { mMessageSelectionListeners = new ArrayList(1); mTable.addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent arg0) { List selectedMessages = getSelectedLogCatMessages(); if (selectedMessages.size() == 0) { return; } for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) { l.messageDoubleClicked(selectedMessages.get(0)); } } }); } public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) { mMessageSelectionListeners.add(l); } private ITableFocusListener mTableFocusListener; /** * Specify the listener to be called when the logcat view gets focus. This interface is * required by DDMS to hook up the menu items for Copy and Select All. * @param listener listener to be notified when logcat view is in focus */ public void setTableFocusListener(ITableFocusListener listener) { mTableFocusListener = listener; final IFocusedTableActivator activator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { copySelectionToClipboard(clipboard); } @Override public void selectAll() { mTable.selectAll(); } }; mTable.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { mTableFocusListener.focusGained(activator); } @Override public void focusLost(FocusEvent e) { mTableFocusListener.focusLost(activator); } }); } /** Copy all selected messages to clipboard. */ public void copySelectionToClipboard(Clipboard clipboard) { StringBuilder sb = new StringBuilder(); for (LogCatMessage m : getSelectedLogCatMessages()) { sb.append(m.toString()); sb.append('\n'); } if (sb.length() > 0) { clipboard.setContents( new Object[] {sb.toString()}, new Transfer[] {TextTransfer.getInstance()} ); } } /** Select all items in the logcat table. */ public void selectAll() { mTable.selectAll(); } private void dispose() { if (mFont != null && !mFont.isDisposed()) { mFont.dispose(); } if (mVerboseColor != null && !mVerboseColor.isDisposed()) { disposeMessageColors(); } } private void disposeMessageColors() { mVerboseColor.dispose(); mDebugColor.dispose(); mInfoColor.dispose(); mWarnColor.dispose(); mErrorColor.dispose(); mAssertColor.dispose(); } private class LogcatFindTarget extends AbstractBufferFindTarget { @Override public void selectAndReveal(int index) { mTable.deselectAll(); mTable.select(index); mTable.showSelection(); } @Override public int getItemCount() { return mTable.getItemCount(); } @Override public String getItem(int index) { Object data = mTable.getItem(index).getData(); if (data != null) { return data.toString(); } return null; } @Override public int getStartingIndex() { // start searches from current selection if present, otherwise from the tail end // of the buffer int s = mTable.getSelectionIndex(); if (s != -1) { return s; } else { return getItemCount() - 1; } }; }; private FindDialog mFindDialog; private LogcatFindTarget mFindTarget = new LogcatFindTarget(); public void showFindDialog() { if (mFindDialog != null) { // if the dialog is already displayed return; } mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), mFindTarget); mFindDialog.open(); // blocks until find dialog is closed mFindDialog = null; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java0100644 0000000 0000000 00000011717 12747325007 025740 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.logcat.LogCatListener; import com.android.ddmlib.logcat.LogCatMessage; import com.android.ddmlib.logcat.LogCatReceiverTask; import org.eclipse.jface.preference.IPreferenceStore; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A class to monitor a device for logcat messages. It stores the received * log messages in a circular buffer. */ public final class LogCatReceiver implements LogCatListener { private static LogCatMessage DEVICE_DISCONNECTED_MESSAGE = new LogCatMessage(LogLevel.ERROR, "Device disconnected"); private LogCatMessageList mLogMessages; private IDevice mCurrentDevice; private LogCatReceiverTask mLogCatReceiverTask; private Set mLogCatMessageListeners; private IPreferenceStore mPrefStore; /** * Construct a LogCat message receiver for provided device. This will launch a * logcat command on the device, and monitor the output of that command in * a separate thread. All logcat messages are then stored in a circular * buffer, which can be retrieved using {@link LogCatReceiver#getMessages()}. * @param device device to monitor for logcat messages * @param prefStore */ public LogCatReceiver(IDevice device, IPreferenceStore prefStore) { mCurrentDevice = device; mPrefStore = prefStore; mLogCatMessageListeners = new HashSet(); mLogMessages = new LogCatMessageList(getFifoSize()); startReceiverThread(); } /** * Stop receiving messages from currently active device. */ public void stop() { if (mLogCatReceiverTask != null) { /* stop the current logcat command */ mLogCatReceiverTask.removeLogCatListener(this); mLogCatReceiverTask.stop(); mLogCatReceiverTask = null; // add a message to the log indicating that the device has been disconnected. log(Collections.singletonList(DEVICE_DISCONNECTED_MESSAGE)); } mCurrentDevice = null; } private int getFifoSize() { int n = mPrefStore.getInt(LogCatMessageList.MAX_MESSAGES_PREFKEY); return n == 0 ? LogCatMessageList.MAX_MESSAGES_DEFAULT : n; } private void startReceiverThread() { if (mCurrentDevice == null) { return; } mLogCatReceiverTask = new LogCatReceiverTask(mCurrentDevice); mLogCatReceiverTask.addLogCatListener(this); Thread t = new Thread(mLogCatReceiverTask); t.setName("LogCat output receiver for " + mCurrentDevice.getSerialNumber()); t.start(); } @Override public void log(List newMessages) { List deletedMessages; synchronized (mLogMessages) { deletedMessages = mLogMessages.ensureSpace(newMessages.size()); mLogMessages.appendMessages(newMessages); } sendLogChangedEvent(newMessages, deletedMessages); } /** * Get the list of logcat messages received from currently active device. * @return list of messages if currently listening, null otherwise */ public LogCatMessageList getMessages() { return mLogMessages; } /** * Clear the list of messages received from the currently active device. */ public void clearMessages() { mLogMessages.clear(); } /** * Add to list of message event listeners. * @param l listener to notified when messages are received from the device */ public void addMessageReceivedEventListener(ILogCatBufferChangeListener l) { mLogCatMessageListeners.add(l); } public void removeMessageReceivedEventListener(ILogCatBufferChangeListener l) { mLogCatMessageListeners.remove(l); } private void sendLogChangedEvent(List addedMessages, List deletedMessages) { for (ILogCatBufferChangeListener l : mLogCatMessageListeners) { l.bufferChanged(addedMessages, deletedMessages); } } /** * Resize the internal FIFO. * @param size new size */ public void resizeFifo(int size) { mLogMessages.resize(size); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java0100644 0000000 0000000 00000007456 12747325007 027275 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.IDevice; import org.eclipse.jface.preference.IPreferenceStore; import java.util.HashMap; import java.util.Map; /** * A factory for {@link LogCatReceiver} objects. Its primary objective is to cache * constructed {@link LogCatReceiver}'s per device and hand them back when requested. */ public class LogCatReceiverFactory { /** Singleton instance. */ public static final LogCatReceiverFactory INSTANCE = new LogCatReceiverFactory(); private Map mReceiverCache = new HashMap(); /** Private constructor: cannot instantiate. */ private LogCatReceiverFactory() { AndroidDebugBridge.addDeviceChangeListener(new IDeviceChangeListener() { @Override public void deviceDisconnected(final IDevice device) { // The deviceDisconnected() is called from DDMS code that holds // multiple locks regarding list of clients, etc. // It so happens that #newReceiver() below adds a clientChangeListener // which requires those locks as well. So if we call // #removeReceiverFor from a DDMS/Monitor thread, we could end up // in a deadlock. As a result, we spawn a separate thread that // doesn't hold any of the DDMS locks to remove the receiver. Thread t = new Thread(new Runnable() { @Override public void run() { removeReceiverFor(device); } }, "Remove logcat receiver for " + device.getSerialNumber()); t.start(); } @Override public void deviceConnected(IDevice device) { } @Override public void deviceChanged(IDevice device, int changeMask) { } }); } /** * Remove existing logcat receivers. This method should not be called from a DDMS thread * context that might be holding locks. Doing so could result in a deadlock with the following * two threads locked up:

    *
  • {@link #removeReceiverFor(IDevice)} waiting to lock {@link LogCatReceiverFactory}, * while holding a DDMS monitor internal lock.
  • *
  • {@link #newReceiver(IDevice, IPreferenceStore)} holding {@link LogCatReceiverFactory} * while attempting to obtain a DDMS monitor lock.
  • *
*/ private synchronized void removeReceiverFor(IDevice device) { LogCatReceiver r = mReceiverCache.get(device.getSerialNumber()); if (r != null) { r.stop(); mReceiverCache.remove(device.getSerialNumber()); } } public synchronized LogCatReceiver newReceiver(IDevice device, IPreferenceStore prefs) { LogCatReceiver r = mReceiverCache.get(device.getSerialNumber()); if (r != null) { return r; } r = new LogCatReceiver(device, prefs); mReceiverCache.put(device.getSerialNumber(), r); return r; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java0100644 0000000 0000000 00000005466 12747325007 027401 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Helper class that can determine if a string matches the exception * stack trace pattern, and if so, can provide the java source file * and line where the exception occured. */ public final class LogCatStackTraceParser { /** Regex to match a stack trace line. E.g.: * at com.foo.Class.method(FileName.extension:10) * extension is typically java, but can be anything (java/groovy/scala/..). */ private static final String EXCEPTION_LINE_REGEX = "\\s*at\\ (.*)\\((.*)\\..*\\:(\\d+)\\)"; //$NON-NLS-1$ private static final Pattern EXCEPTION_LINE_PATTERN = Pattern.compile(EXCEPTION_LINE_REGEX); /** * Identify if a input line matches the expected pattern * for a stack trace from an exception. */ public boolean isValidExceptionTrace(String line) { return EXCEPTION_LINE_PATTERN.matcher(line).find(); } /** * Get fully qualified method name that threw the exception. * @param line line from the stack trace, must have been validated with * {@link LogCatStackTraceParser#isValidExceptionTrace(String)} before calling this method. * @return fully qualified method name */ public String getMethodName(String line) { Matcher m = EXCEPTION_LINE_PATTERN.matcher(line); m.find(); return m.group(1); } /** * Get source file name where exception was generated. Input line must be first validated with * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}. */ public String getFileName(String line) { Matcher m = EXCEPTION_LINE_PATTERN.matcher(line); m.find(); return m.group(2); } /** * Get line number where exception was generated. Input line must be first validated with * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}. */ public int getLineNumber(String line) { Matcher m = EXCEPTION_LINE_PATTERN.matcher(line); m.find(); try { return Integer.parseInt(m.group(3)); } catch (NumberFormatException e) { return 0; } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java0100644 0000000 0000000 00000001552 12747325007 025001 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import org.eclipse.swt.graphics.Color; public class LogColors { public Color infoColor; public Color debugColor; public Color errorColor; public Color warningColor; public Color verboseColor; } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java0100644 0000000 0000000 00000036546 12747325007 025000 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.ddmuilib.annotation.UiThread; import com.android.ddmuilib.logcat.LogPanel.LogMessage; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import java.util.ArrayList; import java.util.regex.PatternSyntaxException; /** logcat output filter class */ public class LogFilter { public final static int MODE_PID = 0x01; public final static int MODE_TAG = 0x02; public final static int MODE_LEVEL = 0x04; private String mName; /** * Filtering mode. Value can be a mix of MODE_PID, MODE_TAG, MODE_LEVEL */ private int mMode = 0; /** * pid used for filtering. Only valid if mMode is MODE_PID. */ private int mPid; /** Single level log level as defined in Log.mLevelChar. Only valid * if mMode is MODE_LEVEL */ private int mLogLevel; /** * log tag filtering. Only valid if mMode is MODE_TAG */ private String mTag; private Table mTable; private TabItem mTabItem; private boolean mIsCurrentTabItem = false; private int mUnreadCount = 0; /** Temp keyword filtering */ private String[] mTempKeywordFilters; /** temp pid filtering */ private int mTempPid = -1; /** temp tag filtering */ private String mTempTag; /** temp log level filtering */ private int mTempLogLevel = -1; private LogColors mColors; private boolean mTempFilteringStatus = false; private final ArrayList mMessages = new ArrayList(); private final ArrayList mNewMessages = new ArrayList(); private boolean mSupportsDelete = true; private boolean mSupportsEdit = true; private int mRemovedMessageCount = 0; /** * Creates a filter with a particular mode. * @param name The name to be displayed in the UI */ public LogFilter(String name) { mName = name; } public LogFilter() { } @Override public String toString() { StringBuilder sb = new StringBuilder(mName); sb.append(':'); sb.append(mMode); if ((mMode & MODE_PID) == MODE_PID) { sb.append(':'); sb.append(mPid); } if ((mMode & MODE_LEVEL) == MODE_LEVEL) { sb.append(':'); sb.append(mLogLevel); } if ((mMode & MODE_TAG) == MODE_TAG) { sb.append(':'); sb.append(mTag); } return sb.toString(); } public boolean loadFromString(String string) { String[] segments = string.split(":"); //$NON-NLS-1$ int index = 0; // get the name mName = segments[index++]; // get the mode mMode = Integer.parseInt(segments[index++]); if ((mMode & MODE_PID) == MODE_PID) { mPid = Integer.parseInt(segments[index++]); } if ((mMode & MODE_LEVEL) == MODE_LEVEL) { mLogLevel = Integer.parseInt(segments[index++]); } if ((mMode & MODE_TAG) == MODE_TAG) { mTag = segments[index++]; } return true; } /** Sets the name of the filter. */ void setName(String name) { mName = name; } /** * Returns the UI display name. */ public String getName() { return mName; } /** * Set the Table ui widget associated with this filter. * @param tabItem The item in the TabFolder * @param table The Table object */ public void setWidgets(TabItem tabItem, Table table) { mTable = table; mTabItem = tabItem; } /** * Returns true if the filter is ready for ui. */ public boolean uiReady() { return (mTable != null && mTabItem != null); } /** * Returns the UI table object. * @return */ public Table getTable() { return mTable; } public void dispose() { mTable.dispose(); mTabItem.dispose(); mTable = null; mTabItem = null; } /** * Resets the filtering mode to be 0 (i.e. no filter). */ public void resetFilteringMode() { mMode = 0; } /** * Returns the current filtering mode. * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL */ public int getFilteringMode() { return mMode; } /** * Adds PID to the current filtering mode. * @param pid */ public void setPidMode(int pid) { if (pid != -1) { mMode |= MODE_PID; } else { mMode &= ~MODE_PID; } mPid = pid; } /** Returns the pid filter if valid, otherwise -1 */ public int getPidFilter() { if ((mMode & MODE_PID) == MODE_PID) return mPid; return -1; } public void setTagMode(String tag) { if (tag != null && tag.length() > 0) { mMode |= MODE_TAG; } else { mMode &= ~MODE_TAG; } mTag = tag; } public String getTagFilter() { if ((mMode & MODE_TAG) == MODE_TAG) return mTag; return null; } public void setLogLevel(int level) { if (level == -1) { mMode &= ~MODE_LEVEL; } else { mMode |= MODE_LEVEL; mLogLevel = level; } } public int getLogLevel() { if ((mMode & MODE_LEVEL) == MODE_LEVEL) { return mLogLevel; } return -1; } public boolean supportsDelete() { return mSupportsDelete ; } public boolean supportsEdit() { return mSupportsEdit; } /** * Sets the selected state of the filter. * @param selected selection state. */ public void setSelectedState(boolean selected) { if (selected) { if (mTabItem != null) { mTabItem.setText(mName); } mUnreadCount = 0; } mIsCurrentTabItem = selected; } /** * Adds a new message and optionally removes an old message. *

The new message is filtered through {@link #accept(LogMessage)}. * Calls to {@link #flush()} from a UI thread will display it (and other * pending messages) to the associated {@link Table}. * @param logMessage the MessageData object to filter * @return true if the message was accepted. */ public boolean addMessage(LogMessage newMessage, LogMessage oldMessage) { synchronized (mMessages) { if (oldMessage != null) { int index = mMessages.indexOf(oldMessage); if (index != -1) { // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. mMessages.remove(index); mRemovedMessageCount++; } // now we look for it in mNewMessages. This can happen if the new message is added // and then removed because too many messages are added between calls to #flush() index = mNewMessages.indexOf(oldMessage); if (index != -1) { // TODO check that index will always be -1 or 0, as only the oldest message is ever removed. mNewMessages.remove(index); } } boolean filter = accept(newMessage); if (filter) { // at this point the message is accepted, we add it to the list mMessages.add(newMessage); mNewMessages.add(newMessage); } return filter; } } /** * Removes all the items in the filter and its {@link Table}. */ public void clear() { mRemovedMessageCount = 0; mNewMessages.clear(); mMessages.clear(); mTable.removeAll(); } /** * Filters a message. * @param logMessage the Message * @return true if the message is accepted by the filter. */ boolean accept(LogMessage logMessage) { // do the regular filtering now if ((mMode & MODE_PID) == MODE_PID && mPid != logMessage.data.pid) { return false; } if ((mMode & MODE_TAG) == MODE_TAG && ( logMessage.data.tag == null || logMessage.data.tag.equals(mTag) == false)) { return false; } int msgLogLevel = logMessage.data.logLevel.getPriority(); // test the temp log filtering first, as it replaces the old one if (mTempLogLevel != -1) { if (mTempLogLevel > msgLogLevel) { return false; } } else if ((mMode & MODE_LEVEL) == MODE_LEVEL && mLogLevel > msgLogLevel) { return false; } // do the temp filtering now. if (mTempKeywordFilters != null) { String msg = logMessage.msg; for (String kw : mTempKeywordFilters) { try { if (msg.contains(kw) == false && msg.matches(kw) == false) { return false; } } catch (PatternSyntaxException e) { // if the string is not a valid regular expression, // this exception is thrown. return false; } } } if (mTempPid != -1 && mTempPid != logMessage.data.pid) { return false; } if (mTempTag != null && mTempTag.length() > 0) { if (mTempTag.equals(logMessage.data.tag) == false) { return false; } } return true; } /** * Takes all the accepted messages and display them. * This must be called from a UI thread. */ @UiThread public void flush() { // if scroll bar is at the bottom, we will scroll ScrollBar bar = mTable.getVerticalBar(); boolean scroll = bar.getMaximum() == bar.getSelection() + bar.getThumb(); // if we are not going to scroll, get the current first item being shown. int topIndex = mTable.getTopIndex(); // disable drawing mTable.setRedraw(false); int totalCount = mNewMessages.size(); try { // remove the items of the old messages. for (int i = 0 ; i < mRemovedMessageCount && mTable.getItemCount() > 0 ; i++) { mTable.remove(0); } mRemovedMessageCount = 0; if (mUnreadCount > mTable.getItemCount()) { mUnreadCount = mTable.getItemCount(); } // add the new items for (int i = 0 ; i < totalCount ; i++) { LogMessage msg = mNewMessages.get(i); addTableItem(msg); } } catch (SWTException e) { // log the error and keep going. Content of the logcat table maybe unexpected // but at least ddms won't crash. Log.e("LogFilter", e); } // redraw mTable.setRedraw(true); // scroll if needed, by showing the last item if (scroll) { totalCount = mTable.getItemCount(); if (totalCount > 0) { mTable.showItem(mTable.getItem(totalCount-1)); } } else if (mRemovedMessageCount > 0) { // we need to make sure the topIndex is still visible. // Because really old items are removed from the list, this could make it disappear // if we don't change the scroll value at all. topIndex -= mRemovedMessageCount; if (topIndex < 0) { // looks like it disappeared. Lets just show the first item mTable.showItem(mTable.getItem(0)); } else { mTable.showItem(mTable.getItem(topIndex)); } } // if this filter is not the current one, we update the tab text // with the amount of unread message if (mIsCurrentTabItem == false) { mUnreadCount += mNewMessages.size(); totalCount = mTable.getItemCount(); if (mUnreadCount > 0) { mTabItem.setText(mName + " (" //$NON-NLS-1$ + (mUnreadCount > totalCount ? totalCount : mUnreadCount) + ")"); //$NON-NLS-1$ } else { mTabItem.setText(mName); //$NON-NLS-1$ } } mNewMessages.clear(); } void setColors(LogColors colors) { mColors = colors; } int getUnreadCount() { return mUnreadCount; } void setUnreadCount(int unreadCount) { mUnreadCount = unreadCount; } void setSupportsDelete(boolean support) { mSupportsDelete = support; } void setSupportsEdit(boolean support) { mSupportsEdit = support; } void setTempKeywordFiltering(String[] segments) { mTempKeywordFilters = segments; mTempFilteringStatus = true; } void setTempPidFiltering(int pid) { mTempPid = pid; mTempFilteringStatus = true; } void setTempTagFiltering(String tag) { mTempTag = tag; mTempFilteringStatus = true; } void resetTempFiltering() { if (mTempPid != -1 || mTempTag != null || mTempKeywordFilters != null) { mTempFilteringStatus = true; } mTempPid = -1; mTempTag = null; mTempKeywordFilters = null; } void resetTempFilteringStatus() { mTempFilteringStatus = false; } boolean getTempFilterStatus() { return mTempFilteringStatus; } /** * Add a TableItem for the index-th item of the buffer * @param filter The index of the table in which to insert the item. */ private void addTableItem(LogMessage msg) { TableItem item = new TableItem(mTable, SWT.NONE); item.setText(0, msg.data.time); item.setText(1, new String(new char[] { msg.data.logLevel.getPriorityLetter() })); item.setText(2, msg.data.pidString); item.setText(3, msg.data.tag); item.setText(4, msg.msg); // add the buffer index as data item.setData(msg); if (msg.data.logLevel == LogLevel.INFO) { item.setForeground(mColors.infoColor); } else if (msg.data.logLevel == LogLevel.DEBUG) { item.setForeground(mColors.debugColor); } else if (msg.data.logLevel == LogLevel.ERROR) { item.setForeground(mColors.errorColor); } else if (msg.data.logLevel == LogLevel.WARN) { item.setForeground(mColors.warningColor); } else { item.setForeground(mColors.verboseColor); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java0100644 0000000 0000000 00000151621 12747325007 024602 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; import com.android.ddmuilib.SelectionDependentPanel; import com.android.ddmuilib.TableHelper; import com.android.ddmuilib.actions.ICommonAction; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.TabFolder; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; public class LogPanel extends SelectionDependentPanel { private static final int STRING_BUFFER_LENGTH = 10000; /** no filtering. Only one tab with everything. */ public static final int FILTER_NONE = 0; /** manual mode for filter. all filters are manually created. */ public static final int FILTER_MANUAL = 1; /** automatic mode for filter (pid mode). * All filters are automatically created. */ public static final int FILTER_AUTO_PID = 2; /** automatic mode for filter (tag mode). * All filters are automatically created. */ public static final int FILTER_AUTO_TAG = 3; /** Manual filtering mode + new filter for debug app, if needed */ public static final int FILTER_DEBUG = 4; public static final int COLUMN_MODE_MANUAL = 0; public static final int COLUMN_MODE_AUTO = 1; public static String PREFS_TIME; public static String PREFS_LEVEL; public static String PREFS_PID; public static String PREFS_TAG; public static String PREFS_MESSAGE; /** * This pattern is meant to parse the first line of a log message with the option * 'logcat -v long'. The first line represents the date, tag, severity, etc.. while the * following lines are the message (can be several line).
* This first line looks something like
* "[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]" *
* Note: severity is one of V, D, I, W, or EM
* Note: the fraction of second value can have any number of digit. * Note the tag should be trim as it may have spaces at the end. */ private static Pattern sLogPattern = Pattern.compile( "^\\[\\s(\\d\\d-\\d\\d\\s\\d\\d:\\d\\d:\\d\\d\\.\\d+)" + //$NON-NLS-1$ "\\s+(\\d*):(0x[0-9a-fA-F]+)\\s([VDIWE])/(.*)\\]$"); //$NON-NLS-1$ /** * Interface for Storage Filter manager. Implementation of this interface * provide a custom way to archive an reload filters. */ public interface ILogFilterStorageManager { public LogFilter[] getFilterFromStore(); public void saveFilters(LogFilter[] filters); public boolean requiresDefaultFilter(); } private Composite mParent; private IPreferenceStore mStore; /** top object in the view */ private TabFolder mFolders; private LogColors mColors; private ILogFilterStorageManager mFilterStorage; private LogCatOuputReceiver mCurrentLogCat; /** * Circular buffer containing the logcat output. This is unfiltered. * The valid content goes from mBufferStart to * mBufferEnd - 1. Therefore its number of item is * mBufferEnd - mBufferStart. */ private LogMessage[] mBuffer = new LogMessage[STRING_BUFFER_LENGTH]; /** Represents the oldest message in the buffer */ private int mBufferStart = -1; /** * Represents the next usable item in the buffer to receive new message. * This can be equal to mBufferStart, but when used mBufferStart will be * incremented as well. */ private int mBufferEnd = -1; /** Filter list */ private LogFilter[] mFilters; /** Default filter */ private LogFilter mDefaultFilter; /** Current filter being displayed */ private LogFilter mCurrentFilter; /** Filtering mode */ private int mFilterMode = FILTER_NONE; /** Device currently running logcat */ private IDevice mCurrentLoggedDevice = null; private ICommonAction mDeleteFilterAction; private ICommonAction mEditFilterAction; private ICommonAction[] mLogLevelActions; /** message data, separated from content for multi line messages */ protected static class LogMessageInfo { public LogLevel logLevel; public int pid; public String pidString; public String tag; public String time; } /** pointer to the latest LogMessageInfo. this is used for multi line * log message, to reuse the info regarding level, pid, etc... */ private LogMessageInfo mLastMessageInfo = null; private boolean mPendingAsyncRefresh = false; private String mDefaultLogSave; private int mColumnMode = COLUMN_MODE_MANUAL; private Font mDisplayFont; private ITableFocusListener mGlobalListener; private LogCatViewInterface mLogCatViewInterface = null; /** message data, separated from content for multi line messages */ protected static class LogMessage { public LogMessageInfo data; public String msg; @Override public String toString() { return data.time + ": " //$NON-NLS-1$ + data.logLevel + "/" //$NON-NLS-1$ + data.tag + "(" //$NON-NLS-1$ + data.pidString + "): " //$NON-NLS-1$ + msg; } } /** * objects able to receive the output of a remote shell command, * specifically a logcat command in this case */ private final class LogCatOuputReceiver extends MultiLineReceiver { public boolean isCancelled = false; public LogCatOuputReceiver() { super(); setTrimLine(false); } @Override public void processNewLines(String[] lines) { if (isCancelled == false) { processLogLines(lines); } } @Override public boolean isCancelled() { return isCancelled; } } /** * Parser class for the output of a "ps" shell command executed on a device. * This class looks for a specific pid to find the process name from it. * Once found, the name is used to update a filter and a tab object * */ private class PsOutputReceiver extends MultiLineReceiver { private LogFilter mFilter; private TabItem mTabItem; private int mPid; /** set to true when we've found the pid we're looking for */ private boolean mDone = false; PsOutputReceiver(int pid, LogFilter filter, TabItem tabItem) { mPid = pid; mFilter = filter; mTabItem = tabItem; } @Override public boolean isCancelled() { return mDone; } @Override public void processNewLines(String[] lines) { for (String line : lines) { if (line.startsWith("USER")) { //$NON-NLS-1$ continue; } // get the pid. int index = line.indexOf(' '); if (index == -1) { continue; } // look for the next non blank char index++; while (line.charAt(index) == ' ') { index++; } // this is the start of the pid. // look for the end. int index2 = line.indexOf(' ', index); // get the line String pidStr = line.substring(index, index2); int pid = Integer.parseInt(pidStr); if (pid != mPid) { continue; } else { // get the process name index = line.lastIndexOf(' '); final String name = line.substring(index + 1); mFilter.setName(name); // update the tab Display d = mFolders.getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { mTabItem.setText(name); } }); // we're done with this ps. mDone = true; return; } } } } /** * Interface implemented by the LogCatView in Eclipse for particular action on double-click. */ public interface LogCatViewInterface { public void onDoubleClick(); } /** * Create the log view with some default parameters * @param colors The display color object * @param filterStorage the storage for user defined filters. * @param mode The filtering mode */ public LogPanel(LogColors colors, ILogFilterStorageManager filterStorage, int mode) { mColors = colors; mFilterMode = mode; mFilterStorage = filterStorage; mStore = DdmUiPreferences.getStore(); } public void setActions(ICommonAction deleteAction, ICommonAction editAction, ICommonAction[] logLevelActions) { mDeleteFilterAction = deleteAction; mEditFilterAction = editAction; mLogLevelActions = logLevelActions; } /** * Sets the column mode. Must be called before creatUI * @param mode the column mode. Valid values are COLUMN_MOD_MANUAL and * COLUMN_MODE_AUTO */ public void setColumnMode(int mode) { mColumnMode = mode; } /** * Sets the display font. * @param font The display font. */ public void setFont(Font font) { mDisplayFont = font; if (mFilters != null) { for (LogFilter f : mFilters) { Table table = f.getTable(); if (table != null) { table.setFont(font); } } } if (mDefaultFilter != null) { Table table = mDefaultFilter.getTable(); if (table != null) { table.setFont(font); } } } /** * Sent when a new device is selected. The new device can be accessed * with {@link #getCurrentDevice()}. */ @Override public void deviceSelected() { startLogCat(getCurrentDevice()); } /** * Sent when a new client is selected. The new client can be accessed * with {@link #getCurrentClient()}. */ @Override public void clientSelected() { // pass } /** * Creates a control capable of displaying some information. This is * called once, when the application is initializing, from the UI thread. */ @Override protected Control createControl(Composite parent) { mParent = parent; Composite top = new Composite(parent, SWT.NONE); top.setLayoutData(new GridData(GridData.FILL_BOTH)); top.setLayout(new GridLayout(1, false)); // create the tab folder mFolders = new TabFolder(top, SWT.NONE); mFolders.setLayoutData(new GridData(GridData.FILL_BOTH)); mFolders.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (mCurrentFilter != null) { mCurrentFilter.setSelectedState(false); } mCurrentFilter = getCurrentFilter(); mCurrentFilter.setSelectedState(true); updateColumns(mCurrentFilter.getTable()); if (mCurrentFilter.getTempFilterStatus()) { initFilter(mCurrentFilter); } selectionChanged(mCurrentFilter); } }); Composite bottom = new Composite(top, SWT.NONE); bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); bottom.setLayout(new GridLayout(3, false)); Label label = new Label(bottom, SWT.NONE); label.setText("Filter:"); final Text filterText = new Text(bottom, SWT.SINGLE | SWT.BORDER); filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); filterText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { updateFilteringWith(filterText.getText()); } }); /* Button addFilterBtn = new Button(bottom, SWT.NONE); addFilterBtn.setImage(mImageLoader.loadImage("add.png", //$NON-NLS-1$ addFilterBtn.getDisplay())); */ // get the filters createFilters(); // for each filter, create a tab. int index = 0; if (mDefaultFilter != null) { createTab(mDefaultFilter, index++, false); } if (mFilters != null) { for (LogFilter f : mFilters) { createTab(f, index++, false); } } return top; } @Override protected void postCreation() { // pass } /** * Sets the focus to the proper object. */ @Override public void setFocus() { mFolders.setFocus(); } /** * Starts a new logcat and set mCurrentLogCat as the current receiver. * @param device the device to connect logcat to. */ public void startLogCat(final IDevice device) { if (device == mCurrentLoggedDevice) { return; } // if we have a logcat already running if (mCurrentLoggedDevice != null) { stopLogCat(false); mCurrentLoggedDevice = null; } resetUI(false); if (device != null) { // create a new output receiver mCurrentLogCat = new LogCatOuputReceiver(); // start the logcat in a different thread new Thread("Logcat") { //$NON-NLS-1$ @Override public void run() { while (device.isOnline() == false && mCurrentLogCat != null && mCurrentLogCat.isCancelled == false) { try { sleep(2000); } catch (InterruptedException e) { return; } } if (mCurrentLogCat == null || mCurrentLogCat.isCancelled) { // logcat was stopped/cancelled before the device became ready. return; } try { mCurrentLoggedDevice = device; device.executeShellCommand("logcat -v long", mCurrentLogCat, 0 /*timeout*/); //$NON-NLS-1$ } catch (Exception e) { Log.e("Logcat", e); } finally { // at this point the command is terminated. mCurrentLogCat = null; mCurrentLoggedDevice = null; } } }.start(); } } /** Stop the current logcat */ public void stopLogCat(boolean inUiThread) { if (mCurrentLogCat != null) { mCurrentLogCat.isCancelled = true; // when the thread finishes, no one will reference that object // and it'll be destroyed mCurrentLogCat = null; // reset the content buffer for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) { mBuffer[i] = null; } // because it's a circular buffer, it's hard to know if // the array is empty with both start/end at 0 or if it's full // with both start/end at 0 as well. So to mean empty, we use -1 mBufferStart = -1; mBufferEnd = -1; resetFilters(); resetUI(inUiThread); } } /** * Adds a new Filter. This methods displays the UI to create the filter * and set up its parameters.
* MUST be called from the ui thread. * */ public void addFilter() { EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell()); if (dlg.open()) { synchronized (mBuffer) { // get the new filter in the array LogFilter filter = dlg.getFilter(); addFilterToArray(filter); int index = mFilters.length - 1; if (mDefaultFilter != null) { index++; } if (false) { for (LogFilter f : mFilters) { if (f.uiReady()) { f.dispose(); } } if (mDefaultFilter != null && mDefaultFilter.uiReady()) { mDefaultFilter.dispose(); } // for each filter, create a tab. int i = 0; if (mFilters != null) { for (LogFilter f : mFilters) { createTab(f, i++, true); } } if (mDefaultFilter != null) { createTab(mDefaultFilter, i++, true); } } else { // create ui for the filter. createTab(filter, index, true); // reset the default as it shouldn't contain the content of // this new filter. if (mDefaultFilter != null) { initDefaultFilter(); } } // select the new filter if (mCurrentFilter != null) { mCurrentFilter.setSelectedState(false); } mFolders.setSelection(index); filter.setSelectedState(true); mCurrentFilter = filter; selectionChanged(filter); // finally we update the filtering mode if needed if (mFilterMode == FILTER_NONE) { mFilterMode = FILTER_MANUAL; } mFilterStorage.saveFilters(mFilters); } } } /** * Edits the current filter. The method displays the UI to edit the filter. */ public void editFilter() { if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { EditFilterDialog dlg = new EditFilterDialog( mFolders.getShell(), mCurrentFilter); if (dlg.open()) { synchronized (mBuffer) { // at this point the filter has been updated. // so we update its content initFilter(mCurrentFilter); // and the content of the "other" filter as well. if (mDefaultFilter != null) { initDefaultFilter(); } mFilterStorage.saveFilters(mFilters); } } } } /** * Deletes the current filter. */ public void deleteFilter() { synchronized (mBuffer) { if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { // remove the filter from the list removeFilterFromArray(mCurrentFilter); mCurrentFilter.dispose(); // select the new filter mFolders.setSelection(0); if (mFilters.length > 0) { mCurrentFilter = mFilters[0]; } else { mCurrentFilter = mDefaultFilter; } selectionChanged(mCurrentFilter); // update the content of the "other" filter to include what was filtered out // by the deleted filter. if (mDefaultFilter != null) { initDefaultFilter(); } mFilterStorage.saveFilters(mFilters); } } } /** * saves the current selection in a text file. * @return false if the saving failed. */ public boolean save() { synchronized (mBuffer) { FileDialog dlg = new FileDialog(mParent.getShell(), SWT.SAVE); String fileName; dlg.setText("Save log..."); dlg.setFileName("log.txt"); String defaultPath = mDefaultLogSave; if (defaultPath == null) { defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ } dlg.setFilterPath(defaultPath); dlg.setFilterNames(new String[] { "Text Files (*.txt)" }); dlg.setFilterExtensions(new String[] { "*.txt" }); fileName = dlg.open(); if (fileName != null) { mDefaultLogSave = dlg.getFilterPath(); // get the current table and its selection Table currentTable = mCurrentFilter.getTable(); int[] selection = currentTable.getSelectionIndices(); // we need to sort the items to be sure. Arrays.sort(selection); // loop on the selection and output the file. FileWriter writer = null; try { writer = new FileWriter(fileName); for (int i : selection) { TableItem item = currentTable.getItem(i); LogMessage msg = (LogMessage)item.getData(); String line = msg.toString(); writer.write(line); writer.write('\n'); } writer.flush(); } catch (IOException e) { return false; } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { // ignore } } } } } return true; } /** * Empty the current circular buffer. */ public void clear() { synchronized (mBuffer) { for (int i = 0 ; i < STRING_BUFFER_LENGTH; i++) { mBuffer[i] = null; } mBufferStart = -1; mBufferEnd = -1; // now we clear the existing filters for (LogFilter filter : mFilters) { filter.clear(); } // and the default one if (mDefaultFilter != null) { mDefaultFilter.clear(); } } } /** * Copies the current selection of the current filter as multiline text. * * @param clipboard The clipboard to place the copied content. */ public void copy(Clipboard clipboard) { // get the current table and its selection Table currentTable = mCurrentFilter.getTable(); copyTable(clipboard, currentTable); } /** * Selects all lines. */ public void selectAll() { Table currentTable = mCurrentFilter.getTable(); currentTable.selectAll(); } /** * Sets a TableFocusListener which will be notified when one of the tables * gets or loses focus. * * @param listener */ public void setTableFocusListener(ITableFocusListener listener) { // record the global listener, to make sure table created after // this call will still be setup. mGlobalListener = listener; // now we setup the existing filters for (LogFilter filter : mFilters) { Table table = filter.getTable(); addTableToFocusListener(table); } // and the default one if (mDefaultFilter != null) { addTableToFocusListener(mDefaultFilter.getTable()); } } /** * Sets up a Table object to notify the global Table Focus listener when it * gets or loses the focus. * * @param table the Table object. */ private void addTableToFocusListener(final Table table) { // create the activator for this table final IFocusedTableActivator activator = new IFocusedTableActivator() { @Override public void copy(Clipboard clipboard) { copyTable(clipboard, table); } @Override public void selectAll() { table.selectAll(); } }; // add the focus listener on the table to notify the global listener table.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { mGlobalListener.focusGained(activator); } @Override public void focusLost(FocusEvent e) { mGlobalListener.focusLost(activator); } }); } /** * Copies the current selection of a Table into the provided Clipboard, as * multi-line text. * * @param clipboard The clipboard to place the copied content. * @param table The table to copy from. */ private static void copyTable(Clipboard clipboard, Table table) { int[] selection = table.getSelectionIndices(); // we need to sort the items to be sure. Arrays.sort(selection); // all lines must be concatenated. StringBuilder sb = new StringBuilder(); // loop on the selection and output the file. for (int i : selection) { TableItem item = table.getItem(i); LogMessage msg = (LogMessage)item.getData(); String line = msg.toString(); sb.append(line); sb.append('\n'); } // now add that to the clipboard clipboard.setContents(new Object[] { sb.toString() }, new Transfer[] { TextTransfer.getInstance() }); } /** * Sets the log level for the current filter, but does not save it. * @param i */ public void setCurrentFilterLogLevel(int i) { LogFilter filter = getCurrentFilter(); filter.setLogLevel(i); initFilter(filter); } /** * Creates a new tab in the folderTab item. Must be called from the ui * thread. * @param filter The filter associated with the tab. * @param index the index of the tab. if -1, the tab will be added at the * end. * @param fillTable If true the table is filled with the current content of * the buffer. * @return The TabItem object that was created. */ private TabItem createTab(LogFilter filter, int index, boolean fillTable) { synchronized (mBuffer) { TabItem item = null; if (index != -1) { item = new TabItem(mFolders, SWT.NONE, index); } else { item = new TabItem(mFolders, SWT.NONE); } item.setText(filter.getName()); // set the control (the parent is the TabFolder item, always) Composite top = new Composite(mFolders, SWT.NONE); item.setControl(top); top.setLayout(new FillLayout()); // create the ui, first the table final Table t = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); t.addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent e) { if (mLogCatViewInterface != null) { mLogCatViewInterface.onDoubleClick(); } } }); if (mDisplayFont != null) { t.setFont(mDisplayFont); } // give the ui objects to the filters. filter.setWidgets(item, t); t.setHeaderVisible(true); t.setLinesVisible(false); if (mGlobalListener != null) { addTableToFocusListener(t); } // create a controllistener that will handle the resizing of all the // columns (except the last) and of the table itself. ControlListener listener = null; if (mColumnMode == COLUMN_MODE_AUTO) { listener = new ControlListener() { @Override public void controlMoved(ControlEvent e) { } @Override public void controlResized(ControlEvent e) { Rectangle r = t.getClientArea(); // get the size of all but the last column int total = t.getColumn(0).getWidth(); total += t.getColumn(1).getWidth(); total += t.getColumn(2).getWidth(); total += t.getColumn(3).getWidth(); if (r.width > total) { t.getColumn(4).setWidth(r.width-total); } } }; t.addControlListener(listener); } // then its column TableColumn col = TableHelper.createTableColumn(t, "Time", SWT.LEFT, "00-00 00:00:00", //$NON-NLS-1$ PREFS_TIME, mStore); if (mColumnMode == COLUMN_MODE_AUTO) { col.addControlListener(listener); } col = TableHelper.createTableColumn(t, "", SWT.CENTER, "D", //$NON-NLS-1$ PREFS_LEVEL, mStore); if (mColumnMode == COLUMN_MODE_AUTO) { col.addControlListener(listener); } col = TableHelper.createTableColumn(t, "pid", SWT.LEFT, "9999", //$NON-NLS-1$ PREFS_PID, mStore); if (mColumnMode == COLUMN_MODE_AUTO) { col.addControlListener(listener); } col = TableHelper.createTableColumn(t, "tag", SWT.LEFT, "abcdefgh", //$NON-NLS-1$ PREFS_TAG, mStore); if (mColumnMode == COLUMN_MODE_AUTO) { col.addControlListener(listener); } col = TableHelper.createTableColumn(t, "Message", SWT.LEFT, "abcdefghijklmnopqrstuvwxyz0123456789", //$NON-NLS-1$ PREFS_MESSAGE, mStore); if (mColumnMode == COLUMN_MODE_AUTO) { // instead of listening on resize for the last column, we make // it non resizable. col.setResizable(false); } if (fillTable) { initFilter(filter); } return item; } } protected void updateColumns(Table table) { if (table != null) { int index = 0; TableColumn col; col = table.getColumn(index++); col.setWidth(mStore.getInt(PREFS_TIME)); col = table.getColumn(index++); col.setWidth(mStore.getInt(PREFS_LEVEL)); col = table.getColumn(index++); col.setWidth(mStore.getInt(PREFS_PID)); col = table.getColumn(index++); col.setWidth(mStore.getInt(PREFS_TAG)); col = table.getColumn(index++); col.setWidth(mStore.getInt(PREFS_MESSAGE)); } } public void resetUI(boolean inUiThread) { if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { if (inUiThread) { mFolders.dispose(); mParent.pack(true); createControl(mParent); } else { Display d = mFolders.getDisplay(); // run sync as we need to update right now. d.syncExec(new Runnable() { @Override public void run() { mFolders.dispose(); mParent.pack(true); createControl(mParent); } }); } } else { // the ui is static we just empty it. if (mFolders.isDisposed() == false) { if (inUiThread) { emptyTables(); } else { Display d = mFolders.getDisplay(); // run sync as we need to update right now. d.syncExec(new Runnable() { @Override public void run() { if (mFolders.isDisposed() == false) { emptyTables(); } } }); } } } } /** * Process new Log lines coming from {@link LogCatOuputReceiver}. * @param lines the new lines */ protected void processLogLines(String[] lines) { // WARNING: this will not work if the string contains more line than // the buffer holds. if (lines.length > STRING_BUFFER_LENGTH) { Log.e("LogCat", "Receiving more lines than STRING_BUFFER_LENGTH"); } // parse the lines and create LogMessage that are stored in a temporary list final ArrayList newMessages = new ArrayList(); synchronized (mBuffer) { for (String line : lines) { // ignore empty lines. if (line.length() > 0) { // check for header lines. Matcher matcher = sLogPattern.matcher(line); if (matcher.matches()) { // this is a header line, parse the header and keep it around. mLastMessageInfo = new LogMessageInfo(); mLastMessageInfo.time = matcher.group(1); mLastMessageInfo.pidString = matcher.group(2); mLastMessageInfo.pid = Integer.valueOf(mLastMessageInfo.pidString); mLastMessageInfo.logLevel = LogLevel.getByLetterString(matcher.group(4)); mLastMessageInfo.tag = matcher.group(5).trim(); } else { // This is not a header line. // Create a new LogMessage and process it. LogMessage mc = new LogMessage(); if (mLastMessageInfo == null) { // The first line of output wasn't preceded // by a header line; make something up so // that users of mc.data don't NPE. mLastMessageInfo = new LogMessageInfo(); mLastMessageInfo.time = "??-?? ??:??:??.???"; //$NON-NLS1$ mLastMessageInfo.pidString = ""; //$NON-NLS1$ mLastMessageInfo.pid = 0; mLastMessageInfo.logLevel = LogLevel.INFO; mLastMessageInfo.tag = ""; //$NON-NLS1$ } // If someone printed a log message with // embedded '\n' characters, there will // one header line followed by multiple text lines. // Use the last header that we saw. mc.data = mLastMessageInfo; // tabs seem to display as only 1 tab so we replace the leading tabs // by 4 spaces. mc.msg = line.replaceAll("\t", " "); //$NON-NLS-1$ //$NON-NLS-2$ // process the new LogMessage. processNewMessage(mc); // store the new LogMessage newMessages.add(mc); } } } // if we don't have a pending Runnable that will do the refresh, we ask the Display // to run one in the UI thread. if (mPendingAsyncRefresh == false) { mPendingAsyncRefresh = true; try { Display display = mFolders.getDisplay(); // run in sync because this will update the buffer start/end indices display.asyncExec(new Runnable() { @Override public void run() { asyncRefresh(); } }); } catch (SWTException e) { // display is disposed, we're probably quitting. Let's stop. stopLogCat(false); } } } } /** * Refreshes the UI with new messages. */ private void asyncRefresh() { if (mFolders.isDisposed() == false) { synchronized (mBuffer) { try { // the circular buffer has been updated, let have the filter flush their // display with the new messages. if (mFilters != null) { for (LogFilter f : mFilters) { f.flush(); } } if (mDefaultFilter != null) { mDefaultFilter.flush(); } } finally { // the pending refresh is done. mPendingAsyncRefresh = false; } } } else { stopLogCat(true); } } /** * Processes a new Message. *

This adds the new message to the buffer, and gives it to the existing filters. * @param newMessage */ private void processNewMessage(LogMessage newMessage) { // if we are in auto filtering mode, make sure we have // a filter for this if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { checkFilter(newMessage.data); } // compute the index where the message goes. // was the buffer empty? int messageIndex = -1; if (mBufferStart == -1) { messageIndex = mBufferStart = 0; mBufferEnd = 1; } else { messageIndex = mBufferEnd; // increment the next usable slot index mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH; // check we aren't overwriting start if (mBufferEnd == mBufferStart) { mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH; } } LogMessage oldMessage = null; // record the message that was there before if (mBuffer[messageIndex] != null) { oldMessage = mBuffer[messageIndex]; } // then add the new one mBuffer[messageIndex] = newMessage; // give the new message to every filters. boolean filtered = false; if (mFilters != null) { for (LogFilter f : mFilters) { filtered |= f.addMessage(newMessage, oldMessage); } } if (filtered == false && mDefaultFilter != null) { mDefaultFilter.addMessage(newMessage, oldMessage); } } private void createFilters() { if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) { // unarchive the filters. mFilters = mFilterStorage.getFilterFromStore(); // set the colors if (mFilters != null) { for (LogFilter f : mFilters) { f.setColors(mColors); } } if (mFilterStorage.requiresDefaultFilter()) { mDefaultFilter = new LogFilter("Log"); mDefaultFilter.setColors(mColors); mDefaultFilter.setSupportsDelete(false); mDefaultFilter.setSupportsEdit(false); } } else if (mFilterMode == FILTER_NONE) { // if the filtering mode is "none", we create a single filter that // will receive all mDefaultFilter = new LogFilter("Log"); mDefaultFilter.setColors(mColors); mDefaultFilter.setSupportsDelete(false); mDefaultFilter.setSupportsEdit(false); } } /** Checks if there's an automatic filter for this md and if not * adds the filter and the ui. * This must be called from the UI! * @param md * @return true if the filter existed already */ private boolean checkFilter(final LogMessageInfo md) { if (true) return true; // look for a filter that matches the pid if (mFilterMode == FILTER_AUTO_PID) { for (LogFilter f : mFilters) { if (f.getPidFilter() == md.pid) { return true; } } } else if (mFilterMode == FILTER_AUTO_TAG) { for (LogFilter f : mFilters) { if (f.getTagFilter().equals(md.tag)) { return true; } } } // if we reach this point, no filter was found. // create a filter with a temporary name of the pid final LogFilter newFilter = new LogFilter(md.pidString); String name = null; if (mFilterMode == FILTER_AUTO_PID) { newFilter.setPidMode(md.pid); // ask the monitor thread if it knows the pid. name = mCurrentLoggedDevice.getClientName(md.pid); } else { newFilter.setTagMode(md.tag); name = md.tag; } addFilterToArray(newFilter); final String fname = name; // create the tabitem final TabItem newTabItem = createTab(newFilter, -1, true); // if the name is unknown if (fname == null) { // we need to find the process running under that pid. // launch a thread do a ps on the device new Thread("remote PS") { //$NON-NLS-1$ @Override public void run() { // create the receiver PsOutputReceiver psor = new PsOutputReceiver(md.pid, newFilter, newTabItem); // execute ps try { mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$ } catch (IOException e) { // Ignore } catch (TimeoutException e) { // Ignore } catch (AdbCommandRejectedException e) { // Ignore } catch (ShellCommandUnresponsiveException e) { // Ignore } } }.start(); } return false; } /** * Adds a new filter to the current filter array, and set its colors * @param newFilter The filter to add */ private void addFilterToArray(LogFilter newFilter) { // set the colors newFilter.setColors(mColors); // add it to the array. if (mFilters != null && mFilters.length > 0) { LogFilter[] newFilters = new LogFilter[mFilters.length+1]; System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length); newFilters[mFilters.length] = newFilter; mFilters = newFilters; } else { mFilters = new LogFilter[1]; mFilters[0] = newFilter; } } private void removeFilterFromArray(LogFilter oldFilter) { // look for the index int index = -1; for (int i = 0 ; i < mFilters.length ; i++) { if (mFilters[i] == oldFilter) { index = i; break; } } if (index != -1) { LogFilter[] newFilters = new LogFilter[mFilters.length-1]; System.arraycopy(mFilters, 0, newFilters, 0, index); System.arraycopy(mFilters, index + 1, newFilters, index, newFilters.length-index); mFilters = newFilters; } } /** * Initialize the filter with already existing buffer. * @param filter */ private void initFilter(LogFilter filter) { // is it empty if (filter.uiReady() == false) { return; } if (filter == mDefaultFilter) { initDefaultFilter(); return; } filter.clear(); if (mBufferStart != -1) { int max = mBufferEnd; if (mBufferEnd < mBufferStart) { max += STRING_BUFFER_LENGTH; } for (int i = mBufferStart; i < max; i++) { int realItemIndex = i % STRING_BUFFER_LENGTH; filter.addMessage(mBuffer[realItemIndex], null /* old message */); } } filter.flush(); filter.resetTempFilteringStatus(); } /** * Refill the default filter. Not to be called directly. * @see initFilter() */ private void initDefaultFilter() { mDefaultFilter.clear(); if (mBufferStart != -1) { int max = mBufferEnd; if (mBufferEnd < mBufferStart) { max += STRING_BUFFER_LENGTH; } for (int i = mBufferStart; i < max; i++) { int realItemIndex = i % STRING_BUFFER_LENGTH; LogMessage msg = mBuffer[realItemIndex]; // first we check that the other filters don't take this message boolean filtered = false; for (LogFilter f : mFilters) { filtered |= f.accept(msg); } if (filtered == false) { mDefaultFilter.addMessage(msg, null /* old message */); } } } mDefaultFilter.flush(); mDefaultFilter.resetTempFilteringStatus(); } /** * Reset the filters, to handle change in device in automatic filter mode */ private void resetFilters() { // if we are in automatic mode, then we need to rmove the current // filter. if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { mFilters = null; // recreate the filters. createFilters(); } } private LogFilter getCurrentFilter() { int index = mFolders.getSelectionIndex(); // if mFilters is null or index is invalid, we return the default // filter. It doesn't matter if that one is null as well, since we // would return null anyway. if (index == 0 || mFilters == null) { return mDefaultFilter; } return mFilters[index-1]; } private void emptyTables() { for (LogFilter f : mFilters) { f.getTable().removeAll(); } if (mDefaultFilter != null) { mDefaultFilter.getTable().removeAll(); } } protected void updateFilteringWith(String text) { synchronized (mBuffer) { // reset the temp filtering for all the filters for (LogFilter f : mFilters) { f.resetTempFiltering(); } if (mDefaultFilter != null) { mDefaultFilter.resetTempFiltering(); } // now we need to figure out the new temp filtering // split each word String[] segments = text.split(" "); //$NON-NLS-1$ ArrayList keywords = new ArrayList(segments.length); // loop and look for temp id/tag int tempPid = -1; String tempTag = null; for (int i = 0 ; i < segments.length; i++) { String s = segments[i]; if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$ // get the pid String[] seg = s.split(":"); //$NON-NLS-1$ if (seg.length == 2) { if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$ tempPid = Integer.valueOf(seg[1]); } } } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$ String seg[] = segments[i].split(":"); //$NON-NLS-1$ if (seg.length == 2) { tempTag = seg[1]; } } else { keywords.add(s); } } // set the temp filtering in the filters if (tempPid != -1 || tempTag != null || keywords.size() > 0) { String[] keywordsArray = keywords.toArray( new String[keywords.size()]); for (LogFilter f : mFilters) { if (tempPid != -1) { f.setTempPidFiltering(tempPid); } if (tempTag != null) { f.setTempTagFiltering(tempTag); } f.setTempKeywordFiltering(keywordsArray); } if (mDefaultFilter != null) { if (tempPid != -1) { mDefaultFilter.setTempPidFiltering(tempPid); } if (tempTag != null) { mDefaultFilter.setTempTagFiltering(tempTag); } mDefaultFilter.setTempKeywordFiltering(keywordsArray); } } initFilter(mCurrentFilter); } } /** * Called when the current filter selection changes. * @param selectedFilter */ private void selectionChanged(LogFilter selectedFilter) { if (mLogLevelActions != null) { // get the log level int level = selectedFilter.getLogLevel(); for (int i = 0 ; i < mLogLevelActions.length; i++) { ICommonAction a = mLogLevelActions[i]; if (i == level - 2) { a.setChecked(true); } else { a.setChecked(false); } } } if (mDeleteFilterAction != null) { mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete()); } if (mEditFilterAction != null) { mEditFilterAction.setEnabled(selectedFilter.supportsEdit()); } } public String getSelectedErrorLineMessage() { Table table = mCurrentFilter.getTable(); int[] selection = table.getSelectionIndices(); if (selection.length == 1) { TableItem item = table.getItem(selection[0]); LogMessage msg = (LogMessage)item.getData(); if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN) return msg.msg; } return null; } public void setLogCatViewInterface(LogCatViewInterface i) { mLogCatViewInterface = i; } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/0040755 0000000 0000000 00000000000 12747325007 021550 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java0100644 0000000 0000000 00000112041 12747325007 025020 0ustar000000000 0000000 /* * 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.ddmuilib.net; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.Client; import com.android.ddmlib.IDevice; import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.DdmUiPreferences; import com.android.ddmuilib.TableHelper; import com.android.ddmuilib.TablePanel; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.DatasetRenderingOrder; import org.jfree.chart.plot.ValueMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2; import org.jfree.chart.renderer.xy.XYAreaRenderer; import org.jfree.data.DefaultKeyedValues2D; import org.jfree.data.time.Millisecond; import org.jfree.data.time.TimePeriod; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.data.xy.AbstractIntervalXYDataset; import org.jfree.data.xy.TableXYDataset; import org.jfree.experimental.chart.swt.ChartComposite; import org.jfree.ui.RectangleAnchor; import org.jfree.ui.TextAnchor; import java.io.IOException; import java.text.DecimalFormat; import java.text.FieldPosition; import java.text.NumberFormat; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Date; import java.util.Formatter; import java.util.Iterator; /** * Displays live network statistics for currently selected {@link Client}. */ public class NetworkPanel extends TablePanel { // TODO: enable view of packets and bytes/packet // TODO: add sash to resize chart and table // TODO: let user edit tags to be meaningful /** Amount of historical data to display. */ private static final long HISTORY_MILLIS = 30 * 1000; private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title"; private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes"; private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets"; private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes"; private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets"; /** Path to network statistics on remote device. */ private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats"; private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY; /** Colors used for tag series data. */ private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] { java.awt.Color.decode("0x2bc4c1"), // teal java.awt.Color.decode("0xD50F25"), // red java.awt.Color.decode("0x3369E8"), // blue java.awt.Color.decode("0xEEB211"), // orange java.awt.Color.decode("0x00bd2e"), // green java.awt.Color.decode("0xae26ae"), // purple }; private Display mDisplay; private Composite mPanel; /** Header panel with configuration options. */ private Composite mHeader; private Label mSpeedLabel; private Combo mSpeedCombo; /** Current sleep between each sample, from {@link #mSpeedCombo}. */ private long mSpeedMillis; private Button mRunningButton; private Button mResetButton; /** Chart of recent network activity. */ private JFreeChart mChart; private ChartComposite mChartComposite; private ValueAxis mDomainAxis; /** Data for total traffic (tag 0x0). */ private TimeSeriesCollection mTotalCollection; private TimeSeries mRxTotalSeries; private TimeSeries mTxTotalSeries; /** Data for detailed tagged traffic. */ private LiveTimeTableXYDataset mRxDetailDataset; private LiveTimeTableXYDataset mTxDetailDataset; private XYAreaRenderer mTotalRenderer; private StackedXYAreaRenderer2 mRenderer; /** Table showing summary of network activity. */ private Table mTable; private TableViewer mTableViewer; /** UID of currently selected {@link Client}. */ private int mActiveUid = -1; /** List of traffic flows being actively tracked. */ private ArrayList mTrackedItems = new ArrayList(); private SampleThread mSampleThread; private class SampleThread extends Thread { private volatile boolean mFinish; public void finish() { mFinish = true; interrupt(); } @Override public void run() { while (!mFinish && !mDisplay.isDisposed()) { performSample(); try { Thread.sleep(mSpeedMillis); } catch (InterruptedException e) { // ignored } } } } /** Last snapshot taken by {@link #performSample()}. */ private NetworkSnapshot mLastSnapshot; @Override protected Control createControl(Composite parent) { mDisplay = parent.getDisplay(); mPanel = new Composite(parent, SWT.NONE); final FormLayout formLayout = new FormLayout(); mPanel.setLayout(formLayout); createHeader(); createChart(); createTable(); return mPanel; } /** * Create header panel with configuration options. */ private void createHeader() { mHeader = new Composite(mPanel, SWT.NONE); final RowLayout layout = new RowLayout(); layout.center = true; mHeader.setLayout(layout); mSpeedLabel = new Label(mHeader, SWT.NONE); mSpeedLabel.setText("Speed:"); mSpeedCombo = new Combo(mHeader, SWT.PUSH); mSpeedCombo.add("Fast (100ms)"); mSpeedCombo.add("Medium (250ms)"); mSpeedCombo.add("Slow (500ms)"); mSpeedCombo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { updateSpeed(); } }); mSpeedCombo.select(1); updateSpeed(); mRunningButton = new Button(mHeader, SWT.PUSH); mRunningButton.setText("Start"); mRunningButton.setEnabled(false); mRunningButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { final boolean alreadyRunning = mSampleThread != null; updateRunning(!alreadyRunning); } }); mResetButton = new Button(mHeader, SWT.PUSH); mResetButton.setText("Reset"); mResetButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { clearTrackedItems(); } }); final FormData data = new FormData(); data.top = new FormAttachment(0); data.left = new FormAttachment(0); data.right = new FormAttachment(100); mHeader.setLayoutData(data); } /** * Create chart of recent network activity. */ private void createChart() { mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false); // create backing datasets and series mRxTotalSeries = new TimeSeries("RX total"); mTxTotalSeries = new TimeSeries("TX total"); mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS); mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS); mTotalCollection = new TimeSeriesCollection(); mTotalCollection.addSeries(mRxTotalSeries); mTotalCollection.addSeries(mTxTotalSeries); mRxDetailDataset = new LiveTimeTableXYDataset(); mTxDetailDataset = new LiveTimeTableXYDataset(); mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA); mRenderer = new StackedXYAreaRenderer2(); final XYPlot xyPlot = mChart.getXYPlot(); xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD); xyPlot.setDataset(0, mTotalCollection); xyPlot.setDataset(1, mRxDetailDataset); xyPlot.setDataset(2, mTxDetailDataset); xyPlot.setRenderer(0, mTotalRenderer); xyPlot.setRenderer(1, mRenderer); xyPlot.setRenderer(2, mRenderer); // we control domain axis manually when taking samples mDomainAxis = xyPlot.getDomainAxis(); mDomainAxis.setAutoRange(false); final NumberAxis axis = new NumberAxis(); axis.setNumberFormatOverride(new BytesFormat(true)); axis.setAutoRangeMinimumSize(50); xyPlot.setRangeAxis(axis); xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); // draw thick line to separate RX versus TX traffic xyPlot.addRangeMarker( new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2))); // label to indicate that positive axis is RX traffic final ValueMarker rxMarker = new ValueMarker(0); rxMarker.setStroke(new java.awt.BasicStroke(0)); rxMarker.setLabel("RX"); rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f)); rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY); rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT); rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT); xyPlot.addRangeMarker(rxMarker); // label to indicate that negative axis is TX traffic final ValueMarker txMarker = new ValueMarker(0); txMarker.setStroke(new java.awt.BasicStroke(0)); txMarker.setLabel("TX"); txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f)); txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY); txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT); txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT); xyPlot.addRangeMarker(txMarker); mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart, ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true, false, true); final FormData data = new FormData(); data.top = new FormAttachment(mHeader); data.left = new FormAttachment(0); data.bottom = new FormAttachment(70); data.right = new FormAttachment(100); mChartComposite.setLayoutData(data); } /** * Create table showing summary of network activity. */ private void createTable() { mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION); final FormData data = new FormData(); data.top = new FormAttachment(mChartComposite); data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER); data.bottom = new FormAttachment(100); mTable.setLayoutData(data); mTable.setHeaderVisible(true); mTable.setLinesVisible(true); final IPreferenceStore store = DdmUiPreferences.getStore(); TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null); TableHelper.createTableColumn( mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store); TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12), PREFS_NETWORK_COL_RX_BYTES, store); TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12), PREFS_NETWORK_COL_RX_PACKETS, store); TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12), PREFS_NETWORK_COL_TX_BYTES, store); TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12), PREFS_NETWORK_COL_TX_PACKETS, store); mTableViewer = new TableViewer(mTable); mTableViewer.setContentProvider(new ContentProvider()); mTableViewer.setLabelProvider(new LabelProvider()); } /** * Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection. */ private void updateSpeed() { switch (mSpeedCombo.getSelectionIndex()) { case 0: mSpeedMillis = 100; break; case 1: mSpeedMillis = 250; break; case 2: mSpeedMillis = 500; break; } } /** * Update if {@link SampleThread} should be actively running. Will create * new thread or finish existing thread to match requested state. */ private void updateRunning(boolean shouldRun) { final boolean alreadyRunning = mSampleThread != null; if (alreadyRunning && !shouldRun) { mSampleThread.finish(); mSampleThread = null; mRunningButton.setText("Start"); mHeader.pack(); } else if (!alreadyRunning && shouldRun) { mSampleThread = new SampleThread(); mSampleThread.start(); mRunningButton.setText("Stop"); mHeader.pack(); } } @Override public void setFocus() { mPanel.setFocus(); } private static java.awt.Color nextSeriesColor(int index) { return SERIES_COLORS[index % SERIES_COLORS.length]; } /** * Find a {@link TrackedItem} that matches the requested UID and tag, or * create one if none exists. */ public TrackedItem findOrCreateTrackedItem(int uid, int tag) { // try searching for existing item for (TrackedItem item : mTrackedItems) { if (item.uid == uid && item.tag == tag) { return item; } } // nothing found; create new item final TrackedItem item = new TrackedItem(uid, tag); if (item.isTotal()) { item.color = TOTAL_COLOR; item.label = "Total"; } else { final int size = mTrackedItems.size(); item.color = nextSeriesColor(size); Formatter formatter = new Formatter(); item.label = "0x" + formatter.format("%08x", tag); formatter.close(); } // create color chip to display as legend in table item.colorImage = new Image(mDisplay, 20, 20); final GC gc = new GC(item.colorImage); gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color .getRed(), item.color.getGreen(), item.color.getBlue())); gc.fillRectangle(item.colorImage.getBounds()); gc.dispose(); mTrackedItems.add(item); return item; } /** * Clear all {@link TrackedItem} and chart history. */ public void clearTrackedItems() { mRxTotalSeries.clear(); mTxTotalSeries.clear(); mRxDetailDataset.clear(); mTxDetailDataset.clear(); mTrackedItems.clear(); mTableViewer.setInput(mTrackedItems); } /** * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}. */ private void updateSeriesPaint() { for (TrackedItem item : mTrackedItems) { final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label); if (seriesIndex >= 0) { mRenderer.setSeriesPaint(seriesIndex, item.color); mRenderer.setSeriesFillPaint(seriesIndex, item.color); } } // series data is always the same color final int count = mTotalCollection.getSeriesCount(); for (int i = 0; i < count; i++) { mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR); mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR); } } /** * Traffic flow being actively tracked, uniquely defined by UID and tag. Can * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for * charting, and into summary statistics for {@link Table} display. */ private class TrackedItem { public final int uid; public final int tag; public java.awt.Color color; public Image colorImage; public String label; public long rxBytes; public long rxPackets; public long txBytes; public long txPackets; public TrackedItem(int uid, int tag) { this.uid = uid; this.tag = tag; } public boolean isTotal() { return tag == 0x0; } /** * Record the given {@link NetworkSnapshot} delta, updating * {@link TimeSeries} and summary statistics. * * @param time Timestamp when delta was observed. * @param deltaMillis Time duration covered by delta, in milliseconds. */ public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) { final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis; final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis; // record values under correct series if (isTotal()) { mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond); mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond); } else { mRxDetailDataset.addValue(rxBytesPerSecond, time, label); mTxDetailDataset.addValue(-txBytesPerSecond, time, label); } rxBytes += delta.rxBytes; rxPackets += delta.rxPackets; txBytes += delta.txBytes; txPackets += delta.txPackets; } } @Override public void deviceSelected() { // treat as client selection to update enabled states clientSelected(); } @Override public void clientSelected() { mActiveUid = -1; final Client client = getCurrentClient(); if (client != null) { final int pid = client.getClientData().getPid(); try { // map PID to UID from device final UidParser uidParser = new UidParser(); getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser); mActiveUid = uidParser.uid; } catch (TimeoutException e) { e.printStackTrace(); } catch (AdbCommandRejectedException e) { e.printStackTrace(); } catch (ShellCommandUnresponsiveException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } clearTrackedItems(); updateRunning(false); final boolean validUid = mActiveUid != -1; mRunningButton.setEnabled(validUid); } @Override public void clientChanged(Client client, int changeMask) { // ignored } /** * Take a snapshot from {@link #getCurrentDevice()}, recording any delta * network traffic to {@link TrackedItem}. */ public void performSample() { final IDevice device = getCurrentDevice(); if (device == null) return; try { final NetworkSnapshotParser parser = new NetworkSnapshotParser(); device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser); if (parser.isError()) { mDisplay.asyncExec(new Runnable() { @Override public void run() { updateRunning(false); final String title = "Problem reading stats"; final String message = "Problem reading xt_qtaguid network " + "statistics from selected device."; Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null); ErrorDialog.openError(mPanel.getShell(), title, title, status); } }); return; } final NetworkSnapshot snapshot = parser.getParsedSnapshot(); // use first snapshot as baseline if (mLastSnapshot == null) { mLastSnapshot = snapshot; return; } final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot); mLastSnapshot = snapshot; // perform delta updates over on UI thread if (!mDisplay.isDisposed()) { mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp)); } } catch (TimeoutException e) { e.printStackTrace(); } catch (AdbCommandRejectedException e) { e.printStackTrace(); } catch (ShellCommandUnresponsiveException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** * Task that updates UI with given {@link NetworkSnapshot} delta. */ private class UpdateDeltaRunnable implements Runnable { private final NetworkSnapshot mDelta; private final long mEndTime; public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) { mDelta = delta; mEndTime = endTime; } @Override public void run() { if (mDisplay.isDisposed()) return; final Millisecond time = new Millisecond(new Date(mEndTime)); for (NetworkSnapshot.Entry entry : mDelta) { if (mActiveUid != entry.uid) continue; final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag); item.recordDelta(time, mDelta.timestamp, entry); } // remove any historical detail data final long beforeMillis = mEndTime - HISTORY_MILLIS; mRxDetailDataset.removeBefore(beforeMillis); mTxDetailDataset.removeBefore(beforeMillis); // trigger refresh from bulk changes above mRxDetailDataset.fireDatasetChanged(); mTxDetailDataset.fireDatasetChanged(); // update axis to show latest 30 second time period mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime); updateSeriesPaint(); // kick table viewer to update mTableViewer.setInput(mTrackedItems); } } /** * Parser that extracts UID from remote {@code /proc/pid/status} file. */ private static class UidParser extends MultiLineReceiver { public int uid = -1; @Override public boolean isCancelled() { return false; } @Override public void processNewLines(String[] lines) { for (String line : lines) { if (line.startsWith("Uid:")) { // we care about the "real" UID final String[] cols = line.split("\t"); uid = Integer.parseInt(cols[1]); } } } } /** * Parser that populates {@link NetworkSnapshot} based on contents of remote * {@link NetworkPanel#PROC_XT_QTAGUID} file. */ private static class NetworkSnapshotParser extends MultiLineReceiver { private NetworkSnapshot mSnapshot; public NetworkSnapshotParser() { mSnapshot = new NetworkSnapshot(System.currentTimeMillis()); } public boolean isError() { return mSnapshot == null; } public NetworkSnapshot getParsedSnapshot() { return mSnapshot; } @Override public boolean isCancelled() { return false; } @Override public void processNewLines(String[] lines) { for (String line : lines) { if (line.endsWith("No such file or directory")) { mSnapshot = null; return; } // ignore header line if (line.startsWith("idx")) { continue; } final String[] cols = line.split(" "); if (cols.length < 9) continue; // iface and set are currently ignored, which groups those // entries together. final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry(); entry.iface = null; //cols[1]; entry.uid = Integer.parseInt(cols[3]); entry.set = -1; //Integer.parseInt(cols[4]); entry.tag = kernelToTag(cols[2]); entry.rxBytes = Long.parseLong(cols[5]); entry.rxPackets = Long.parseLong(cols[6]); entry.txBytes = Long.parseLong(cols[7]); entry.txPackets = Long.parseLong(cols[8]); mSnapshot.combine(entry); } } /** * Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming * format like {@code 0x7fffffff00000000}. * Matches code in android.server.NetworkManagementSocketTagger */ public static int kernelToTag(String string) { int length = string.length(); if (length > 10) { return Long.decode(string.substring(0, length - 8)).intValue(); } else { return 0; } } } /** * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time. */ private static class NetworkSnapshot implements Iterable { private ArrayList mStats = new ArrayList(); public final long timestamp; /** Single parsed statistics row. */ public static class Entry { public String iface; public int uid; public int set; public int tag; public long rxBytes; public long rxPackets; public long txBytes; public long txPackets; public boolean isEmpty() { return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0; } } public NetworkSnapshot(long timestamp) { this.timestamp = timestamp; } public void clear() { mStats.clear(); } /** * Combine the given {@link Entry} with any existing {@link Entry}, or * insert if none exists. */ public void combine(Entry entry) { final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag); if (existing != null) { existing.rxBytes += entry.rxBytes; existing.rxPackets += entry.rxPackets; existing.txBytes += entry.txBytes; existing.txPackets += entry.txPackets; } else { mStats.add(entry); } } @Override public Iterator iterator() { return mStats.iterator(); } public Entry findEntry(String iface, int uid, int set, int tag) { for (Entry entry : mStats) { if (entry.uid == uid && entry.set == set && entry.tag == tag && equal(entry.iface, iface)) { return entry; } } return null; } /** * Subtract the two given {@link NetworkSnapshot} objects, returning the * delta between them. */ public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) { final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp); // for each row on left, subtract value from right side for (Entry leftEntry : left) { final Entry rightEntry = right.findEntry( leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag); if (rightEntry == null) continue; final Entry resultEntry = new Entry(); resultEntry.iface = leftEntry.iface; resultEntry.uid = leftEntry.uid; resultEntry.set = leftEntry.set; resultEntry.tag = leftEntry.tag; resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes; resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets; resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes; resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets; result.combine(resultEntry); } return result; } } /** * Provider of {@link #mTrackedItems}. */ private class ContentProvider implements IStructuredContentProvider { @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } @Override public void dispose() { // pass } @Override public Object[] getElements(Object inputElement) { return mTrackedItems.toArray(); } } /** * Provider of labels for {@Link TrackedItem} values. */ private static class LabelProvider implements ITableLabelProvider { private final DecimalFormat mFormat = new DecimalFormat("#,###"); @Override public Image getColumnImage(Object element, int columnIndex) { if (element instanceof TrackedItem) { final TrackedItem item = (TrackedItem) element; switch (columnIndex) { case 0: return item.colorImage; } } return null; } @Override public String getColumnText(Object element, int columnIndex) { if (element instanceof TrackedItem) { final TrackedItem item = (TrackedItem) element; switch (columnIndex) { case 0: return null; case 1: return item.label; case 2: return mFormat.format(item.rxBytes); case 3: return mFormat.format(item.rxPackets); case 4: return mFormat.format(item.txBytes); case 5: return mFormat.format(item.txPackets); } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public void dispose() { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } /** * Format that displays simplified byte units for when given values are * large enough. */ private static class BytesFormat extends NumberFormat { private final String[] mUnits; private final DecimalFormat mFormat = new DecimalFormat("#.#"); public BytesFormat(boolean perSecond) { if (perSecond) { mUnits = new String[] { "B/s", "KB/s", "MB/s" }; } else { mUnits = new String[] { "B", "KB", "MB" }; } } @Override public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { double value = Math.abs(number); int i = 0; while (value > 1024 && i < mUnits.length - 1) { value /= 1024; i++; } toAppendTo.append(mFormat.format(value)); toAppendTo.append(mUnits[i]); return toAppendTo; } @Override public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { return format((long) number, toAppendTo, pos); } @Override public Number parse(String source, ParsePosition parsePosition) { return null; } } public static boolean equal(Object a, Object b) { return a == b || (a != null && a.equals(b)); } /** * Build stub string of requested length, usually for measurement. */ private static String buildSampleText(int length) { final StringBuilder builder = new StringBuilder(length); for (int i = 0; i < length; i++) { builder.append("X"); } return builder.toString(); } /** * Dataset that contains live measurements. Exposes * {@link #removeBefore(long)} to efficiently remove old data, and enables * batched {@link #fireDatasetChanged()} events. */ public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements TableXYDataset { private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true); /** * Caller is responsible for triggering {@link #fireDatasetChanged()}. */ public void addValue(Number value, TimePeriod rowKey, String columnKey) { mValues.addValue(value, rowKey, columnKey); } /** * Caller is responsible for triggering {@link #fireDatasetChanged()}. */ public void removeBefore(long beforeMillis) { while(mValues.getRowCount() > 0) { final TimePeriod period = (TimePeriod) mValues.getRowKey(0); if (period.getEnd().getTime() < beforeMillis) { mValues.removeRow(0); } else { break; } } } public int getColumnIndex(String key) { return mValues.getColumnIndex(key); } public void clear() { mValues.clear(); fireDatasetChanged(); } @Override public void fireDatasetChanged() { super.fireDatasetChanged(); } @Override public int getItemCount() { return mValues.getRowCount(); } @Override public int getItemCount(int series) { return mValues.getRowCount(); } @Override public int getSeriesCount() { return mValues.getColumnCount(); } @Override public Comparable getSeriesKey(int series) { return mValues.getColumnKey(series); } @Override public double getXValue(int series, int item) { final TimePeriod period = (TimePeriod) mValues.getRowKey(item); return period.getStart().getTime(); } @Override public double getStartXValue(int series, int item) { return getXValue(series, item); } @Override public double getEndXValue(int series, int item) { return getXValue(series, item); } @Override public Number getX(int series, int item) { return getXValue(series, item); } @Override public Number getStartX(int series, int item) { return getXValue(series, item); } @Override public Number getEndX(int series, int item) { return getXValue(series, item); } @Override public Number getY(int series, int item) { return mValues.getValue(item, series); } @Override public Number getStartY(int series, int item) { return getY(series, item); } @Override public Number getEndY(int series, int item) { return getY(series, item); } } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/0040755 0000000 0000000 00000000000 12747325007 023440 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java0100644 0000000 0000000 00000012134 12747325007 030344 0ustar000000000 0000000 /* * Copyright (C) 2013 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.ddmuilib.screenrecord; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ddmlib.CollectingOutputReceiver; import com.android.ddmlib.IDevice; import com.android.ddmlib.ScreenRecorderOptions; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.window.Window; import org.eclipse.swt.widgets.Shell; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class ScreenRecorderAction { private static final String TITLE = "Screen Recorder"; private static final String REMOTE_PATH = "/sdcard/ddmsrec.mp4"; private final Shell mParentShell; private final IDevice mDevice; public ScreenRecorderAction(Shell parent, IDevice device) { mParentShell = parent; mDevice = device; } public void performAction() { ScreenRecorderOptionsDialog optionsDialog = new ScreenRecorderOptionsDialog(mParentShell); if (optionsDialog.open() == Window.CANCEL) { return; } final ScreenRecorderOptions options = new ScreenRecorderOptions.Builder() .setBitRate(optionsDialog.getBitRate()) .setSize(optionsDialog.getWidth(), optionsDialog.getHeight()) .build(); final CountDownLatch latch = new CountDownLatch(1); final CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch); new Thread(new Runnable() { @Override public void run() { try { mDevice.startScreenRecorder(REMOTE_PATH, options, receiver); } catch (Exception e) { showError("Unexpected error while launching screenrecorder", e); latch.countDown(); } } }, "Screen Recorder").start(); try { new ProgressMonitorDialog(mParentShell).run(true, true, new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { int timeInSecond = 0; monitor.beginTask("Recording...", IProgressMonitor.UNKNOWN); while (true) { // Wait for a second to see if the command has completed if (latch.await(1, TimeUnit.SECONDS)) { break; } // update recording time in second monitor.subTask(String.format("Recording...%d seconds elapsed", timeInSecond++)); // If not, check if user has cancelled if (monitor.isCanceled()) { receiver.cancel(); monitor.subTask("Stopping..."); // wait for an additional second to make sure that the command // completed and screenrecorder finishes writing the output latch.await(1, TimeUnit.SECONDS); break; } } } }); } catch (InvocationTargetException e) { showError("Unexpected error while recording: ", e.getTargetException()); return; } catch (InterruptedException ignored) { } try { mDevice.pullFile(REMOTE_PATH, optionsDialog.getDestination().getAbsolutePath()); } catch (Exception e) { showError("Unexpected error while copying video recording from device", e); } MessageDialog.openInformation(mParentShell, TITLE, "Screen recording saved at " + optionsDialog.getDestination().getAbsolutePath()); } private void showError(@NonNull final String message, @Nullable final Throwable e) { mParentShell.getDisplay().asyncExec(new Runnable() { @Override public void run() { String msg = message; if (e != null) { msg += e.getLocalizedMessage() != null ? ": " + e.getLocalizedMessage() : ""; } MessageDialog.openError(mParentShell, TITLE, msg); } }); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java0100644 0000000 0000000 00000017534 12747325007 031713 0ustar000000000 0000000 /* * Copyright (C) 2013 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.ddmuilib.screenrecord; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.TitleAreaDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.io.File; import java.util.Calendar; public class ScreenRecorderOptionsDialog extends TitleAreaDialog { private static final int DEFAULT_BITRATE_MBPS = 4; private static String sLastSavedFolder = System.getProperty("user.home"); private static String sLastFileName = suggestFileName(); private static int sBitRateMbps = DEFAULT_BITRATE_MBPS; private static int sWidth = 0; private static int sHeight = 0; private Text mBitRateText; private Text mWidthText; private Text mHeightText; private Text mDestinationText; public ScreenRecorderOptionsDialog(Shell parentShell) { super(parentShell); setShellStyle(getShellStyle() | SWT.RESIZE); } @Override protected Control createDialogArea(Composite shell) { setTitle("Screen Recorder Options"); setMessage("Provide screen recorder options. Leave empty to use defaults."); Composite parent = (Composite) super.createDialogArea(shell); Composite c = new Composite(parent, SWT.BORDER); c.setLayout(new GridLayout(3, false)); c.setLayoutData(new GridData(GridData.FILL_BOTH)); createLabel(c, "Bit Rate (in Mbps)"); mBitRateText = new Text(c, SWT.BORDER); mBitRateText.setText(Integer.toString(sBitRateMbps)); mBitRateText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); createLabel(c, ""); // empty label for 3rd column createLabel(c, "Video width (in px, defaults to screen width)"); mWidthText = new Text(c, SWT.BORDER); mWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); if (sWidth > 0) { mWidthText.setText(Integer.toString(sWidth)); } createLabel(c, ""); // empty label for 3rd column createLabel(c, "Video height (in px, defaults to screen height)"); mHeightText = new Text(c, SWT.BORDER); mHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); if (sHeight > 0) { mHeightText.setText(Integer.toString(sHeight)); } createLabel(c, ""); // empty label for 3rd column ModifyListener m = new ModifyListener() { @Override public void modifyText(ModifyEvent modifyEvent) { validateAndUpdateState(); } }; mBitRateText.addModifyListener(m); mWidthText.addModifyListener(m); mHeightText.addModifyListener(m); createLabel(c, "Save Video as: "); mDestinationText = new Text(c, SWT.BORDER); mDestinationText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mDestinationText.setText(getFilePath()); Button browseButton = new Button(c, SWT.PUSH); browseButton.setText("Browse"); browseButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent selectionEvent) { FileDialog dlg = new FileDialog(getShell(), SWT.SAVE); dlg.setText("Save Video..."); dlg.setFileName(sLastFileName != null ? sLastFileName : suggestFileName()); if (sLastSavedFolder != null) { dlg.setFilterPath(sLastSavedFolder); } dlg.setFilterNames(new String[] { "MP4 files (*.mp4)" }); dlg.setFilterExtensions(new String[] { "*.mp4" }); String filePath = dlg.open(); if (filePath != null) { if (!filePath.endsWith(".mp4")) { filePath += ".mp4"; } mDestinationText.setText(filePath); validateAndUpdateState(); } } }); return c; } private static String getFilePath() { return sLastSavedFolder + File.separatorChar + sLastFileName; } private static String suggestFileName() { Calendar now = Calendar.getInstance(); return String.format("device-%tF-%tH%tM%tS.mp4", now, now, now, now); } private void createLabel(Composite c, String text) { Label l = new Label(c, SWT.NONE); l.setText(text); GridData gd = new GridData(); gd.horizontalAlignment = SWT.RIGHT; l.setLayoutData(gd); } private void validateAndUpdateState() { int intValue; if ((intValue = validateInteger(mBitRateText.getText().trim(), "Bit Rate has to be an integer")) < 0) { return; } sBitRateMbps = intValue > 0 ? intValue : DEFAULT_BITRATE_MBPS; if ((intValue = validateInteger(mWidthText.getText().trim(), "Recorded video resolution width has to be a valid integer.")) < 0) { return; } if (intValue % 16 != 0) { setErrorMessage("Width must be a multiple of 16"); setOkButtonEnabled(false); return; } sWidth = intValue; if ((intValue = validateInteger(mHeightText.getText().trim(), "Recorded video resolution height has to be a valid integer.")) < 0) { return; } if (intValue % 16 != 0) { setErrorMessage("Height must be a multiple of 16"); setOkButtonEnabled(false); return; } sHeight = intValue; String filePath = mDestinationText.getText(); File f = new File(filePath); if (!f.getParentFile().isDirectory()) { setErrorMessage("The path '" + f.getParentFile().getAbsolutePath() + "' is not a valid directory."); setOkButtonEnabled(false); return; } sLastFileName = f.getName(); sLastSavedFolder = f.getParentFile().getAbsolutePath(); setErrorMessage(null); setOkButtonEnabled(true); } private int validateInteger(String s, String errorMessage) { if (!s.isEmpty()) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { setErrorMessage(errorMessage); setOkButtonEnabled(false); return -1; } } return 0; } private void setOkButtonEnabled(boolean en) { getButton(IDialogConstants.OK_ID).setEnabled(en); } public int getBitRate() { return sBitRateMbps; } public int getWidth() { return sWidth; } public int getHeight() { return sHeight; } public File getDestination() { return new File(sLastSavedFolder, sLastFileName); } } ddms/ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/0040755 0000000 0000000 00000000000 12747325007 022423 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java0100644 0000000 0000000 00000013561 12747325007 027326 0ustar000000000 0000000 /* * Copyright (C) 2013 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.ddmuilib.vmtrace; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; /** Dialog that allows users to select between method tracing or sampler based profiling. */ public class VmTraceOptionsDialog extends Dialog { private static final int DEFAULT_SAMPLING_INTERVAL_US = 1000; // Static variables that maintain state across invocations of the dialog private static boolean sTracingEnabled = false; private static int sSamplingIntervalUs = DEFAULT_SAMPLING_INTERVAL_US; public VmTraceOptionsDialog(Shell parentShell) { super(parentShell); } @Override protected void configureShell(Shell newShell) { super.configureShell(newShell); newShell.setText("Profiling Options"); } @Override protected Control createDialogArea(Composite shell) { int horizontalIndent = 30; Composite parent = (Composite) super.createDialogArea(shell); Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(2, false)); c.setLayoutData(new GridData(GridData.FILL_BOTH)); final Button useSamplingButton = new Button(c, SWT.RADIO); useSamplingButton.setText("Sample based profiling"); useSamplingButton.setSelection(!sTracingEnabled); GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, true, 2, 1); useSamplingButton.setLayoutData(gd); Label l = new Label(c, SWT.NONE); l.setText("Sample based profiling works by interrupting the VM at a given frequency and \n" + "collecting the call stacks at that time. The overhead is proportional to the \n" + "sampling frequency."); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, true, 2, 1); gd.horizontalIndent = horizontalIndent; l.setLayoutData(gd); l = new Label(c, SWT.NONE); l.setText("Sampling frequency (microseconds): "); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_END, false, true); gd.horizontalIndent = horizontalIndent; l.setLayoutData(gd); final Text samplingIntervalTextField = new Text(c, SWT.BORDER); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, true); gd.widthHint = 100; samplingIntervalTextField.setLayoutData(gd); samplingIntervalTextField.setEnabled(!sTracingEnabled); samplingIntervalTextField.setText(Integer.toString(sSamplingIntervalUs)); samplingIntervalTextField.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent modifyEvent) { int v = getIntegerValue(samplingIntervalTextField.getText()); getButton(IDialogConstants.OK_ID).setEnabled(v > 0); sSamplingIntervalUs = v > 0 ? v : DEFAULT_SAMPLING_INTERVAL_US; } private int getIntegerValue(String text) { try { return Integer.parseInt(text); } catch (NumberFormatException e) { return -1; } } }); final Button useTracingButton = new Button(c, SWT.RADIO); useTracingButton.setText("Trace based profiling"); useTracingButton.setSelection(sTracingEnabled); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, true, 2, 1); useTracingButton.setLayoutData(gd); l = new Label(c, SWT.NONE); l.setText("Trace based profiling works by tracing the entry and exit of every method.\n" + "This captures the execution of all methods, no matter how small, and hence\n" + "has a high overhead."); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, true, 2, 1); gd.horizontalIndent = horizontalIndent; l.setLayoutData(gd); SelectionAdapter selectionAdapter = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { sTracingEnabled = useTracingButton.getSelection(); samplingIntervalTextField.setEnabled(!sTracingEnabled); } }; useTracingButton.addSelectionListener(selectionAdapter); useSamplingButton.addSelectionListener(selectionAdapter); return c; } public boolean shouldUseTracing() { return sTracingEnabled; } public int getSamplingIntervalMicros() { return sSamplingIntervalUs; } } ddms/ddmuilib/src/main/java/images/0040755 0000000 0000000 00000000000 12747325007 016240 5ustar000000000 0000000 ddms/ddmuilib/src/main/java/images/add.png0100644 0000000 0000000 00000000222 12747325007 017467 0ustar000000000 0000000 PNG  IHDRaYIDAT8c`hS;j x~gA\EΤLF]Lt@r<~<--IV#IENDB`ddms/ddmuilib/src/main/java/images/android.png0100644 0000000 0000000 00000007031 12747325007 020364 0ustar000000000 0000000 PNG  IHDRa pHYs   OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FDIDATx$k\Uܙd&$fiXQRqR˪+QEF]PF|*"*S@[csw=soK߾H93M5.0Ul.*Qe9j;6T[(Y@;8a$qo]ՙs,}~/x*e|j%Ngq^xtǎcZ[ tJ!Ze8#>1Z]>@gntw QXk"K%J%Fg(bYE|q}AoG1ZJV3MgAhdR]nIk-$۫?xsՎM=Q('‚ q gdJ|ooe!vgB`s2r۾:bxrR`rpjYI)R˳݁{IENDB`ddms/ddmuilib/src/main/java/images/backward.png0100644 0000000 0000000 00000000210 12747325007 020512 0ustar000000000 0000000 PNG  IHDRaOIDAT8c` `!W#`h$ @P#.LhF7,l#9DwAD fYa FIENDB`ddms/ddmuilib/src/main/java/images/capture.png0100644 0000000 0000000 00000001263 12747325007 020410 0ustar000000000 0000000 PNG  IHDRa pHYs  eIDAT8S_HQ}+zMz Z5*}ك$bE-{ YHA`12|$jl's]=wnd ҁsϹ{)]g.`#਱v>v{F`_;, iLKpJ}(D)tyr;wAYcB -YSǮ(7PB9t}v Gc@r򼪃O}|<8SU]&89NYOqCvNpïqlcj8|.PjOp0%Rzd" _%  K𾘇 މpaxZC*#M`{ لKxHl2b2)b!߈F u jCDcIP=fX,F,$S2N~B*54fa- ҂ZQWw*\>'V{ HX!|$=~ IWi?A|{#r*Ցֵܮ(;[5.ƞ+5 =-lOrIENDB`ddms/ddmuilib/src/main/java/images/clear.png0100644 0000000 0000000 00000000331 12747325007 020026 0ustar000000000 0000000 PNG  IHDRaIDAT8[!E/Iwۘnۀ8Q| 8pu-77mqbb```:u*^gsg,b?#{ =`ؿ<ß?N0yb?<ȑ' <@1Η`x͟?~pw+g``~33ɓ14gggi?B1?$MO<9t"8IENDB`ddms/ddmuilib/src/main/java/images/debug-attach.png0100644 0000000 0000000 00000000234 12747325007 021272 0ustar000000000 0000000 PNG  IHDRacIDAT8RG CqJ)xWN¬(K̊ei^R€,@FXI (Sny3~d( 9X^IENDB`ddms/ddmuilib/src/main/java/images/debug-error.png0100644 0000000 0000000 00000000336 12747325007 021162 0ustar000000000 0000000 PNG  IHDRaIDAT8SI0Mg!%Q䀵1& 4'@Xr/.c < `usdȕmCyUkhñOWXL ge7x`+M4#4&ۖtr"%,RYmŷOu KFIENDB`ddms/ddmuilib/src/main/java/images/debug-wait.png0100644 0000000 0000000 00000000234 12747325007 020772 0ustar000000000 0000000 PNG  IHDRacIDAT8RG S!Ƃ`X)er|A{T*:Vik-Aܮͅ!χ%>'+2}ud;F.]NaUꎝgR- LIENDB`ddms/ddmuilib/src/main/java/images/delete.png0100644 0000000 0000000 00000000153 12747325007 020204 0ustar000000000 0000000 PNG  IHDRa2IDAT8c`a Ih ˂,(OHl&RlP\Q0[IIENDB`ddms/ddmuilib/src/main/java/images/device.png0100644 0000000 0000000 00000000207 12747325007 020201 0ustar000000000 0000000 PNG  IHDRaNIDAT8cdg````!B3>l;©DmxTF.0fBtpV\0s6!wIENDB`ddms/ddmuilib/src/main/java/images/diff.png0100644 0000000 0000000 00000000325 12747325007 017653 0ustar000000000 0000000 PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<RIDAT8c?%"݄ (׉u19 9.HmD,,ȜJ%%LkwN@ XȞ9IENDB`ddms/ddmuilib/src/main/java/images/displayfilters.png0100644 0000000 0000000 00000000362 12747325007 022002 0ustar000000000 0000000 PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<oIDAT8퓱 0 Q֠c a2.۶mJ F c^> L4 HBtDIENDB`ddms/ddmuilib/src/main/java/images/groupby.png0100644 0000000 0000000 00000000635 12747325007 020436 0ustar000000000 0000000 PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8N0ϱ2,1DS}> ]QW I]zxyD`F@Y3w㒊u)9pd$K4>km>F}3IENDB`ddms/ddmuilib/src/main/java/images/halt.png0100644 0000000 0000000 00000000305 12747325007 017671 0ustar000000000 0000000 PNG  IHDRaIDAT8Q0Cݫ7f%É)@l#0!IPj\mBBߟ$`Ce +'%缉> `&O5'YQLoxG~!IZvNoӗK#b݅;KZ%fIENDB`ddms/ddmuilib/src/main/java/images/heap.png0100644 0000000 0000000 00000000336 12747325007 017662 0ustar000000000 0000000 PNG  IHDRaIDAT8c`0Oz\?!\U3!1QD860@#0`:li'',ٶ]0=ܲXq0<=}`M12+ Su-IENDB`ddms/ddmuilib/src/main/java/images/i.png0100644 0000000 0000000 00000000762 12747325007 017200 0ustar000000000 0000000 PNG  IHDRaIDATxb?:`ddd``?#b1,## gΆn8b?߿333||?1b?cfffa(bVgg_CvvdbIGG[&L`:ujb ''bb```hhh b"?bBv9bb```:u*ab–< ?0L:a9F_(wD› 300߿3ɓ14gggi?B1?$MO<9 lvIENDB`ddms/ddmuilib/src/main/java/images/importBug.png0100644 0000000 0000000 00000000277 12747325007 020721 0ustar000000000 0000000 PNG  IHDRaIDAT8c`hO@uH^ir&h PM Hu_n/D b\7.@ҁ Di~ WȄ,x4pR500 :E /0P IENDB`ddms/ddmuilib/src/main/java/images/load.png0100644 0000000 0000000 00000000243 12747325007 017661 0ustar000000000 0000000 PNG  IHDRajIDAT8c`hO@kHnmq"h va;6E,ؕh"lmqb0!*&J4_Ђi)Q U3` Q 2 :-"{C7IENDB`ddms/ddmuilib/src/main/java/images/pause.png0100644 0000000 0000000 00000000142 12747325007 020055 0ustar000000000 0000000 PNG  IHDRa)IDAT8c`hI8.5`ԀaIENDB`ddms/ddmuilib/src/main/java/images/play.png0100644 0000000 0000000 00000000212 12747325007 017703 0ustar000000000 0000000 PNG  IHDRaQIDAT8 P"j`/"rj  @3@@0@@.a2xu.wkE|-Vj ^NgNBjblĕtgmq =y.z_h+u ^F/`4BU Cʈ>g|} >NÁ] 0 )fsƝ{`J\P~$<+i?mfT IENDB`ddms/ddmuilib/src/main/java/images/push.png0100644 0000000 0000000 00000000344 12747325007 017723 0ustar000000000 0000000 PNG  IHDRaIDAT81 EUę:u`͝:pTvXܡ$-`'Ēe oC71tܠ'z2[_+pC wd$~d$Y &f 5)p5Zf+ hkDij/Uϵm8u qVcCp|sR|yIENDB`ddms/ddmuilib/src/main/java/images/save.png0100644 0000000 0000000 00000000360 12747325007 017700 0ustar000000000 0000000 PNG  IHDRaIDAT8c`0"3000\@,0.pEaASfbeXnLݞ[30000m@# pK m'ɀs$ĸ %A? - @P[YK 39 GȂ!I11028Y`O>* 5XIENDB`ddms/ddmuilib/src/main/java/images/scroll_lock.png0100644 0000000 0000000 00000000443 12747325007 021252 0ustar000000000 0000000 PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8M P /Ѕ=zow|^߿g3ɓ14 B1?$MFCU222000001B bb```Ɂ(ԋ; Q5Kjfbb```:u*?<V[ bBI /!؟w?0L:a9߿3pj2/ =nld`xqOA«͛7~w+_\\?3ɓ14gggi?B1?$MO<9f2~ IENDB`ddms/ddmuilib/src/main/java/images/warning.png0100644 0000000 0000000 00000000223 12747325007 020405 0ustar000000000 0000000 PNG  IHDRaZIDAT8c`1OH63ل]A  Q[-^@5 @_XP=0l' .e.0lGDlqK07EDdIENDB`ddms/ddmuilib/src/main/java/images/zygote.png0100644 0000000 0000000 00000000531 12747325007 020263 0ustar000000000 0000000 PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8?JQӍQ 3xodcee!XHFh data = BugReportParser.readCpuDataset(br); assertEquals(4, data.size()); assertEquals("system_server (user)", data.get(0).name); assertEquals("Idle", data.get(3).name); } public void testParseJbCpuDataSet() throws IOException { String cpuInfo = "Load: 1.0 / 1.02 / 0.97\n" + "CPU usage from 96307ms to 36303ms ago:\n" + " 0.4% 675/system_server: 0.3% user + 0.1% kernel / faults: 198 minor\n" + " 0.1% 173/mpdecision: 0% user + 0.1% kernel\n" + " 0% 2856/kworker/0:2: 0% user + 0% kernel\n" + " 0% 3128/kworker/0:0: 0% user + 0% kernel\n" + "0.3% TOTAL: 0.1% user + 0% kernel + 0% iowait\n"; BufferedReader br = new BufferedReader(new StringReader(cpuInfo)); List data = BugReportParser.readCpuDataset(br); assertEquals(4, data.size()); assertEquals("675/system_server (user)", data.get(0).name); assertEquals("Idle", data.get(3).name); } public void testParseProcRankEclair() throws IOException { String memInfo = " 51 39408K 37908K 18731K 14936K system_server\n" + " 96 27432K 27432K 9501K 6816K android.process.acore\n" + " 27 248K 248K 83K 76K /system/bin/debuggerd\n"; BufferedReader br = new BufferedReader(new StringReader(memInfo)); List data = BugReportParser.readProcRankDataset(br, " PID Vss Rss Pss Uss cmdline\n"); assertEquals(3, data.size()); assertEquals("debuggerd", data.get(2).name); if (data.get(0).value - 18731 > 0.0002) { fail("Unexpected PSS Value " + data.get(0).value); } } public void testParseProcRankJb() throws IOException { String memInfo = " 675 101120K 100928K 63452K 52624K system_server\n" + "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" + " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" + " ------ ------ ------\n" + " 480598K 394172K TOTAL\n" + "\n" + "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n"; BufferedReader br = new BufferedReader(new StringReader(memInfo)); List data = BugReportParser.readProcRankDataset(br, " PID Vss Rss Pss Uss cmdline\n"); assertEquals(3, data.size()); } public void testParseMeminfoEclair() throws IOException { String memInfo = "------ MEMORY INFO ------\n" + "MemTotal: 516528 kB\n" + "MemFree: 401036 kB\n" + "Buffers: 0 kB\n" + " PID Vss Rss Pss Uss cmdline\n" + " 51 39408K 37908K 18731K 14936K system_server\n" + " 96 27432K 27432K 9501K 6816K android.process.acore\n" + " 297 23348K 23348K 5245K 2276K com.android.gallery\n"; BufferedReader br = new BufferedReader(new StringReader(memInfo)); List data = BugReportParser.readMeminfoDataset(br); assertEquals(5, data.size()); assertEquals("Free", data.get(0).name); } public void testParseMeminfoJb() throws IOException { String memInfo = // note: This dataset does not have all entries, so the totals will be off "------ MEMORY INFO ------\n" + "MemTotal: 1916984 kB\n" + "MemFree: 888048 kB\n" + "Buffers: 72036 kB\n" + " PID Vss Rss Pss Uss cmdline\n" + " 675 101120K 100928K 63452K 52624K system_server\n" + "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" + " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" + " ------ ------ ------\n" + " 480598K 394172K TOTAL\n" + "\n" + "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n"; BufferedReader br = new BufferedReader(new StringReader(memInfo)); List data = BugReportParser.readMeminfoDataset(br); assertEquals(6, data.size()); } public void testParseGfxInfo() throws IOException { String gfxinfo = "Applications Graphics Acceleration Info:\n" + "Uptime: 78455570 Realtime: 78455565\n" + "\n" + "** Graphics info for pid 20517 [com.android.launcher] **\n" + "\n" + "Recent DisplayList operations\n" + " DrawDisplayList\n" + " \n" + " RestoreToCount\n" + "\n" + "Caches:\n" + "Current memory usage / total memory usage (bytes):\n" + " TextureCache 4663920 / 25165824\n" + " \n" + " FontRenderer 0 262144 / 262144\n" + "Other:\n" + " FboCache 2 / 16\n" + " PatchCache 9 / 512\n" + "Total memory usage:\n" + " 13274756 bytes, 12.66 MB\n" + "\n" + "Profile data in ms:\n" + "\n" + " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" + " Draw Process Execute\n" + " 0.85 1.10 0.61\n" + " 54.45 0.85 0.52\n" + " 1.04 2.17 0.73\n" + " 0.15 0.46 1.01\n" + "\n" + "View hierarchy:\n" + "\n" + " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" + " 276 views, 27.16 kB of display lists, 228 frames rendered\n" + "\n" + "\n" + "Total ViewRootImpl: 1\n" + "Total Views: 276\n" + "Total DisplayList: 27.16 kB\n"; BufferedReader br = new BufferedReader(new StringReader(gfxinfo)); List gfxProfile = BugReportParser.parseGfxInfo(br); assertEquals(4, gfxProfile.size()); assertEquals(0.85, gfxProfile.get(0).draw); assertEquals(1.01, gfxProfile.get(3).execute); } } ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/0040755 0000000 0000000 00000000000 12747325007 021732 5ustar000000000 0000000 ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java0100644 0000000 0000000 00000005314 12747325007 027775 0ustar000000000 0000000 /* * 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.ddmuilib.heap; import com.android.ddmlib.NativeAllocationInfo; import com.android.ddmlib.NativeStackCallInfo; import junit.framework.TestCase; import org.eclipse.core.runtime.NullProgressMonitor; import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.util.List; public class NativeHeapDataImporterTest extends TestCase { private static final String BASIC_TEXT = "Allocations: 1\n" + "Size: 524292\n" + "TotalSize: 524292\n" + "BeginStacktrace:\n" + " 40170bd8 /libc_malloc_leak.so --- getbacktrace --- /b/malloc_leak.c:258\n" + " 400910d6 /lib/libc.so --- ca110c --- /bionic/malloc_debug_common.c:227\n" + " 5dd6abfe /lib/libcgdrv.so --- 5dd6abfe ---\n" + " 5dd98a8e /lib/libcgdrv.so --- 5dd98a8e ---\n" + "EndStacktrace\n"; private NativeHeapDataImporter mImporter; public void testImportValidAllocation() { mImporter = createImporter(BASIC_TEXT); try { mImporter.run(new NullProgressMonitor()); } catch (InvocationTargetException e) { fail("Unexpected exception while parsing text: " + e.getTargetException().getMessage()); } catch (InterruptedException e) { fail("Tests are not interrupted!"); } NativeHeapSnapshot snapshot = mImporter.getImportedSnapshot(); assertNotNull(snapshot); // check whether all details have been parsed correctly assertEquals(1, snapshot.getAllocations().size()); NativeAllocationInfo info = snapshot.getAllocations().get(0); assertEquals(1, info.getAllocationCount()); assertEquals(524292, info.getSize()); assertEquals(true, info.isStackCallResolved()); List stack = info.getResolvedStackCall(); assertEquals(4, stack.size()); } private NativeHeapDataImporter createImporter(String contentsToParse) { StringReader r = new StringReader(contentsToParse); return new NativeHeapDataImporter(r); } } ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/0040755 0000000 0000000 00000000000 12747325007 022266 5ustar000000000 0000000 ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java0100644 0000000 0000000 00000006075 12747325007 032070 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmlib.Log.LogLevel; import com.android.ddmlib.logcat.LogCatFilter; import java.util.Arrays; import java.util.HashMap; import java.util.List; import junit.framework.TestCase; public class LogCatFilterSettingsSerializerTest extends TestCase { /* test that decode(encode(f)) = f */ public void testSerializer() { LogCatFilter fs = new LogCatFilter( "TestFilter", //$NON-NLS-1$ "Tag'.*Regex", //$NON-NLS-1$ "regexForTextField..''", //$NON-NLS-1$ "123", //$NON-NLS-1$ "TestAppName.*", //$NON-NLS-1$ LogLevel.ERROR); LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); String s = serializer.encodeToPreferenceString(Arrays.asList(fs), new HashMap()); List decodedFiltersList = serializer.decodeFromPreferenceString(s); assertEquals(1, decodedFiltersList.size()); LogCatFilter dfs = decodedFiltersList.get(0); assertEquals(fs.getName(), dfs.getName()); assertEquals(fs.getTag(), dfs.getTag()); assertEquals(fs.getText(), dfs.getText()); assertEquals(fs.getPid(), dfs.getPid()); assertEquals(fs.getAppName(), dfs.getAppName()); assertEquals(fs.getLogLevel(), dfs.getLogLevel()); } /* test that transient filters are not persisted */ public void testTransientFilters() { LogCatFilter fs = new LogCatFilter( "TestFilter", //$NON-NLS-1$ "Tag'.*Regex", //$NON-NLS-1$ "regexForTextField..''", //$NON-NLS-1$ "123", //$NON-NLS-1$ "TestAppName.*", //$NON-NLS-1$ LogLevel.ERROR); LogCatFilterData fd = new LogCatFilterData(fs); fd.setTransient(); HashMap fdMap = new HashMap(); fdMap.put(fs, fd); LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); String s = serializer.encodeToPreferenceString(Arrays.asList(fs), fdMap); List decodedFiltersList = serializer.decodeFromPreferenceString(s); assertEquals(0, decodedFiltersList.size()); } } ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java0100644 0000000 0000000 00000004043 12747325007 030262 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import junit.framework.TestCase; public class LogCatStackTraceParserTest extends TestCase { private LogCatStackTraceParser mTranslator; private static final String SAMPLE_METHOD = "com.foo.Class.method"; //$NON-NLS-1$ private static final String SAMPLE_FNAME = "FileName"; //$NON-NLS-1$ private static final int SAMPLE_LINENUM = 20; private static final String SAMPLE_TRACE = String.format(" at %s(%s.groovy:%d)", //$NON-NLS-1$ SAMPLE_METHOD, SAMPLE_FNAME, SAMPLE_LINENUM); @Override protected void setUp() throws Exception { mTranslator = new LogCatStackTraceParser(); } public void testIsValidExceptionTrace() { assertTrue(mTranslator.isValidExceptionTrace(SAMPLE_TRACE)); assertFalse(mTranslator.isValidExceptionTrace( "java.lang.RuntimeException: message")); //$NON-NLS-1$ assertFalse(mTranslator.isValidExceptionTrace( "at com.foo.test(Ins.java:unknown)")); //$NON-NLS-1$ } public void testGetMethodName() { assertEquals(SAMPLE_METHOD, mTranslator.getMethodName(SAMPLE_TRACE)); } public void testGetFileName() { assertEquals(SAMPLE_FNAME, mTranslator.getFileName(SAMPLE_TRACE)); } public void testGetLineNumber() { assertEquals(SAMPLE_LINENUM, mTranslator.getLineNumber(SAMPLE_TRACE)); } } ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java0100644 0000000 0000000 00000007013 12747325007 027330 0ustar000000000 0000000 /* * 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.ddmuilib.logcat; import com.android.ddmuilib.AbstractBufferFindTarget; import junit.framework.TestCase; import java.util.Arrays; import java.util.List; public class RollingBufferFindTest extends TestCase { public class FindTarget extends AbstractBufferFindTarget { private int mSelectedItem = -1; private int mItemReadCount = 0; private List mItems = Arrays.asList( "abc", "def", "abc", null, "xyz" ); @Override public int getItemCount() { return mItems.size(); } @Override public String getItem(int index) { mItemReadCount++; return mItems.get(index); } @Override public void selectAndReveal(int index) { mSelectedItem = index; } @Override public int getStartingIndex() { return mItems.size() - 1; } } FindTarget mFindTarget = new FindTarget(); public void testMultipleMatch() { mFindTarget.mSelectedItem = -1; String text = "abc"; int lastIndex = mFindTarget.mItems.lastIndexOf(text); int firstIndex = mFindTarget.mItems.indexOf(text); // the first time we search through the buffer we should hit the item at lastIndex assertTrue(mFindTarget.findAndSelect(text, true, false)); assertEquals(lastIndex, mFindTarget.mSelectedItem); // subsequent search should hit the item at first index assertTrue(mFindTarget.findAndSelect(text, false, false)); assertEquals(firstIndex, mFindTarget.mSelectedItem); // search again should roll over and hit the last index assertTrue(mFindTarget.findAndSelect(text, false, false)); assertEquals(lastIndex, mFindTarget.mSelectedItem); } public void testMissingItem() { mFindTarget.mSelectedItem = -1; mFindTarget.mItemReadCount = 0; // should not match assertFalse(mFindTarget.findAndSelect("nonexistent", true, false)); // no item should be selected assertEquals(-1, mFindTarget.mSelectedItem); // but all items should have been read in once assertEquals(mFindTarget.getItemCount(), mFindTarget.mItemReadCount); } public void testSearchDirection() { String text = "abc"; int lastIndex = mFindTarget.mItems.lastIndexOf(text); int firstIndex = mFindTarget.mItems.indexOf(text); // the first time we search through the buffer we should hit the "abc" from the last assertTrue(mFindTarget.findAndSelect(text, true, false)); assertEquals(lastIndex, mFindTarget.mSelectedItem); // searching forward from there should also hit the first index assertTrue(mFindTarget.findAndSelect(text, false, true)); assertEquals(firstIndex, mFindTarget.mSelectedItem); } } hierarchyviewer2/0040755 0000000 0000000 00000000000 12747325007 013101 5ustar000000000 0000000 hierarchyviewer2/MODULE_LICENSE_APACHE20100644 0000000 0000000 00000000000 12747325007 016221 0ustar000000000 0000000 hierarchyviewer2/app/0040755 0000000 0000000 00000000000 12747325007 013661 5ustar000000000 0000000 hierarchyviewer2/app/.classpath0100644 0000000 0000000 00000001522 12747325007 015641 0ustar000000000 0000000 hierarchyviewer2/app/.project0100644 0000000 0000000 00000000567 12747325007 015335 0ustar000000000 0000000 hierarchyviewer2 org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature hierarchyviewer2/app/.settings/0040755 0000000 0000000 00000000000 12747325007 015577 5ustar000000000 0000000 hierarchyviewer2/app/.settings/README.txt0100644 0000000 0000000 00000000203 12747325007 017265 0ustar000000000 0000000 Copy this in eclipse project as a .settings folder at the root. This ensure proper compilation compliance and warning/error levels.hierarchyviewer2/app/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 022564 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 hierarchyviewer2/app/NOTICE0100644 0000000 0000000 00000024707 12747325007 014574 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS hierarchyviewer2/app/README0100755 0000000 0000000 00000004177 12747325007 014552 0ustar000000000 0000000 Using the Eclipse project HierarchyViewer ----------------------------------------- HierarchyViewer requires some external libraries to compile. If you build HierarchyViewer using the makefile, you have nothing to configure. However if you want to develop on HierarchyViewer using Eclipse, you need to perform the following configuration. ------- 1- Projects required in Eclipse ------- To run HierarchyViewer from Eclipse, you need to import the following 5 projects: - sdk/hierarchyviewer2/app - sdk/hierarchyviewer2/libs/hierarchyviewerlib/ - sdk/ddms/libs/ddmlib - sdk/ddms/libs/ddmuilib - sdk/sdkmanager/libs/sdklib ------- 2- HierarchyViewer requires some SWT JARs to compile. ------- SWT is available in the tree under prebuild//swt Because the build path cannot contain relative path that are not inside the project directory, the .classpath file references a user library called ANDROID_SWT. In order to compile the project: - Open Preferences > Java > Build Path > User Libraries - Create a new user library named ANDROID_SWT - Add the following 4 JAR files: - prebuilt//swt/swt.jar - prebuilt/common/eclipse/org.eclipse.core.commands_3.*.jar - prebuilt/common/eclipse/org.eclipse.equinox.common_3.*.jar - prebuilt/common/eclipse/org.eclipse.jface_3.*.jar ------- 3- HierarchyViewer also requires the compiled SwtMenuBar library. ------- Build the swtmenubar library: $ cd $TOP (top of Android tree) $ . build/envsetup.sh && lunch sdk-eng $ sdk/eclipse/scripts/create_sdkman_symlinks.sh Define a classpath variable in Eclipse: - Open Preferences > Java > Build Path > Classpath Variables - Create a new classpath variable named ANDROID_OUT_FRAMEWORK - Set its folder value to /out/host//framework - Create a new classpath variable named ANDROID_SRC - Set its folder value to You might need to clean the ddms project (Project > Clean...) after you add the new classpath variable, otherwise previous errors might not go away automatically. The ANDROID_SRC part should be optional. It allows you to have access to the SwtMenuBar generic parts from the Java editor. -- EOF hierarchyviewer2/app/build.gradle0100644 0000000 0000000 00000001412 12747325007 016133 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'hierarchyviewer2' dependencies { compile project(':base:common') compile project(':base:sdklib') compile project(':base:ddmlib') compile project(':swt:ddmuilib') compile project(':swt:hierarchyviewer2lib') compile project(':swt:swtmenubar') } sdk { linux { item('etc/hierarchyviewer') { executable true } } mac { item('etc/hierarchyviewer') { executable true } } windows { item 'etc/hierarchyviewer.bat' } } // include swt for compilation sourceSets.main.compileClasspath += configurations.swt // configure the manifest of the buildDistributionJar task. sdkJar.manifest.attributes("Main-Class": "com.android.hierarchyviewer.HierarchyViewerApplication") hierarchyviewer2/app/etc/0040755 0000000 0000000 00000000000 12747325007 014434 5ustar000000000 0000000 hierarchyviewer2/app/etc/hierarchyviewer0100755 0000000 0000000 00000006610 12747325007 017562 0ustar000000000 0000000 #!/bin/sh # Copyright 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. # Set up prog to be the path of this script, including following symlinks, # and set up progdir to be the fully-qualified pathname of its directory. prog="$0" while [ -h "${prog}" ]; do newProg=`/bin/ls -ld "${prog}"` newProg=`expr "${newProg}" : ".* -> \(.*\)$"` if expr "x${newProg}" : 'x/' >/dev/null; then prog="${newProg}" else progdir=`dirname "${prog}"` prog="${progdir}/${newProg}" fi done oldwd=`pwd` progdir=`dirname "${prog}"` cd "${progdir}" progdir=`pwd` prog="${progdir}"/`basename "${prog}"` cd "${oldwd}" jarfile=hierarchyviewer2.jar frameworkdir="$progdir" libdir="$progdir" if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/tools/lib libdir=`dirname "$progdir"`/tools/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/framework libdir=`dirname "$progdir"`/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then echo `basename "$prog"`": can't find $jarfile" exit 1 fi # Check args. if [ debug = "$1" ]; then # add this in for debugging java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y shift 1 else java_debug= fi javaCmd="java" # Mac OS X needs an additional arg, or you get an "illegal thread" complaint. if [ `uname` = "Darwin" ]; then os_opts="-XstartOnFirstThread" else os_opts= fi if [ `uname` = "Linux" ]; then export GDK_NATIVE_WINDOWS=true fi jarpath="$frameworkdir/$jarfile:$frameworkdir/swtmenubar.jar" # Figure out the path to the swt.jar for the current architecture. # if ANDROID_SWT is defined, then just use this. # else, if running in the Android source tree, then look for the correct swt folder in prebuilt # else, look for the correct swt folder in the SDK under tools/lib/ swtpath="" if [ -n "$ANDROID_SWT" ]; then swtpath="$ANDROID_SWT" else vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` if [ -n "$ANDROID_BUILD_TOP" ]; then osname=`uname -s | tr A-Z a-z` swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" else swtpath="${frameworkdir}/${vmarch}" fi fi if [ ! -d "$swtpath" ]; then echo "SWT folder '${swtpath}' does not exist." echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." exit 1 fi if [ -x $progdir/monitor ]; then echo "The standalone version of hieararchyviewer is deprecated." echo "Please use Android Device Monitor (tools/monitor) instead." fi # need to use "java.ext.dirs" because "-jar" causes classpath to be ignored # might need more memory, e.g. -Xmx128M exec "$javaCmd" \ -Xmx512M $os_opts $java_debug \ -Dcom.android.hierarchyviewer.bindir="$progdir" \ -classpath "$jarpath:$swtpath/swt.jar" \ com.android.hierarchyviewer.HierarchyViewerApplication "$@" hierarchyviewer2/app/etc/hierarchyviewer.bat0100755 0000000 0000000 00000004747 12747325007 020340 0ustar000000000 0000000 @echo off rem Copyright (C) 2008 The Android Open Source Project rem rem Licensed under the Apache License, Version 2.0 (the "License"); rem you may not use this file except in compliance with the License. rem You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem don't modify the caller's environment setlocal rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 rem Change current directory and drive to where the script is, to avoid rem issues with directories containing whitespaces. cd /d %~dp0 rem Get the CWD as a full path with short names only (without spaces) for %%i in ("%cd%") do set prog_dir=%%~fsi rem Check we have a valid Java.exe in the path. set java_exe= call lib\find_java.bat if not defined java_exe goto :EOF set jarfile=hierarchyviewer2.jar set frameworkdir=. set libdir= if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=lib if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=..\framework :JarFileOk if debug NEQ "%1" goto NoDebug set java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y shift 1 :NoDebug set jarpath=%frameworkdir%\%jarfile%;%frameworkdir%\hierarchyviewerlib.jar;%frameworkdir%\swtmenubar.jar if not defined ANDROID_SWT goto QueryArch set swt_path=%ANDROID_SWT% goto SwtDone :QueryArch for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a :SwtDone if exist "%swt_path%" goto SetPath echo SWT folder '%swt_path%' does not exist. echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. exit /B :SetPath echo The standalone version of hieararchyviewer is deprecated. echo Please use Android Device Monitor (tools/monitor.bat) instead. call "%java_exe%" %java_debug% -Xmx512m "-Dcom.android.hierarchyviewer.bindir=%prog_dir%" -classpath "%jarpath%;%swt_path%\swt.jar" com.android.hierarchyviewer.HierarchyViewerApplication %* hierarchyviewer2/app/hierarchyviewer2.iml0100644 0000000 0000000 00000001417 12747325007 017646 0ustar000000000 0000000 hierarchyviewer2/app/src/0040755 0000000 0000000 00000000000 12747325007 014450 5ustar000000000 0000000 hierarchyviewer2/app/src/main/0040755 0000000 0000000 00000000000 12747325007 015374 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/0040755 0000000 0000000 00000000000 12747325007 016315 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 017073 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 020513 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/0040755 0000000 0000000 00000000000 12747325007 023713 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/AboutDialog.java0100644 0000000 0000000 00000005533 12747325007 026753 0ustar000000000 0000000 /* * 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.hierarchyviewer; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; public class AboutDialog extends Dialog { private Image mAboutImage; private Image mSmallImage; public AboutDialog(Shell shell) { super(shell); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mSmallImage = imageLoader.loadImage("sdk-hierarchyviewer-16.png", Display.getDefault()); //$NON-NLS-1$ mAboutImage = imageLoader.loadImage("sdk-hierarchyviewer-128.png", Display.getDefault()); //$NON-NLS-1$ } @Override protected void createButtonsForButtonBar(Composite parent) { createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); } @Override protected Control createDialogArea(Composite parent) { Composite control = new Composite(parent, SWT.NONE); control.setLayout(new GridLayout(2, true)); Composite imageControl = new Composite(control, SWT.BORDER); imageControl.setLayout(new FillLayout()); imageControl.setLayoutData(new GridData(GridData.FILL_VERTICAL)); Label imageLabel = new Label(imageControl, SWT.CENTER); imageLabel.setImage(mAboutImage); CLabel textLabel = new CLabel(control, SWT.NONE); // TODO: update with new year date (search this to find other occurrences to update) textLabel.setText("Hierarchy Viewer\nCopyright 2012, The Android Open Source Project\nAll Rights Reserved."); textLabel.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, true, true)); getShell().setText("About..."); getShell().setImage(mSmallImage); return control; } } hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplication.java0100644 0000000 0000000 00000113352 12747325007 032044 0ustar000000000 0000000 /* * 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.hierarchyviewer; import com.android.ddmlib.Log; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewer.actions.AboutAction; import com.android.hierarchyviewer.actions.LoadAllViewsAction; import com.android.hierarchyviewer.actions.QuitAction; import com.android.hierarchyviewer.actions.ShowOverlayAction; import com.android.hierarchyviewer.util.ActionButton; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.actions.CapturePSDAction; import com.android.hierarchyviewerlib.actions.DisplayViewAction; import com.android.hierarchyviewerlib.actions.DumpDisplayListAction; import com.android.hierarchyviewerlib.actions.DumpThemeAction; import com.android.hierarchyviewerlib.actions.EvaluateContrastAction; import com.android.hierarchyviewerlib.actions.InspectScreenshotAction; import com.android.hierarchyviewerlib.actions.InvalidateAction; import com.android.hierarchyviewerlib.actions.LoadOverlayAction; import com.android.hierarchyviewerlib.actions.LoadViewHierarchyAction; import com.android.hierarchyviewerlib.actions.PixelPerfectAutoRefreshAction; import com.android.hierarchyviewerlib.actions.ProfileNodesAction; import com.android.hierarchyviewerlib.actions.RefreshPixelPerfectAction; import com.android.hierarchyviewerlib.actions.RefreshPixelPerfectTreeAction; import com.android.hierarchyviewerlib.actions.RefreshViewAction; import com.android.hierarchyviewerlib.actions.RefreshWindowsAction; import com.android.hierarchyviewerlib.actions.RequestLayoutAction; import com.android.hierarchyviewerlib.actions.SavePixelPerfectAction; import com.android.hierarchyviewerlib.actions.SaveTreeViewAction; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.ui.DeviceSelector; import com.android.hierarchyviewerlib.ui.InvokeMethodPrompt; import com.android.hierarchyviewerlib.ui.LayoutViewer; import com.android.hierarchyviewerlib.ui.PixelPerfect; import com.android.hierarchyviewerlib.ui.PixelPerfectControls; import com.android.hierarchyviewerlib.ui.PixelPerfectLoupe; import com.android.hierarchyviewerlib.ui.PixelPerfectPixelPanel; import com.android.hierarchyviewerlib.ui.PixelPerfectTree; import com.android.hierarchyviewerlib.ui.PropertyViewer; import com.android.hierarchyviewerlib.ui.TreeView; import com.android.hierarchyviewerlib.ui.TreeViewControls; import com.android.hierarchyviewerlib.ui.TreeViewOverview; import com.android.menubar.IMenuBarEnhancer; import com.android.menubar.IMenuBarEnhancer.MenuBarMode; import com.android.menubar.MenuBarEnhancer; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.window.ApplicationWindow; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.ProgressBar; import org.eclipse.swt.widgets.Shell; public class HierarchyViewerApplication extends ApplicationWindow { private static final String APP_NAME = "Hierarchy Viewer"; private static final int INITIAL_WIDTH = 1280; private static final int INITIAL_HEIGHT = 800; private static HierarchyViewerApplication sMainWindow; // Images for moving between the 3 main windows. private Image mDeviceViewImage; private Image mPixelPerfectImage; private Image mTreeViewImage; private Image mDeviceViewSelectedImage; private Image mPixelPerfectSelectedImage; private Image mTreeViewSelectedImage; // And their buttons private Button mTreeViewButton; private Button mPixelPerfectButton; private Button mDeviceViewButton; private Label mProgressLabel; private ProgressBar mProgressBar; private String mProgressString; private Composite mDeviceSelectorPanel; private Composite mTreeViewPanel; private Composite mPixelPerfectPanel; private StackLayout mMainWindowStackLayout; private DeviceSelector mDeviceSelector; private Composite mStatusBar; private TreeView mTreeView; private Composite mMainWindow; private Image mOnBlackImage; private Image mOnWhiteImage; private Button mOnBlackWhiteButton; private Button mShowExtras; private LayoutViewer mLayoutViewer; private PixelPerfectLoupe mPixelPerfectLoupe; private Composite mTreeViewControls; private InvokeMethodPrompt mInvokeMethodPrompt; private ActionButton dumpDisplayList; private HierarchyViewerDirector mDirector; /* * If a thread bails with an uncaught exception, bring the whole * thing down. */ private static class UncaughtHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { Log.e("HierarchyViewer", "shutting down due to uncaught exception"); Log.e("HierarchyViewer", e); System.exit(1); } } public static final HierarchyViewerApplication getMainWindow() { return sMainWindow; } public HierarchyViewerApplication() { super(null /*shell*/); sMainWindow = this; addMenuBar(); } @Override protected void configureShell(Shell shell) { super.configureShell(shell); shell.setText(APP_NAME); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); Image image = imageLoader.loadImage("sdk-hierarchyviewer-128.png", Display.getDefault()); //$NON-NLS-1$ shell.setImage(image); } @Override public MenuManager createMenuManager() { return new MenuManager(); } public void run() { setBlockOnOpen(true); try { open(); } catch (SWTException e) { // Ignore "widget disposed" errors after we closed. if (!getShell().isDisposed()) { throw e; } } TreeViewModel.getModel().removeTreeChangeListener(mTreeChangeListener); PixelPerfectModel.getModel().removeImageChangeListener(mImageChangeListener); ImageLoader.dispose(); mDirector.stopListenForDevices(); mDirector.stopDebugBridge(); mDirector.terminate(); } @Override protected void initializeBounds() { Rectangle monitorArea = Display.getDefault().getPrimaryMonitor().getBounds(); getShell().setSize(Math.min(monitorArea.width, INITIAL_WIDTH), Math.min(monitorArea.height, INITIAL_HEIGHT)); getShell().setLocation(monitorArea.x + (monitorArea.width - INITIAL_WIDTH) / 2, monitorArea.y + (monitorArea.height - INITIAL_HEIGHT) / 2); } private void loadResources() { ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mTreeViewImage = imageLoader.loadImage("tree-view.png", Display.getDefault()); //$NON-NLS-1$ mTreeViewSelectedImage = imageLoader.loadImage("tree-view-selected.png", Display.getDefault()); //$NON-NLS-1$ mPixelPerfectImage = imageLoader.loadImage("pixel-perfect-view.png", Display.getDefault()); //$NON-NLS-1$ mPixelPerfectSelectedImage = imageLoader.loadImage("pixel-perfect-view-selected.png", Display.getDefault()); //$NON-NLS-1$ mDeviceViewImage = imageLoader.loadImage("device-view.png", Display.getDefault()); //$NON-NLS-1$ mDeviceViewSelectedImage = imageLoader.loadImage("device-view-selected.png", Display.getDefault()); //$NON-NLS-1$ mOnBlackImage = imageLoader.loadImage("on-black.png", Display.getDefault()); //$NON-NLS-1$ mOnWhiteImage = imageLoader.loadImage("on-white.png", Display.getDefault()); //$NON-NLS-1$ } @Override protected Control createContents(Composite parent) { // create this only once the window is opened to please SWT on Mac mDirector = HierarchyViewerApplicationDirector.createDirector(); mDirector.initDebugBridge(); mDirector.startListenForDevices(); mDirector.populateDeviceSelectionModel(); TreeViewModel.getModel().addTreeChangeListener(mTreeChangeListener); PixelPerfectModel.getModel().addImageChangeListener(mImageChangeListener); loadResources(); Composite control = new Composite(parent, SWT.NONE); GridLayout mainLayout = new GridLayout(); mainLayout.marginHeight = mainLayout.marginWidth = 0; mainLayout.verticalSpacing = mainLayout.horizontalSpacing = 0; control.setLayout(mainLayout); mMainWindow = new Composite(control, SWT.NONE); mMainWindow.setLayoutData(new GridData(GridData.FILL_BOTH)); mMainWindowStackLayout = new StackLayout(); mMainWindow.setLayout(mMainWindowStackLayout); buildDeviceSelectorPanel(mMainWindow); buildTreeViewPanel(mMainWindow); buildPixelPerfectPanel(mMainWindow); buildStatusBar(control); showDeviceSelector(); return control; } private void buildStatusBar(Composite parent) { mStatusBar = new Composite(parent, SWT.NONE); mStatusBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); FormLayout statusBarLayout = new FormLayout(); statusBarLayout.marginHeight = statusBarLayout.marginWidth = 2; mStatusBar.setLayout(statusBarLayout); mDeviceViewButton = new Button(mStatusBar, SWT.TOGGLE); mDeviceViewButton.setImage(mDeviceViewImage); mDeviceViewButton.setToolTipText("Switch to the window selection view"); mDeviceViewButton.addSelectionListener(deviceViewButtonSelectionListener); FormData deviceViewButtonFormData = new FormData(); deviceViewButtonFormData.left = new FormAttachment(); mDeviceViewButton.setLayoutData(deviceViewButtonFormData); mTreeViewButton = new Button(mStatusBar, SWT.TOGGLE); mTreeViewButton.setImage(mTreeViewImage); mTreeViewButton.setEnabled(false); mTreeViewButton.setToolTipText("Switch to the tree view"); mTreeViewButton.addSelectionListener(treeViewButtonSelectionListener); FormData treeViewButtonFormData = new FormData(); treeViewButtonFormData.left = new FormAttachment(mDeviceViewButton, 2); mTreeViewButton.setLayoutData(treeViewButtonFormData); mPixelPerfectButton = new Button(mStatusBar, SWT.TOGGLE); mPixelPerfectButton.setImage(mPixelPerfectImage); mPixelPerfectButton.setEnabled(false); mPixelPerfectButton.setToolTipText("Switch to the pixel perfect view"); mPixelPerfectButton.addSelectionListener(pixelPerfectButtonSelectionListener); FormData pixelPerfectButtonFormData = new FormData(); pixelPerfectButtonFormData.left = new FormAttachment(mTreeViewButton, 2); mPixelPerfectButton.setLayoutData(pixelPerfectButtonFormData); // Tree View control panel... mTreeViewControls = new TreeViewControls(mStatusBar); FormData treeViewControlsFormData = new FormData(); treeViewControlsFormData.left = new FormAttachment(mPixelPerfectButton, 2); treeViewControlsFormData.top = new FormAttachment(mTreeViewButton, 0, SWT.CENTER); treeViewControlsFormData.width = 552; mTreeViewControls.setLayoutData(treeViewControlsFormData); // Progress stuff mProgressLabel = new Label(mStatusBar, SWT.RIGHT); mProgressBar = new ProgressBar(mStatusBar, SWT.HORIZONTAL | SWT.INDETERMINATE | SWT.SMOOTH); FormData progressBarFormData = new FormData(); progressBarFormData.right = new FormAttachment(100, 0); progressBarFormData.top = new FormAttachment(mTreeViewButton, 0, SWT.CENTER); mProgressBar.setLayoutData(progressBarFormData); FormData progressLabelFormData = new FormData(); progressLabelFormData.right = new FormAttachment(mProgressBar, -2); progressLabelFormData.top = new FormAttachment(mTreeViewButton, 0, SWT.CENTER); mProgressLabel.setLayoutData(progressLabelFormData); if (mProgressString == null) { mProgressLabel.setVisible(false); mProgressBar.setVisible(false); } else { mProgressLabel.setText(mProgressString); } } private void buildDeviceSelectorPanel(Composite parent) { mDeviceSelectorPanel = new Composite(parent, SWT.NONE); GridLayout gridLayout = new GridLayout(); gridLayout.marginWidth = gridLayout.marginHeight = 0; gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0; mDeviceSelectorPanel.setLayout(gridLayout); Composite buttonPanel = new Composite(mDeviceSelectorPanel, SWT.NONE); buttonPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); GridLayout buttonLayout = new GridLayout(); buttonLayout.marginWidth = buttonLayout.marginHeight = 0; buttonLayout.horizontalSpacing = buttonLayout.verticalSpacing = 0; buttonPanel.setLayout(buttonLayout); Composite innerButtonPanel = new Composite(buttonPanel, SWT.NONE); innerButtonPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); GridLayout innerButtonPanelLayout = new GridLayout(3, true); innerButtonPanelLayout.marginWidth = innerButtonPanelLayout.marginHeight = 2; innerButtonPanelLayout.horizontalSpacing = innerButtonPanelLayout.verticalSpacing = 2; innerButtonPanel.setLayout(innerButtonPanelLayout); ActionButton refreshWindows = new ActionButton(innerButtonPanel, RefreshWindowsAction.getAction()); refreshWindows.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton loadViewHierarchyButton = new ActionButton(innerButtonPanel, LoadViewHierarchyAction.getAction()); loadViewHierarchyButton.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton inspectScreenshotButton = new ActionButton(innerButtonPanel, InspectScreenshotAction.getAction()); inspectScreenshotButton.setLayoutData(new GridData(GridData.FILL_BOTH)); Composite deviceSelectorContainer = new Composite(mDeviceSelectorPanel, SWT.BORDER); deviceSelectorContainer.setLayoutData(new GridData(GridData.FILL_BOTH)); deviceSelectorContainer.setLayout(new FillLayout()); mDeviceSelector = new DeviceSelector(deviceSelectorContainer, true, true); } public void buildTreeViewPanel(Composite parent) { mTreeViewPanel = new Composite(parent, SWT.NONE); GridLayout gridLayout = new GridLayout(); gridLayout.marginWidth = gridLayout.marginHeight = 0; gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0; mTreeViewPanel.setLayout(gridLayout); Composite buttonPanel = new Composite(mTreeViewPanel, SWT.NONE); buttonPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); GridLayout buttonLayout = new GridLayout(); buttonLayout.marginWidth = buttonLayout.marginHeight = 0; buttonLayout.horizontalSpacing = buttonLayout.verticalSpacing = 0; buttonPanel.setLayout(buttonLayout); Composite innerButtonPanel = new Composite(buttonPanel, SWT.NONE); innerButtonPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); GridLayout innerButtonPanelLayout = new GridLayout(5, true); innerButtonPanelLayout.marginWidth = innerButtonPanelLayout.marginHeight = 2; innerButtonPanelLayout.horizontalSpacing = innerButtonPanelLayout.verticalSpacing = 2; innerButtonPanel.setLayout(innerButtonPanelLayout); ActionButton saveTreeView = new ActionButton(innerButtonPanel, SaveTreeViewAction.getAction(getShell())); saveTreeView.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton capturePSD = new ActionButton(innerButtonPanel, CapturePSDAction.getAction(getShell())); capturePSD.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton refreshViewAction = new ActionButton(innerButtonPanel, RefreshViewAction.getAction()); refreshViewAction.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton evaluateContrast = new ActionButton(innerButtonPanel, EvaluateContrastAction.getAction(getShell())); evaluateContrast.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton displayView = new ActionButton(innerButtonPanel, DisplayViewAction.getAction(getShell())); displayView.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton invalidate = new ActionButton(innerButtonPanel, InvalidateAction.getAction()); invalidate.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton requestLayout = new ActionButton(innerButtonPanel, RequestLayoutAction.getAction()); requestLayout.setLayoutData(new GridData(GridData.FILL_BOTH)); dumpDisplayList = new ActionButton(innerButtonPanel, DumpDisplayListAction.getAction()); dumpDisplayList.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton dumpTheme = new ActionButton(innerButtonPanel, DumpThemeAction.getAction(getShell())); dumpTheme.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton profileNodes = new ActionButton(innerButtonPanel, ProfileNodesAction.getAction()); profileNodes.setLayoutData(new GridData(GridData.FILL_BOTH)); SashForm mainSash = new SashForm(mTreeViewPanel, SWT.HORIZONTAL | SWT.SMOOTH); mainSash.setLayoutData(new GridData(GridData.FILL_BOTH)); Composite treeViewContainer = new Composite(mainSash, SWT.BORDER); treeViewContainer.setLayout(new FillLayout()); mTreeView = new TreeView(treeViewContainer); SashForm sideSash = new SashForm(mainSash, SWT.VERTICAL | SWT.SMOOTH); mainSash.SASH_WIDTH = 4; mainSash.setWeights(new int[] { 7, 3 }); Composite treeViewOverviewContainer = new Composite(sideSash, SWT.BORDER); treeViewOverviewContainer.setLayout(new FillLayout()); new TreeViewOverview(treeViewOverviewContainer); Composite propertyViewerContainer = new Composite(sideSash, SWT.BORDER); propertyViewerContainer.setLayout(new GridLayout()); PropertyViewer pv = new PropertyViewer(propertyViewerContainer); pv.setLayoutData(new GridData(GridData.FILL_BOTH)); mInvokeMethodPrompt = new InvokeMethodPrompt(propertyViewerContainer); mInvokeMethodPrompt.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Composite layoutViewerContainer = new Composite(sideSash, SWT.NONE); GridLayout layoutViewerLayout = new GridLayout(); layoutViewerLayout.marginWidth = layoutViewerLayout.marginHeight = 0; layoutViewerLayout.horizontalSpacing = layoutViewerLayout.verticalSpacing = 1; layoutViewerContainer.setLayout(layoutViewerLayout); Composite fullButtonBar = new Composite(layoutViewerContainer, SWT.NONE); fullButtonBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); GridLayout fullButtonBarLayout = new GridLayout(2, false); fullButtonBarLayout.marginWidth = fullButtonBarLayout.marginHeight = 0; fullButtonBarLayout.marginRight = 2; fullButtonBarLayout.horizontalSpacing = fullButtonBarLayout.verticalSpacing = 0; fullButtonBar.setLayout(fullButtonBarLayout); Composite buttonBar = new Composite(fullButtonBar, SWT.NONE); buttonBar.setLayoutData(new GridData(GridData.FILL_VERTICAL)); RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); rowLayout.marginLeft = rowLayout.marginRight = rowLayout.marginTop = rowLayout.marginBottom = 0; rowLayout.pack = true; rowLayout.center = true; buttonBar.setLayout(rowLayout); mOnBlackWhiteButton = new Button(buttonBar, SWT.PUSH); mOnBlackWhiteButton.setImage(mOnWhiteImage); mOnBlackWhiteButton.addSelectionListener(onBlackWhiteSelectionListener); mOnBlackWhiteButton.setToolTipText("Change layout viewer background color"); mShowExtras = new Button(buttonBar, SWT.CHECK); mShowExtras.setText("Show Extras"); mShowExtras.addSelectionListener(showExtrasSelectionListener); mShowExtras.setToolTipText("Show images"); ActionButton loadAllViewsButton = new ActionButton(fullButtonBar, LoadAllViewsAction.getAction()); loadAllViewsButton.setLayoutData(new GridData(GridData.END, GridData.CENTER, true, true)); loadAllViewsButton.addSelectionListener(loadAllViewsSelectionListener); Composite layoutViewerMainContainer = new Composite(layoutViewerContainer, SWT.BORDER); layoutViewerMainContainer.setLayoutData(new GridData(GridData.FILL_BOTH)); layoutViewerMainContainer.setLayout(new FillLayout()); mLayoutViewer = new LayoutViewer(layoutViewerMainContainer); sideSash.SASH_WIDTH = 4; sideSash.setWeights(new int[] { 238, 332, 416 }); } private void buildPixelPerfectPanel(Composite parent) { mPixelPerfectPanel = new Composite(parent, SWT.NONE); GridLayout gridLayout = new GridLayout(); gridLayout.marginWidth = gridLayout.marginHeight = 0; gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 0; mPixelPerfectPanel.setLayout(gridLayout); Composite buttonPanel = new Composite(mPixelPerfectPanel, SWT.NONE); buttonPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); GridLayout buttonLayout = new GridLayout(); buttonLayout.marginWidth = buttonLayout.marginHeight = 0; buttonLayout.horizontalSpacing = buttonLayout.verticalSpacing = 0; buttonPanel.setLayout(buttonLayout); Composite innerButtonPanel = new Composite(buttonPanel, SWT.NONE); innerButtonPanel.setLayoutData(new GridData(GridData.FILL_VERTICAL)); GridLayout innerButtonPanelLayout = new GridLayout(6, true); innerButtonPanelLayout.marginWidth = innerButtonPanelLayout.marginHeight = 2; innerButtonPanelLayout.horizontalSpacing = innerButtonPanelLayout.verticalSpacing = 2; innerButtonPanel.setLayout(innerButtonPanelLayout); ActionButton saveTreeView = new ActionButton(innerButtonPanel, SavePixelPerfectAction.getAction(getShell())); saveTreeView.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton refreshPixelPerfect = new ActionButton(innerButtonPanel, RefreshPixelPerfectAction.getAction()); refreshPixelPerfect.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton refreshPixelPerfectTree = new ActionButton(innerButtonPanel, RefreshPixelPerfectTreeAction.getAction()); refreshPixelPerfectTree.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton loadOverlay = new ActionButton(innerButtonPanel, LoadOverlayAction.getAction(getShell())); loadOverlay.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton showInLoupe = new ActionButton(innerButtonPanel, ShowOverlayAction.getAction()); showInLoupe.setLayoutData(new GridData(GridData.FILL_BOTH)); ActionButton autoRefresh = new ActionButton(innerButtonPanel, PixelPerfectAutoRefreshAction.getAction()); autoRefresh.setLayoutData(new GridData(GridData.FILL_BOTH)); SashForm mainSash = new SashForm(mPixelPerfectPanel, SWT.HORIZONTAL | SWT.SMOOTH); mainSash.setLayoutData(new GridData(GridData.FILL_BOTH)); mainSash.SASH_WIDTH = 4; Composite pixelPerfectTreeContainer = new Composite(mainSash, SWT.BORDER); pixelPerfectTreeContainer.setLayout(new FillLayout()); new PixelPerfectTree(pixelPerfectTreeContainer); Composite pixelPerfectLoupeContainer = new Composite(mainSash, SWT.NONE); GridLayout loupeLayout = new GridLayout(); loupeLayout.marginWidth = loupeLayout.marginHeight = 0; loupeLayout.horizontalSpacing = loupeLayout.verticalSpacing = 0; pixelPerfectLoupeContainer.setLayout(loupeLayout); Composite pixelPerfectLoupeBorder = new Composite(pixelPerfectLoupeContainer, SWT.BORDER); pixelPerfectLoupeBorder.setLayoutData(new GridData(GridData.FILL_BOTH)); GridLayout pixelPerfectLoupeBorderGridLayout = new GridLayout(); pixelPerfectLoupeBorderGridLayout.marginWidth = pixelPerfectLoupeBorderGridLayout.marginHeight = 0; pixelPerfectLoupeBorderGridLayout.horizontalSpacing = pixelPerfectLoupeBorderGridLayout.verticalSpacing = 0; pixelPerfectLoupeBorder.setLayout(pixelPerfectLoupeBorderGridLayout); mPixelPerfectLoupe = new PixelPerfectLoupe(pixelPerfectLoupeBorder); mPixelPerfectLoupe.setLayoutData(new GridData(GridData.FILL_BOTH)); PixelPerfectPixelPanel pixelPerfectPixelPanel = new PixelPerfectPixelPanel(pixelPerfectLoupeBorder); pixelPerfectPixelPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); PixelPerfectControls pixelPerfectControls = new PixelPerfectControls(pixelPerfectLoupeContainer); pixelPerfectControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Composite pixelPerfectContainer = new Composite(mainSash, SWT.BORDER); pixelPerfectContainer.setLayout(new FillLayout()); new PixelPerfect(pixelPerfectContainer); mainSash.setWeights(new int[] { 272, 376, 346 }); } public void showOverlayInLoupe(boolean value) { mPixelPerfectLoupe.setShowOverlay(value); } // Shows the progress notification... public void startTask(final String taskName) { mProgressString = taskName; Display.getDefault().syncExec(new Runnable() { @Override public void run() { if (mProgressLabel != null && mProgressBar != null) { mProgressLabel.setText(taskName); mProgressLabel.setVisible(true); mProgressBar.setVisible(true); mStatusBar.layout(); } } }); } // And hides it! public void endTask() { mProgressString = null; Display.getDefault().syncExec(new Runnable() { @Override public void run() { if (mProgressLabel != null && mProgressBar != null) { mProgressLabel.setVisible(false); mProgressBar.setVisible(false); } } }); } public void showDeviceSelector() { // Show the menus. MenuManager mm = getMenuBarManager(); mm.removeAll(); MenuManager file = new MenuManager("&File"); IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenuManager( APP_NAME, getShell().getDisplay(), file, AboutAction.getAction(getShell()), null /*preferencesAction*/, QuitAction.getAction()); if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) { mm.add(file); } MenuManager device = new MenuManager("&Devices"); mm.add(device); device.add(RefreshWindowsAction.getAction()); device.add(LoadViewHierarchyAction.getAction()); device.add(InspectScreenshotAction.getAction()); mm.updateAll(true); mDeviceViewButton.setSelection(true); mDeviceViewButton.setImage(mDeviceViewSelectedImage); mTreeViewButton.setSelection(false); mTreeViewButton.setImage(mTreeViewImage); mPixelPerfectButton.setSelection(false); mPixelPerfectButton.setImage(mPixelPerfectImage); mMainWindowStackLayout.topControl = mDeviceSelectorPanel; mMainWindow.layout(); mDeviceSelector.setFocus(); mTreeViewControls.setVisible(false); } public void showTreeView() { // Show the menus. MenuManager mm = getMenuBarManager(); mm.removeAll(); MenuManager file = new MenuManager("&File"); IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenuManager( APP_NAME, getShell().getDisplay(), file, AboutAction.getAction(getShell()), null /*preferencesAction*/, QuitAction.getAction()); if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) { mm.add(file); } MenuManager treeViewMenu = new MenuManager("&Tree View"); mm.add(treeViewMenu); treeViewMenu.add(SaveTreeViewAction.getAction(getShell())); treeViewMenu.add(CapturePSDAction.getAction(getShell())); treeViewMenu.add(new Separator()); treeViewMenu.add(RefreshViewAction.getAction()); treeViewMenu.add(DisplayViewAction.getAction(getShell())); IHvDevice hvDevice = DeviceSelectionModel.getModel().getSelectedDevice(); if (hvDevice.supportsDisplayListDump()) { treeViewMenu.add(DumpDisplayListAction.getAction()); dumpDisplayList.setVisible(true); } else { dumpDisplayList.setVisible(false); } treeViewMenu.add(new Separator()); treeViewMenu.add(InvalidateAction.getAction()); treeViewMenu.add(RequestLayoutAction.getAction()); mm.updateAll(true); mDeviceViewButton.setSelection(false); mDeviceViewButton.setImage(mDeviceViewImage); mTreeViewButton.setSelection(true); mTreeViewButton.setImage(mTreeViewSelectedImage); mInvokeMethodPrompt.setEnabled(hvDevice.isViewUpdateEnabled()); mPixelPerfectButton.setSelection(false); mPixelPerfectButton.setImage(mPixelPerfectImage); mMainWindowStackLayout.topControl = mTreeViewPanel; mMainWindow.layout(); mTreeView.setFocus(); mTreeViewControls.setVisible(true); } public void showPixelPerfect() { // Show the menus. MenuManager mm = getMenuBarManager(); mm.removeAll(); MenuManager file = new MenuManager("&File"); IMenuBarEnhancer enhancer = MenuBarEnhancer.setupMenuManager( APP_NAME, getShell().getDisplay(), file, AboutAction.getAction(getShell()), null /*preferencesAction*/, QuitAction.getAction()); if (enhancer.getMenuBarMode() == MenuBarMode.GENERIC) { mm.add(file); } MenuManager pixelPerfect = new MenuManager("&Pixel Perfect"); pixelPerfect.add(SavePixelPerfectAction.getAction(getShell())); pixelPerfect.add(RefreshPixelPerfectAction.getAction()); pixelPerfect.add(RefreshPixelPerfectTreeAction.getAction()); pixelPerfect.add(PixelPerfectAutoRefreshAction.getAction()); pixelPerfect.add(new Separator()); pixelPerfect.add(LoadOverlayAction.getAction(getShell())); pixelPerfect.add(ShowOverlayAction.getAction()); mm.add(pixelPerfect); mm.updateAll(true); mDeviceViewButton.setSelection(false); mDeviceViewButton.setImage(mDeviceViewImage); mTreeViewButton.setSelection(false); mTreeViewButton.setImage(mTreeViewImage); mPixelPerfectButton.setSelection(true); mPixelPerfectButton.setImage(mPixelPerfectSelectedImage); mMainWindowStackLayout.topControl = mPixelPerfectPanel; mMainWindow.layout(); mPixelPerfectLoupe.setFocus(); mTreeViewControls.setVisible(false); } private SelectionListener deviceViewButtonSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { mDeviceViewButton.setSelection(true); showDeviceSelector(); } }; private SelectionListener treeViewButtonSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { mTreeViewButton.setSelection(true); showTreeView(); } }; private SelectionListener pixelPerfectButtonSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { mPixelPerfectButton.setSelection(true); showPixelPerfect(); } }; private SelectionListener onBlackWhiteSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { if (mLayoutViewer.getOnBlack()) { mLayoutViewer.setOnBlack(false); mOnBlackWhiteButton.setImage(mOnBlackImage); } else { mLayoutViewer.setOnBlack(true); mOnBlackWhiteButton.setImage(mOnWhiteImage); } } }; private SelectionListener showExtrasSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { mLayoutViewer.setShowExtras(mShowExtras.getSelection()); } }; private SelectionListener loadAllViewsSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { mShowExtras.setSelection(true); showExtrasSelectionListener.widgetSelected(null); } }; private ITreeChangeListener mTreeChangeListener = new ITreeChangeListener() { @Override public void selectionChanged() { // pass } @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { if (TreeViewModel.getModel().getTree() == null) { showDeviceSelector(); mTreeViewButton.setEnabled(false); } else { showTreeView(); mTreeViewButton.setEnabled(true); } } }); } @Override public void viewportChanged() { // pass } @Override public void zoomChanged() { // pass } }; private IImageChangeListener mImageChangeListener = new IImageChangeListener() { @Override public void crosshairMoved() { // pass } @Override public void treeChanged() { // pass } @Override public void imageChanged() { // pass } @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { if (PixelPerfectModel.getModel().getImage() == null) { mPixelPerfectButton.setEnabled(false); showDeviceSelector(); } else { mPixelPerfectButton.setEnabled(true); showPixelPerfect(); } } }); } @Override public void overlayChanged() { // pass } @Override public void overlayTransparencyChanged() { // pass } @Override public void selectionChanged() { // pass } @Override public void zoomChanged() { // pass } }; public static void main(String[] args) { Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler()); Display.setAppName("HierarchyViewer"); new HierarchyViewerApplication().run(); } } ./PaxHeaders.X/hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplic0100644 0000000 0000000 00000000160 12747325007 032560 xustar000000000 0000000 112 path=hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplicationDirector.java hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/HierarchyViewerApplicationDirector.ja0100644 0000000 0000000 00000006121 12747325007 033204 0ustar000000000 0000000 /* * 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.hierarchyviewer; import com.android.SdkConstants; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import java.io.File; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * This is the application version of the director. */ public class HierarchyViewerApplicationDirector extends HierarchyViewerDirector { private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); public static HierarchyViewerDirector createDirector() { return sDirector = new HierarchyViewerApplicationDirector(); } @Override public void terminate() { super.terminate(); mExecutor.shutdown(); } /* * Gets the location of adb. The script that runs the hierarchy viewer * defines com.android.hierarchyviewer.bindir. */ @Override public String getAdbLocation() { String hvParentLocation = System.getProperty("com.android.hierarchyviewer.bindir"); //$NON-NLS-1$ // in the new SDK, adb is in the platform-tools, but when run from the command line // in the Android source tree, then adb is in $ANDROID_HOST_OUT/bin/adb if (hvParentLocation != null && hvParentLocation.length() != 0) { // check if there's a platform-tools folder File platformTools = new File(new File(hvParentLocation).getParent(), SdkConstants.FD_PLATFORM_TOOLS); if (platformTools.isDirectory()) { return platformTools.getAbsolutePath() + File.separator + SdkConstants.FN_ADB; } String androidOut = System.getenv("ANDROID_HOST_OUT"); if (androidOut != null) { return androidOut + File.separator + "bin" + File.separator + SdkConstants.FN_ADB; } } return SdkConstants.FN_ADB; } /* * In the application, we handle background tasks using a single thread, * just to get rid of possible race conditions that can occur. We update the * progress bar to show that we are doing something in the background. */ @Override public void executeInBackground(final String taskName, final Runnable task) { mExecutor.execute(new Runnable() { @Override public void run() { HierarchyViewerApplication.getMainWindow().startTask(taskName); task.run(); HierarchyViewerApplication.getMainWindow().endTask(); } }); } } hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/0040755 0000000 0000000 00000000000 12747325007 025353 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/AboutAction.java0100644 0000000 0000000 00000004017 12747325007 030425 0ustar000000000 0000000 /* * 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.hierarchyviewer.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewer.AboutDialog; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.actions.ImageAction; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class AboutAction extends Action implements ImageAction { private static AboutAction sAction; private Image mImage; private Shell mShell; private AboutAction(Shell shell) { super("&About"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'A'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("sdk-hierarchyviewer-16.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Shows the about dialog"); } public static AboutAction getAction(Shell shell) { if (sAction == null) { sAction = new AboutAction(shell); } return sAction; } @Override public void run() { new AboutDialog(mShell).open(); } @Override public Image getImage() { return mImage; } } hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/LoadAllViewsAction.java0100644 0000000 0000000 00000003712 12747325007 031702 0ustar000000000 0000000 /* * 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.hierarchyviewer.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.actions.ImageAction; import com.android.hierarchyviewerlib.actions.TreeViewEnabledAction; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class LoadAllViewsAction extends TreeViewEnabledAction implements ImageAction { private static LoadAllViewsAction sAction; private Image mImage; private LoadAllViewsAction() { super("Load All &Views"); setAccelerator(SWT.MOD1 + 'V'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("load-all-views.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Load all view images"); } public static LoadAllViewsAction getAction() { if (sAction == null) { sAction = new LoadAllViewsAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().loadAllViews(); } @Override public Image getImage() { return mImage; } } hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/QuitAction.java0100644 0000000 0000000 00000002331 12747325007 030272 0ustar000000000 0000000 /* * 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.hierarchyviewer.actions; import com.android.hierarchyviewer.HierarchyViewerApplication; import org.eclipse.jface.action.Action; import org.eclipse.swt.SWT; public class QuitAction extends Action { private static QuitAction sAction; private QuitAction() { super("E&xit"); setAccelerator(SWT.MOD1 + 'Q'); } public static QuitAction getAction() { if (sAction == null) { sAction = new QuitAction(); } return sAction; } @Override public void run() { HierarchyViewerApplication.getMainWindow().close(); } } hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/actions/ShowOverlayAction.java0100644 0000000 0000000 00000006534 12747325007 031643 0ustar000000000 0000000 /* * 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.hierarchyviewer.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewer.HierarchyViewerApplication; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.actions.ImageAction; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class ShowOverlayAction extends Action implements ImageAction, IImageChangeListener { private static ShowOverlayAction sAction; private Image mImage; private ShowOverlayAction() { super("Show In &Loupe", Action.AS_CHECK_BOX); setAccelerator(SWT.MOD1 + 'L'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("show-overlay.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Show the overlay in the loupe view"); setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null); PixelPerfectModel.getModel().addImageChangeListener(this); } public static ShowOverlayAction getAction() { if (sAction == null) { sAction = new ShowOverlayAction(); } return sAction; } @Override public void run() { HierarchyViewerApplication.getMainWindow().showOverlayInLoupe(sAction.isChecked()); } @Override public Image getImage() { return mImage; } @Override public void crosshairMoved() { // pass } @Override public void treeChanged() { // pass } @Override public void imageChanged() { // pass } @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); setEnabled(overlayImage != null); } }); } @Override public void overlayChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null); } }); } @Override public void overlayTransparencyChanged() { // pass } @Override public void selectionChanged() { // pass } @Override public void zoomChanged() { // pass } } hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/util/0040755 0000000 0000000 00000000000 12747325007 024670 5ustar000000000 0000000 hierarchyviewer2/app/src/main/java/com/android/hierarchyviewer/util/ActionButton.java0100644 0000000 0000000 00000005352 12747325007 030146 0ustar000000000 0000000 /* * 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.hierarchyviewer.util; import com.android.hierarchyviewerlib.actions.ImageAction; import org.eclipse.jface.action.Action; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; public class ActionButton implements IPropertyChangeListener, SelectionListener { private Button mButton; private Action mAction; public ActionButton(Composite parent, ImageAction action) { this.mAction = (Action) action; if (this.mAction.getStyle() == Action.AS_CHECK_BOX) { mButton = new Button(parent, SWT.CHECK); } else { mButton = new Button(parent, SWT.PUSH); } mButton.setText(action.getText()); mButton.setImage(action.getImage()); this.mAction.addPropertyChangeListener(this); mButton.addSelectionListener(this); mButton.setToolTipText(action.getToolTipText()); mButton.setEnabled(this.mAction.isEnabled()); } @Override public void propertyChange(PropertyChangeEvent e) { if (e.getProperty().toUpperCase().equals("ENABLED")) { //$NON-NLS-1$ mButton.setEnabled((Boolean) e.getNewValue()); } else if (e.getProperty().toUpperCase().equals("CHECKED")) { //$NON-NLS-1$ mButton.setSelection(mAction.isChecked()); } } public void setLayoutData(Object data) { mButton.setLayoutData(data); } @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { if (mAction.getStyle() == Action.AS_CHECK_BOX) { mAction.setChecked(mButton.getSelection()); } mAction.run(); } public void addSelectionListener(SelectionListener listener) { mButton.addSelectionListener(listener); } public void setVisible(boolean visible) { mButton.setVisible(visible); } } hierarchyviewer2/hierarchyviewer2lib/0040755 0000000 0000000 00000000000 12747325007 017052 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/.classpath0100644 0000000 0000000 00000001074 12747325007 021034 0ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/.project0100644 0000000 0000000 00000000572 12747325007 020522 0ustar000000000 0000000 hierarchyviewer2lib org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature hierarchyviewer2/hierarchyviewer2lib/.settings/0040755 0000000 0000000 00000000000 12747325007 020770 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/.settings/README.txt0100644 0000000 0000000 00000000203 12747325007 022456 0ustar000000000 0000000 Copy this in eclipse project as a .settings folder at the root. This ensure proper compilation compliance and warning/error levels.hierarchyviewer2/hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 025755 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 hierarchyviewer2/hierarchyviewer2lib/NOTICE0100644 0000000 0000000 00000024707 12747325007 017765 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS hierarchyviewer2/hierarchyviewer2lib/build.gradle0100644 0000000 0000000 00000000524 12747325007 021327 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'hierarchyviewer2lib' dependencies { compile project(':base:ddmlib') compile project(':swt:ddmuilib') compile 'com.google.guava:guava:15.0' testCompile 'junit:junit:3.8.1' } sourceSets { main.resources.srcDir 'src/main/java' test.resources.srcDir 'src/test/java' } hierarchyviewer2/hierarchyviewer2lib/hierarchyviewer2lib.iml0100644 0000000 0000000 00000001437 12747325007 023530 0ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/0040755 0000000 0000000 00000000000 12747325007 017641 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/main/0040755 0000000 0000000 00000000000 12747325007 020565 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/main/java/0040755 0000000 0000000 00000000000 12747325007 021506 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 022264 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 023704 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/0040755 0000000 0000000 00000000000 12747325007 027573 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/Hi0100644 0000000 0000000 00000000170 12747325007 032550 xustar000000000 0000000 120 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDir0100644 0000000 0000000 00000074166 12747325007 033270 0ustar000000000 0000000 /* * 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.hierarchyviewerlib; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.device.DeviceBridge; import com.android.hierarchyviewerlib.device.HvDeviceFactory; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.device.WindowUpdater; import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.ThemeModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import com.android.hierarchyviewerlib.ui.CaptureDisplay; import com.android.hierarchyviewerlib.ui.DumpThemeDisplay; import com.android.hierarchyviewerlib.ui.EvaluateContrastDisplay; import com.android.hierarchyviewerlib.ui.TreeView; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.PsdFile; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Shell; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; /** * This is the class where most of the logic resides. */ public abstract class HierarchyViewerDirector implements IDeviceChangeListener, IWindowChangeListener { private static final boolean sIsUsingDdmProtocol; static { String sHvProtoEnvVar = System.getenv("ANDROID_HVPROTO"); //$NON-NLS-1$ sIsUsingDdmProtocol = "ddm".equalsIgnoreCase(sHvProtoEnvVar); } protected static HierarchyViewerDirector sDirector; public static final String TAG = "hierarchyviewer"; private int mPixelPerfectRefreshesInProgress = 0; private Timer mPixelPerfectRefreshTimer = new Timer(); private boolean mAutoRefresh = false; public static final int DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL = 5; private int mPixelPerfectAutoRefreshInterval = DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL; private PixelPerfectAutoRefreshTask mCurrentAutoRefreshTask; private String mFilterText = ""; //$NON-NLS-1$ private static final Object mDevicesLock = new Object(); private Map mDevices = new HashMap(10); public static boolean isUsingDdmProtocol() { return sIsUsingDdmProtocol; } public void terminate() { WindowUpdater.terminate(); mPixelPerfectRefreshTimer.cancel(); } public abstract String getAdbLocation(); public static HierarchyViewerDirector getDirector() { return sDirector; } /** * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. * @param bridge the bridge object to use */ public void acquireBridge(AndroidDebugBridge bridge) { DeviceBridge.acquireBridge(bridge); } /** * Creates an {@link AndroidDebugBridge} connected to adb at the given location. * * If a bridge is already running, this disconnects it and creates a new one. * * @param adbLocation the location to adb. */ public void initDebugBridge() { DeviceBridge.initDebugBridge(getAdbLocation()); } public void stopDebugBridge() { DeviceBridge.terminate(); } public void populateDeviceSelectionModel() { IDevice[] devices = DeviceBridge.getDevices(); for (IDevice device : devices) { deviceConnected(device); } } public void startListenForDevices() { DeviceBridge.startListenForDevices(this); } public void stopListenForDevices() { DeviceBridge.stopListenForDevices(this); } public abstract void executeInBackground(String taskName, Runnable task); @Override public void deviceConnected(final IDevice device) { executeInBackground("Connecting device", new Runnable() { @Override public void run() { if (!device.isOnline()) { return; } IHvDevice hvDevice; synchronized (mDevicesLock) { hvDevice = mDevices.get(device); if (hvDevice == null) { hvDevice = HvDeviceFactory.create(device); hvDevice.initializeViewDebug(); hvDevice.addWindowChangeListener(getDirector()); mDevices.put(device, hvDevice); } else { // attempt re-initializing view server if device state has changed hvDevice.initializeViewDebug(); } } DeviceSelectionModel.getModel().addDevice(hvDevice); focusChanged(device); } }); } @Override public void deviceDisconnected(final IDevice device) { executeInBackground("Disconnecting device", new Runnable() { @Override public void run() { IHvDevice hvDevice; synchronized (mDevicesLock) { hvDevice = mDevices.get(device); if (hvDevice != null) { mDevices.remove(device); } } if (hvDevice == null) { return; } hvDevice.terminateViewDebug(); hvDevice.removeWindowChangeListener(getDirector()); DeviceSelectionModel.getModel().removeDevice(hvDevice); if (PixelPerfectModel.getModel().getDevice() == device) { PixelPerfectModel.getModel().setData(null, null, null); } Window treeViewWindow = TreeViewModel.getModel().getWindow(); if (treeViewWindow != null && treeViewWindow.getDevice() == device) { TreeViewModel.getModel().setData(null, null); mFilterText = ""; //$NON-NLS-1$ } } }); } @Override public void windowsChanged(final IDevice device) { executeInBackground("Refreshing windows", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(device); hvDevice.reloadWindows(); DeviceSelectionModel.getModel().updateDevice(hvDevice); } }); } @Override public void focusChanged(final IDevice device) { executeInBackground("Updating focus", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(device); int focusedWindow = hvDevice.getFocusedWindow(); DeviceSelectionModel.getModel().updateFocusedWindow(hvDevice, focusedWindow); } }); } @Override public void deviceChanged(IDevice device, int changeMask) { if ((changeMask & IDevice.CHANGE_STATE) != 0 && device.isOnline()) { deviceConnected(device); } } public void refreshPixelPerfect() { final IDevice device = PixelPerfectModel.getModel().getDevice(); if (device != null) { // Some interesting logic here. We don't want to refresh the pixel // perfect view 1000 times in a row if the focus keeps changing. We // just // want it to refresh following the last focus change. boolean proceed = false; synchronized (this) { if (mPixelPerfectRefreshesInProgress <= 1) { proceed = true; mPixelPerfectRefreshesInProgress++; } } if (proceed) { executeInBackground("Refreshing pixel perfect screenshot", new Runnable() { @Override public void run() { Image screenshotImage = getScreenshotImage(getHvDevice(device)); if (screenshotImage != null) { PixelPerfectModel.getModel().setImage(screenshotImage); } synchronized (HierarchyViewerDirector.this) { mPixelPerfectRefreshesInProgress--; } } }); } } } public void refreshPixelPerfectTree() { final IDevice device = PixelPerfectModel.getModel().getDevice(); if (device != null) { executeInBackground("Refreshing pixel perfect tree", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(device); ViewNode viewNode = hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice)); if (viewNode != null) { PixelPerfectModel.getModel().setTree(viewNode); } } }); } } public void loadPixelPerfectData(final IHvDevice hvDevice) { executeInBackground("Loading pixel perfect data", new Runnable() { @Override public void run() { Image screenshotImage = getScreenshotImage(hvDevice); if (screenshotImage != null) { ViewNode viewNode = hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice)); if (viewNode != null) { PixelPerfectModel.getModel().setData(hvDevice.getDevice(), screenshotImage, viewNode); } } } }); } private IHvDevice getHvDevice(IDevice device) { synchronized (mDevicesLock) { return mDevices.get(device); } } private Image getScreenshotImage(IHvDevice hvDevice) { return (hvDevice == null) ? null : hvDevice.getScreenshotImage(); } public void loadViewTreeData(final Window window) { executeInBackground("Loading view hierarchy", new Runnable() { @Override public void run() { mFilterText = ""; //$NON-NLS-1$ IHvDevice hvDevice = window.getHvDevice(); ViewNode viewNode = hvDevice.loadWindowData(window); if (viewNode != null) { viewNode.setViewCount(); TreeViewModel.getModel().setData(window, viewNode); } } }); } public void loadOverlay(final Shell shell) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { FileDialog fileDialog = new FileDialog(shell, SWT.OPEN); fileDialog.setFilterExtensions(new String[] { "*.jpg;*.jpeg;*.png;*.gif;*.bmp" //$NON-NLS-1$ }); fileDialog.setFilterNames(new String[] { "Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)" }); fileDialog.setText("Choose an overlay image"); String fileName = fileDialog.open(); if (fileName != null) { try { Image image = new Image(Display.getDefault(), fileName); PixelPerfectModel.getModel().setOverlayImage(image); } catch (SWTException e) { Log.e(TAG, "Unable to load image from " + fileName); } } } }); } public void showCapture(final Shell shell, final ViewNode viewNode) { executeInBackground("Capturing node", new Runnable() { @Override public void run() { final Image image = loadCapture(viewNode); if (image != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { CaptureDisplay.show(shell, viewNode, image); } }); } } }); } public void showEvaluateContrast(final Shell shell) { executeInBackground("Capturing node and evaluating contrast", new Runnable() { @Override public void run() { mFilterText = ""; //$NON-NLS-1$ Window window = TreeViewModel.getModel().getWindow(); IHvDevice hvDevice = window.getHvDevice(); final ViewNode viewNode = hvDevice.loadWindowData(window); if (viewNode != null) { viewNode.setViewCount(); TreeViewModel.getModel().setData(window, viewNode); } final Image image = loadCapture(viewNode); if (image != null && viewNode != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { EvaluateContrastDisplay.show(shell, viewNode, image); } }); } } }); } public void showDumpTheme(final Shell shell) { executeInBackground("Capturing node and dumping theme", new Runnable() { @Override public void run() { ViewNode viewNode; Window window = TreeViewModel.getModel().getWindow(); IHvDevice hvDevice = window.getHvDevice(); DrawableViewNode tree = TreeViewModel.getModel().getTree(); if (tree == null) { viewNode = hvDevice.loadWindowData(window); } else { viewNode = tree.viewNode; } final ThemeModel model = hvDevice.dumpTheme(viewNode); if (model != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { DumpThemeDisplay.show(shell, model); } }); } else { Log.e(TAG, "Unable to dump theme."); } } }); } public Image loadCapture(ViewNode viewNode) { IHvDevice hvDevice = viewNode.window.getHvDevice(); final Image image = hvDevice.loadCapture(viewNode.window, viewNode); if (image != null) { viewNode.image = image; // Force the layout viewer to redraw. TreeViewModel.getModel().notifySelectionChanged(); } return image; } public void loadCaptureInBackground(final ViewNode viewNode) { executeInBackground("Capturing node", new Runnable() { @Override public void run() { loadCapture(viewNode); } }); } public void showCapture(Shell shell) { DrawableViewNode viewNode = TreeViewModel.getModel().getSelection(); if (viewNode != null) { showCapture(shell, viewNode.viewNode); } } public void refreshWindows() { executeInBackground("Refreshing windows", new Runnable() { @Override public void run() { IHvDevice[] hvDevicesA = DeviceSelectionModel.getModel().getDevices(); IDevice[] devicesA = new IDevice[hvDevicesA.length]; for (int i = 0; i < hvDevicesA.length; i++) { devicesA[i] = hvDevicesA[i].getDevice(); } IDevice[] devicesB = DeviceBridge.getDevices(); HashSet deviceSet = new HashSet(); for (int i = 0; i < devicesB.length; i++) { deviceSet.add(devicesB[i]); } for (int i = 0; i < devicesA.length; i++) { if (deviceSet.contains(devicesA[i])) { windowsChanged(devicesA[i]); deviceSet.remove(devicesA[i]); } else { deviceDisconnected(devicesA[i]); } } for (IDevice device : deviceSet) { deviceConnected(device); } } }); } public void loadViewHierarchy() { Window window = DeviceSelectionModel.getModel().getSelectedWindow(); if (window != null) { loadViewTreeData(window); } } public void inspectScreenshot() { IHvDevice device = DeviceSelectionModel.getModel().getSelectedDevice(); if (device != null) { loadPixelPerfectData(device); } } public void saveTreeView(final Shell shell) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { final DrawableViewNode viewNode = TreeViewModel.getModel().getTree(); if (viewNode != null) { FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); fileDialog.setFilterExtensions(new String[] { "*.png" //$NON-NLS-1$ }); fileDialog.setFilterNames(new String[] { "Portable Network Graphics File (*.png)" }); fileDialog.setText("Choose where to save the tree image"); final String fileName = fileDialog.open(); if (fileName != null) { executeInBackground("Saving tree view", new Runnable() { @Override public void run() { Image image = TreeView.paintToImage(viewNode); ImageLoader imageLoader = new ImageLoader(); imageLoader.data = new ImageData[] { image.getImageData() }; String extensionedFileName = fileName; if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ extensionedFileName += ".png"; //$NON-NLS-1$ } try { imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); } catch (SWTException e) { Log.e(TAG, "Unable to save tree view as a PNG image at " + fileName); } image.dispose(); } }); } } } }); } public void savePixelPerfect(final Shell shell) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { Image untouchableImage = PixelPerfectModel.getModel().getImage(); if (untouchableImage != null) { final ImageData imageData = untouchableImage.getImageData(); FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); fileDialog.setFilterExtensions(new String[] { "*.png" //$NON-NLS-1$ }); fileDialog.setFilterNames(new String[] { "Portable Network Graphics File (*.png)" }); fileDialog.setText("Choose where to save the screenshot"); final String fileName = fileDialog.open(); if (fileName != null) { executeInBackground("Saving pixel perfect", new Runnable() { @Override public void run() { ImageLoader imageLoader = new ImageLoader(); imageLoader.data = new ImageData[] { imageData }; String extensionedFileName = fileName; if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ extensionedFileName += ".png"; //$NON-NLS-1$ } try { imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); } catch (SWTException e) { Log.e(TAG, "Unable to save tree view as a PNG image at " + fileName); } } }); } } } }); } public void capturePSD(final Shell shell) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { final Window window = TreeViewModel.getModel().getWindow(); if (window != null) { FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); fileDialog.setFilterExtensions(new String[] { "*.psd" //$NON-NLS-1$ }); fileDialog.setFilterNames(new String[] { "Photoshop Document (*.psd)" }); fileDialog.setText("Choose where to save the window layers"); final String fileName = fileDialog.open(); if (fileName != null) { executeInBackground("Saving window layers", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(window.getDevice()); PsdFile psdFile = hvDevice.captureLayers(window); if (psdFile != null) { String extensionedFileName = fileName; if (!extensionedFileName.toLowerCase().endsWith(".psd")) { //$NON-NLS-1$ extensionedFileName += ".psd"; //$NON-NLS-1$ } try { psdFile.write(new FileOutputStream(extensionedFileName)); } catch (FileNotFoundException e) { Log.e(TAG, "Unable to write to file " + fileName); } } } }); } } } }); } public void reloadViewHierarchy() { Window window = TreeViewModel.getModel().getWindow(); if (window != null) { loadViewTreeData(window); } } public void invalidateCurrentNode() { final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); if (selectedNode != null) { executeInBackground("Invalidating view", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); hvDevice.invalidateView(selectedNode.viewNode); } }); } } public void relayoutCurrentNode() { final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); if (selectedNode != null) { executeInBackground("Request layout", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); hvDevice.requestLayout(selectedNode.viewNode); } }); } } public void dumpDisplayListForCurrentNode() { final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); if (selectedNode != null) { executeInBackground("Dump displaylist", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); hvDevice.outputDisplayList(selectedNode.viewNode); } }); } } public void profileCurrentNode() { final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); if (selectedNode != null) { executeInBackground("Profile Node", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); hvDevice.loadProfileData(selectedNode.viewNode.window, selectedNode.viewNode); // Force the layout viewer to redraw. TreeViewModel.getModel().notifySelectionChanged(); } }); } } public void invokeMethodOnSelectedView(final String method, final List args) { final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); if (selectedNode != null) { executeInBackground("Invoke View Method", new Runnable() { @Override public void run() { IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); hvDevice.invokeViewMethod(selectedNode.viewNode.window, selectedNode.viewNode, method, args); } }); } } public void loadAllViews() { executeInBackground("Loading all views", new Runnable() { @Override public void run() { DrawableViewNode tree = TreeViewModel.getModel().getTree(); if (tree != null) { loadViewRecursive(tree.viewNode); // Force the layout viewer to redraw. TreeViewModel.getModel().notifySelectionChanged(); } } }); } private void loadViewRecursive(ViewNode viewNode) { IHvDevice hvDevice = getHvDevice(viewNode.window.getDevice()); Image image = hvDevice.loadCapture(viewNode.window, viewNode); if (image == null) { return; } viewNode.image = image; final int N = viewNode.children.size(); for (int i = 0; i < N; i++) { loadViewRecursive(viewNode.children.get(i)); } } public void filterNodes(String filterText) { this.mFilterText = filterText; DrawableViewNode tree = TreeViewModel.getModel().getTree(); if (tree != null) { tree.viewNode.filter(filterText); // Force redraw TreeViewModel.getModel().notifySelectionChanged(); } } public String getFilterText() { return mFilterText; } private static class PixelPerfectAutoRefreshTask extends TimerTask { @Override public void run() { HierarchyViewerDirector.getDirector().refreshPixelPerfect(); } }; public void setPixelPerfectAutoRefresh(boolean value) { synchronized (mPixelPerfectRefreshTimer) { if (value == mAutoRefresh) { return; } mAutoRefresh = value; if (mAutoRefresh) { mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, mPixelPerfectAutoRefreshInterval * 1000, mPixelPerfectAutoRefreshInterval * 1000); } else { mCurrentAutoRefreshTask.cancel(); mCurrentAutoRefreshTask = null; } } } public void setPixelPerfectAutoRefreshInterval(int value) { synchronized (mPixelPerfectRefreshTimer) { if (mPixelPerfectAutoRefreshInterval == value) { return; } mPixelPerfectAutoRefreshInterval = value; if (mAutoRefresh) { mCurrentAutoRefreshTask.cancel(); long timeLeft = Math.max(0, mPixelPerfectAutoRefreshInterval * 1000 - (System.currentTimeMillis() - mCurrentAutoRefreshTask .scheduledExecutionTime())); mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, timeLeft, mPixelPerfectAutoRefreshInterval * 1000); } } } public int getPixelPerfectAutoRefreshInverval() { return mPixelPerfectAutoRefreshInterval; } } hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/0040755 0000000 0000000 00000000000 12747325007 031233 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000171 12747325007 032574 xustar000000000 0000000 121 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSD0100644 0000000 0000000 00000003732 12747325007 033132 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class CapturePSDAction extends TreeViewEnabledAction implements ImageAction { private static CapturePSDAction sAction; private Image mImage; private Shell mShell; private CapturePSDAction(Shell shell) { super("&Capture Layers"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'C'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("capture-psd.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Capture the window layers as a photoshop document"); } public static CapturePSDAction getAction(Shell shell) { if (sAction == null) { sAction = new CapturePSDAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().capturePSD(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000172 12747325007 032575 xustar000000000 0000000 122 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayVie0100644 0000000 0000000 00000003741 12747325007 033231 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class DisplayViewAction extends SelectedNodeEnabledAction implements ImageAction { private static DisplayViewAction sAction; private Image mImage; private Shell mShell; private DisplayViewAction(Shell shell) { super("&Display View"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'D'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Display the selected view image in a separate window"); } public static DisplayViewAction getAction(Shell shell) { if (sAction == null) { sAction = new DisplayViewAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().showCapture(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000176 12747325007 032601 xustar000000000 0000000 126 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDispla0100644 0000000 0000000 00000003523 12747325007 033220 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class DumpDisplayListAction extends SelectedNodeEnabledAction implements ImageAction { private static DumpDisplayListAction sAction; private Image mImage; private DumpDisplayListAction() { super("Dump DisplayList"); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Request the view to output its displaylist to logcat"); } public static DumpDisplayListAction getAction() { if (sAction == null) { sAction = new DumpDisplayListAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().dumpDisplayListForCurrentNode(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000170 12747325007 032573 xustar000000000 0000000 120 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeA0100644 0000000 0000000 00000003207 12747325007 033146 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.actions; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.action.Action; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Shell; public class DumpThemeAction extends Action implements ImageAction { private static DumpThemeAction sAction; private Image mImage; private Shell mShell; private DumpThemeAction(Shell shell) { super("&Dump Theme"); mShell = shell; setAccelerator(SWT.MOD1 + 'D'); setToolTipText("Dumping the resources in this View's Theme."); // TODO: Get icon for Button } public static DumpThemeAction getAction(Shell shell) { if (sAction == null) { sAction = new DumpThemeAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().showDumpTheme(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000177 12747325007 032602 xustar000000000 0000000 127 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateCo0100644 0000000 0000000 00000003472 12747325007 033211 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class EvaluateContrastAction extends Action implements ImageAction { private static EvaluateContrastAction sAction; private Image mImage; private Shell mShell; private EvaluateContrastAction(Shell shell) { super("&Evaluate Contrast"); mShell = shell; setAccelerator(SWT.MOD1 + 'D'); setToolTipText("Evaluate the contrast ratio of this view."); // TODO: Get icon for Button } public static EvaluateContrastAction getAction(Shell shell) { if (sAction == null) { sAction = new EvaluateContrastAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().showEvaluateContrast(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000164 12747325007 032576 xustar000000000 0000000 116 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageActio0100644 0000000 0000000 00000001507 12747325007 033160 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import org.eclipse.swt.graphics.Image; public interface ImageAction { public Image getImage(); public String getText(); public String getToolTipText(); } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000200 12747325007 032565 xustar000000000 0000000 128 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScr0100644 0000000 0000000 00000006015 12747325007 033232 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; import com.android.hierarchyviewerlib.models.Window; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class InspectScreenshotAction extends Action implements ImageAction, IWindowChangeListener { private static InspectScreenshotAction sAction; private Image mImage; private InspectScreenshotAction() { super("Inspect &Screenshot"); setAccelerator(SWT.MOD1 + 'S'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("inspect-screenshot.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Inspect a screenshot in the pixel perfect view"); setEnabled( DeviceSelectionModel.getModel().getSelectedDevice() != null); DeviceSelectionModel.getModel().addWindowChangeListener(this); } public static InspectScreenshotAction getAction() { if (sAction == null) { sAction = new InspectScreenshotAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().inspectScreenshot(); } @Override public Image getImage() { return mImage; } @Override public void deviceChanged(IHvDevice device) { // pass } @Override public void deviceConnected(IHvDevice device) { // pass } @Override public void deviceDisconnected(IHvDevice device) { // pass } @Override public void focusChanged(IHvDevice device) { // pass } @Override public void selectionChanged(final IHvDevice device, final Window window) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { InspectScreenshotAction.getAction().setEnabled(device != null); } }); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000171 12747325007 032574 xustar000000000 0000000 121 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/Invalidate0100644 0000000 0000000 00000003547 12747325007 033244 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class InvalidateAction extends SelectedNodeEnabledAction implements ImageAction { private static InvalidateAction sAction; private Image mImage; private InvalidateAction() { super("&Invalidate Layout"); setAccelerator(SWT.MOD1 + 'I'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("invalidate.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Invalidate the layout for the current window"); } public static InvalidateAction getAction() { if (sAction == null) { sAction = new InvalidateAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().invalidateCurrentNode(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000172 12747325007 032575 xustar000000000 0000000 122 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverla0100644 0000000 0000000 00000003731 12747325007 033207 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class LoadOverlayAction extends PixelPerfectEnabledAction implements ImageAction { private static LoadOverlayAction sAction; private Image mImage; private Shell mShell; private LoadOverlayAction(Shell shell) { super("Load &Overlay"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'O'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("load-overlay.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Load an image to overlay the screenshot"); } public static LoadOverlayAction getAction(Shell shell) { if (sAction == null) { sAction = new LoadOverlayAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().loadOverlay(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000200 12747325007 032565 xustar000000000 0000000 128 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHi0100644 0000000 0000000 00000006013 12747325007 033146 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; import com.android.hierarchyviewerlib.models.Window; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class LoadViewHierarchyAction extends Action implements ImageAction, IWindowChangeListener { private static LoadViewHierarchyAction sAction; private Image mImage; private LoadViewHierarchyAction() { super("Load View &Hierarchy"); setAccelerator(SWT.MOD1 + 'H'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Load the view hierarchy into the tree view"); setEnabled( DeviceSelectionModel.getModel().getSelectedWindow() != null); DeviceSelectionModel.getModel().addWindowChangeListener(this); } public static LoadViewHierarchyAction getAction() { if (sAction == null) { sAction = new LoadViewHierarchyAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().loadViewHierarchy(); } @Override public Image getImage() { return mImage; } @Override public void deviceChanged(IHvDevice device) { // pass } @Override public void deviceConnected(IHvDevice device) { // pass } @Override public void deviceDisconnected(IHvDevice device) { // pass } @Override public void focusChanged(IHvDevice device) { // pass } @Override public void selectionChanged(final IHvDevice device, final Window window) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { LoadViewHierarchyAction.getAction().setEnabled(window != null); } }); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000206 12747325007 032573 xustar000000000 0000000 134 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfe0100644 0000000 0000000 00000003762 12747325007 033226 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class PixelPerfectAutoRefreshAction extends PixelPerfectEnabledAction implements ImageAction { private static PixelPerfectAutoRefreshAction sAction; private Image mImage; private PixelPerfectAutoRefreshAction() { super("Auto &Refresh", Action.AS_CHECK_BOX); setAccelerator(SWT.MOD1 + 'R'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("auto-refresh.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Automatically refresh the screenshot"); } public static PixelPerfectAutoRefreshAction getAction() { if (sAction == null) { sAction = new PixelPerfectAutoRefreshAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefresh(sAction.isChecked()); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000202 12747325007 032567 xustar000000000 0000000 130 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfe0100644 0000000 0000000 00000004240 12747325007 033216 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.jface.action.Action; import org.eclipse.swt.widgets.Display; public class PixelPerfectEnabledAction extends Action implements IImageChangeListener { public PixelPerfectEnabledAction(String name) { super(name); setEnabled(PixelPerfectModel.getModel().getImage() != null); PixelPerfectModel.getModel().addImageChangeListener(this); } public PixelPerfectEnabledAction(String name, int type) { super(name, type); setEnabled(PixelPerfectModel.getModel().getImage() != null); PixelPerfectModel.getModel().addImageChangeListener(this); } @Override public void crosshairMoved() { // pass } @Override public void imageChanged() { // } @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { setEnabled(PixelPerfectModel.getModel().getImage() != null); } }); } @Override public void overlayChanged() { // pass } @Override public void overlayTransparencyChanged() { // pass } @Override public void selectionChanged() { // pass } @Override public void treeChanged() { // pass } @Override public void zoomChanged() { // pass } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000173 12747325007 032576 xustar000000000 0000000 123 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNod0100644 0000000 0000000 00000003447 12747325007 033224 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class ProfileNodesAction extends SelectedNodeEnabledAction implements ImageAction { private static ProfileNodesAction sAction; private Image mImage; public ProfileNodesAction() { super("Profile Node"); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("profile.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Obtain layout times for tree rooted at selected node"); } public static ProfileNodesAction getAction() { if (sAction == null) { sAction = new ProfileNodesAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().profileCurrentNode(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000202 12747325007 032567 xustar000000000 0000000 130 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPix0100644 0000000 0000000 00000003572 12747325007 033241 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class RefreshPixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { private static RefreshPixelPerfectAction sAction; private Image mImage; private RefreshPixelPerfectAction() { super("&Refresh Screenshot"); setAccelerator(SWT.F5); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Refresh the screenshot"); } public static RefreshPixelPerfectAction getAction() { if (sAction == null) { sAction = new RefreshPixelPerfectAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().refreshPixelPerfect(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000206 12747325007 032573 xustar000000000 0000000 134 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPix0100644 0000000 0000000 00000003622 12747325007 033235 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class RefreshPixelPerfectTreeAction extends PixelPerfectEnabledAction implements ImageAction { private static RefreshPixelPerfectTreeAction sAction; private Image mImage; private RefreshPixelPerfectTreeAction() { super("Refresh &Tree"); setAccelerator(SWT.MOD1 + 'T'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Refresh the tree"); } public static RefreshPixelPerfectTreeAction getAction() { if (sAction == null) { sAction = new RefreshPixelPerfectTreeAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().refreshPixelPerfectTree(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000172 12747325007 032575 xustar000000000 0000000 122 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshVie0100644 0000000 0000000 00000003536 12747325007 033224 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class RefreshViewAction extends TreeViewEnabledAction implements ImageAction { private static RefreshViewAction sAction; private Image mImage; private RefreshViewAction() { super("Load View &Hierarchy"); setAccelerator(SWT.MOD1 + 'H'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Reload the view hierarchy"); } public static RefreshViewAction getAction() { if (sAction == null) { sAction = new RefreshViewAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().reloadViewHierarchy(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000175 12747325007 032600 xustar000000000 0000000 125 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWin0100644 0000000 0000000 00000003553 12747325007 033235 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class RefreshWindowsAction extends Action implements ImageAction { private static RefreshWindowsAction sAction; private Image mImage; private RefreshWindowsAction() { super("&Refresh"); setAccelerator(SWT.F5); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Refresh the list of devices"); } public static RefreshWindowsAction getAction() { if (sAction == null) { sAction = new RefreshWindowsAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().refreshWindows(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000174 12747325007 032577 xustar000000000 0000000 124 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLay0100644 0000000 0000000 00000003544 12747325007 033257 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; public class RequestLayoutAction extends SelectedNodeEnabledAction implements ImageAction { private static RequestLayoutAction sAction; private Image mImage; private RequestLayoutAction() { super("Request &Layout"); setAccelerator(SWT.MOD1 + 'L'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("request-layout.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Request the view to lay out"); } public static RequestLayoutAction getAction() { if (sAction == null) { sAction = new RequestLayoutAction(); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().relayoutCurrentNode(); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000177 12747325007 032602 xustar000000000 0000000 127 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelP0100644 0000000 0000000 00000003751 12747325007 033201 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class SavePixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { private static SavePixelPerfectAction sAction; private Image mImage; private Shell mShell; private SavePixelPerfectAction(Shell shell) { super("&Save as PNG"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'S'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Save the screenshot as a PNG image"); } public static SavePixelPerfectAction getAction(Shell shell) { if (sAction == null) { sAction = new SavePixelPerfectAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().savePixelPerfect(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000173 12747325007 032576 xustar000000000 0000000 123 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeVi0100644 0000000 0000000 00000003714 12747325007 033175 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class SaveTreeViewAction extends TreeViewEnabledAction implements ImageAction { private static SaveTreeViewAction sAction; private Image mImage; private Shell mShell; private SaveTreeViewAction(Shell shell) { super("&Save as PNG"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'S'); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Save the tree view as a PNG image"); } public static SaveTreeViewAction getAction(Shell shell) { if (sAction == null) { sAction = new SaveTreeViewAction(shell); } return sAction; } @Override public void run() { HierarchyViewerDirector.getDirector().saveTreeView(mShell); } @Override public Image getImage() { return mImage; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000202 12747325007 032567 xustar000000000 0000000 130 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNo0100644 0000000 0000000 00000003766 12747325007 033214 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import org.eclipse.jface.action.Action; import org.eclipse.swt.widgets.Display; public class SelectedNodeEnabledAction extends Action implements ITreeChangeListener { public SelectedNodeEnabledAction(String name) { super(name); setEnabled(TreeViewModel.getModel().getTree() != null && TreeViewModel.getModel().getSelection() != null); TreeViewModel.getModel().addTreeChangeListener(this); } @Override public void selectionChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { setEnabled(TreeViewModel.getModel().getTree() != null && TreeViewModel.getModel().getSelection() != null); } }); } @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { setEnabled(TreeViewModel.getModel().getTree() != null && TreeViewModel.getModel().getSelection() != null); } }); } @Override public void viewportChanged() { } @Override public void zoomChanged() { } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ac0100644 0000000 0000000 00000000176 12747325007 032601 xustar000000000 0000000 126 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEn0100644 0000000 0000000 00000003126 12747325007 033172 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.actions; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import org.eclipse.jface.action.Action; import org.eclipse.swt.widgets.Display; public class TreeViewEnabledAction extends Action implements ITreeChangeListener { public TreeViewEnabledAction(String name) { super(name); setEnabled(TreeViewModel.getModel().getTree() != null); TreeViewModel.getModel().addTreeChangeListener(this); } @Override public void selectionChanged() { // pass } @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { setEnabled(TreeViewModel.getModel().getTree() != null); } }); } @Override public void viewportChanged() { } @Override public void zoomChanged() { } } hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/0040755 0000000 0000000 00000000000 12747325007 031032 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000170 12747325007 032600 xustar000000000 0000000 120 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvD0100644 0000000 0000000 00000005120 12747325007 033115 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.device; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.RawImage; import com.android.ddmlib.TimeoutException; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.widgets.Display; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; public abstract class AbstractHvDevice implements IHvDevice { private static final String TAG = "HierarchyViewer"; @Override public Image getScreenshotImage() { IDevice device = getDevice(); final AtomicReference imageRef = new AtomicReference(); try { final RawImage screenshot = device.getScreenshot(); if (screenshot == null) { return null; } Display.getDefault().syncExec(new Runnable() { @Override public void run() { ImageData imageData = new ImageData(screenshot.width, screenshot.height, screenshot.bpp, new PaletteData(screenshot.getRedMask(), screenshot .getGreenMask(), screenshot.getBlueMask()), 1, screenshot.data); imageRef.set(new Image(Display.getDefault(), imageData)); } }); return imageRef.get(); } catch (IOException e) { Log.e(TAG, "Unable to load screenshot from device " + device.getName()); } catch (TimeoutException e) { Log.e(TAG, "Timeout loading screenshot from device " + device.getName()); } catch (AdbCommandRejectedException e) { Log.e(TAG, "Adb rejected command to load screenshot from device " + device.getName()); } return null; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000172 12747325007 032602 xustar000000000 0000000 122 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebu0100644 0000000 0000000 00000031673 12747325007 033123 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.device; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.HandleViewDebug; import com.android.ddmlib.HandleViewDebug.ViewDumpHandler; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; import com.android.hierarchyviewerlib.models.ThemeModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import com.android.hierarchyviewerlib.ui.util.PsdFile; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; public class DdmViewDebugDevice extends AbstractHvDevice implements IDeviceChangeListener { private static final String TAG = "DdmViewDebugDevice"; private final IDevice mDevice; private Map> mViewRootsPerClient = new HashMap>(40); public DdmViewDebugDevice(IDevice device) { mDevice = device; } @Override public boolean initializeViewDebug() { AndroidDebugBridge.addDeviceChangeListener(this); return reloadWindows(); } private static class ListViewRootsHandler extends ViewDumpHandler { private List mViewRoots = Collections.synchronizedList(new ArrayList(10)); public ListViewRootsHandler() { super(HandleViewDebug.CHUNK_VULW); } @Override protected void handleViewDebugResult(ByteBuffer data) { int nWindows = data.getInt(); for (int i = 0; i < nWindows; i++) { int len = data.getInt(); mViewRoots.add(getString(data, len)); } } public List getViewRoots(long timeout, TimeUnit unit) { waitForResult(timeout, unit); return mViewRoots; } } private static class CaptureByteArrayHandler extends ViewDumpHandler { public CaptureByteArrayHandler(int type) { super(type); } private AtomicReference mData = new AtomicReference(); @Override protected void handleViewDebugResult(ByteBuffer data) { byte[] b = new byte[data.remaining()]; data.get(b); mData.set(b); } public byte[] getData(long timeout, TimeUnit unit) { waitForResult(timeout, unit); return mData.get(); } } private static class CaptureLayersHandler extends ViewDumpHandler { private AtomicReference mPsd = new AtomicReference(); public CaptureLayersHandler() { super(HandleViewDebug.CHUNK_VURT); } @Override protected void handleViewDebugResult(ByteBuffer data) { byte[] b = new byte[data.remaining()]; data.get(b); DataInputStream dis = new DataInputStream(new ByteArrayInputStream(b)); try { mPsd.set(DeviceBridge.parsePsd(dis)); } catch (IOException e) { Log.e(TAG, e); } } public PsdFile getPsdFile(long timeout, TimeUnit unit) { waitForResult(timeout, unit); return mPsd.get(); } } @Override public boolean reloadWindows() { mViewRootsPerClient = new HashMap>(40); for (Client c : mDevice.getClients()) { ClientData cd = c.getClientData(); if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) { ListViewRootsHandler handler = new ListViewRootsHandler(); try { HandleViewDebug.listViewRoots(c, handler); } catch (IOException e) { Log.i(TAG, "No connection to client: " + cd.getClientDescription()); continue; } List viewRoots = new ArrayList( handler.getViewRoots(200, TimeUnit.MILLISECONDS)); mViewRootsPerClient.put(c, viewRoots); } } return true; } @Override public void terminateViewDebug() { // nothing to terminate } @Override public boolean isViewDebugEnabled() { return true; } @Override public boolean supportsDisplayListDump() { return true; } @Override public Window[] getWindows() { List windows = new ArrayList(10); for (Client c: mViewRootsPerClient.keySet()) { for (String viewRoot: mViewRootsPerClient.get(c)) { windows.add(new Window(this, viewRoot, c)); } } return windows.toArray(new Window[windows.size()]); } @Override public int getFocusedWindow() { // TODO: add support for identifying view in focus return -1; } @Override public IDevice getDevice() { return mDevice; } @Override public ViewNode loadWindowData(Window window) { Client c = window.getClient(); if (c == null) { return null; } String viewRoot = window.getTitle(); CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT); try { HandleViewDebug.dumpViewHierarchy(c, viewRoot, false /* skipChildren */, true /* includeProperties */, handler); } catch (IOException e) { Log.e(TAG, e); return null; } byte[] data = handler.getData(20, TimeUnit.SECONDS); if (data == null) { return null; } String viewHierarchy = new String(data, Charset.forName("UTF-8")); return DeviceBridge.parseViewHierarchy(new BufferedReader(new StringReader(viewHierarchy)), window); } @Override public void loadProfileData(Window window, ViewNode viewNode) { Client c = window.getClient(); if (c == null) { return; } String viewRoot = window.getTitle(); CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP); try { HandleViewDebug.profileView(c, viewRoot, viewNode.toString(), handler); } catch (IOException e) { Log.e(TAG, e); return; } byte[] data = handler.getData(30, TimeUnit.SECONDS); if (data == null) { Log.e(TAG, "Timed out waiting for profile data"); return; } try { boolean success = DeviceBridge.loadProfileDataRecursive(viewNode, new BufferedReader(new StringReader(new String(data)))); if (success) { viewNode.setProfileRatings(); } } catch (IOException e) { Log.e(TAG, e); return; } } @Override public Image loadCapture(Window window, ViewNode viewNode) { Client c = window.getClient(); if (c == null) { return null; } String viewRoot = window.getTitle(); CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP); try { HandleViewDebug.captureView(c, viewRoot, viewNode.toString(), handler); } catch (IOException e) { Log.e(TAG, e); return null; } byte[] data = handler.getData(10, TimeUnit.SECONDS); return (data == null) ? null : new Image(Display.getDefault(), new ByteArrayInputStream(data)); } @Override public PsdFile captureLayers(Window window) { Client c = window.getClient(); if (c == null) { return null; } String viewRoot = window.getTitle(); CaptureLayersHandler handler = new CaptureLayersHandler(); try { HandleViewDebug.captureLayers(c, viewRoot, handler); } catch (IOException e) { Log.e(TAG, e); return null; } return handler.getPsdFile(20, TimeUnit.SECONDS); } @Override public void invalidateView(ViewNode viewNode) { Window window = viewNode.window; Client c = window.getClient(); if (c == null) { return; } String viewRoot = window.getTitle(); try { HandleViewDebug.invalidateView(c, viewRoot, viewNode.toString()); } catch (IOException e) { Log.e(TAG, e); } } @Override public void requestLayout(ViewNode viewNode) { Window window = viewNode.window; Client c = window.getClient(); if (c == null) { return; } String viewRoot = window.getTitle(); try { HandleViewDebug.requestLayout(c, viewRoot, viewNode.toString()); } catch (IOException e) { Log.e(TAG, e); } } @Override public void outputDisplayList(ViewNode viewNode) { Window window = viewNode.window; Client c = window.getClient(); if (c == null) { return; } String viewRoot = window.getTitle(); try { HandleViewDebug.dumpDisplayList(c, viewRoot, viewNode.toString()); } catch (IOException e) { Log.e(TAG, e); } } @Override public ThemeModel dumpTheme(ViewNode viewNode) { Window window = viewNode.window; Client c = window.getClient(); if (c == null) { return null; } String viewRoot = window.getTitle(); CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT); try { HandleViewDebug.dumpTheme(c, viewRoot, handler); } catch (IOException e) { Log.e(TAG, e); return null; } byte[] data = handler.getData(20, TimeUnit.SECONDS); if (data == null) { return null; } String themeDump = new String(data, Charset.forName("UTF-8")); return DeviceBridge.parseThemeDump(new BufferedReader(new StringReader(themeDump))); } @Override public void addWindowChangeListener(IWindowChangeListener l) { // TODO: add support for listening to view root changes } @Override public void removeWindowChangeListener(IWindowChangeListener l) { // TODO: add support for listening to view root changes } @Override public void deviceConnected(IDevice device) { // pass } @Override public void deviceDisconnected(IDevice device) { // pass } @Override public void deviceChanged(IDevice device, int changeMask) { if ((changeMask & IDevice.CHANGE_CLIENT_LIST) != 0) { reloadWindows(); } } @Override public boolean isViewUpdateEnabled() { return true; } @Override public void invokeViewMethod(Window window, ViewNode viewNode, String method, List args) { Client c = window.getClient(); if (c == null) { return; } String viewRoot = window.getTitle(); try { HandleViewDebug.invokeMethod(c, viewRoot, viewNode.toString(), method, args.toArray()); } catch (IOException e) { Log.e(TAG, e); } } @Override public boolean setLayoutParameter(Window window, ViewNode viewNode, String property, int value) { Client c = window.getClient(); if (c == null) { return false; } String viewRoot = window.getTitle(); try { HandleViewDebug.setLayoutParameter(c, viewRoot, viewNode.toString(), property, value); } catch (IOException e) { Log.e(TAG, e); return false; } return true; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000164 12747325007 032603 xustar000000000 0000000 116 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridg0100644 0000000 0000000 00000065611 12747325007 033132 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.device; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.ThemeModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import com.android.hierarchyviewerlib.ui.util.PsdFile; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import java.awt.Graphics2D; import java.awt.Point; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; /** * A bridge to the device. */ public class DeviceBridge { public static final String TAG = "hierarchyviewer"; private static final int DEFAULT_SERVER_PORT = 4939; // These codes must match the auto-generated codes in IWindowManager.java // See IWindowManager.aidl as well private static final int SERVICE_CODE_START_SERVER = 1; private static final int SERVICE_CODE_STOP_SERVER = 2; private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3; private static AndroidDebugBridge sBridge; private static final HashMap sDevicePortMap = new HashMap(); private static final HashMap sViewServerInfo = new HashMap(); private static int sNextLocalPort = DEFAULT_SERVER_PORT; public static class ViewServerInfo { public final int protocolVersion; public final int serverVersion; ViewServerInfo(int serverVersion, int protocolVersion) { this.protocolVersion = protocolVersion; this.serverVersion = serverVersion; } } /** * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. * @param bridge the bridge object to use */ public static void acquireBridge(AndroidDebugBridge bridge) { sBridge = bridge; } /** * Creates an {@link AndroidDebugBridge} connected to adb at the given location. * * If a bridge is already running, this disconnects it and creates a new one. * * @param adbLocation the location to adb. */ public static void initDebugBridge(String adbLocation) { if (sBridge == null) { /* debugger support required only if hv is using ddm protocol */ AndroidDebugBridge.init(HierarchyViewerDirector.isUsingDdmProtocol()); } if (sBridge == null || !sBridge.isConnected()) { sBridge = AndroidDebugBridge.createBridge(adbLocation, true); } } /** Disconnects the current {@link AndroidDebugBridge}. */ public static void terminate() { AndroidDebugBridge.terminate(); } public static IDevice[] getDevices() { if (sBridge == null) { return new IDevice[0]; } return sBridge.getDevices(); } /* * This adds a listener to the debug bridge. The listener is notified of * connecting/disconnecting devices, devices coming online, etc. */ public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { AndroidDebugBridge.addDeviceChangeListener(listener); } public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { AndroidDebugBridge.removeDeviceChangeListener(listener); } /** * Sets up a just-connected device to work with the view server. *

* This starts a port forwarding between a local port and a port on the * device. * * @param device */ public static void setupDeviceForward(IDevice device) { synchronized (sDevicePortMap) { if (device.getState() == IDevice.DeviceState.ONLINE) { int localPort = sNextLocalPort++; try { device.createForward(localPort, DEFAULT_SERVER_PORT); sDevicePortMap.put(device, localPort); } catch (TimeoutException e) { Log.e(TAG, "Timeout setting up port forwarding for " + device); } catch (AdbCommandRejectedException e) { Log.e(TAG, String.format("Adb rejected forward command for device %1$s: %2$s", device, e.getMessage())); } catch (IOException e) { Log.e(TAG, String.format("Failed to create forward for device %1$s: %2$s", device, e.getMessage())); } } } } public static void removeDeviceForward(IDevice device) { synchronized (sDevicePortMap) { final Integer localPort = sDevicePortMap.get(device); if (localPort != null) { try { device.removeForward(localPort, DEFAULT_SERVER_PORT); sDevicePortMap.remove(device); } catch (TimeoutException e) { Log.e(TAG, "Timeout removing port forwarding for " + device); } catch (AdbCommandRejectedException e) { // In this case, we want to fail silently. } catch (IOException e) { Log.e(TAG, String.format("Failed to remove forward for device %1$s: %2$s", device, e.getMessage())); } } } } public static int getDeviceLocalPort(IDevice device) { synchronized (sDevicePortMap) { Integer port = sDevicePortMap.get(device); if (port != null) { return port; } Log.e(TAG, "Missing forwarded port for " + device.getSerialNumber()); return -1; } } public static boolean isViewServerRunning(IDevice device) { final boolean[] result = new boolean[1]; try { if (device.isOnline()) { device.executeShellCommand(buildIsServerRunningShellCommand(), new BooleanResultReader(result)); if (!result[0]) { ViewServerInfo serverInfo = loadViewServerInfo(device); if (serverInfo != null && serverInfo.protocolVersion > 2) { result[0] = true; } } } } catch (TimeoutException e) { Log.e(TAG, "Timeout checking status of view server on device " + device); } catch (IOException e) { Log.e(TAG, "Unable to check status of view server on device " + device); } catch (AdbCommandRejectedException e) { Log.e(TAG, "Adb rejected command to check status of view server on device " + device); } catch (ShellCommandUnresponsiveException e) { Log.e(TAG, "Unable to execute command to check status of view server on device " + device); } return result[0]; } public static boolean startViewServer(IDevice device) { return startViewServer(device, DEFAULT_SERVER_PORT); } public static boolean startViewServer(IDevice device, int port) { final boolean[] result = new boolean[1]; try { if (device.isOnline()) { device.executeShellCommand(buildStartServerShellCommand(port), new BooleanResultReader(result)); } } catch (TimeoutException e) { Log.e(TAG, "Timeout starting view server on device " + device); } catch (IOException e) { Log.e(TAG, "Unable to start view server on device " + device); } catch (AdbCommandRejectedException e) { Log.e(TAG, "Adb rejected command to start view server on device " + device); } catch (ShellCommandUnresponsiveException e) { Log.e(TAG, "Unable to execute command to start view server on device " + device); } return result[0]; } public static boolean stopViewServer(IDevice device) { final boolean[] result = new boolean[1]; try { if (device.isOnline()) { device.executeShellCommand(buildStopServerShellCommand(), new BooleanResultReader( result)); } } catch (TimeoutException e) { Log.e(TAG, "Timeout stopping view server on device " + device); } catch (IOException e) { Log.e(TAG, "Unable to stop view server on device " + device); } catch (AdbCommandRejectedException e) { Log.e(TAG, "Adb rejected command to stop view server on device " + device); } catch (ShellCommandUnresponsiveException e) { Log.e(TAG, "Unable to execute command to stop view server on device " + device); } return result[0]; } private static String buildStartServerShellCommand(int port) { return String.format("service call window %d i32 %d", SERVICE_CODE_START_SERVER, port); //$NON-NLS-1$ } private static String buildStopServerShellCommand() { return String.format("service call window %d", SERVICE_CODE_STOP_SERVER); //$NON-NLS-1$ } private static String buildIsServerRunningShellCommand() { return String.format("service call window %d", SERVICE_CODE_IS_SERVER_RUNNING); //$NON-NLS-1$ } private static class BooleanResultReader extends MultiLineReceiver { private final boolean[] mResult; public BooleanResultReader(boolean[] result) { mResult = result; } @Override public void processNewLines(String[] strings) { if (strings.length > 0) { Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); //$NON-NLS-1$ Matcher matcher = pattern.matcher(strings[0]); if (matcher.matches()) { if (Integer.parseInt(matcher.group(1)) == 1) { mResult[0] = true; } } } } @Override public boolean isCancelled() { return false; } } public static ViewServerInfo loadViewServerInfo(IDevice device) { int server = -1; int protocol = -1; DeviceConnection connection = null; try { connection = new DeviceConnection(device); connection.sendCommand("SERVER"); //$NON-NLS-1$ String line = connection.getInputStream().readLine(); if (line != null) { server = Integer.parseInt(line); } } catch (Exception e) { Log.e(TAG, "Unable to get view server version from device " + device); } finally { if (connection != null) { connection.close(); } } connection = null; try { connection = new DeviceConnection(device); connection.sendCommand("PROTOCOL"); //$NON-NLS-1$ String line = connection.getInputStream().readLine(); if (line != null) { protocol = Integer.parseInt(line); } } catch (Exception e) { Log.e(TAG, "Unable to get view server protocol version from device " + device); } finally { if (connection != null) { connection.close(); } } if (server == -1 || protocol == -1) { return null; } ViewServerInfo returnValue = new ViewServerInfo(server, protocol); synchronized (sViewServerInfo) { sViewServerInfo.put(device, returnValue); } return returnValue; } public static ViewServerInfo getViewServerInfo(IDevice device) { synchronized (sViewServerInfo) { return sViewServerInfo.get(device); } } public static void removeViewServerInfo(IDevice device) { synchronized (sViewServerInfo) { sViewServerInfo.remove(device); } } /* * This loads the list of windows from the specified device. The format is: * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE. */ public static Window[] loadWindows(IHvDevice hvDevice, IDevice device) { ArrayList windows = new ArrayList(); DeviceConnection connection = null; ViewServerInfo serverInfo = getViewServerInfo(device); try { connection = new DeviceConnection(device); connection.sendCommand("LIST"); //$NON-NLS-1$ BufferedReader in = connection.getInputStream(); String line; while ((line = in.readLine()) != null) { if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$ break; } int index = line.indexOf(' '); if (index != -1) { String windowId = line.substring(0, index); int id; if (serverInfo.serverVersion > 2) { id = (int) Long.parseLong(windowId, 16); } else { id = Integer.parseInt(windowId, 16); } Window w = new Window(hvDevice, line.substring(index + 1), id); windows.add(w); } } // Automatic refreshing of windows was added in protocol version 3. // Before, the user needed to specify explicitly that he wants to // get the focused window, which was done using a special type of // window with hash code -1. if (serverInfo.protocolVersion < 3) { windows.add(Window.getFocusedWindow(hvDevice)); } } catch (Exception e) { Log.e(TAG, "Unable to load the window list from device " + device); } finally { if (connection != null) { connection.close(); } } // The server returns the list of windows from the window at the bottom // to the top. We want the reverse order to put the top window on top of // the list. Window[] returnValue = new Window[windows.size()]; for (int i = windows.size() - 1; i >= 0; i--) { returnValue[returnValue.length - i - 1] = windows.get(i); } return returnValue; } /* * This gets the hash code of the window that has focus. Only works with * protocol version 3 and above. */ public static int getFocusedWindow(IDevice device) { DeviceConnection connection = null; try { connection = new DeviceConnection(device); connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$ String line = connection.getInputStream().readLine(); if (line == null || line.length() == 0) { return -1; } return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16); } catch (Exception e) { Log.e(TAG, "Unable to get the focused window from device " + device); } finally { if (connection != null) { connection.close(); } } return -1; } public static ViewNode loadWindowData(Window window) { DeviceConnection connection = null; try { connection = new DeviceConnection(window.getDevice()); connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$ BufferedReader in = connection.getInputStream(); ViewNode currentNode = parseViewHierarchy(in, window); ViewServerInfo serverInfo = getViewServerInfo(window.getDevice()); if (serverInfo != null) { currentNode.protocolVersion = serverInfo.protocolVersion; } return currentNode; } catch (Exception e) { Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device " + window.getDevice()); Log.e(TAG, e.getMessage()); } finally { if (connection != null) { connection.close(); } } return null; } public static ViewNode parseViewHierarchy(BufferedReader in, Window window) { ViewNode currentNode = null; int currentDepth = -1; String line; try { while ((line = in.readLine()) != null) { if ("DONE.".equalsIgnoreCase(line)) { break; } int depth = 0; while (line.charAt(depth) == ' ') { depth++; } while (depth <= currentDepth) { if (currentNode != null) { currentNode = currentNode.parent; } currentDepth--; } currentNode = new ViewNode(window, currentNode, line.substring(depth)); currentDepth = depth; } } catch (IOException e) { Log.e(TAG, "Error reading view hierarchy stream: " + e.getMessage()); return null; } if (currentNode == null) { return null; } while (currentNode.parent != null) { currentNode = currentNode.parent; } return currentNode; } public static boolean loadProfileData(Window window, ViewNode viewNode) { DeviceConnection connection = null; try { connection = new DeviceConnection(window.getDevice()); connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ BufferedReader in = connection.getInputStream(); int protocol; synchronized (sViewServerInfo) { protocol = sViewServerInfo.get(window.getDevice()).protocolVersion; } if (protocol < 3) { return loadProfileData(viewNode, in); } else { boolean ret = loadProfileDataRecursive(viewNode, in); if (ret) { viewNode.setProfileRatings(); } return ret; } } catch (Exception e) { Log.e(TAG, "Unable to load profiling data for window " + window.getTitle() + " on device " + window.getDevice()); } finally { if (connection != null) { connection.close(); } } return false; } private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException { String line; if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$ || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$ return false; } String[] data = line.split(" "); node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0; node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0; node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0; return true; } public static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in) throws IOException { if (!loadProfileData(node, in)) { return false; } for (int i = 0; i < node.children.size(); i++) { if (!loadProfileDataRecursive(node.children.get(i), in)) { return false; } } return true; } public static Image loadCapture(Window window, ViewNode viewNode) { DeviceConnection connection = null; try { connection = new DeviceConnection(window.getDevice()); connection.getSocket().setSoTimeout(5000); connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ return new Image(Display.getDefault(), connection.getSocket().getInputStream()); } catch (Exception e) { Log.e(TAG, "Unable to capture data for node " + viewNode + " in window " + window.getTitle() + " on device " + window.getDevice()); } finally { if (connection != null) { connection.close(); } } return null; } public static PsdFile captureLayers(Window window) { DeviceConnection connection = null; DataInputStream in = null; try { connection = new DeviceConnection(window.getDevice()); connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$ in = new DataInputStream(new BufferedInputStream(connection.getSocket() .getInputStream())); return parsePsd(in); } catch (IOException e) { Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device " + window.getDevice()); } finally { if (in != null) { try { in.close(); } catch (Exception ex) { } } if (connection != null) { connection.close(); } } return null; } public static PsdFile parsePsd(DataInputStream in) throws IOException { int width = in.readInt(); int height = in.readInt(); PsdFile psd = new PsdFile(width, height); while (readLayer(in, psd)) { } return psd; } private static boolean readLayer(DataInputStream in, PsdFile psd) { try { if (in.read() == 2) { return false; } String name = in.readUTF(); boolean visible = in.read() == 1; int x = in.readInt(); int y = in.readInt(); int dataSize = in.readInt(); byte[] data = new byte[dataSize]; int read = 0; while (read < dataSize) { read += in.read(data, read, dataSize - read); } ByteArrayInputStream arrayIn = new ByteArrayInputStream(data); BufferedImage chunk = ImageIO.read(arrayIn); // Ensure the image is in the right format BufferedImage image = new BufferedImage(chunk.getWidth(), chunk.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); g.drawImage(chunk, null, 0, 0); g.dispose(); psd.addLayer(name, image, new Point(x, y), visible); return true; } catch (Exception e) { return false; } } public static void invalidateView(ViewNode viewNode) { DeviceConnection connection = null; try { connection = new DeviceConnection(viewNode.window.getDevice()); connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ } catch (Exception e) { Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window + " on device " + viewNode.window.getDevice()); } finally { if (connection != null) { connection.close(); } } } public static void requestLayout(ViewNode viewNode) { DeviceConnection connection = null; try { connection = new DeviceConnection(viewNode.window.getDevice()); connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ } catch (Exception e) { Log.e(TAG, "Unable to request layout for node " + viewNode + " in window " + viewNode.window + " on device " + viewNode.window.getDevice()); } finally { if (connection != null) { connection.close(); } } } public static void outputDisplayList(ViewNode viewNode) { DeviceConnection connection = null; try { connection = new DeviceConnection(viewNode.window.getDevice()); connection.sendCommand("OUTPUT_DISPLAYLIST " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ } catch (Exception e) { Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window " + viewNode.window + " on device " + viewNode.window.getDevice()); } finally { if (connection != null) { connection.close(); } } } public static ThemeModel dumpTheme(ViewNode viewNode) { DeviceConnection connection = null; ThemeModel model = null; try { connection = new DeviceConnection(viewNode.window.getDevice()); connection.sendCommand("DUMP_THEME " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ BufferedReader in = connection.getInputStream(); model = parseThemeDump(in); } catch (Exception e) { Log.e(TAG, "Unable to dump theme for node " + viewNode + " in window " + viewNode.window + " on device " + viewNode.window.getDevice()); return null; } finally { if (connection != null) { connection.close(); } } return model; } public static ThemeModel parseThemeDump(BufferedReader in) { ThemeModel model = new ThemeModel(); String resourceName; String resourceValue; try { while ((resourceName = in.readLine()) != null) { if ("DONE.".equalsIgnoreCase(resourceName)) { break; } if ((resourceValue = in.readLine()) == null) { return null; } model.add(resourceName, resourceValue); } } catch (IOException e) { Log.e(TAG, "Error reading theme dump: " + e.getMessage()); return null; } return model; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000170 12747325007 032600 xustar000000000 0000000 120 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConne0100644 0000000 0000000 00000005571 12747325007 033144 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.device; import com.android.ddmlib.IDevice; import com.google.common.base.Charsets; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.SocketChannel; /** * This class is used for connecting to a device in debug mode running the view * server. */ public class DeviceConnection { // Now a socket channel, since socket channels are friendly with interrupts. private SocketChannel mSocketChannel; private BufferedReader mIn; private BufferedWriter mOut; public DeviceConnection(IDevice device) throws IOException { mSocketChannel = SocketChannel.open(); int port = DeviceBridge.getDeviceLocalPort(device); if (port == -1) { throw new IOException(); } mSocketChannel.connect(new InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$ mSocketChannel.socket().setSoTimeout(40000); } public BufferedReader getInputStream() throws IOException { if (mIn == null) { mIn = new BufferedReader(new InputStreamReader( mSocketChannel.socket().getInputStream(), Charsets.UTF_8)); } return mIn; } public BufferedWriter getOutputStream() throws IOException { if (mOut == null) { mOut = new BufferedWriter(new OutputStreamWriter( mSocketChannel.socket().getOutputStream(), Charsets.UTF_8)); } return mOut; } public Socket getSocket() { return mSocketChannel.socket(); } public void sendCommand(String command) throws IOException { BufferedWriter out = getOutputStream(); out.write(command); out.newLine(); out.flush(); } public void close() { try { if (mIn != null) { mIn.close(); } } catch (IOException e) { } try { if (mOut != null) { mOut.close(); } } catch (IOException e) { } try { mSocketChannel.close(); } catch (IOException e) { } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000167 12747325007 032606 xustar000000000 0000000 119 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFac0100644 0000000 0000000 00000003711 12747325007 033063 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.device; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ddmlib.IDevice; import com.android.hierarchyviewerlib.HierarchyViewerDirector; public class HvDeviceFactory { public static IHvDevice create(IDevice device) { // default to old mechanism until the new one is fully tested if (!HierarchyViewerDirector.isUsingDdmProtocol()) { return new ViewServerDevice(device); } // Wait for a few seconds after the device has been connected to // allow all the clients to be initialized. Specifically, we need to wait // until the client data is filled with the list of features supported // by the client. try { Thread.sleep(2000); } catch (InterruptedException e) { // ignore } boolean ddmViewHierarchy = false; // see if any of the clients on the device support view hierarchy via DDMS for (Client c : device.getClients()) { ClientData cd = c.getClientData(); if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) { ddmViewHierarchy = true; break; } } return ddmViewHierarchy ? new DdmViewDebugDevice(device) : new ViewServerDevice(device); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000161 12747325007 032600 xustar000000000 0000000 113 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.j0100644 0000000 0000000 00000004416 12747325007 033015 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.device; import com.android.ddmlib.IDevice; import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; import com.android.hierarchyviewerlib.models.ThemeModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import com.android.hierarchyviewerlib.ui.util.PsdFile; import org.eclipse.swt.graphics.Image; import java.util.List; /** Represents a device that can perform view debug operations. */ public interface IHvDevice { /** * Initializes view debugging on the device. * @return true if the on device component was successfully initialized */ boolean initializeViewDebug(); boolean reloadWindows(); void terminateViewDebug(); boolean isViewDebugEnabled(); boolean supportsDisplayListDump(); Window[] getWindows(); int getFocusedWindow(); IDevice getDevice(); Image getScreenshotImage(); ViewNode loadWindowData(Window window); void loadProfileData(Window window, ViewNode viewNode); Image loadCapture(Window window, ViewNode viewNode); PsdFile captureLayers(Window window); void invalidateView(ViewNode viewNode); void requestLayout(ViewNode viewNode); void outputDisplayList(ViewNode viewNode); ThemeModel dumpTheme(ViewNode viewNode); boolean isViewUpdateEnabled(); void invokeViewMethod(Window window, ViewNode viewNode, String method, List args); boolean setLayoutParameter(Window window, ViewNode viewNode, String property, int value); void addWindowChangeListener(IWindowChangeListener l); void removeWindowChangeListener(IWindowChangeListener l); } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000170 12747325007 032600 xustar000000000 0000000 120 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerD0100644 0000000 0000000 00000011442 12747325007 033161 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.device; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.device.DeviceBridge.ViewServerInfo; import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; import com.android.hierarchyviewerlib.models.ThemeModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.Window; import com.android.hierarchyviewerlib.ui.util.PsdFile; import org.eclipse.swt.graphics.Image; import java.util.List; public class ViewServerDevice extends AbstractHvDevice { static final String TAG = "ViewServerDevice"; final IDevice mDevice; private ViewServerInfo mViewServerInfo; private Window[] mWindows; public ViewServerDevice(IDevice device) { mDevice = device; } @Override public boolean initializeViewDebug() { if (!mDevice.isOnline()) { return false; } DeviceBridge.setupDeviceForward(mDevice); return reloadWindows(); } @Override public boolean reloadWindows() { if (!DeviceBridge.isViewServerRunning(mDevice)) { if (!DeviceBridge.startViewServer(mDevice)) { Log.e(TAG, "Unable to debug device: " + mDevice.getName()); DeviceBridge.removeDeviceForward(mDevice); return false; } } mViewServerInfo = DeviceBridge.loadViewServerInfo(mDevice); if (mViewServerInfo == null) { return false; } mWindows = DeviceBridge.loadWindows(this, mDevice); return true; } @Override public boolean supportsDisplayListDump() { return mViewServerInfo != null && mViewServerInfo.protocolVersion >= 4; } @Override public void terminateViewDebug() { DeviceBridge.removeDeviceForward(mDevice); DeviceBridge.removeViewServerInfo(mDevice); } @Override public boolean isViewDebugEnabled() { return mViewServerInfo != null; } @Override public Window[] getWindows() { return mWindows; } @Override public int getFocusedWindow() { return DeviceBridge.getFocusedWindow(mDevice); } @Override public IDevice getDevice() { return mDevice; } @Override public ViewNode loadWindowData(Window window) { return DeviceBridge.loadWindowData(window); } @Override public void loadProfileData(Window window, ViewNode viewNode) { DeviceBridge.loadProfileData(window, viewNode); } @Override public Image loadCapture(Window window, ViewNode viewNode) { return DeviceBridge.loadCapture(window, viewNode); } @Override public PsdFile captureLayers(Window window) { return DeviceBridge.captureLayers(window); } @Override public void invalidateView(ViewNode viewNode) { DeviceBridge.invalidateView(viewNode); } @Override public void requestLayout(ViewNode viewNode) { DeviceBridge.requestLayout(viewNode); } @Override public void outputDisplayList(ViewNode viewNode) { DeviceBridge.outputDisplayList(viewNode); } @Override public ThemeModel dumpTheme(ViewNode viewNode) { return DeviceBridge.dumpTheme(viewNode); } @Override public void addWindowChangeListener(IWindowChangeListener l) { if (mViewServerInfo != null && mViewServerInfo.protocolVersion >= 3) { WindowUpdater.startListenForWindowChanges(l, mDevice); } } @Override public void removeWindowChangeListener(IWindowChangeListener l) { if (mViewServerInfo != null && mViewServerInfo.protocolVersion >= 3) { WindowUpdater.stopListenForWindowChanges(l, mDevice); } } @Override public boolean isViewUpdateEnabled() { return false; } @Override public void invokeViewMethod(Window window, ViewNode viewNode, String method, List args) { // not supported } @Override public boolean setLayoutParameter(Window window, ViewNode viewNode, String property, int value) { // not supported return false; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/de0100644 0000000 0000000 00000000165 12747325007 032604 xustar000000000 0000000 117 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdat0100644 0000000 0000000 00000013563 12747325007 033227 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.device; import com.android.ddmlib.IDevice; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; /** * This class handles automatic updating of the list of windows in the device * selector for device with protocol version 3 or above of the view server. It * connects to the devices, keeps the connection open and listens for messages. * It notifies all it's listeners of changes. */ public class WindowUpdater { private static HashMap> sWindowChangeListeners = new HashMap>(); private static HashMap sListeningThreads = new HashMap(); public static interface IWindowChangeListener { public void windowsChanged(IDevice device); public void focusChanged(IDevice device); } public static void terminate() { synchronized (sListeningThreads) { for (IDevice device : sListeningThreads.keySet()) { sListeningThreads.get(device).interrupt(); } } } public static void startListenForWindowChanges(IWindowChangeListener listener, IDevice device) { synchronized (sWindowChangeListeners) { // In this case, a listening thread already exists, so we don't need // to create another one. if (sWindowChangeListeners.containsKey(device)) { sWindowChangeListeners.get(device).add(listener); return; } ArrayList listeners = new ArrayList(); listeners.add(listener); sWindowChangeListeners.put(device, listeners); } // Start listening Thread listeningThread = new Thread(new WindowChangeMonitor(device)); synchronized (sListeningThreads) { sListeningThreads.put(device, listeningThread); } listeningThread.start(); } public static void stopListenForWindowChanges(IWindowChangeListener listener, IDevice device) { synchronized (sWindowChangeListeners) { ArrayList listeners = sWindowChangeListeners.get(device); if (listeners == null) { return; } listeners.remove(listener); // There are more listeners, so don't stop the listening thread. if (listeners.size() != 0) { return; } sWindowChangeListeners.remove(device); } // Everybody left, so the party's over! Thread listeningThread; synchronized (sListeningThreads) { listeningThread = sListeningThreads.get(device); sListeningThreads.remove(device); } listeningThread.interrupt(); } private static IWindowChangeListener[] getWindowChangeListenersAsArray(IDevice device) { IWindowChangeListener[] listeners; synchronized (sWindowChangeListeners) { ArrayList windowChangeListenerList = sWindowChangeListeners.get(device); if (windowChangeListenerList == null) { return null; } listeners = windowChangeListenerList .toArray(new IWindowChangeListener[windowChangeListenerList.size()]); } return listeners; } public static void notifyWindowsChanged(IDevice device) { IWindowChangeListener[] listeners = getWindowChangeListenersAsArray(device); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].windowsChanged(device); } } } public static void notifyFocusChanged(IDevice device) { IWindowChangeListener[] listeners = getWindowChangeListenersAsArray(device); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].focusChanged(device); } } } private static class WindowChangeMonitor implements Runnable { private IDevice device; public WindowChangeMonitor(IDevice device) { this.device = device; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { DeviceConnection connection = null; try { connection = new DeviceConnection(device); connection.sendCommand("AUTOLIST"); String line; while (!Thread.currentThread().isInterrupted() && (line = connection.getInputStream().readLine()) != null) { if (line.equalsIgnoreCase("LIST UPDATE")) { notifyWindowsChanged(device); } else if (line.equalsIgnoreCase("FOCUS UPDATE")) { notifyFocusChanged(device); } } } catch (IOException e) { } finally { if (connection != null) { connection.close(); } } } } } } hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/0040755 0000000 0000000 00000000000 12747325007 031056 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000174 12747325007 032627 xustar000000000 0000000 124 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelec0100644 0000000 0000000 00000020632 12747325007 033154 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.models; import com.android.hierarchyviewerlib.device.IHvDevice; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * This class stores the list of windows for each connected device. It notifies * listeners of any changes as well as knows which window is currently selected * in the device selector. */ public class DeviceSelectionModel { private final Map mDeviceMap = new HashMap(10); private final Map mFocusedWindowHashes = new HashMap(20); private final ArrayList mWindowChangeListeners = new ArrayList(); private IHvDevice mSelectedDevice; private Window mSelectedWindow; private static DeviceSelectionModel sModel; private static class DeviceInfo { Window[] windows; private DeviceInfo(Window[] windows) { this.windows = windows; } } public static DeviceSelectionModel getModel() { if (sModel == null) { sModel = new DeviceSelectionModel(); } return sModel; } public void addDevice(IHvDevice hvDevice) { synchronized (mDeviceMap) { DeviceInfo info = new DeviceInfo(hvDevice.getWindows()); mDeviceMap.put(hvDevice, info); } notifyDeviceConnected(hvDevice); } public void removeDevice(IHvDevice hvDevice) { boolean selectionChanged = false; synchronized (mDeviceMap) { mDeviceMap.remove(hvDevice); mFocusedWindowHashes.remove(hvDevice); if (mSelectedDevice == hvDevice) { mSelectedDevice = null; mSelectedWindow = null; selectionChanged = true; } } notifyDeviceDisconnected(hvDevice); if (selectionChanged) { notifySelectionChanged(mSelectedDevice, mSelectedWindow); } } public void updateDevice(IHvDevice hvDevice) { boolean selectionChanged = false; synchronized (mDeviceMap) { Window[] windows = hvDevice.getWindows(); mDeviceMap.put(hvDevice, new DeviceInfo(windows)); // If the selected window no longer exists, we clear the selection. if (mSelectedDevice == hvDevice && mSelectedWindow != null) { boolean windowStillExists = false; for (int i = 0; i < windows.length && !windowStillExists; i++) { if (windows[i].equals(mSelectedWindow)) { windowStillExists = true; } } if (!windowStillExists) { mSelectedDevice = null; mSelectedWindow = null; selectionChanged = true; } } } notifyDeviceChanged(hvDevice); if (selectionChanged) { notifySelectionChanged(mSelectedDevice, mSelectedWindow); } } /* * Change which window has focus and notify the listeners. */ public void updateFocusedWindow(IHvDevice device, int focusedWindow) { Integer oldValue = null; synchronized (mDeviceMap) { oldValue = mFocusedWindowHashes.put(device, new Integer(focusedWindow)); } // Only notify if the values are different. It would be cool if Java // containers accepted basic types like int. if (oldValue == null || (oldValue != null && oldValue.intValue() != focusedWindow)) { notifyFocusChanged(device); } } public static interface IWindowChangeListener { public void deviceConnected(IHvDevice device); public void deviceChanged(IHvDevice device); public void deviceDisconnected(IHvDevice device); public void focusChanged(IHvDevice device); public void selectionChanged(IHvDevice device, Window window); } private IWindowChangeListener[] getWindowChangeListenerList() { IWindowChangeListener[] listeners = null; synchronized (mWindowChangeListeners) { if (mWindowChangeListeners.size() == 0) { return null; } listeners = mWindowChangeListeners.toArray(new IWindowChangeListener[mWindowChangeListeners .size()]); } return listeners; } private void notifyDeviceConnected(IHvDevice device) { IWindowChangeListener[] listeners = getWindowChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].deviceConnected(device); } } } private void notifyDeviceChanged(IHvDevice device) { IWindowChangeListener[] listeners = getWindowChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].deviceChanged(device); } } } private void notifyDeviceDisconnected(IHvDevice device) { IWindowChangeListener[] listeners = getWindowChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].deviceDisconnected(device); } } } private void notifyFocusChanged(IHvDevice device) { IWindowChangeListener[] listeners = getWindowChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].focusChanged(device); } } } private void notifySelectionChanged(IHvDevice device, Window window) { IWindowChangeListener[] listeners = getWindowChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].selectionChanged(device, window); } } } public void addWindowChangeListener(IWindowChangeListener listener) { synchronized (mWindowChangeListeners) { mWindowChangeListeners.add(listener); } } public void removeWindowChangeListener(IWindowChangeListener listener) { synchronized (mWindowChangeListeners) { mWindowChangeListeners.remove(listener); } } public IHvDevice[] getDevices() { synchronized (mDeviceMap) { Set devices = mDeviceMap.keySet(); return devices.toArray(new IHvDevice[devices.size()]); } } public Window[] getWindows(IHvDevice device) { synchronized (mDeviceMap) { DeviceInfo info = mDeviceMap.get(device); if (info != null) { return info.windows; } } return null; } // Returns the window that currently has focus or -1. Note that this means // that a window with hashcode -1 gets highlighted. If you remember, this is // the infamous public int getFocusedWindow(IHvDevice device) { synchronized (mDeviceMap) { Integer focusedWindow = mFocusedWindowHashes.get(device); if (focusedWindow == null) { return -1; } return focusedWindow.intValue(); } } public void setSelection(IHvDevice device, Window window) { synchronized (mDeviceMap) { mSelectedDevice = device; mSelectedWindow = window; } notifySelectionChanged(device, window); } public IHvDevice getSelectedDevice() { synchronized (mDeviceMap) { return mSelectedDevice; } } public Window getSelectedWindow() { synchronized (mDeviceMap) { return mSelectedWindow; } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000175 12747325007 032630 xustar000000000 0000000 125 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/EvaluateContrastModel.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/EvaluateCon0100644 0000000 0000000 00000035641 12747325007 033215 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.models; import com.android.annotations.Nullable; import com.google.common.collect.Lists; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import java.awt.Color; import java.awt.Rectangle; import java.lang.Math; import java.lang.Integer; import java.lang.String; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; /** *

* This class uses the Web Content Accessibility Guidelines (WCAG) 2.0 (http://www.w3.org/TR/WCAG20) * to evaluate the contrast ratio between the text and background colors of a view. *

*

* Given an image of a view and the bounds of where the view is within the image (x, y, width, * height), this class will extract the luminance values (measure of brightness) of the entire view * to try and determine the text and background color. If known, the constructor accepts text size * and text color to provide a more accurate result. *

*

* The {@link #calculateLuminance(int)} method calculates the luminance value of an {@code int} * representation of a {@link Color}. We use two of these values, the text and background * luminances, to determine the contrast ratio.

*

*

* "Sufficient contrast" is defined as having a contrast ratio of 4.5:1 in general, except for: *

    *
  • Large text (>= 18 points, or >= 14 points and bold), * which can have a contrast ratio of only 3:1.
  • *
  • Inactive components or pure decorations.
  • *
  • Text that is part of a logo or brand name.
  • *
*/ public class EvaluateContrastModel { public enum ContrastResult { PASS, FAIL, INDETERMINATE } public static final String CONTRAST_RATIO_FORMAT = "%.2f:1"; public static final double CONTRAST_RATIO_NORMAL_TEXT = 4.5; public static final double CONTRAST_RATIO_LARGE_TEXT = 3.0; public static final int NORMAL_TEXT_SZ_PTS = 18; public static final int NORMAL_TEXT_BOLD_SZ_PTS = 14; public static final String NOT_APPLICABLE = "N/A"; private static final double MAX_RGB_VALUE = 255.0; private ImageData mImageData; /** The bounds of the view within the image */ private Rectangle mViewBounds; /** Maps an int representation of a {@link Color} to its luminance value. */ private HashMap mLuminanceMap; /** Keeps track of how many times a luminance value occurs in this view. */ private HashMap mLuminanceHistogram; private final List mBackgroundColors; private final List mForegroundColors; private double mBackgroundLuminance; private double mForegroundLuminance; private double mContrastRatio; private Integer mTextColor; private Double mTextSize; private boolean mIsBold; /** *

* Constructs an EvaluateContrastModel to extract and process properties from the image related * to contrast and luminance. *

*

* NOTE: Invoking this constructor performs image processing tasks, which are relatively * heavywheight. *

* * @param image Screenshot of the view. * @param textColor Color of the text. If null, we will estimate the color of the text. * @param textSize Size of the text. If null, we may have an indeterminate result where it * passes only one of the tests. * @param x Starting x-coordinate of the view in the image. * @param y Starting y-coordinate of the view in the image. * @param width The width of the view in the image. * @param height The height of the view in the image. * @param isBold True if we know the text is bold, false otherwise. */ public EvaluateContrastModel(Image image, @Nullable Integer textColor, @Nullable Double textSize, int x, int y, int width, int height, boolean isBold) { mImageData = image.getImageData(); mTextColor = textColor; mTextSize = textSize; mViewBounds = new Rectangle(x, y, width, height); mIsBold = isBold; mBackgroundColors = new LinkedList(); mForegroundColors = new LinkedList(); mLuminanceMap = new HashMap(); mLuminanceHistogram = new HashMap(); processSwatch(); } /** * Formula derived from http://gmazzocato.altervista.org/colorwheel/algo.php. * More information can be found at http://www.w3.org/TR/WCAG20/relative-luminance.xml. */ public static double calculateLuminance(int color) { Color colorObj = new Color(color); float[] sRGB = new float[4]; colorObj.getRGBComponents(sRGB); final double[] lumRGB = new double[4]; for (int i = 0; i < sRGB.length; ++i) { lumRGB[i] = (sRGB[i] <= 0.03928d) ? sRGB[i] / 12.92d : Math.pow(((sRGB[i] + 0.055d) / 1.055d), 2.4d); } return 0.2126d * lumRGB[0] + 0.7152d * lumRGB[1] + 0.0722d * lumRGB[2]; } public static double calculateContrastRatio(double lum1, double lum2) { if ((lum1 < 0.0d) || (lum2 < 0.0d)) { throw new IllegalArgumentException("Luminance values may not be negative."); } return (Math.max(lum1, lum2) + 0.05d) / (Math.min(lum1, lum2) + 0.05d); } public static String intToHexString(int color) { return String.format("#%06X", (0xFFFFFF & color)); } private void processSwatch() { processLuminanceData(); extractFgBgData(); double textLuminance = mTextColor == null ? mForegroundLuminance : calculateLuminance(calculateTextColor(mTextColor, mBackgroundColors.get(0))); // Two-decimal digits of precision for the contrast ratio mContrastRatio = Math.round(calculateContrastRatio( textLuminance, mBackgroundLuminance) * 100.0d) / 100.0d; } private void processLuminanceData() { for (int x = mViewBounds.x; x < mViewBounds.width; ++x) { for (int y = mViewBounds.y; y < mViewBounds.height; ++y) { final int color = mImageData.getPixel(x, y); final double luminance = calculateLuminance(color); if (!mLuminanceMap.containsKey(color)) { mLuminanceMap.put(color, luminance); } if (!mLuminanceHistogram.containsKey(luminance)) { mLuminanceHistogram.put(luminance, 0); } mLuminanceHistogram.put(luminance, mLuminanceHistogram.get(luminance) + 1); } } } private void extractFgBgData() { if (mLuminanceMap.isEmpty()) { // An empty luminance map indicates we've encountered a 0px area // image. It has no luminance. mBackgroundLuminance = mForegroundLuminance = 0; mBackgroundColors.add(0); mForegroundColors.add(0); } else if (mLuminanceMap.size() == 1) { // Deal with views that only contain a single color mBackgroundLuminance = mForegroundLuminance = mLuminanceHistogram.keySet().iterator() .next(); final int singleColor = mLuminanceMap.keySet().iterator().next(); mForegroundColors.add(singleColor); mBackgroundColors.add(singleColor); } else { // Sort all luminance values seen from low to high final ArrayList> colorsByLuminance = Lists.newArrayList(mLuminanceMap.entrySet()); Collections.sort(colorsByLuminance, new Comparator>() { @Override public int compare(Entry lhs, Entry rhs) { return Double.compare(lhs.getValue(), rhs.getValue()); } }); // Sort luminance values seen by frequency in the image final ArrayList> luminanceByFrequency = Lists.newArrayList(mLuminanceHistogram.entrySet()); Collections.sort(luminanceByFrequency, new Comparator>() { @Override public int compare(Entry lhs, Entry rhs) { return lhs.getValue() - rhs.getValue(); } }); // Find the average luminance value within the set of luminances for // purposes of splitting luminance values into high-luminance and // low-luminance buckets. This is explicitly not a weighted average. double luminanceSum = 0; for (Entry luminanceCount : luminanceByFrequency) { luminanceSum += luminanceCount.getKey(); } final double averageLuminance = luminanceSum / luminanceByFrequency.size(); // Select the highest and lowest luminance values that contribute to // most number of pixels in the image -- our background and // foreground colors. double lowLuminanceContributor = 0.0d; for (int i = luminanceByFrequency.size() - 1; i >= 0; --i) { final double luminanceValue = luminanceByFrequency.get(i).getKey(); if (luminanceValue < averageLuminance) { lowLuminanceContributor = luminanceValue; break; } } double highLuminanceContributor = 1.0d; for (int i = luminanceByFrequency.size() - 1; i >= 0; --i) { final double luminanceValue = luminanceByFrequency.get(i).getKey(); if (luminanceValue >= averageLuminance) { highLuminanceContributor = luminanceValue; break; } } // Background luminance is that which occurs more frequently if (mLuminanceHistogram.get(highLuminanceContributor) > mLuminanceHistogram.get(lowLuminanceContributor)) { mBackgroundLuminance = highLuminanceContributor; mForegroundLuminance = lowLuminanceContributor; } else { mBackgroundLuminance = lowLuminanceContributor; mForegroundLuminance = highLuminanceContributor; } // Determine the contributing colors for those luminance values // TODO: Optimize (find an alternative to reiterating through whole image) for (Entry colorLuminance : mLuminanceMap.entrySet()) { if (colorLuminance.getValue() == mBackgroundLuminance) { mBackgroundColors.add(colorLuminance.getKey()); } if (colorLuminance.getValue() == mForegroundLuminance) { mForegroundColors.add(colorLuminance.getKey()); } } } } /** * Calculates a more accurate text color for how the text in the view appears by using its alpha * value to determine how much it needs to be blended into its background color. * * @param textColor Text color. * @param backgroundColor Background color. * @return Calculated text color. */ private int calculateTextColor(int textColor, int backgroundColor) { Color text = new Color(textColor, true); Color background = new Color(backgroundColor, true); int alpha = text.getAlpha(); double alphaPercentage = alpha / MAX_RGB_VALUE; double alphaCompliment = 1 - alphaPercentage; int red = (int) (alphaPercentage * text.getRed() + alphaCompliment * background.getRed()); int green = (int) (alphaPercentage * text.getGreen() + alphaCompliment * background.getGreen()); int blue = (int) (alphaPercentage * text.getBlue() + alphaCompliment * background.getBlue()); Color rgb = new Color(red, green, blue, (int) MAX_RGB_VALUE); mTextColor = rgb.getRGB(); return mTextColor; } public ContrastResult getContrastResult() { ContrastResult normalTest = getContrastResultForNormalText(); ContrastResult largeTest = getContrastResultForLargeText(); if (normalTest == largeTest) { return normalTest; } else if (mTextSize == null) { return ContrastResult.INDETERMINATE; } else if (mTextSize >= NORMAL_TEXT_BOLD_SZ_PTS && mIsBold || mTextSize > NORMAL_TEXT_SZ_PTS) { return largeTest; } else { return normalTest; } } public ContrastResult getContrastResultForLargeText() { return mContrastRatio >= CONTRAST_RATIO_LARGE_TEXT ? ContrastResult.PASS : ContrastResult.FAIL; } public ContrastResult getContrastResultForNormalText() { if (mIsBold && mTextSize != null && mTextSize >= NORMAL_TEXT_BOLD_SZ_PTS) { return getContrastResultForLargeText(); } return mContrastRatio >= CONTRAST_RATIO_NORMAL_TEXT ? ContrastResult.PASS : ContrastResult.FAIL; } public double getContrastRatio() { return mContrastRatio; } public double getBackgroundLuminance() { return mBackgroundLuminance; } public String getTextSize() { if (mTextSize == null ){ return NOT_APPLICABLE; } return Double.toString(mTextSize); } public int getTextColor() { Integer textColor; if (mTextColor != null) { textColor = mTextColor; } else { // assumes that the foreground color is the luminance value that occurs the least // frequently; which is also the best estimate we have for text color. textColor = mForegroundColors.get(0); } return textColor.intValue(); } public String getTextColorHex() { return intToHexString(getTextColor()); } public int getBackgroundColor() { return mBackgroundColors.get(0); } public String getBackgroundColorHex() { return intToHexString(mBackgroundColors.get(0)); } public boolean isIndeterminate() { return mTextSize == null && getContrastResult() == ContrastResult.INDETERMINATE; } public boolean isBold() { return mIsBold; } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000171 12747325007 032624 xustar000000000 0000000 121 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfec0100644 0000000 0000000 00000024244 12747325007 033212 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.models; import com.android.ddmlib.IDevice; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Display; import java.util.ArrayList; public class PixelPerfectModel { public static final int MIN_ZOOM = 2; public static final int MAX_ZOOM = 24; public static final int DEFAULT_ZOOM = 8; public static final int DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE = 50; private IDevice mDevice; private Image mImage; private Point mCrosshairLocation; private ViewNode mViewNode; private ViewNode mSelectedNode; private int mZoom; private final ArrayList mImageChangeListeners = new ArrayList(); private Image mOverlayImage; private double mOverlayTransparency = DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE / 100.0; private static PixelPerfectModel sModel; public static PixelPerfectModel getModel() { if (sModel == null) { sModel = new PixelPerfectModel(); } return sModel; } public void setData(final IDevice device, final Image image, final ViewNode viewNode) { final Image toDispose = this.mImage; final Image toDispose2 = this.mOverlayImage; Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (PixelPerfectModel.this) { PixelPerfectModel.this.mDevice = device; PixelPerfectModel.this.mImage = image; PixelPerfectModel.this.mViewNode = viewNode; if (image != null) { PixelPerfectModel.this.mCrosshairLocation = new Point(image.getBounds().width / 2, image.getBounds().height / 2); } else { PixelPerfectModel.this.mCrosshairLocation = null; } mOverlayImage = null; PixelPerfectModel.this.mSelectedNode = null; mZoom = DEFAULT_ZOOM; } } }); notifyImageLoaded(); if (toDispose != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { toDispose.dispose(); } }); } if (toDispose2 != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { toDispose2.dispose(); } }); } } public void setCrosshairLocation(int x, int y) { synchronized (this) { mCrosshairLocation = new Point(x, y); } notifyCrosshairMoved(); } public void setSelected(ViewNode selected) { synchronized (this) { this.mSelectedNode = selected; } notifySelectionChanged(); } public void setTree(final ViewNode viewNode) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (PixelPerfectModel.this) { PixelPerfectModel.this.mViewNode = viewNode; PixelPerfectModel.this.mSelectedNode = null; } } }); notifyTreeChanged(); } public void setImage(final Image image) { final Image toDispose = this.mImage; Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (PixelPerfectModel.this) { PixelPerfectModel.this.mImage = image; } } }); notifyImageChanged(); if (toDispose != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { toDispose.dispose(); } }); } } public void setZoom(int newZoom) { synchronized (this) { if (newZoom < MIN_ZOOM) { newZoom = MIN_ZOOM; } if (newZoom > MAX_ZOOM) { newZoom = MAX_ZOOM; } mZoom = newZoom; } notifyZoomChanged(); } public void setOverlayImage(final Image overlayImage) { final Image toDispose = this.mOverlayImage; Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (PixelPerfectModel.this) { PixelPerfectModel.this.mOverlayImage = overlayImage; } } }); notifyOverlayChanged(); if (toDispose != null) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { toDispose.dispose(); } }); } } public void setOverlayTransparency(double value) { synchronized (this) { value = Math.max(value, 0); value = Math.min(value, 1); mOverlayTransparency = value; } notifyOverlayTransparencyChanged(); } public ViewNode getViewNode() { synchronized (this) { return mViewNode; } } public Point getCrosshairLocation() { synchronized (this) { return mCrosshairLocation; } } public Image getImage() { synchronized (this) { return mImage; } } public ViewNode getSelected() { synchronized (this) { return mSelectedNode; } } public IDevice getDevice() { synchronized (this) { return mDevice; } } public int getZoom() { synchronized (this) { return mZoom; } } public Image getOverlayImage() { synchronized (this) { return mOverlayImage; } } public double getOverlayTransparency() { synchronized (this) { return mOverlayTransparency; } } public static interface IImageChangeListener { public void imageLoaded(); public void imageChanged(); public void crosshairMoved(); public void selectionChanged(); public void treeChanged(); public void zoomChanged(); public void overlayChanged(); public void overlayTransparencyChanged(); } private IImageChangeListener[] getImageChangeListenerList() { IImageChangeListener[] listeners = null; synchronized (mImageChangeListeners) { if (mImageChangeListeners.size() == 0) { return null; } listeners = mImageChangeListeners.toArray(new IImageChangeListener[mImageChangeListeners .size()]); } return listeners; } public void notifyImageLoaded() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].imageLoaded(); } } } public void notifyImageChanged() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].imageChanged(); } } } public void notifyCrosshairMoved() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].crosshairMoved(); } } } public void notifySelectionChanged() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].selectionChanged(); } } } public void notifyTreeChanged() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].treeChanged(); } } } public void notifyZoomChanged() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].zoomChanged(); } } } public void notifyOverlayChanged() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].overlayChanged(); } } } public void notifyOverlayTransparencyChanged() { IImageChangeListener[] listeners = getImageChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].overlayTransparencyChanged(); } } } public void addImageChangeListener(IImageChangeListener listener) { synchronized (mImageChangeListeners) { mImageChangeListeners.add(listener); } } public void removeImageChangeListener(IImageChangeListener listener) { synchronized (mImageChangeListeners) { mImageChangeListeners.remove(listener); } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000162 12747325007 032624 xustar000000000 0000000 114 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.0100644 0000000 0000000 00000003054 12747325007 033101 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.models; import com.android.annotations.NonNull; import java.util.ArrayList; import java.util.List; /** * This class manages the resource data that was dumped from a View's Theme. */ public class ThemeModel { private List data; public ThemeModel() { data = new ArrayList(); } public void add(@NonNull String name, @NonNull String value) { data.add(new ThemeModelData(name, value)); } public List getData() { return data; } public class ThemeModelData { private String mName; private String mValue; public ThemeModelData(@NonNull String name, @NonNull String value) { mName = name; mValue = value; } public String getName() { return mName; } public String getValue() { return mValue; } } }./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000165 12747325007 032627 xustar000000000 0000000 117 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewMod0100644 0000000 0000000 00000014171 12747325007 033174 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.models; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; import java.util.ArrayList; public class TreeViewModel { public static final double MAX_ZOOM = 2; public static final double MIN_ZOOM = 0.2; private Window mWindow; private DrawableViewNode mTree; private DrawableViewNode mSelectedNode; private Rectangle mViewport; private double mZoom; private final ArrayList mTreeChangeListeners = new ArrayList(); private static TreeViewModel sModel; public static TreeViewModel getModel() { if (sModel == null) { sModel = new TreeViewModel(); } return sModel; } public void setData(Window window, ViewNode viewNode) { synchronized (this) { if (mTree != null) { mTree.viewNode.dispose(); } this.mWindow = window; if (viewNode == null) { mTree = null; } else { mTree = new DrawableViewNode(viewNode); mTree.setLeft(); mTree.placeRoot(); } mViewport = null; mZoom = 1; mSelectedNode = null; } notifyTreeChanged(); } public void setSelection(DrawableViewNode selectedNode) { synchronized (this) { this.mSelectedNode = selectedNode; } notifySelectionChanged(); } public void setViewport(Rectangle viewport) { synchronized (this) { this.mViewport = viewport; } notifyViewportChanged(); } public void setZoom(double newZoom) { Point zoomPoint = null; synchronized (this) { if (mTree != null && mViewport != null) { zoomPoint = new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height / 2); } } zoomOnPoint(newZoom, zoomPoint); } public void zoomOnPoint(double newZoom, Point zoomPoint) { synchronized (this) { if (mTree != null && this.mViewport != null) { if (newZoom < MIN_ZOOM) { newZoom = MIN_ZOOM; } if (newZoom > MAX_ZOOM) { newZoom = MAX_ZOOM; } mViewport.x = zoomPoint.x - (zoomPoint.x - mViewport.x) * mZoom / newZoom; mViewport.y = zoomPoint.y - (zoomPoint.y - mViewport.y) * mZoom / newZoom; mViewport.width = mViewport.width * mZoom / newZoom; mViewport.height = mViewport.height * mZoom / newZoom; mZoom = newZoom; } } notifyZoomChanged(); } public DrawableViewNode getTree() { synchronized (this) { return mTree; } } public Window getWindow() { synchronized (this) { return mWindow; } } public Rectangle getViewport() { synchronized (this) { return mViewport; } } public double getZoom() { synchronized (this) { return mZoom; } } public DrawableViewNode getSelection() { synchronized (this) { return mSelectedNode; } } public static interface ITreeChangeListener { public void treeChanged(); public void selectionChanged(); public void viewportChanged(); public void zoomChanged(); } private ITreeChangeListener[] getTreeChangeListenerList() { ITreeChangeListener[] listeners = null; synchronized (mTreeChangeListeners) { if (mTreeChangeListeners.size() == 0) { return null; } listeners = mTreeChangeListeners.toArray(new ITreeChangeListener[mTreeChangeListeners.size()]); } return listeners; } public void notifyTreeChanged() { ITreeChangeListener[] listeners = getTreeChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].treeChanged(); } } } public void notifySelectionChanged() { ITreeChangeListener[] listeners = getTreeChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].selectionChanged(); } } } public void notifyViewportChanged() { ITreeChangeListener[] listeners = getTreeChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].viewportChanged(); } } } public void notifyZoomChanged() { ITreeChangeListener[] listeners = getTreeChangeListenerList(); if (listeners != null) { for (int i = 0; i < listeners.length; i++) { listeners[i].zoomChanged(); } } } public void addTreeChangeListener(ITreeChangeListener listener) { synchronized (mTreeChangeListeners) { mTreeChangeListeners.add(listener); } } public void removeTreeChangeListener(ITreeChangeListener listener) { synchronized (mTreeChangeListeners) { mTreeChangeListeners.remove(listener); } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000160 12747325007 032622 xustar000000000 0000000 112 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.ja0100644 0000000 0000000 00000031231 12747325007 033107 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.models; import org.eclipse.swt.graphics.Image; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; public class ViewNode { public static enum ProfileRating { RED, YELLOW, GREEN, NONE }; private static final double RED_THRESHOLD = 0.8; private static final double YELLOW_THRESHOLD = 0.5; public static final String MISCELLANIOUS = "miscellaneous"; public String id; public String name; public String hashCode; public List properties = new ArrayList(); public Map namedProperties = new HashMap(); public ViewNode parent; public List children = new ArrayList(); public int left; public int top; public int width; public int height; public int scrollX; public int scrollY; public int paddingLeft; public int paddingRight; public int paddingTop; public int paddingBottom; public int marginLeft; public int marginRight; public int marginTop; public int marginBottom; public int baseline; public boolean willNotDraw; public boolean hasMargins; public boolean hasFocus; public int index; public double measureTime; public double layoutTime; public double drawTime; public ProfileRating measureRating = ProfileRating.NONE; public ProfileRating layoutRating = ProfileRating.NONE; public ProfileRating drawRating = ProfileRating.NONE; public Set categories = new TreeSet(); public Window window; public Image image; public int imageReferences = 1; public int viewCount; public boolean filtered; public int protocolVersion; public ViewNode(Window window, ViewNode parent, String data) { this.window = window; this.parent = parent; index = this.parent == null ? 0 : this.parent.children.size(); if (this.parent != null) { this.parent.children.add(this); } int delimIndex = data.indexOf('@'); if (delimIndex < 0) { throw new IllegalArgumentException("Invalid format for ViewNode, missing @: " + data); } name = data.substring(0, delimIndex); data = data.substring(delimIndex + 1); delimIndex = data.indexOf(' '); hashCode = data.substring(0, delimIndex); if (data.length() > delimIndex + 1) { loadProperties(data.substring(delimIndex + 1).trim()); } else { // defaults in case properties are not available id = "unknown"; width = height = 10; } measureTime = -1; layoutTime = -1; drawTime = -1; } public void dispose() { final int N = children.size(); for (int i = 0; i < N; i++) { children.get(i).dispose(); } dereferenceImage(); } public void referenceImage() { imageReferences++; } public void dereferenceImage() { imageReferences--; if (image != null && imageReferences == 0) { image.dispose(); } } private void loadProperties(String data) { int start = 0; boolean stop; do { int index = data.indexOf('=', start); ViewNode.Property property = new ViewNode.Property(); property.name = data.substring(start, index); int index2 = data.indexOf(',', index + 1); int length = Integer.parseInt(data.substring(index + 1, index2)); start = index2 + 1 + length; property.value = data.substring(index2 + 1, index2 + 1 + length); properties.add(property); namedProperties.put(property.name, property); stop = start >= data.length(); if (!stop) { start += 1; } } while (!stop); Collections.sort(properties, new Comparator() { @Override public int compare(ViewNode.Property source, ViewNode.Property destination) { return source.name.compareTo(destination.name); } }); id = namedProperties.get("mID").value; //$NON-NLS-1$ left = namedProperties.containsKey("mLeft") ? getInt("mLeft", 0) : getInt("layout:mLeft", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 0); top = namedProperties.containsKey("mTop") ? getInt("mTop", 0) : getInt("layout:mTop", 0); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ width = namedProperties.containsKey("getWidth()") ? getInt("getWidth()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "layout:getWidth()", 0); //$NON-NLS-1$ height = namedProperties.containsKey("getHeight()") ? getInt("getHeight()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "layout:getHeight()", 0); //$NON-NLS-1$ scrollX = namedProperties.containsKey("mScrollX") ? getInt("mScrollX", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "scrolling:mScrollX", 0); //$NON-NLS-1$ scrollY = namedProperties.containsKey("mScrollY") ? getInt("mScrollY", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "scrolling:mScrollY", 0); //$NON-NLS-1$ paddingLeft = namedProperties.containsKey("mPaddingLeft") ? getInt("mPaddingLeft", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "padding:mPaddingLeft", 0); //$NON-NLS-1$ paddingRight = namedProperties.containsKey("mPaddingRight") ? getInt("mPaddingRight", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "padding:mPaddingRight", 0); //$NON-NLS-1$ paddingTop = namedProperties.containsKey("mPaddingTop") ? getInt("mPaddingTop", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "padding:mPaddingTop", 0); //$NON-NLS-1$ paddingBottom = namedProperties.containsKey("mPaddingBottom") ? getInt("mPaddingBottom", 0) //$NON-NLS-1$ //$NON-NLS-2$ : getInt("padding:mPaddingBottom", 0); //$NON-NLS-1$ marginLeft = namedProperties.containsKey("layout_leftMargin") ? getInt("layout_leftMargin", //$NON-NLS-1$ //$NON-NLS-2$ Integer.MIN_VALUE) : getInt("layout:layout_leftMargin", Integer.MIN_VALUE); //$NON-NLS-1$ marginRight = namedProperties.containsKey("layout_rightMargin") ? getInt("layout_rightMargin", //$NON-NLS-1$ //$NON-NLS-2$ Integer.MIN_VALUE) : getInt("layout:layout_rightMargin", Integer.MIN_VALUE); //$NON-NLS-1$ marginTop = namedProperties.containsKey("layout_topMargin") ? getInt("layout_topMargin", //$NON-NLS-1$ //$NON-NLS-2$ Integer.MIN_VALUE) : getInt("layout:layout_topMargin", Integer.MIN_VALUE); //$NON-NLS-1$ marginBottom = namedProperties.containsKey("layout_bottomMargin") ? getInt("layout_bottomMargin", //$NON-NLS-1$ //$NON-NLS-2$ Integer.MIN_VALUE) : getInt("layout:layout_bottomMargin", Integer.MIN_VALUE); //$NON-NLS-1$ baseline = namedProperties.containsKey("getBaseline()") ? getInt("getBaseline()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ "layout:getBaseline()", 0); //$NON-NLS-1$ willNotDraw = namedProperties.containsKey("willNotDraw()") ? getBoolean("willNotDraw()", false) //$NON-NLS-1$ //$NON-NLS-2$ : getBoolean("drawing:willNotDraw()", false); //$NON-NLS-1$ hasFocus = namedProperties.containsKey("hasFocus()") ? getBoolean("hasFocus()", false) //$NON-NLS-1$ //$NON-NLS-2$ : getBoolean("focus:hasFocus()", false); //$NON-NLS-1$ hasMargins = marginLeft != Integer.MIN_VALUE && marginRight != Integer.MIN_VALUE && marginTop != Integer.MIN_VALUE && marginBottom != Integer.MIN_VALUE; for (String name : namedProperties.keySet()) { int index = name.indexOf(':'); if (index != -1) { categories.add(name.substring(0, index)); } } if (categories.size() != 0) { categories.add(MISCELLANIOUS); } } public void setProfileRatings() { final int N = children.size(); if (N > 1) { double totalMeasure = 0; double totalLayout = 0; double totalDraw = 0; for (int i = 0; i < N; i++) { ViewNode child = children.get(i); totalMeasure += child.measureTime; totalLayout += child.layoutTime; totalDraw += child.drawTime; } for (int i = 0; i < N; i++) { ViewNode child = children.get(i); if (child.measureTime / totalMeasure >= RED_THRESHOLD) { child.measureRating = ProfileRating.RED; } else if (child.measureTime / totalMeasure >= YELLOW_THRESHOLD) { child.measureRating = ProfileRating.YELLOW; } else { child.measureRating = ProfileRating.GREEN; } if (child.layoutTime / totalLayout >= RED_THRESHOLD) { child.layoutRating = ProfileRating.RED; } else if (child.layoutTime / totalLayout >= YELLOW_THRESHOLD) { child.layoutRating = ProfileRating.YELLOW; } else { child.layoutRating = ProfileRating.GREEN; } if (child.drawTime / totalDraw >= RED_THRESHOLD) { child.drawRating = ProfileRating.RED; } else if (child.drawTime / totalDraw >= YELLOW_THRESHOLD) { child.drawRating = ProfileRating.YELLOW; } else { child.drawRating = ProfileRating.GREEN; } } } for (int i = 0; i < N; i++) { children.get(i).setProfileRatings(); } } public void setViewCount() { viewCount = 1; final int N = children.size(); for (int i = 0; i < N; i++) { ViewNode child = children.get(i); child.setViewCount(); viewCount += child.viewCount; } } public void filter(String text) { int dotIndex = name.lastIndexOf('.'); String shortName = (dotIndex == -1) ? name : name.substring(dotIndex + 1); filtered = !text.equals("") //$NON-NLS-1$ && (shortName.toLowerCase().contains(text.toLowerCase()) || (!id .equals("NO_ID") && id.toLowerCase().contains(text.toLowerCase()))); //$NON-NLS-1$ final int N = children.size(); for (int i = 0; i < N; i++) { children.get(i).filter(text); } } private boolean getBoolean(String name, boolean defaultValue) { Property p = namedProperties.get(name); if (p != null) { try { return Boolean.parseBoolean(p.value); } catch (NumberFormatException e) { return defaultValue; } } return defaultValue; } private int getInt(String name, int defaultValue) { Property p = namedProperties.get(name); if (p != null) { try { return Integer.parseInt(p.value); } catch (NumberFormatException e) { return defaultValue; } } return defaultValue; } @Override public String toString() { return name + "@" + hashCode; //$NON-NLS-1$ } public static class Property { public String name; public String value; @Override public String toString() { return name + '=' + value; } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000156 12747325007 032627 xustar000000000 0000000 110 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java0100644 0000000 0000000 00000006264 12747325007 033175 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.models; import com.android.ddmlib.Client; import com.android.ddmlib.IDevice; import com.android.hierarchyviewerlib.device.IHvDevice; /** * Used for storing a window from the window manager service on the device. * These are the windows that the device selector shows. */ public class Window { private final String mTitle; private final int mHashCode; private final IHvDevice mHvDevice; private final Client mClient; public Window(IHvDevice device, String title, int hashCode) { mHvDevice = device; mTitle = title; mHashCode = hashCode; mClient = null; } public Window(IHvDevice device, String title, Client c) { mHvDevice = device; mTitle = title; mClient = c; mHashCode = c.hashCode(); } public String getTitle() { return mTitle; } public int getHashCode() { return mHashCode; } public String encode() { return Integer.toHexString(mHashCode); } @Override public String toString() { return mTitle; } public IHvDevice getHvDevice() { return mHvDevice; } public IDevice getDevice() { return mHvDevice.getDevice(); } public Client getClient() { return mClient; } public static Window getFocusedWindow(IHvDevice device) { return new Window(device, "", -1); } /* * After each refresh of the windows in the device selector, the windows are * different instances and automatically reselecting the same window doesn't * work in the device selector unless the equals method is defined here. */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Window other = (Window) obj; if (mHvDevice == null) { if (other.mHvDevice != null) return false; } else if (!mHvDevice.getDevice().getSerialNumber().equals( other.mHvDevice.getDevice().getSerialNumber())) return false; if (mHashCode != other.mHashCode) return false; return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mHvDevice == null) ? 0 : mHvDevice.getDevice().getSerialNumber().hashCode()); result = prime * result + mHashCode; return result; } } hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/0040755 0000000 0000000 00000000000 12747325007 030210 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000162 12747325007 032626 xustar000000000 0000000 114 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.0100644 0000000 0000000 00000020154 12747325007 033141 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.ViewNode; import org.eclipse.swt.SWT; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class CaptureDisplay { private static Shell sShell; private static Canvas sCanvas; private static Image sImage; private static ViewNode sViewNode; private static Composite sButtonBar; private static Button sOnWhite; private static Button sOnBlack; private static Button sShowExtras; public static void show(Shell parentShell, ViewNode viewNode, Image image) { if (sShell == null) { createShell(); } if (sShell.isVisible() && CaptureDisplay.sViewNode != null) { CaptureDisplay.sViewNode.dereferenceImage(); } CaptureDisplay.sImage = image; CaptureDisplay.sViewNode = viewNode; viewNode.referenceImage(); sShell.setText(viewNode.name); boolean shellVisible = sShell.isVisible(); if (!shellVisible) { sShell.setSize(0, 0); } Rectangle bounds = sShell.computeTrim(0, 0, Math.max(sButtonBar.getBounds().width, image.getBounds().width), sButtonBar.getBounds().height + image.getBounds().height + 5); sShell.setSize(bounds.width, bounds.height); if (!shellVisible) { sShell.setLocation(parentShell.getBounds().x + (parentShell.getBounds().width - bounds.width) / 2, parentShell.getBounds().y + (parentShell.getBounds().height - bounds.height) / 2); } sShell.open(); if (shellVisible) { sCanvas.redraw(); } } private static void createShell() { sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); GridLayout gridLayout = new GridLayout(); gridLayout.marginWidth = 0; gridLayout.marginHeight = 0; sShell.setLayout(gridLayout); sButtonBar = new Composite(sShell, SWT.NONE); RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); rowLayout.pack = true; rowLayout.center = true; sButtonBar.setLayout(rowLayout); Composite buttons = new Composite(sButtonBar, SWT.NONE); buttons.setLayout(new FillLayout()); sOnWhite = new Button(buttons, SWT.TOGGLE); sOnWhite.setText("On White"); sOnBlack = new Button(buttons, SWT.TOGGLE); sOnBlack.setText("On Black"); sOnBlack.setSelection(true); sOnWhite.addSelectionListener(sWhiteSelectionListener); sOnBlack.addSelectionListener(sBlackSelectionListener); sShowExtras = new Button(sButtonBar, SWT.CHECK); sShowExtras.setText("Show Extras"); sShowExtras.addSelectionListener(sExtrasSelectionListener); sCanvas = new Canvas(sShell, SWT.NONE); sCanvas.setLayoutData(new GridData(GridData.FILL_BOTH)); sCanvas.addPaintListener(sPaintListener); sShell.addShellListener(sShellListener); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); Image image = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$ sShell.setImage(image); } private static PaintListener sPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { if (sOnWhite.getSelection()) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); } else { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); } e.gc.fillRectangle(0, 0, sCanvas.getBounds().width, sCanvas.getBounds().height); if (sImage != null) { int width = sImage.getBounds().width; int height = sImage.getBounds().height; int x = (sCanvas.getBounds().width - width) / 2; int y = (sCanvas.getBounds().height - height) / 2; e.gc.drawImage(sImage, x, y); if (sShowExtras.getSelection()) { if ((sViewNode.paddingLeft | sViewNode.paddingRight | sViewNode.paddingTop | sViewNode.paddingBottom) != 0) { e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE)); e.gc.drawRectangle(x + sViewNode.paddingLeft, y + sViewNode.paddingTop, width - sViewNode.paddingLeft - sViewNode.paddingRight - 1, height - sViewNode.paddingTop - sViewNode.paddingBottom - 1); } if (sViewNode.hasMargins) { e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GREEN)); e.gc.drawRectangle(x - sViewNode.marginLeft, y - sViewNode.marginTop, width + sViewNode.marginLeft + sViewNode.marginRight - 1, height + sViewNode.marginTop + sViewNode.marginBottom - 1); } if (sViewNode.baseline != -1) { e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); e.gc.drawLine(x, y + sViewNode.baseline, x + width - 1, sViewNode.baseline); } } } } }; private static ShellAdapter sShellListener = new ShellAdapter() { @Override public void shellClosed(ShellEvent e) { e.doit = false; sShell.setVisible(false); if (sViewNode != null) { sViewNode.dereferenceImage(); } } }; private static SelectionListener sWhiteSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { sOnWhite.setSelection(true); sOnBlack.setSelection(false); sCanvas.redraw(); } }; private static SelectionListener sBlackSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { sOnBlack.setSelection(true); sOnWhite.setSelection(false); sCanvas.redraw(); } }; private static SelectionListener sExtrasSelectionListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { sCanvas.redraw(); } }; } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000200 12747325007 032617 xustar000000000 0000000 128 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyE0100644 0000000 0000000 00000025307 12747325007 033210 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.ui; import com.android.SdkConstants; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.ViewNode.Property; import com.android.utils.SdkUtils; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; import java.text.ParseException; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; public class DevicePropertyEditingSupport { public enum PropertyType { INTEGER, INTEGER_OR_CONSTANT, ENUM, }; private static final List sDevicePropertyEditors = Arrays.asList( new LayoutPropertyEditor(), new PaddingPropertyEditor() ); public boolean canEdit(Property p) { return getPropertyEditorFor(p) != null; } private IDevicePropertyEditor getPropertyEditorFor(Property p) { for (IDevicePropertyEditor pe: sDevicePropertyEditors) { if (pe.canEdit(p)) { return pe; } } return null; } public PropertyType getPropertyType(Property p) { return getPropertyEditorFor(p).getType(p); } public String[] getPropertyRange(Property p) { return getPropertyEditorFor(p).getPropertyRange(p); } public boolean setValue(Collection properties, Property p, Object newValue, ViewNode viewNode, IHvDevice device) { return getPropertyEditorFor(p).setValue(properties, p, newValue, viewNode, device); } private static String stripCategoryPrefix(String name) { return name.substring(name.indexOf(':') + 1); } private interface IDevicePropertyEditor { boolean canEdit(Property p); PropertyType getType(Property p); String[] getPropertyRange(Property p); boolean setValue(Collection properties, Property p, Object newValue, ViewNode viewNode, IHvDevice device); } private static class LayoutPropertyEditor implements IDevicePropertyEditor { private static final Set sLayoutPropertiesWithStringValues = ImmutableSet.of(SdkConstants.ATTR_LAYOUT_WIDTH, SdkConstants.ATTR_LAYOUT_HEIGHT, SdkConstants.ATTR_LAYOUT_GRAVITY); private static final int MATCH_PARENT = -1; private static final int FILL_PARENT = -1; private static final int WRAP_CONTENT = -2; private enum LayoutGravity { top(0x30), bottom(0x50), left(0x03), right(0x05), center_vertical(0x10), fill_vertical(0x70), center_horizontal(0x01), fill_horizontal(0x07), center(0x11), fill(0x77), clip_vertical(0x80), clip_horizontal(0x08), start(0x00800003), end(0x00800005); private final int mValue; private LayoutGravity(int v) { mValue = v; } } /** * Returns true if this is a layout property with either a known string value, or an * integer value. */ @Override public boolean canEdit(Property p) { String name = stripCategoryPrefix(p.name); if (!name.startsWith(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX)) { return false; } if (sLayoutPropertiesWithStringValues.contains(name)) { return true; } try { SdkUtils.parseLocalizedInt(p.value); return true; } catch (ParseException e) { return false; } } @Override public PropertyType getType(Property p) { String name = stripCategoryPrefix(p.name); if (sLayoutPropertiesWithStringValues.contains(name)) { return PropertyType.INTEGER_OR_CONSTANT; } else { return PropertyType.INTEGER; } } @Override public String[] getPropertyRange(Property p) { return new String[0]; } @Override public boolean setValue(Collection properties, Property p, Object newValue, ViewNode viewNode, IHvDevice device) { String name = stripCategoryPrefix(p.name); // nothing to do if same as current value if (p.value.equals(newValue)) { return false; } int value = -1; String textValue = null; if (SdkConstants.ATTR_LAYOUT_GRAVITY.equals(name)) { value = 0; StringBuilder sb = new StringBuilder(20); for (String attr: Splitter.on('|').split((String) newValue)) { LayoutGravity g; try { g = LayoutGravity.valueOf(attr); } catch (IllegalArgumentException e) { // ignore this gravity attribute continue; } value |= g.mValue; if (sb.length() > 0) { sb.append('|'); } sb.append(g.name()); } textValue = sb.toString(); } else if (SdkConstants.ATTR_LAYOUT_HEIGHT.equals(name) || SdkConstants.ATTR_LAYOUT_WIDTH.equals(name)) { // newValue is of type string, but its contents may be a named constant or a integer String s = (String) newValue; if (s.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) { textValue = SdkConstants.VALUE_MATCH_PARENT; value = MATCH_PARENT; } else if (s.equalsIgnoreCase(SdkConstants.VALUE_FILL_PARENT)) { textValue = SdkConstants.VALUE_FILL_PARENT; value = FILL_PARENT; } else if (s.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) { textValue = SdkConstants.VALUE_WRAP_CONTENT; value = WRAP_CONTENT; } } if (textValue == null) { try { value = Integer.parseInt((String) newValue); } catch (NumberFormatException e) { return false; } } // attempt to set the value on the device name = name.substring(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX.length()); if (device.setLayoutParameter(viewNode.window, viewNode, name, value)) { p.value = textValue != null ? textValue : (String) newValue; } return true; } } private static class PaddingPropertyEditor implements IDevicePropertyEditor { // These names should match the field names used for padding in the Framework's View class private static final String PADDING_LEFT = "mPaddingLeft"; //$NON-NLS-1$ private static final String PADDING_RIGHT = "mPaddingRight"; //$NON-NLS-1$ private static final String PADDING_TOP = "mPaddingTop"; //$NON-NLS-1$ private static final String PADDING_BOTTOM = "mPaddingBottom"; //$NON-NLS-1$ private static final Set sPaddingProperties = ImmutableSet.of( PADDING_LEFT, PADDING_RIGHT, PADDING_TOP, PADDING_BOTTOM); @Override public boolean canEdit(Property p) { return sPaddingProperties.contains(stripCategoryPrefix(p.name)); } @Override public PropertyType getType(Property p) { return PropertyType.INTEGER; } @Override public String[] getPropertyRange(Property p) { return new String[0]; } /** * Set padding: Since the only view method is setPadding(l, t, r, b), we need access * to all 4 padding's to update any particular one. */ @Override public boolean setValue(Collection properties, Property prop, Object newValue, ViewNode viewNode, IHvDevice device) { int v; try { v = Integer.parseInt((String) newValue); } catch (NumberFormatException e) { return false; } int pLeft = 0; int pRight = 0; int pTop = 0; int pBottom = 0; String propName = stripCategoryPrefix(prop.name); for (Property p: properties) { String name = stripCategoryPrefix(p.name); if (!sPaddingProperties.contains(name)) { continue; } if (name.equals(PADDING_LEFT)) { pLeft = propName.equals(PADDING_LEFT) ? v : SdkUtils.parseLocalizedInt(p.value, 0); } else if (name.equals(PADDING_RIGHT)) { pRight = propName.equals(PADDING_RIGHT) ? v : SdkUtils.parseLocalizedInt(p.value, 0); } else if (name.equals(PADDING_TOP)) { pTop = propName.equals(PADDING_TOP) ? v : SdkUtils.parseLocalizedInt(p.value, 0); } else if (name.equals(PADDING_BOTTOM)) { pBottom = propName.equals(PADDING_BOTTOM) ? v : SdkUtils.parseLocalizedInt(p.value, 0); } } // invoke setPadding() on the device device.invokeViewMethod(viewNode.window, viewNode, "setPadding", Arrays.asList( Integer.valueOf(pLeft), Integer.valueOf(pTop), Integer.valueOf(pRight), Integer.valueOf(pBottom) )); // update the value set in the property (to avoid reading all properties back from // the device) prop.value = Integer.toString(v); return true; } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000162 12747325007 032626 xustar000000000 0000000 114 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.0100644 0000000 0000000 00000026777 12747325007 033131 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; import com.android.hierarchyviewerlib.models.Window; import org.eclipse.jface.viewers.IFontProvider; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; public class DeviceSelector extends Composite implements IWindowChangeListener, SelectionListener { private TreeViewer mTreeViewer; private Tree mTree; private DeviceSelectionModel mModel; private Font mBoldFont; private Image mDeviceImage; private Image mEmulatorImage; private final static int ICON_WIDTH = 16; private boolean mDoTreeViewStuff; private boolean mDoPixelPerfectStuff; private class ContentProvider implements ITreeContentProvider, ILabelProvider, IFontProvider { @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof IHvDevice && mDoTreeViewStuff) { Window[] list = mModel.getWindows((IHvDevice) parentElement); if (list != null) { return list; } } return new Object[0]; } @Override public Object getParent(Object element) { if (element instanceof Window) { return ((Window) element).getDevice(); } return null; } @Override public boolean hasChildren(Object element) { if (element instanceof IHvDevice && mDoTreeViewStuff) { Window[] list = mModel.getWindows((IHvDevice) element); if (list != null) { return list.length != 0; } } return false; } @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof DeviceSelectionModel) { return mModel.getDevices(); } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } @Override public Image getImage(Object element) { if (element instanceof IHvDevice) { if (((IHvDevice) element).getDevice().isEmulator()) { return mEmulatorImage; } return mDeviceImage; } return null; } @Override public String getText(Object element) { if (element instanceof IHvDevice) { return ((IHvDevice) element).getDevice().getName(); } else if (element instanceof Window) { return ((Window) element).getTitle(); } return null; } @Override public Font getFont(Object element) { if (element instanceof Window) { int focusedWindow = mModel.getFocusedWindow(((Window) element).getHvDevice()); if (focusedWindow == ((Window) element).getHashCode()) { return mBoldFont; } } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } public DeviceSelector(Composite parent, boolean doTreeViewStuff, boolean doPixelPerfectStuff) { super(parent, SWT.NONE); this.mDoTreeViewStuff = doTreeViewStuff; this.mDoPixelPerfectStuff = doPixelPerfectStuff; setLayout(new FillLayout()); mTreeViewer = new TreeViewer(this, SWT.SINGLE); mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); mTree = mTreeViewer.getTree(); mTree.setLinesVisible(true); mTree.addSelectionListener(this); addDisposeListener(mDisposeListener); loadResources(); mModel = DeviceSelectionModel.getModel(); ContentProvider contentProvider = new ContentProvider(); mTreeViewer.setContentProvider(contentProvider); mTreeViewer.setLabelProvider(contentProvider); mModel.addWindowChangeListener(this); mTreeViewer.setInput(mModel); addControlListener(mControlListener); } public void loadResources() { Display display = Display.getDefault(); Font systemFont = display.getSystemFont(); FontData[] fontData = systemFont.getFontData(); FontData[] newFontData = new FontData[fontData.length]; for (int i = 0; i < fontData.length; i++) { newFontData[i] = new FontData(fontData[i].getName(), fontData[i].getHeight(), fontData[i] .getStyle() | SWT.BOLD); } mBoldFont = new Font(Display.getDefault(), newFontData); ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mDeviceImage = loader.loadImage(display, "device.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$ .getSystemColor(SWT.COLOR_RED)); mEmulatorImage = loader.loadImage(display, "emulator.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$ .getSystemColor(SWT.COLOR_BLUE)); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeWindowChangeListener(DeviceSelector.this); mBoldFont.dispose(); } }; // If the window gets too small, hide the data, otherwise SWT throws an // ERROR. private ControlListener mControlListener = new ControlAdapter() { private boolean noInput = false; @Override public void controlResized(ControlEvent e) { if (getBounds().height <= 38) { mTreeViewer.setInput(null); noInput = true; } else if (noInput) { mTreeViewer.setInput(mModel); noInput = false; } } }; @Override public boolean setFocus() { return mTree.setFocus(); } public void setMode(boolean doTreeViewStuff, boolean doPixelPerfectStuff) { if (this.mDoTreeViewStuff != doTreeViewStuff || this.mDoPixelPerfectStuff != doPixelPerfectStuff) { final boolean expandAll = !this.mDoTreeViewStuff && doTreeViewStuff; this.mDoTreeViewStuff = doTreeViewStuff; this.mDoPixelPerfectStuff = doPixelPerfectStuff; Display.getDefault().syncExec(new Runnable() { @Override public void run() { mTreeViewer.refresh(); if (expandAll) { mTreeViewer.expandAll(); } } }); } } @Override public void deviceConnected(final IHvDevice device) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mTreeViewer.refresh(); mTreeViewer.setExpandedState(device, true); } }); } @Override public void deviceChanged(final IHvDevice device) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { TreeSelection selection = (TreeSelection) mTreeViewer.getSelection(); mTreeViewer.refresh(device); if (selection.getFirstElement() instanceof Window && ((Window) selection.getFirstElement()).getDevice() == device) { mTreeViewer.setSelection(selection, true); } } }); } @Override public void deviceDisconnected(final IHvDevice device) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mTreeViewer.refresh(); } }); } @Override public void focusChanged(final IHvDevice device) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { TreeSelection selection = (TreeSelection) mTreeViewer.getSelection(); mTreeViewer.refresh(device); if (selection.getFirstElement() instanceof Window && ((Window) selection.getFirstElement()).getDevice() == device) { mTreeViewer.setSelection(selection, true); } } }); } @Override public void selectionChanged(IHvDevice device, Window window) { // pass } @Override public void widgetDefaultSelected(SelectionEvent e) { Object selection = ((TreeItem) e.item).getData(); if (selection instanceof IHvDevice && mDoPixelPerfectStuff) { HierarchyViewerDirector.getDirector().loadPixelPerfectData((IHvDevice) selection); } else if (selection instanceof Window && mDoTreeViewStuff) { HierarchyViewerDirector.getDirector().loadViewTreeData((Window) selection); } } @Override public void widgetSelected(SelectionEvent e) { TreeItem item = (TreeItem) e.item; if (item == null) return; Object selection = item.getData(); if (selection instanceof IHvDevice) { mModel.setSelection((IHvDevice) selection, null); } else if (selection instanceof Window) { mModel.setSelection(((Window) selection).getHvDevice(), (Window) selection); } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000164 12747325007 032630 xustar000000000 0000000 116 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDispla0100644 0000000 0000000 00000010743 12747325007 033162 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.ui; import com.android.annotations.NonNull; import com.android.hierarchyviewerlib.models.ThemeModel; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import java.util.List; public class DumpThemeDisplay { private static final int DEFAULT_HEIGHT = 600; // px private static final int NUM_COLUMNS = 2; private static Shell sShell; private static ThemeModel sModel; private static Text sSearchText; private static Table sTable; public static void show(Shell parentShell, ThemeModel model) { if (sShell == null) { buildContents(); } else { sSearchText.setText(""); sTable.removeAll(); } sModel = model; addTableItems("", sModel.getData()); // configure size and placement sShell.setLocation(parentShell.getBounds().x, parentShell.getBounds().y); for (int i = 0; i < NUM_COLUMNS; ++i) { sTable.getColumn(i).pack(); } sTable.setLayoutData(GridDataFactory.swtDefaults().hint( sTable.computeSize(SWT.DEFAULT, SWT.DEFAULT).x, DEFAULT_HEIGHT).create()); sShell.pack(); sShell.open(); } private static void addTableItem(String name, String value) { TableItem row = new TableItem(sTable, SWT.NONE); row.setText(0, name); row.setText(1, value); } private static String sanitize(@NonNull String text) { return text.toLowerCase().trim(); } private static void addTableItems(String searchText, List list) { for (ThemeModel.ThemeModelData data : list) { searchText = sanitize(searchText); if ("".equals(searchText)) { addTableItem(data.getName(), data.getValue()); } else { if (sanitize(data.getName()).contains(searchText) || sanitize(data.getValue()).contains(searchText)) { addTableItem(data.getName(), data.getValue()); } } } } private static void buildContents() { sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); sShell.setText("Dump Theme"); sShell.addShellListener(sShellListener); sShell.setLayout(new GridLayout()); sSearchText = new Text(sShell, SWT.SINGLE | SWT.BORDER); sSearchText.setMessage("Enter text to search list"); sSearchText.addModifyListener(sModifyListener); sTable = new Table(sShell, SWT.BORDER | SWT.FULL_SELECTION); sTable.setHeaderVisible(true); sTable.setLinesVisible(true); String[] headers = { "Resource Name", "Resource Value" }; for (int i = 0; i < headers.length; ++i) { TableColumn column = new TableColumn(sTable, SWT.NONE); column.setText(headers[i]); } } private static ModifyListener sModifyListener = new ModifyListener() { @Override public void modifyText(ModifyEvent modifyEvent) { String searchText = sanitize(sSearchText.getText()); sTable.removeAll(); addTableItems(searchText, sModel.getData()); } }; private static ShellAdapter sShellListener = new ShellAdapter() { @Override public void shellClosed(ShellEvent e) { e.doit = false; sShell.setVisible(false); } }; } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000173 12747325007 032630 xustar000000000 0000000 123 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContras0100644 0000000 0000000 00000053147 12747325007 033242 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.ui; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.EvaluateContrastModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; import com.android.hierarchyviewerlib.models.ViewNode.Property; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import java.lang.Math; import java.lang.Override; import java.lang.StringBuilder; import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.HashMap; public class EvaluateContrastDisplay { private static final int DEFAULT_HEIGHT = 600; // px private static final int MARGIN = 30; // px private static final int PALLETE_IMAGE_SIZE = 16; // px private static final int IMAGE_WIDTH = 800; // px private static final int RESULTS_PANEL_WIDTH = 300; // px private static final int MAX_NUM_CHARACTERS = 35; private static final String ABBREVIATE_SUFFIX = "...\""; private static Shell sShell; private static Canvas sCanvas; private static Composite sResultsPanel; private static Tree sResultsTree; private static Image sImage; private static Point sImageOffset; private static ScrollBar sImageScrollBar; private static int sImageWidth; private static int sImageHeight; private static Image sYellowImage; private static Image sRedImage; private static Image sGreenImage; private static ViewNode sSelectedNode; private static org.eclipse.swt.graphics.Color sBorderColorPass; private static org.eclipse.swt.graphics.Color sBorderColorFail; private static org.eclipse.swt.graphics.Color sBorderColorIndeterminate; private static org.eclipse.swt.graphics.Color sBorderColorCurrentlySelected; private static HashMap sRectangleForViewNode; private static HashMap sBorderColorForViewNode; private static HashMap sViewNodeForModel; private static HashMap sImageForColor; private static HashMap sViewNodeForTreeItem; private static double sScaleFactor; static { sImageForColor = new HashMap(); sViewNodeForTreeItem = new HashMap(); ImageLoader loader = ImageLoader.getLoader(EvaluateContrastDisplay.class); sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); sRedImage = loader.loadImage("red.png", Display.getDefault()); sGreenImage = loader.loadImage("green.png", Display.getDefault()); sRectangleForViewNode = new HashMap(); sBorderColorForViewNode = new HashMap(); sViewNodeForModel = new HashMap(); } private static org.eclipse.swt.graphics.Color getBorderColorPass() { if (sBorderColorPass == null) { sBorderColorPass = /** green */ new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(0, 255, 0)); } return sBorderColorPass; } private static org.eclipse.swt.graphics.Color getBorderColorFail() { if (sBorderColorFail == null) { sBorderColorFail = /** red */ new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(255, 0, 0)); } return sBorderColorFail; } private static org.eclipse.swt.graphics.Color getBorderColorIndeterminate() { if (sBorderColorIndeterminate == null) { sBorderColorIndeterminate = /** yellow */ new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(255, 255, 0)); } return sBorderColorIndeterminate; } private static org.eclipse.swt.graphics.Color getBorderColorCurrentlySelected() { if (sBorderColorCurrentlySelected == null) { sBorderColorCurrentlySelected = /** blue */ new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(0, 0, 255)); } return sBorderColorCurrentlySelected; } private static void clear(boolean shellIsNull) { sRectangleForViewNode.clear(); sBorderColorForViewNode.clear(); sViewNodeForModel.clear(); if (!shellIsNull) { sImage.dispose(); for (Image image : sImageForColor.values()) { image.dispose(); } sImageForColor.clear(); sViewNodeForTreeItem.clear(); for (Control item : sShell.getChildren()) { item.dispose(); } } if (sBorderColorPass != null) { sBorderColorPass.dispose(); sBorderColorPass = null; } if (sBorderColorFail != null) { sBorderColorFail.dispose(); sBorderColorFail = null; } if (sBorderColorIndeterminate != null) { sBorderColorIndeterminate.dispose(); sBorderColorIndeterminate = null; } if (sBorderColorCurrentlySelected != null) { sBorderColorCurrentlySelected.dispose(); sBorderColorCurrentlySelected = null; } } private static Image scaleImage(Image image, int width, int height) { Image scaled = new Image(Display.getDefault(), width, height); GC gc = new GC(scaled); gc.setInterpolation(SWT.HIGH); gc.setAntialias(SWT.ON); gc.drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, 0, 0, width, height); image.dispose(); gc.dispose(); return scaled; } public static void show(Shell parentShell, ViewNode rootNode, Image image) { clear(sShell == null); sScaleFactor = Math.min(IMAGE_WIDTH / (double) image.getBounds().width, 1.0); sImage = scaleImage(image, IMAGE_WIDTH, (int) Math.round(image.getBounds().height * sScaleFactor)); sImageWidth = sImage.getBounds().width; sImageHeight = sImage.getBounds().height; if (sShell == null) { sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); sShell.setText("Evaluate Contrast"); sShell.addShellListener(sShellListener); sShell.setLayout(new GridLayout(2, false)); } buildContents(sShell); processEvaluatableChildViews(rootNode); sShell.setLocation(parentShell.getBounds().x, parentShell.getBounds().y); sShell.setSize(IMAGE_WIDTH + RESULTS_PANEL_WIDTH + MARGIN, DEFAULT_HEIGHT + (MARGIN * 2)); sImageScrollBar.setMaximum(sImage.getBounds().height); sImageScrollBar.setThumb(DEFAULT_HEIGHT); sShell.open(); sShell.layout(); } private static void buildContents(Composite shell) { buildResultsPanel(); buildImagePanel(shell); } private static void buildResultsPanel() { sResultsPanel = new Composite(sShell, SWT.NONE); sResultsPanel.setLayout(new FillLayout()); GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, true); sResultsPanel.setLayoutData(gridData); ScrolledComposite scrolledComposite = new ScrolledComposite(sResultsPanel, SWT.VERTICAL); sResultsTree = new Tree(scrolledComposite, SWT.NONE); sResultsTree.setLinesVisible(true); scrolledComposite.setContent(sResultsTree); sResultsTree.setSize(RESULTS_PANEL_WIDTH, DEFAULT_HEIGHT); sResultsTree.addListener(SWT.PaintItem, new Listener() { @Override public void handleEvent(Event event) { TreeItem item = (TreeItem) event.item; Image image = (Image) item.getData(); if (image != null) { int x = event.x + event.width; int itemHeight = sResultsTree.getItemHeight(); int imageHeight = image.getBounds().height; int y = event.y + (itemHeight - imageHeight) / 2; event.gc.drawImage(image, x, y); } } }); Listener listener = new Listener() { @Override public void handleEvent(Event e) { TreeItem treeItem = (TreeItem) e.item; if (treeItem.getItemCount() == 0) { do { treeItem = treeItem.getParentItem(); } while (treeItem.getParentItem() != null); } ViewNode node = sViewNodeForTreeItem.get(treeItem); if (sSelectedNode != node) { sSelectedNode = sViewNodeForTreeItem.get(treeItem); sCanvas.redraw(); } } }; sResultsTree.addListener(SWT.Selection, listener); sResultsTree.addListener(SWT.DefaultSelection, listener); } private static void buildImagePanel(Composite parent) { sImageOffset = new Point(0, 0); sCanvas = new Canvas(parent, SWT.V_SCROLL | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE); sCanvas.addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { GC gc = e.gc; gc.drawImage(sImage, sImageOffset.x, sImageOffset.y); for (ViewNode viewNode : sRectangleForViewNode.keySet()) { Rectangle rectangle = sRectangleForViewNode.get(viewNode); if (sSelectedNode == viewNode) { e.gc.setForeground(getBorderColorCurrentlySelected()); } else { e.gc.setForeground(sBorderColorForViewNode.get(viewNode)); } e.gc.drawRectangle(Math.max(0, sImageOffset.x + rectangle.x - 1), sImageOffset.y + rectangle.y - 1, rectangle.width - 1, rectangle.height - 1); } Rectangle rect = sImage.getBounds(); Rectangle client = sCanvas.getClientArea(); int marginWidth = client.width - rect.width; if (marginWidth > 0) { gc.fillRectangle(rect.width, 0, marginWidth, client.height); } int marginHeight = client.height - rect.height; if (marginHeight > 0) { gc.fillRectangle(0, rect.height, client.width, marginHeight); } } }); sImageScrollBar = sCanvas.getVerticalBar(); sImageScrollBar.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { int offset = sImageScrollBar.getSelection(); Rectangle imageBounds = sImage.getBounds(); sImageOffset.y = -offset; int y = -offset - sImageOffset.y; sCanvas.scroll(0, y, 0, 0, imageBounds.width, imageBounds.height, false); sCanvas.redraw(); } }); GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, true, true); gridData.widthHint = IMAGE_WIDTH + MARGIN; gridData.heightHint = DEFAULT_HEIGHT; sCanvas.setLayoutData(gridData); } private static void processEvaluatableChildViews(ViewNode root){ List children = getEvaluatableChildViews(root); for (final ViewNode child : children) { calculateRectangleForViewNode(child); EvaluateContrastModel evaluateContrastModel = evaluateContrastForView(child); if (evaluateContrastModel != null) { calculateBorderColorForViewNode(child, evaluateContrastModel.getContrastResult()); buildTreeItem(evaluateContrastModel, child); sViewNodeForModel.put(child, evaluateContrastModel); } else { sRectangleForViewNode.remove(child); } } } private static void buildTreeItem(EvaluateContrastModel model, final ViewNode child) { int dotIndex = child.name.lastIndexOf('.'); String shortName = (dotIndex == -1) ? child.name : child.name.substring(dotIndex + 1); String text = shortName + ": \"" + child.namedProperties.get("text:mText").value + "\""; TreeItem item = new TreeItem(sResultsTree, SWT.NONE); item.setText(transformText(text, MAX_NUM_CHARACTERS)); item.setImage(getResultImage(model.getContrastResult())); sViewNodeForTreeItem.put(item, child); buildTreeItemsForModel(model, item); } private static Image buildImageForColor(int color) { Image image = sImageForColor.get(color); if (image == null) { image = new Image(Display.getDefault(), PALLETE_IMAGE_SIZE, PALLETE_IMAGE_SIZE); GC gc = new GC(image); org.eclipse.swt.graphics.Color swtColor = awtColortoSwtColor(new java.awt.Color(color)); gc.setBackground(swtColor); swtColor.dispose(); gc.fillRectangle(0, 0, PALLETE_IMAGE_SIZE, PALLETE_IMAGE_SIZE); swtColor = awtColortoSwtColor(java.awt.Color.BLACK); gc.setForeground(swtColor); swtColor.dispose(); gc.drawRectangle(0, 0, PALLETE_IMAGE_SIZE - 1, PALLETE_IMAGE_SIZE - 1); gc.dispose(); sImageForColor.put(color, image); } return image; } public static org.eclipse.swt.graphics.Color awtColortoSwtColor(java.awt.Color color) { return new org.eclipse.swt.graphics.Color(Display.getDefault(), color.getRed(), color.getGreen(), color.getBlue()); } private static void buildTreeItemsForModel(EvaluateContrastModel model, TreeItem parent) { TreeItem item = new TreeItem(parent, SWT.NONE); item.setText("Text color: " + model.getTextColorHex()); item.setData(buildImageForColor(model.getTextColor())); item = new TreeItem(parent, SWT.NONE); item.setText("Background color: " + model.getBackgroundColorHex()); item.setData(buildImageForColor(model.getBackgroundColor())); new TreeItem(parent, SWT.NONE).setText("Text size: " + model.getTextSize()); new TreeItem(parent, SWT.NONE).setText("Contrast ratio: " + String.format( EvaluateContrastModel.CONTRAST_RATIO_FORMAT, model.getContrastRatio())); if (!model.isIndeterminate()) { new TreeItem(parent, SWT.NONE).setText("Test: " + model.getContrastResult().name()); } else { item = new TreeItem(parent, SWT.NONE); item.setText("Normal Text Test: " + model.getContrastResultForNormalText().name()); item.setImage(getResultImage(model.getContrastResultForNormalText())); item = new TreeItem(parent, SWT.NONE); item.setText("Large Text Test: " + model.getContrastResultForLargeText().name()); item.setImage(getResultImage(model.getContrastResultForLargeText())); } } private static List getEvaluatableChildViews(ViewNode root) { List children = new ArrayList(); children.add(root); for (int i = 0; i < children.size(); ++i) { ViewNode node = children.get(i); List temp = node.children; for (ViewNode child: temp) { if (!children.contains(child)) { children.add(child); } } } List evalutableChildren = new ArrayList(); for (final ViewNode child : children) { if (child.namedProperties.get("text:mText") != null) { evalutableChildren.add(child); } } return evalutableChildren; } private static void calculateBorderColorForViewNode(ViewNode node, ContrastResult result) { org.eclipse.swt.graphics.Color borderColor; switch (result) { case PASS: borderColor = getBorderColorPass(); break; case FAIL: borderColor = getBorderColorFail(); break; case INDETERMINATE: default: borderColor = getBorderColorIndeterminate(); } sBorderColorForViewNode.put(node, borderColor); } private static Image getResultImage(ContrastResult result) { switch (result) { case PASS: return sGreenImage; case FAIL: return sRedImage; default: return sYellowImage; } } private static String transformText(String text, int maxNumCharacters) { if (text.length() == maxNumCharacters) { return text; } else if (text.length() < maxNumCharacters) { char[] filler = new char[maxNumCharacters - text.length()]; Arrays.fill(filler,' '); return text + new String(filler); } StringBuilder abbreviatedText = new StringBuilder(); abbreviatedText.append(text.substring(0, maxNumCharacters - ABBREVIATE_SUFFIX.length())); abbreviatedText.append(ABBREVIATE_SUFFIX); return abbreviatedText.toString(); } private static void calculateRectangleForViewNode(ViewNode viewNode) { int leftShift = 0; int topShift = 0; int nodeLeft = (int) Math.round(viewNode.left * sScaleFactor); int nodeTop = (int) Math.round(viewNode.top * sScaleFactor); int nodeWidth = (int) Math.round(viewNode.width * sScaleFactor); int nodeHeight = (int) Math.round(viewNode.height * sScaleFactor); ViewNode current = viewNode; while (current.parent != null) { leftShift += (int) Math.round( sScaleFactor * (current.parent.left - current.parent.scrollX)); topShift += (int) Math.round( sScaleFactor * (current.parent.top - current.parent.scrollY)); current = current.parent; } sRectangleForViewNode.put(viewNode, new Rectangle(leftShift + nodeLeft, topShift + nodeTop, nodeWidth, nodeHeight)); } private static EvaluateContrastModel evaluateContrastForView(ViewNode node) { Map namedProperties = node.namedProperties; Property textColorProperty = namedProperties.get("text:mCurTextColor"); Integer textColor = textColorProperty == null ? null : Integer.valueOf(textColorProperty.value); Property textSizeProperty = namedProperties.get("text:getScaledTextSize()"); Double textSize = textSizeProperty == null ? null : Double.valueOf(textSizeProperty.value); Rectangle rectangle = sRectangleForViewNode.get(node); Property boldProperty = namedProperties.get("text:getTypefaceStyle()"); boolean isBold = boldProperty != null && boldProperty.value.equals("BOLD"); // TODO: also remove views that are covered by other views if (rectangle.x < 0 || rectangle.x > sImageWidth || rectangle.y < 0 || rectangle.y > sImageHeight || rectangle.width == 0 || rectangle.height == 0) { // not viewable in screenshot, therefore can't parse background color return null; } int x = Math.max(0, rectangle.x); int y = Math.max(0, rectangle.y); int width = Math.min(sImageWidth, rectangle.x + rectangle.width); int height = Math.min(sImageHeight, rectangle.y + rectangle.height); return new EvaluateContrastModel( sImage, textColor, textSize, x, y, width, height, isBold); } private static ShellAdapter sShellListener = new ShellAdapter() { @Override public void shellClosed(ShellEvent e) { e.doit = false; sShell.setVisible(false); clear(sShell == null); } }; } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000166 12747325007 032632 xustar000000000 0000000 118 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPro0100644 0000000 0000000 00000012652 12747325007 033213 0ustar000000000 0000000 /* * Copyright (C) 2013 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.hierarchyviewerlib.ui; import com.android.ddmlib.Log; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Text; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class InvokeMethodPrompt extends Composite implements ITreeChangeListener { private TreeViewModel mModel; private DrawableViewNode mSelectedNode; private Text mText; private static final Splitter CMD_SPLITTER = Splitter.on(CharMatcher.anyOf(", ")) .trimResults().omitEmptyStrings(); public InvokeMethodPrompt(Composite parent) { super(parent, SWT.NONE); setLayout(new FillLayout()); mText = new Text(this, SWT.BORDER); mText.addKeyListener(new KeyListener() { @Override public void keyReleased(KeyEvent ke) { } @Override public void keyPressed(KeyEvent ke) { onKeyPress(ke); } }); mModel = TreeViewModel.getModel(); mModel.addTreeChangeListener(this); } private void onKeyPress(KeyEvent ke) { if (ke.keyCode == SWT.CR) { String cmd = mText.getText().trim(); if (!cmd.isEmpty()) { invokeViewMethod(cmd); } mText.setText(""); } } private void invokeViewMethod(String cmd) { Iterator segmentIterator = CMD_SPLITTER.split(cmd).iterator(); String method = null; if (segmentIterator.hasNext()) { method = segmentIterator.next(); } else { return; } List args = new ArrayList(10); while (segmentIterator.hasNext()) { String arg = segmentIterator.next(); // check for boolean if (arg.equalsIgnoreCase("true")) { args.add(Boolean.TRUE); continue; } else if (arg.equalsIgnoreCase("false")) { args.add(Boolean.FALSE); continue; } // see if last character gives a clue regarding the argument type char typeSpecifier = Character.toUpperCase(arg.charAt(arg.length() - 1)); try { switch (typeSpecifier) { case 'L': args.add(Long.valueOf(arg.substring(0, arg.length()))); break; case 'D': args.add(Double.valueOf(arg.substring(0, arg.length()))); break; case 'F': args.add(Float.valueOf(arg.substring(0, arg.length()))); break; case 'S': args.add(Short.valueOf(arg.substring(0, arg.length()))); break; case 'B': args.add(Byte.valueOf(arg.substring(0, arg.length()))); break; default: // default to integer args.add(Integer.valueOf(arg)); break; } } catch (NumberFormatException e) { Log.e("hv", "Unable to parse method argument: " + arg); return; } } HierarchyViewerDirector.getDirector().invokeMethodOnSelectedView(method, args); } @Override public void selectionChanged() { mSelectedNode = mModel.getSelection(); refresh(); } private boolean isViewUpdateEnabled(ViewNode viewNode) { IHvDevice device = viewNode.window.getHvDevice(); return device != null && device.isViewUpdateEnabled(); } private void refresh() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mText.setEnabled(mSelectedNode != null && isViewUpdateEnabled(mSelectedNode.viewNode)); } }); } @Override public void treeChanged() { selectionChanged(); } @Override public void viewportChanged() { } @Override public void zoomChanged() { } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000160 12747325007 032624 xustar000000000 0000000 112 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.ja0100644 0000000 0000000 00000033235 12747325007 033166 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Transform; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import java.util.ArrayList; public class LayoutViewer extends Canvas implements ITreeChangeListener { private TreeViewModel mModel; private DrawableViewNode mTree; private DrawableViewNode mSelectedNode; private Transform mTransform; private Transform mInverse; private double mScale; private boolean mShowExtras = false; private boolean mOnBlack = true; public LayoutViewer(Composite parent) { super(parent, SWT.NONE); mModel = TreeViewModel.getModel(); mModel.addTreeChangeListener(this); addDisposeListener(mDisposeListener); addPaintListener(mPaintListener); addListener(SWT.Resize, mResizeListener); addMouseListener(mMouseListener); mTransform = new Transform(Display.getDefault()); mInverse = new Transform(Display.getDefault()); treeChanged(); } public void setShowExtras(boolean show) { mShowExtras = show; doRedraw(); } public void setOnBlack(boolean value) { mOnBlack = value; doRedraw(); } public boolean getOnBlack() { return mOnBlack; } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeTreeChangeListener(LayoutViewer.this); mTransform.dispose(); mInverse.dispose(); if (mSelectedNode != null) { mSelectedNode.viewNode.dereferenceImage(); } } }; private Listener mResizeListener = new Listener() { @Override public void handleEvent(Event e) { synchronized (this) { setTransform(); } } }; private MouseListener mMouseListener = new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { if (mSelectedNode != null) { HierarchyViewerDirector.getDirector() .showCapture(getShell(), mSelectedNode.viewNode); } } @Override public void mouseDown(MouseEvent e) { boolean selectionChanged = false; DrawableViewNode newSelection = null; synchronized (LayoutViewer.this) { if (mTree != null) { float[] pt = { e.x, e.y }; mInverse.transform(pt); newSelection = updateSelection(mTree, pt[0], pt[1], 0, 0, 0, 0, mTree.viewNode.width, mTree.viewNode.height); if (mSelectedNode != newSelection) { selectionChanged = true; } } } if (selectionChanged) { mModel.setSelection(newSelection); } } @Override public void mouseUp(MouseEvent e) { // pass } }; private DrawableViewNode updateSelection(DrawableViewNode node, float x, float y, int left, int top, int clipX, int clipY, int clipWidth, int clipHeight) { if (!node.treeDrawn) { return null; } // Update the clip int x1 = Math.max(left, clipX); int x2 = Math.min(left + node.viewNode.width, clipX + clipWidth); int y1 = Math.max(top, clipY); int y2 = Math.min(top + node.viewNode.height, clipY + clipHeight); clipX = x1; clipY = y1; clipWidth = x2 - x1; clipHeight = y2 - y1; if (x < clipX || x > clipX + clipWidth || y < clipY || y > clipY + clipHeight) { return null; } final int N = node.children.size(); for (int i = N - 1; i >= 0; i--) { DrawableViewNode child = node.children.get(i); DrawableViewNode ret = updateSelection(child, x, y, left + child.viewNode.left - node.viewNode.scrollX, top + child.viewNode.top - node.viewNode.scrollY, clipX, clipY, clipWidth, clipHeight); if (ret != null) { return ret; } } return node; } private PaintListener mPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { synchronized (LayoutViewer.this) { if (mOnBlack) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); } else { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); } e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); if (mTree != null) { e.gc.setLineWidth((int) Math.ceil(0.3 / mScale)); e.gc.setTransform(mTransform); if (mOnBlack) { e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); } else { e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); } Rectangle parentClipping = e.gc.getClipping(); e.gc.setClipping(0, 0, mTree.viewNode.width + (int) Math.ceil(0.3 / mScale), mTree.viewNode.height + (int) Math.ceil(0.3 / mScale)); paintRecursive(e.gc, mTree, 0, 0, true); if (mSelectedNode != null) { e.gc.setClipping(parentClipping); // w00t, let's be nice and display the whole path in // light red and the selected node in dark red. ArrayList rightLeftDistances = new ArrayList(); int left = 0; int top = 0; DrawableViewNode currentNode = mSelectedNode; while (currentNode != mTree) { left += currentNode.viewNode.left; top += currentNode.viewNode.top; currentNode = currentNode.parent; left -= currentNode.viewNode.scrollX; top -= currentNode.viewNode.scrollY; rightLeftDistances.add(new Point(left, top)); } e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED)); currentNode = mSelectedNode.parent; final int N = rightLeftDistances.size(); for (int i = 0; i < N; i++) { e.gc.drawRectangle((int) (left - rightLeftDistances.get(i).x), (int) (top - rightLeftDistances.get(i).y), currentNode.viewNode.width, currentNode.viewNode.height); currentNode = currentNode.parent; } if (mShowExtras && mSelectedNode.viewNode.image != null) { e.gc.drawImage(mSelectedNode.viewNode.image, left, top); if (mOnBlack) { e.gc.setForeground(Display.getDefault().getSystemColor( SWT.COLOR_WHITE)); } else { e.gc.setForeground(Display.getDefault().getSystemColor( SWT.COLOR_BLACK)); } paintRecursive(e.gc, mSelectedNode, left, top, true); } e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); e.gc.setLineWidth((int) Math.ceil(2 / mScale)); e.gc.drawRectangle(left, top, mSelectedNode.viewNode.width, mSelectedNode.viewNode.height); } } } } }; private void paintRecursive(GC gc, DrawableViewNode node, int left, int top, boolean root) { if (!node.treeDrawn) { return; } // Don't shift the root if (!root) { left += node.viewNode.left; top += node.viewNode.top; } Rectangle parentClipping = gc.getClipping(); int x1 = Math.max(parentClipping.x, left); int x2 = Math.min(parentClipping.x + parentClipping.width, left + node.viewNode.width + (int) Math.ceil(0.3 / mScale)); int y1 = Math.max(parentClipping.y, top); int y2 = Math.min(parentClipping.y + parentClipping.height, top + node.viewNode.height + (int) Math.ceil(0.3 / mScale)); // Clipping is weird... You set it to -5 and it comes out 17 or // something. if (x2 <= x1 || y2 <= y1) { return; } gc.setClipping(x1, y1, x2 - x1, y2 - y1); final int N = node.children.size(); for (int i = 0; i < N; i++) { paintRecursive(gc, node.children.get(i), left - node.viewNode.scrollX, top - node.viewNode.scrollY, false); } gc.setClipping(parentClipping); if (!node.viewNode.willNotDraw) { gc.drawRectangle(left, top, node.viewNode.width, node.viewNode.height); } } private void doRedraw() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { redraw(); } }); } private void setTransform() { if (mTree != null) { Rectangle bounds = getBounds(); int leftRightPadding = bounds.width <= 30 ? 0 : 5; int topBottomPadding = bounds.height <= 30 ? 0 : 5; mScale = Math.min(1.0 * (bounds.width - leftRightPadding * 2) / mTree.viewNode.width, 1.0 * (bounds.height - topBottomPadding * 2) / mTree.viewNode.height); int scaledWidth = (int) Math.ceil(mTree.viewNode.width * mScale); int scaledHeight = (int) Math.ceil(mTree.viewNode.height * mScale); mTransform.identity(); mInverse.identity(); mTransform.translate((bounds.width - scaledWidth) / 2.0f, (bounds.height - scaledHeight) / 2.0f); mInverse.translate((bounds.width - scaledWidth) / 2.0f, (bounds.height - scaledHeight) / 2.0f); mTransform.scale((float) mScale, (float) mScale); mInverse.scale((float) mScale, (float) mScale); if (bounds.width != 0 && bounds.height != 0) { mInverse.invert(); } } } @Override public void selectionChanged() { synchronized (this) { if (mSelectedNode != null) { mSelectedNode.viewNode.dereferenceImage(); } mSelectedNode = mModel.getSelection(); if (mSelectedNode != null) { mSelectedNode.viewNode.referenceImage(); } } doRedraw(); } // Note the syncExec and then synchronized... It avoids deadlock @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { if (mSelectedNode != null) { mSelectedNode.viewNode.dereferenceImage(); } mTree = mModel.getTree(); mSelectedNode = mModel.getSelection(); if (mSelectedNode != null) { mSelectedNode.viewNode.referenceImage(); } setTransform(); } } }); doRedraw(); } @Override public void viewportChanged() { // pass } @Override public void zoomChanged() { // pass } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000160 12747325007 032624 xustar000000000 0000000 112 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.ja0100644 0000000 0000000 00000034524 12747325007 033123 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; public class PixelPerfect extends ScrolledComposite implements IImageChangeListener { private Canvas mCanvas; private PixelPerfectModel mModel; private Image mImage; private Color mCrosshairColor; private Color mMarginColor; private Color mBorderColor; private Color mPaddingColor; private int mWidth; private int mHeight; private Point mCrosshairLocation; private ViewNode mSelectedNode; private Image mOverlayImage; private double mOverlayTransparency; public PixelPerfect(Composite parent) { super(parent, SWT.H_SCROLL | SWT.V_SCROLL); mCanvas = new Canvas(this, SWT.NONE); setContent(mCanvas); setExpandHorizontal(true); setExpandVertical(true); mModel = PixelPerfectModel.getModel(); mModel.addImageChangeListener(this); mCanvas.addPaintListener(mPaintListener); mCanvas.addMouseListener(mMouseListener); mCanvas.addMouseMoveListener(mMouseMoveListener); mCanvas.addKeyListener(mKeyListener); addDisposeListener(mDisposeListener); mCrosshairColor = new Color(Display.getDefault(), new RGB(0, 255, 255)); mBorderColor = new Color(Display.getDefault(), new RGB(255, 0, 0)); mMarginColor = new Color(Display.getDefault(), new RGB(0, 255, 0)); mPaddingColor = new Color(Display.getDefault(), new RGB(0, 0, 255)); imageLoaded(); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeImageChangeListener(PixelPerfect.this); mCrosshairColor.dispose(); mBorderColor.dispose(); mPaddingColor.dispose(); } }; @Override public boolean setFocus() { return mCanvas.setFocus(); } private MouseListener mMouseListener = new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { // pass } @Override public void mouseDown(MouseEvent e) { handleMouseEvent(e); } @Override public void mouseUp(MouseEvent e) { handleMouseEvent(e); } }; private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { if (e.stateMask != 0) { handleMouseEvent(e); } } }; private void handleMouseEvent(MouseEvent e) { synchronized (PixelPerfect.this) { if (mImage == null) { return; } int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2; int topOffset = mCanvas.getSize().y / 2 - mHeight / 2; e.x -= leftOffset; e.y -= topOffset; e.x = Math.max(e.x, 0); e.x = Math.min(e.x, mWidth - 1); e.y = Math.max(e.y, 0); e.y = Math.min(e.y, mHeight - 1); } mModel.setCrosshairLocation(e.x, e.y); } private KeyListener mKeyListener = new KeyListener() { @Override public void keyPressed(KeyEvent e) { boolean crosshairMoved = false; synchronized (PixelPerfect.this) { if (mImage != null) { switch (e.keyCode) { case SWT.ARROW_UP: if (mCrosshairLocation.y != 0) { mCrosshairLocation.y--; crosshairMoved = true; } break; case SWT.ARROW_DOWN: if (mCrosshairLocation.y != mHeight - 1) { mCrosshairLocation.y++; crosshairMoved = true; } break; case SWT.ARROW_LEFT: if (mCrosshairLocation.x != 0) { mCrosshairLocation.x--; crosshairMoved = true; } break; case SWT.ARROW_RIGHT: if (mCrosshairLocation.x != mWidth - 1) { mCrosshairLocation.x++; crosshairMoved = true; } break; } } } if (crosshairMoved) { mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y); } } @Override public void keyReleased(KeyEvent e) { // pass } }; private PaintListener mPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { synchronized (PixelPerfect.this) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); e.gc.fillRectangle(0, 0, mCanvas.getSize().x, mCanvas.getSize().y); if (mImage != null) { // Let's be cool and put it in the center... int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2; int topOffset = mCanvas.getSize().y / 2 - mHeight / 2; e.gc.drawImage(mImage, leftOffset, topOffset); if (mOverlayImage != null) { e.gc.setAlpha((int) (mOverlayTransparency * 255)); int overlayTopOffset = mCanvas.getSize().y / 2 + mHeight / 2 - mOverlayImage.getBounds().height; e.gc.drawImage(mOverlayImage, leftOffset, overlayTopOffset); e.gc.setAlpha(255); } if (mSelectedNode != null) { // If the screen is in landscape mode, the // coordinates are backwards. int leftShift = 0; int topShift = 0; int nodeLeft = mSelectedNode.left; int nodeTop = mSelectedNode.top; int nodeWidth = mSelectedNode.width; int nodeHeight = mSelectedNode.height; int nodeMarginLeft = mSelectedNode.marginLeft; int nodeMarginTop = mSelectedNode.marginTop; int nodeMarginRight = mSelectedNode.marginRight; int nodeMarginBottom = mSelectedNode.marginBottom; int nodePadLeft = mSelectedNode.paddingLeft; int nodePadTop = mSelectedNode.paddingTop; int nodePadRight = mSelectedNode.paddingRight; int nodePadBottom = mSelectedNode.paddingBottom; ViewNode cur = mSelectedNode; while (cur.parent != null) { leftShift += cur.parent.left - cur.parent.scrollX; topShift += cur.parent.top - cur.parent.scrollY; cur = cur.parent; } // Everything is sideways. if (cur.width > cur.height) { e.gc.setForeground(mPaddingColor); e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight + nodePadBottom, topOffset + leftShift + nodeLeft + nodePadLeft, nodeHeight - nodePadBottom - nodePadTop, nodeWidth - nodePadRight - nodePadLeft); e.gc.setForeground(mMarginColor); e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight - nodeMarginBottom, topOffset + leftShift + nodeLeft - nodeMarginLeft, nodeHeight + nodeMarginBottom + nodeMarginTop, nodeWidth + nodeMarginRight + nodeMarginLeft); e.gc.setForeground(mBorderColor); e.gc.drawRectangle( leftOffset + mWidth - nodeTop - topShift - nodeHeight, topOffset + leftShift + nodeLeft, nodeHeight, nodeWidth); } else { e.gc.setForeground(mPaddingColor); e.gc.drawRectangle(leftOffset + leftShift + nodeLeft + nodePadLeft, topOffset + topShift + nodeTop + nodePadTop, nodeWidth - nodePadRight - nodePadLeft, nodeHeight - nodePadBottom - nodePadTop); e.gc.setForeground(mMarginColor); e.gc.drawRectangle(leftOffset + leftShift + nodeLeft - nodeMarginLeft, topOffset + topShift + nodeTop - nodeMarginTop, nodeWidth + nodeMarginRight + nodeMarginLeft, nodeHeight + nodeMarginBottom + nodeMarginTop); e.gc.setForeground(mBorderColor); e.gc.drawRectangle(leftOffset + leftShift + nodeLeft, topOffset + topShift + nodeTop, nodeWidth, nodeHeight); } } if (mCrosshairLocation != null) { e.gc.setForeground(mCrosshairColor); e.gc.drawLine(leftOffset, topOffset + mCrosshairLocation.y, leftOffset + mWidth - 1, topOffset + mCrosshairLocation.y); e.gc.drawLine(leftOffset + mCrosshairLocation.x, topOffset, leftOffset + mCrosshairLocation.x, topOffset + mHeight - 1); } } } } }; private void doRedraw() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mCanvas.redraw(); } }); } private void loadImage() { mImage = mModel.getImage(); if (mImage != null) { mWidth = mImage.getBounds().width; mHeight = mImage.getBounds().height; } else { mWidth = 0; mHeight = 0; } setMinSize(mWidth, mHeight); } @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { loadImage(); mCrosshairLocation = mModel.getCrosshairLocation(); mSelectedNode = mModel.getSelected(); mOverlayImage = mModel.getOverlayImage(); mOverlayTransparency = mModel.getOverlayTransparency(); } } }); doRedraw(); } @Override public void imageChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { loadImage(); } } }); doRedraw(); } @Override public void crosshairMoved() { synchronized (this) { mCrosshairLocation = mModel.getCrosshairLocation(); } doRedraw(); } @Override public void selectionChanged() { synchronized (this) { mSelectedNode = mModel.getSelected(); } doRedraw(); } // Note the syncExec and then synchronized... It avoids deadlock @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mSelectedNode = mModel.getSelected(); } } }); doRedraw(); } @Override public void zoomChanged() { // pass } @Override public void overlayChanged() { synchronized (this) { mOverlayImage = mModel.getOverlayImage(); mOverlayTransparency = mModel.getOverlayTransparency(); } doRedraw(); } @Override public void overlayTransparencyChanged() { synchronized (this) { mOverlayTransparency = mModel.getOverlayTransparency(); } doRedraw(); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000170 12747325007 032625 xustar000000000 0000000 120 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectCon0100644 0000000 0000000 00000026431 12747325007 033170 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Slider; public class PixelPerfectControls extends Composite implements IImageChangeListener { private Slider mOverlaySlider; private Slider mZoomSlider; private Slider mAutoRefreshSlider; public PixelPerfectControls(Composite parent) { super(parent, SWT.NONE); setLayout(new FormLayout()); Label overlayTransparencyRight = new Label(this, SWT.NONE); overlayTransparencyRight.setText("100%"); FormData overlayTransparencyRightData = new FormData(); overlayTransparencyRightData.right = new FormAttachment(100, -2); overlayTransparencyRightData.top = new FormAttachment(0, 2); overlayTransparencyRight.setLayoutData(overlayTransparencyRightData); Label refreshRight = new Label(this, SWT.NONE); refreshRight.setText("40s"); FormData refreshRightData = new FormData(); refreshRightData.right = new FormAttachment(100, -2); refreshRightData.top = new FormAttachment(overlayTransparencyRight, 2); refreshRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT); refreshRight.setLayoutData(refreshRightData); Label zoomRight = new Label(this, SWT.NONE); zoomRight.setText("24x"); FormData zoomRightData = new FormData(); zoomRightData.right = new FormAttachment(100, -2); zoomRightData.top = new FormAttachment(refreshRight, 2); zoomRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT); zoomRight.setLayoutData(zoomRightData); Label overlayTransparency = new Label(this, SWT.NONE); Label refresh = new Label(this, SWT.NONE); overlayTransparency.setText("Overlay:"); FormData overlayTransparencyData = new FormData(); overlayTransparencyData.left = new FormAttachment(0, 2); overlayTransparencyData.top = new FormAttachment(0, 2); overlayTransparencyData.right = new FormAttachment(refresh, 0, SWT.RIGHT); overlayTransparency.setLayoutData(overlayTransparencyData); refresh.setText("Refresh Rate:"); FormData refreshData = new FormData(); refreshData.top = new FormAttachment(overlayTransparency, 2); refreshData.left = new FormAttachment(0, 2); refresh.setLayoutData(refreshData); Label zoom = new Label(this, SWT.NONE); zoom.setText("Zoom:"); FormData zoomData = new FormData(); zoomData.right = new FormAttachment(refresh, 0, SWT.RIGHT); zoomData.top = new FormAttachment(refresh, 2); zoomData.left = new FormAttachment(0, 2); zoom.setLayoutData(zoomData); Label overlayTransparencyLeft = new Label(this, SWT.RIGHT); overlayTransparencyLeft.setText("0%"); FormData overlayTransparencyLeftData = new FormData(); overlayTransparencyLeftData.top = new FormAttachment(0, 2); overlayTransparencyLeftData.left = new FormAttachment(overlayTransparency, 2); overlayTransparencyLeft.setLayoutData(overlayTransparencyLeftData); Label refreshLeft = new Label(this, SWT.RIGHT); refreshLeft.setText("1s"); FormData refreshLeftData = new FormData(); refreshLeftData.top = new FormAttachment(overlayTransparencyLeft, 2); refreshLeftData.left = new FormAttachment(refresh, 2); refreshLeft.setLayoutData(refreshLeftData); Label zoomLeft = new Label(this, SWT.RIGHT); zoomLeft.setText("2x"); FormData zoomLeftData = new FormData(); zoomLeftData.top = new FormAttachment(refreshLeft, 2); zoomLeftData.left = new FormAttachment(zoom, 2); zoomLeft.setLayoutData(zoomLeftData); mOverlaySlider = new Slider(this, SWT.HORIZONTAL); mOverlaySlider.setMinimum(0); mOverlaySlider.setMaximum(101); mOverlaySlider.setThumb(1); mOverlaySlider.setSelection((int) Math.round(PixelPerfectModel.getModel() .getOverlayTransparency() * 100)); Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); mOverlaySlider.setEnabled(overlayImage != null); FormData overlaySliderData = new FormData(); overlaySliderData.right = new FormAttachment(overlayTransparencyRight, -4); overlaySliderData.top = new FormAttachment(0, 2); overlaySliderData.left = new FormAttachment(overlayTransparencyLeft, 4); mOverlaySlider.setLayoutData(overlaySliderData); mOverlaySlider.addSelectionListener(overlaySliderSelectionListener); mAutoRefreshSlider = new Slider(this, SWT.HORIZONTAL); mAutoRefreshSlider.setMinimum(1); mAutoRefreshSlider.setMaximum(41); mAutoRefreshSlider.setThumb(1); mAutoRefreshSlider.setSelection(HierarchyViewerDirector.getDirector() .getPixelPerfectAutoRefreshInverval()); FormData refreshSliderData = new FormData(); refreshSliderData.right = new FormAttachment(overlayTransparencyRight, -4); refreshSliderData.top = new FormAttachment(overlayTransparencyRight, 2); refreshSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT); mAutoRefreshSlider.setLayoutData(refreshSliderData); mAutoRefreshSlider.addSelectionListener(mRefreshSliderSelectionListener); mZoomSlider = new Slider(this, SWT.HORIZONTAL); mZoomSlider.setMinimum(2); mZoomSlider.setMaximum(25); mZoomSlider.setThumb(1); mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); FormData zoomSliderData = new FormData(); zoomSliderData.right = new FormAttachment(overlayTransparencyRight, -4); zoomSliderData.top = new FormAttachment(refreshRight, 2); zoomSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT); mZoomSlider.setLayoutData(zoomSliderData); mZoomSlider.addSelectionListener(mZoomSliderSelectionListener); addDisposeListener(mDisposeListener); PixelPerfectModel.getModel().addImageChangeListener(this); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); } }; private SelectionListener overlaySliderSelectionListener = new SelectionListener() { private int oldValue; @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { int newValue = mOverlaySlider.getSelection(); if (oldValue != newValue) { PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); PixelPerfectModel.getModel().setOverlayTransparency(newValue / 100.0); PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this); oldValue = newValue; } } }; private SelectionListener mRefreshSliderSelectionListener = new SelectionListener() { private int oldValue; @Override public void widgetDefaultSelected(final SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { int newValue = mAutoRefreshSlider.getSelection(); if (oldValue != newValue) { HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefreshInterval(newValue); } } }; private SelectionListener mZoomSliderSelectionListener = new SelectionListener() { private int oldValue; @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { int newValue = mZoomSlider.getSelection(); if (oldValue != newValue) { PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); PixelPerfectModel.getModel().setZoom(newValue); PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this); oldValue = newValue; } } }; @Override public void crosshairMoved() { // pass } @Override public void treeChanged() { // pass } @Override public void imageChanged() { // pass } @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); mOverlaySlider.setEnabled(overlayImage != null); if (PixelPerfectModel.getModel().getImage() == null) { } else { mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); } } }); } @Override public void overlayChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); mOverlaySlider.setEnabled(overlayImage != null); } }); } @Override public void overlayTransparencyChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mOverlaySlider.setSelection((int) (PixelPerfectModel.getModel() .getOverlayTransparency() * 100)); } }); } @Override public void selectionChanged() { // pass } @Override public void zoomChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); } }); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000165 12747325007 032631 xustar000000000 0000000 117 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLou0100644 0000000 0000000 00000031572 12747325007 033212 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseWheelListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Transform; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; public class PixelPerfectLoupe extends Canvas implements IImageChangeListener { private PixelPerfectModel mModel; private Image mImage; private Image mGrid; private Color mCrosshairColor; private int mWidth; private int mHeight; private Point mCrosshairLocation; private int mZoom; private Transform mTransform; private int mCanvasWidth; private int mCanvasHeight; private Image mOverlayImage; private double mOverlayTransparency; private boolean mShowOverlay = false; public PixelPerfectLoupe(Composite parent) { super(parent, SWT.NONE); mModel = PixelPerfectModel.getModel(); mModel.addImageChangeListener(this); addPaintListener(mPaintListener); addMouseListener(mMouseListener); addMouseWheelListener(mMouseWheelListener); addDisposeListener(mDisposeListener); addKeyListener(mKeyListener); mCrosshairColor = new Color(Display.getDefault(), new RGB(255, 94, 254)); mTransform = new Transform(Display.getDefault()); imageLoaded(); } public void setShowOverlay(boolean value) { synchronized (this) { mShowOverlay = value; } doRedraw(); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeImageChangeListener(PixelPerfectLoupe.this); mCrosshairColor.dispose(); mTransform.dispose(); if (mGrid != null) { mGrid.dispose(); } } }; private MouseListener mMouseListener = new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { // pass } @Override public void mouseDown(MouseEvent e) { handleMouseEvent(e); } @Override public void mouseUp(MouseEvent e) { // } }; private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { @Override public void mouseScrolled(MouseEvent e) { int newZoom = -1; synchronized (PixelPerfectLoupe.this) { if (mImage != null && mCrosshairLocation != null) { if (e.count > 0) { newZoom = mZoom + 1; } else { newZoom = mZoom - 1; } } } if (newZoom != -1) { mModel.setZoom(newZoom); } } }; private void handleMouseEvent(MouseEvent e) { int newX = -1; int newY = -1; synchronized (PixelPerfectLoupe.this) { if (mImage == null) { return; } int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2; int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2; int x = (e.x - zoomedX) / mZoom; int y = (e.y - zoomedY) / mZoom; if (x >= 0 && x < mWidth && y >= 0 && y < mHeight) { newX = x; newY = y; } } if (newX != -1) { mModel.setCrosshairLocation(newX, newY); } } private KeyListener mKeyListener = new KeyListener() { @Override public void keyPressed(KeyEvent e) { boolean crosshairMoved = false; synchronized (PixelPerfectLoupe.this) { if (mImage != null) { switch (e.keyCode) { case SWT.ARROW_UP: if (mCrosshairLocation.y != 0) { mCrosshairLocation.y--; crosshairMoved = true; } break; case SWT.ARROW_DOWN: if (mCrosshairLocation.y != mHeight - 1) { mCrosshairLocation.y++; crosshairMoved = true; } break; case SWT.ARROW_LEFT: if (mCrosshairLocation.x != 0) { mCrosshairLocation.x--; crosshairMoved = true; } break; case SWT.ARROW_RIGHT: if (mCrosshairLocation.x != mWidth - 1) { mCrosshairLocation.x++; crosshairMoved = true; } break; } } } if (crosshairMoved) { mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y); } } @Override public void keyReleased(KeyEvent e) { // pass } }; private PaintListener mPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { synchronized (PixelPerfectLoupe.this) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); e.gc.fillRectangle(0, 0, getSize().x, getSize().y); if (mImage != null && mCrosshairLocation != null) { int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2; int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2; mTransform.translate(zoomedX, zoomedY); mTransform.scale(mZoom, mZoom); e.gc.setInterpolation(SWT.NONE); e.gc.setTransform(mTransform); e.gc.drawImage(mImage, 0, 0); if (mShowOverlay && mOverlayImage != null) { e.gc.setAlpha((int) (mOverlayTransparency * 255)); e.gc.drawImage(mOverlayImage, 0, mHeight - mOverlayImage.getBounds().height); e.gc.setAlpha(255); } mTransform.identity(); e.gc.setTransform(mTransform); // If the size of the canvas has changed, we need to make // another grid. if (mGrid != null && (mCanvasWidth != getBounds().width || mCanvasHeight != getBounds().height)) { mGrid.dispose(); mGrid = null; } mCanvasWidth = getBounds().width; mCanvasHeight = getBounds().height; if (mGrid == null) { // Make a transparent image; ImageData imageData = new ImageData(mCanvasWidth + mZoom + 1, mCanvasHeight + mZoom + 1, 1, new PaletteData(new RGB[] { new RGB(0, 0, 0) })); imageData.transparentPixel = 0; // Draw the grid. mGrid = new Image(Display.getDefault(), imageData); GC gc = new GC(mGrid); gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); for (int x = 0; x <= mCanvasWidth + mZoom; x += mZoom) { gc.drawLine(x, 0, x, mCanvasHeight + mZoom); } for (int y = 0; y <= mCanvasHeight + mZoom; y += mZoom) { gc.drawLine(0, y, mCanvasWidth + mZoom, y); } gc.dispose(); } e.gc.setClipping(new Rectangle(zoomedX, zoomedY, mWidth * mZoom + 1, mHeight * mZoom + 1)); e.gc.setAlpha(76); e.gc.drawImage(mGrid, (mCanvasWidth / 2 - mZoom / 2) % mZoom - mZoom, (mCanvasHeight / 2 - mZoom / 2) % mZoom - mZoom); e.gc.setAlpha(255); e.gc.setForeground(mCrosshairColor); e.gc.drawLine(0, mCanvasHeight / 2, mCanvasWidth - 1, mCanvasHeight / 2); e.gc.drawLine(mCanvasWidth / 2, 0, mCanvasWidth / 2, mCanvasHeight - 1); } } } }; private void doRedraw() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { redraw(); } }); } private void loadImage() { mImage = mModel.getImage(); if (mImage != null) { mWidth = mImage.getBounds().width; mHeight = mImage.getBounds().height; } else { mWidth = 0; mHeight = 0; } } // Note the syncExec and then synchronized... It avoids deadlock @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { loadImage(); mCrosshairLocation = mModel.getCrosshairLocation(); mZoom = mModel.getZoom(); mOverlayImage = mModel.getOverlayImage(); mOverlayTransparency = mModel.getOverlayTransparency(); } } }); doRedraw(); } @Override public void imageChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { loadImage(); } } }); doRedraw(); } @Override public void crosshairMoved() { synchronized (this) { mCrosshairLocation = mModel.getCrosshairLocation(); } doRedraw(); } @Override public void selectionChanged() { // pass } @Override public void treeChanged() { // pass } @Override public void zoomChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { if (mGrid != null) { // To notify that the zoom level has changed, we get rid // of the // grid. mGrid.dispose(); mGrid = null; } mZoom = mModel.getZoom(); } } }); doRedraw(); } @Override public void overlayChanged() { synchronized (this) { mOverlayImage = mModel.getOverlayImage(); mOverlayTransparency = mModel.getOverlayTransparency(); } doRedraw(); } @Override public void overlayTransparencyChanged() { synchronized (this) { mOverlayTransparency = mModel.getOverlayTransparency(); } doRedraw(); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000172 12747325007 032627 xustar000000000 0000000 122 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPix0100644 0000000 0000000 00000017143 12747325007 033211 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; public class PixelPerfectPixelPanel extends Canvas implements IImageChangeListener { private PixelPerfectModel mModel; private Image mImage; private Image mOverlayImage; private Point mCrosshairLocation; public static final int PREFERRED_WIDTH = 180; public static final int PREFERRED_HEIGHT = 52; public PixelPerfectPixelPanel(Composite parent) { super(parent, SWT.NONE); mModel = PixelPerfectModel.getModel(); mModel.addImageChangeListener(this); addPaintListener(mPaintListener); addDisposeListener(mDisposeListener); imageLoaded(); } @Override public Point computeSize(int wHint, int hHint, boolean changed) { int height = PREFERRED_HEIGHT; int width = (wHint == SWT.DEFAULT) ? PREFERRED_WIDTH : wHint; return new Point(width, height); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeImageChangeListener(PixelPerfectPixelPanel.this); } }; private PaintListener mPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { synchronized (PixelPerfectPixelPanel.this) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); if (mImage != null) { RGB pixel = mImage.getImageData().palette.getRGB(mImage.getImageData().getPixel( mCrosshairLocation.x, mCrosshairLocation.y)); Color rgbColor = new Color(Display.getDefault(), pixel); e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); e.gc.setBackground(rgbColor); e.gc.drawRectangle(4, 4, 60, 30); e.gc.fillRectangle(5, 5, 59, 29); rgbColor.dispose(); e.gc.drawText("#" + Integer .toHexString( (1 << 24) + (pixel.red << 16) + (pixel.green << 8) + pixel.blue).substring(1), 4, 35, true); e.gc.drawText("R:", 80, 4, true); e.gc.drawText("G:", 80, 20, true); e.gc.drawText("B:", 80, 35, true); e.gc.drawText(Integer.toString(pixel.red), 97, 4, true); e.gc.drawText(Integer.toString(pixel.green), 97, 20, true); e.gc.drawText(Integer.toString(pixel.blue), 97, 35, true); e.gc.drawText("X:", 132, 4, true); e.gc.drawText("Y:", 132, 20, true); e.gc.drawText(Integer.toString(mCrosshairLocation.x) + " px", 149, 4, true); e.gc.drawText(Integer.toString(mCrosshairLocation.y) + " px", 149, 20, true); if (mOverlayImage != null) { int xInOverlay = mCrosshairLocation.x; int yInOverlay = mCrosshairLocation.y - (mImage.getBounds().height - mOverlayImage.getBounds().height); if (xInOverlay >= 0 && yInOverlay >= 0 && xInOverlay < mOverlayImage.getBounds().width && yInOverlay < mOverlayImage.getBounds().height) { pixel = mOverlayImage.getImageData().palette.getRGB(mOverlayImage .getImageData().getPixel(xInOverlay, yInOverlay)); rgbColor = new Color(Display.getDefault(), pixel); e.gc .setForeground(Display.getDefault().getSystemColor( SWT.COLOR_WHITE)); e.gc.setBackground(rgbColor); e.gc.drawRectangle(204, 4, 60, 30); e.gc.fillRectangle(205, 5, 59, 29); rgbColor.dispose(); e.gc.drawText("#" + Integer.toHexString( (1 << 24) + (pixel.red << 16) + (pixel.green << 8) + pixel.blue).substring(1), 204, 35, true); e.gc.drawText("R:", 280, 4, true); e.gc.drawText("G:", 280, 20, true); e.gc.drawText("B:", 280, 35, true); e.gc.drawText(Integer.toString(pixel.red), 297, 4, true); e.gc.drawText(Integer.toString(pixel.green), 297, 20, true); e.gc.drawText(Integer.toString(pixel.blue), 297, 35, true); } } } } } }; private void doRedraw() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { redraw(); } }); } @Override public void crosshairMoved() { synchronized (this) { mCrosshairLocation = mModel.getCrosshairLocation(); } doRedraw(); } @Override public void imageChanged() { synchronized (this) { mImage = mModel.getImage(); } doRedraw(); } @Override public void imageLoaded() { synchronized (this) { mImage = mModel.getImage(); mCrosshairLocation = mModel.getCrosshairLocation(); mOverlayImage = mModel.getOverlayImage(); } doRedraw(); } @Override public void overlayChanged() { synchronized (this) { mOverlayImage = mModel.getOverlayImage(); } doRedraw(); } @Override public void overlayTransparencyChanged() { // pass } @Override public void selectionChanged() { // pass } @Override public void treeChanged() { // pass } @Override public void zoomChanged() { // pass } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000164 12747325007 032630 xustar000000000 0000000 116 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTre0100644 0000000 0000000 00000015222 12747325007 033177 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import java.util.List; public class PixelPerfectTree extends Composite implements IImageChangeListener, SelectionListener { private TreeViewer mTreeViewer; private Tree mTree; private PixelPerfectModel mModel; private Image mFolderImage; private Image mFileImage; private class ContentProvider implements ITreeContentProvider, ILabelProvider { @Override public Object[] getChildren(Object element) { if (element instanceof ViewNode) { List children = ((ViewNode) element).children; return children.toArray(new ViewNode[children.size()]); } return null; } @Override public Object getParent(Object element) { if (element instanceof ViewNode) { return ((ViewNode) element).parent; } return null; } @Override public boolean hasChildren(Object element) { if (element instanceof ViewNode) { return ((ViewNode) element).children.size() != 0; } return false; } @Override public Object[] getElements(Object element) { if (element instanceof PixelPerfectModel) { ViewNode viewNode = ((PixelPerfectModel) element).getViewNode(); if (viewNode == null) { return new Object[0]; } return new Object[] { viewNode }; } return new Object[0]; } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } @Override public Image getImage(Object element) { if (element instanceof ViewNode) { if (hasChildren(element)) { return mFolderImage; } return mFileImage; } return null; } @Override public String getText(Object element) { if (element instanceof ViewNode) { return ((ViewNode) element).name; } return null; } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } public PixelPerfectTree(Composite parent) { super(parent, SWT.NONE); setLayout(new FillLayout()); mTreeViewer = new TreeViewer(this, SWT.SINGLE); mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); mTree = mTreeViewer.getTree(); mTree.addSelectionListener(this); loadResources(); addDisposeListener(mDisposeListener); mModel = PixelPerfectModel.getModel(); ContentProvider contentProvider = new ContentProvider(); mTreeViewer.setContentProvider(contentProvider); mTreeViewer.setLabelProvider(contentProvider); mTreeViewer.setInput(mModel); mModel.addImageChangeListener(this); } private void loadResources() { ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mFileImage = loader.loadImage("file.png", Display.getDefault()); mFolderImage = loader.loadImage("folder.png", Display.getDefault()); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeImageChangeListener(PixelPerfectTree.this); } }; @Override public boolean setFocus() { return mTree.setFocus(); } @Override public void imageLoaded() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mTreeViewer.refresh(); mTreeViewer.expandAll(); } }); } @Override public void imageChanged() { // pass } @Override public void crosshairMoved() { // pass } @Override public void selectionChanged() { // pass } @Override public void treeChanged() { imageLoaded(); } @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { // To combat phantom selection... if (((TreeSelection) mTreeViewer.getSelection()).isEmpty()) { mModel.setSelected(null); } else { mModel.setSelected((ViewNode) e.item.getData()); } } @Override public void zoomChanged() { // pass } @Override public void overlayChanged() { // pass } @Override public void overlayTransparencyChanged() { // pass } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000162 12747325007 032626 xustar000000000 0000000 114 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.0100644 0000000 0000000 00000035232 12747325007 033221 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.ViewNode.Property; import com.android.hierarchyviewerlib.ui.DevicePropertyEditingSupport.PropertyType; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.ComboBoxCellEditor; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TextCellEditor; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import java.util.ArrayList; import java.util.Collection; public class PropertyViewer extends Composite implements ITreeChangeListener { private static final String PROPERTY_GET_PREFIX = "get"; private static final String EMPTY_ARGUMENT_LIST = "()"; private TreeViewModel mModel; private TreeViewer mTreeViewer; private Tree mTree; private TreeViewerColumn mValueColumn; private PropertyValueEditingSupport mPropertyValueEditingSupport; private Image mImage; private DrawableViewNode mSelectedNode; @VisibleForTesting static @NonNull String parseColumnTextName(@NonNull String name) { int start = 0; int end = name.length(); int index = name.indexOf(':'); if (index != -1) { start = index + 1; } index = name.indexOf(PROPERTY_GET_PREFIX); int prefixOffset = start + PROPERTY_GET_PREFIX.length(); if (index == start && prefixOffset < end && Character.isUpperCase(name.charAt(prefixOffset))) { start = prefixOffset; } if (name.endsWith(EMPTY_ARGUMENT_LIST)) { end -= EMPTY_ARGUMENT_LIST.length(); } if (start < end && !Character.isLowerCase(name.charAt(start))) { return Character.toLowerCase(name.charAt(start)) + (start + 1 < end ? name.substring(start + 1, end) : ""); } return name.substring(start, end); } private class ContentProvider implements ITreeContentProvider, ITableLabelProvider { @Override public Object[] getChildren(Object parentElement) { synchronized (PropertyViewer.this) { if (mSelectedNode != null && parentElement instanceof String) { String category = (String) parentElement; ArrayList returnValue = new ArrayList(); for (Property property : mSelectedNode.viewNode.properties) { if (category.equals(ViewNode.MISCELLANIOUS)) { if (property.name.indexOf(':') == -1) { returnValue.add(property); } } else { if (property.name.startsWith(((String) parentElement) + ":")) { returnValue.add(property); } } } return returnValue.toArray(new Property[returnValue.size()]); } return new Object[0]; } } @Override public Object getParent(Object element) { synchronized (PropertyViewer.this) { if (mSelectedNode != null && element instanceof Property) { if (mSelectedNode.viewNode.categories.size() == 0) { return null; } String name = ((Property) element).name; int index = name.indexOf(':'); if (index == -1) { return ViewNode.MISCELLANIOUS; } return name.substring(0, index); } return null; } } @Override public boolean hasChildren(Object element) { synchronized (PropertyViewer.this) { if (mSelectedNode != null && element instanceof String) { String category = (String) element; for (String name : mSelectedNode.viewNode.namedProperties.keySet()) { if (category.equals(ViewNode.MISCELLANIOUS)) { if (name.indexOf(':') == -1) { return true; } } else { if (name.startsWith(((String) element) + ":")) { return true; } } } } return false; } } @Override public Object[] getElements(Object inputElement) { synchronized (PropertyViewer.this) { if (mSelectedNode != null && inputElement instanceof TreeViewModel) { if (mSelectedNode.viewNode.categories.size() == 0) { return mSelectedNode.viewNode.properties .toArray(new Property[mSelectedNode.viewNode.properties.size()]); } else { return mSelectedNode.viewNode.categories .toArray(new String[mSelectedNode.viewNode.categories.size()]); } } return new Object[0]; } } @Override public void dispose() { // pass } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // pass } @Override public Image getColumnImage(Object element, int column) { if (mSelectedNode == null) { return null; } if (column == 1 && mPropertyValueEditingSupport.canEdit(element)) { return mImage; } return null; } @Override public String getColumnText(Object element, int column) { synchronized (PropertyViewer.this) { if (mSelectedNode != null) { if (element instanceof String && column == 0) { String category = (String) element; return Character.toUpperCase(category.charAt(0)) + category.substring(1); } else if (element instanceof Property) { if (column == 0) { return parseColumnTextName(((Property) element).name); } else if (column == 1) { return ((Property) element).value; } } } return ""; } } @Override public void addListener(ILabelProviderListener listener) { // pass } @Override public boolean isLabelProperty(Object element, String property) { // pass return false; } @Override public void removeListener(ILabelProviderListener listener) { // pass } } private class PropertyValueEditingSupport extends EditingSupport { private DevicePropertyEditingSupport mDevicePropertyEditingSupport = new DevicePropertyEditingSupport(); public PropertyValueEditingSupport(ColumnViewer viewer) { super(viewer); } @Override protected boolean canEdit(Object element) { if (mSelectedNode == null) { return false; } return element instanceof Property && mSelectedNode.viewNode.window.getHvDevice().isViewUpdateEnabled() && mDevicePropertyEditingSupport.canEdit((Property) element); } @Override protected CellEditor getCellEditor(Object element) { Property p = (Property) element; PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p); Composite parent = (Composite) getViewer().getControl(); switch (type) { case INTEGER: case INTEGER_OR_CONSTANT: return new TextCellEditor(parent); case ENUM: String[] items = mDevicePropertyEditingSupport.getPropertyRange(p); return new ComboBoxCellEditor(parent, items, SWT.READ_ONLY); } return null; } @Override protected Object getValue(Object element) { Property p = (Property) element; PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p); if (type == PropertyType.ENUM) { // for enums, return the index of the current value in the list of possible values String[] items = mDevicePropertyEditingSupport.getPropertyRange(p); return Integer.valueOf(indexOf(p.value, items)); } return ((Property) element).value; } private int indexOf(String item, String[] items) { for (int i = 0; i < items.length; i++) { if (items[i].equals(item)) { return i; } } return -1; } @Override protected void setValue(Object element, Object newValue) { Property p = (Property) element; IHvDevice device = mSelectedNode.viewNode.window.getHvDevice(); Collection properties = mSelectedNode.viewNode.namedProperties.values(); if (mDevicePropertyEditingSupport.setValue(properties, p, newValue, mSelectedNode.viewNode, device)) { doRefresh(); } } } public PropertyViewer(Composite parent) { super(parent, SWT.NONE); setLayout(new FillLayout()); mTreeViewer = new TreeViewer(this, SWT.NONE); mTree = mTreeViewer.getTree(); mTree.setLinesVisible(true); mTree.setHeaderVisible(true); TreeColumn propertyColumn = new TreeColumn(mTree, SWT.NONE); propertyColumn.setText("Property"); TreeColumn valueColumn = new TreeColumn(mTree, SWT.NONE); valueColumn.setText("Value"); mValueColumn = new TreeViewerColumn(mTreeViewer, valueColumn); mPropertyValueEditingSupport = new PropertyValueEditingSupport(mTreeViewer); mValueColumn.setEditingSupport(mPropertyValueEditingSupport); mModel = TreeViewModel.getModel(); ContentProvider contentProvider = new ContentProvider(); mTreeViewer.setContentProvider(contentProvider); mTreeViewer.setLabelProvider(contentProvider); mTreeViewer.setInput(mModel); mModel.addTreeChangeListener(this); addDisposeListener(mDisposeListener); @SuppressWarnings("unused") TreeColumnResizer resizer = new TreeColumnResizer(this, propertyColumn, valueColumn); addControlListener(mControlListener); ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); mImage = imageLoader.loadImage("picker.png", Display.getDefault()); //$NON-NLS-1$ treeChanged(); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeTreeChangeListener(PropertyViewer.this); } }; // If the window gets too small, hide the data, otherwise SWT throws an // ERROR. private ControlListener mControlListener = new ControlAdapter() { private boolean noInput = false; private boolean noHeader = false; @Override public void controlResized(ControlEvent e) { if (getBounds().height <= 20) { mTree.setHeaderVisible(false); noHeader = true; } else if (noHeader) { mTree.setHeaderVisible(true); noHeader = false; } if (getBounds().height <= 38) { mTreeViewer.setInput(null); noInput = true; } else if (noInput) { mTreeViewer.setInput(mModel); noInput = false; } } }; @Override public void selectionChanged() { synchronized (this) { mSelectedNode = mModel.getSelection(); } doRefresh(); } @Override public void treeChanged() { synchronized (this) { mSelectedNode = mModel.getSelection(); } doRefresh(); } @Override public void viewportChanged() { // pass } @Override public void zoomChanged() { // pass } private void doRefresh() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mTreeViewer.refresh(); } }); } } hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java0100644 0000000 0000000 00000132576 12747325007 032620 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.models.ViewNode.ProfileRating; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.MouseWheelListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Path; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Transform; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import java.text.DecimalFormat; public class TreeView extends Canvas implements ITreeChangeListener { private TreeViewModel mModel; private DrawableViewNode mTree; private DrawableViewNode mSelectedNode; private Rectangle mViewport; private Transform mTransform; private Transform mInverse; private double mZoom; private Point mLastPoint; private boolean mAlreadySelectedOnMouseDown; private boolean mDoubleClicked; private boolean mNodeMoved; private DrawableViewNode mDraggedNode; public static final int LINE_PADDING = 10; public static final float BEZIER_FRACTION = 0.35f; private static Image sRedImage; private static Image sYellowImage; private static Image sGreenImage; private static Image sNotSelectedImage; private static Image sSelectedImage; private static Image sFilteredImage; private static Image sFilteredSelectedImage; private static Font sSystemFont; private Color mBoxColor; private Color mTextBackgroundColor; private Rectangle mSelectedRectangleLocation; private Point mButtonCenter; private static final int BUTTON_SIZE = 13; private Image mScaledSelectedImage; private boolean mButtonClicked; private DrawableViewNode mLastDrawnSelectedViewNode; // The profile-image box needs to be moved to, // so add some dragging leeway. private static final int DRAG_LEEWAY = 220; // Profile-image box constants private static final int RECT_WIDTH = 190; private static final int RECT_HEIGHT = 224; private static final int BUTTON_RIGHT_OFFSET = 5; private static final int BUTTON_TOP_OFFSET = 5; private static final int IMAGE_WIDTH = 125; private static final int IMAGE_HEIGHT = 120; private static final int IMAGE_OFFSET = 6; private static final int IMAGE_ROUNDING = 8; private static final int RECTANGLE_SIZE = 5; private static final int TEXT_SIDE_OFFSET = 8; private static final int TEXT_TOP_OFFSET = 4; private static final int TEXT_SPACING = 2; private static final int TEXT_ROUNDING = 20; public TreeView(Composite parent) { super(parent, SWT.NONE); mModel = TreeViewModel.getModel(); mModel.addTreeChangeListener(this); addPaintListener(mPaintListener); addMouseListener(mMouseListener); addMouseMoveListener(mMouseMoveListener); addMouseWheelListener(mMouseWheelListener); addListener(SWT.Resize, mResizeListener); addDisposeListener(mDisposeListener); addKeyListener(mKeyListener); addTraverseListener(new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { if (e.detail == SWT.TRAVERSE_TAB_NEXT || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) { e.doit = true; } } }); loadResources(); mTransform = new Transform(Display.getDefault()); mInverse = new Transform(Display.getDefault()); loadAllData(); } private void loadResources() { ImageLoader loader = ImageLoader.getLoader(this.getClass()); sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$ sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$ sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$ sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$ sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$ mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225)); mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82)); if (mScaledSelectedImage != null) { mScaledSelectedImage.dispose(); } sSystemFont = Display.getDefault().getSystemFont(); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeTreeChangeListener(TreeView.this); mTransform.dispose(); mInverse.dispose(); mBoxColor.dispose(); mTextBackgroundColor.dispose(); if (mTree != null) { mModel.setViewport(null); } } }; private Listener mResizeListener = new Listener() { @Override public void handleEvent(Event e) { synchronized (TreeView.this) { if (mTree != null && mViewport != null) { // Keep the center in the same place. Point viewCenter = new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height / 2); mViewport.width = getBounds().width / mZoom; mViewport.height = getBounds().height / mZoom; mViewport.x = viewCenter.x - mViewport.width / 2; mViewport.y = viewCenter.y - mViewport.height / 2; } } if (mViewport != null) { mModel.setViewport(mViewport); } } }; private KeyListener mKeyListener = new KeyListener() { @Override public void keyPressed(KeyEvent e) { boolean selectionChanged = false; DrawableViewNode clickedNode = null; synchronized (TreeView.this) { if (mTree != null && mViewport != null && mSelectedNode != null) { switch (e.keyCode) { case SWT.ARROW_LEFT: if (mSelectedNode.parent != null) { mSelectedNode = mSelectedNode.parent; selectionChanged = true; } break; case SWT.ARROW_UP: // On up and down, it is cool to go up and down only // the leaf nodes. // It goes well with the layout viewer DrawableViewNode currentNode = mSelectedNode; while (currentNode.parent != null && currentNode.viewNode.index == 0) { currentNode = currentNode.parent; } if (currentNode.parent != null) { selectionChanged = true; currentNode = currentNode.parent.children .get(currentNode.viewNode.index - 1); while (currentNode.children.size() != 0) { currentNode = currentNode.children .get(currentNode.children.size() - 1); } } if (selectionChanged) { mSelectedNode = currentNode; } break; case SWT.ARROW_DOWN: currentNode = mSelectedNode; while (currentNode.parent != null && currentNode.viewNode.index + 1 == currentNode.parent.children .size()) { currentNode = currentNode.parent; } if (currentNode.parent != null) { selectionChanged = true; currentNode = currentNode.parent.children .get(currentNode.viewNode.index + 1); while (currentNode.children.size() != 0) { currentNode = currentNode.children.get(0); } } if (selectionChanged) { mSelectedNode = currentNode; } break; case SWT.ARROW_RIGHT: DrawableViewNode rightNode = null; double mostOverlap = 0; final int N = mSelectedNode.children.size(); // We consider all the children and pick the one // who's tree overlaps the most. for (int i = 0; i < N; i++) { DrawableViewNode child = mSelectedNode.children.get(i); DrawableViewNode topMostChild = child; while (topMostChild.children.size() != 0) { topMostChild = topMostChild.children.get(0); } double overlap = Math.min(DrawableViewNode.NODE_HEIGHT, Math.min( mSelectedNode.top + DrawableViewNode.NODE_HEIGHT - topMostChild.top, topMostChild.top + child.treeHeight - mSelectedNode.top)); if (overlap > mostOverlap) { mostOverlap = overlap; rightNode = child; } } if (rightNode != null) { mSelectedNode = rightNode; selectionChanged = true; } break; case SWT.CR: clickedNode = mSelectedNode; break; } } } if (selectionChanged) { mModel.setSelection(mSelectedNode); } if (clickedNode != null) { HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); } } @Override public void keyReleased(KeyEvent e) { } }; private MouseListener mMouseListener = new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { DrawableViewNode clickedNode = null; synchronized (TreeView.this) { if (mTree != null && mViewport != null) { Point pt = transformPoint(e.x, e.y); clickedNode = mTree.getSelected(pt.x, pt.y); } } if (clickedNode != null) { HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); mDoubleClicked = true; } } @Override public void mouseDown(MouseEvent e) { boolean selectionChanged = false; synchronized (TreeView.this) { if (mTree != null && mViewport != null) { Point pt = transformPoint(e.x, e.y); // Ignore profiling rectangle, except for... if (mSelectedRectangleLocation != null && pt.x >= mSelectedRectangleLocation.x && pt.x < mSelectedRectangleLocation.x + mSelectedRectangleLocation.width && pt.y >= mSelectedRectangleLocation.y && pt.y < mSelectedRectangleLocation.y + mSelectedRectangleLocation.height) { // the small button! if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x) + (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) { mButtonClicked = true; doRedraw(); } return; } mDraggedNode = mTree.getSelected(pt.x, pt.y); // Update the selection. if (mDraggedNode != null && mDraggedNode != mSelectedNode) { mSelectedNode = mDraggedNode; selectionChanged = true; mAlreadySelectedOnMouseDown = false; } else if (mDraggedNode != null) { mAlreadySelectedOnMouseDown = true; } // Can't drag the root. if (mDraggedNode == mTree) { mDraggedNode = null; } if (mDraggedNode != null) { mLastPoint = pt; } else { mLastPoint = new Point(e.x, e.y); } mNodeMoved = false; mDoubleClicked = false; } } if (selectionChanged) { mModel.setSelection(mSelectedNode); } } @Override public void mouseUp(MouseEvent e) { boolean redraw = false; boolean redrawButton = false; boolean viewportChanged = false; boolean selectionChanged = false; synchronized (TreeView.this) { if (mTree != null && mViewport != null && mLastPoint != null) { if (mDraggedNode == null) { // The viewport moves. handleMouseDrag(new Point(e.x, e.y)); viewportChanged = true; } else { // The nodes move. handleMouseDrag(transformPoint(e.x, e.y)); } // Deselect on the second click... // This is in the mouse up, because mouse up happens after a // double click event. // During a double click, we don't want to deselect. Point pt = transformPoint(e.x, e.y); DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y); if (mouseUpOn != null && mouseUpOn == mSelectedNode && mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) { mSelectedNode = null; selectionChanged = true; } mLastPoint = null; mDraggedNode = null; redraw = true; } // Just clicked the button here. if (mButtonClicked) { HierarchyViewerDirector.getDirector().showCapture(getShell(), mSelectedNode.viewNode); mButtonClicked = false; redrawButton = true; } } // Complicated. if (viewportChanged) { mModel.setViewport(mViewport); } else if (redraw) { mModel.removeTreeChangeListener(TreeView.this); mModel.notifyViewportChanged(); if (selectionChanged) { mModel.setSelection(mSelectedNode); } mModel.addTreeChangeListener(TreeView.this); doRedraw(); } else if (redrawButton) { doRedraw(); } } }; private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { boolean redraw = false; boolean viewportChanged = false; synchronized (TreeView.this) { if (mTree != null && mViewport != null && mLastPoint != null) { if (mDraggedNode == null) { handleMouseDrag(new Point(e.x, e.y)); viewportChanged = true; } else { handleMouseDrag(transformPoint(e.x, e.y)); } redraw = true; } } if (viewportChanged) { mModel.setViewport(mViewport); } else if (redraw) { mModel.removeTreeChangeListener(TreeView.this); mModel.notifyViewportChanged(); mModel.addTreeChangeListener(TreeView.this); doRedraw(); } } }; private void handleMouseDrag(Point pt) { // Case 1: a node is dragged. DrawableViewNode knows how to handle this. if (mDraggedNode != null) { if (mLastPoint.y - pt.y != 0) { mNodeMoved = true; } mDraggedNode.move(mLastPoint.y - pt.y); mLastPoint = pt; return; } // Case 2: the viewport is dragged. We have to make sure we respect the // bounds - don't let the user drag way out... + some leeway for the // profiling box. double xDif = (mLastPoint.x - pt.x) / mZoom; double yDif = (mLastPoint.y - pt.y) / mZoom; double treeX = mTree.bounds.x - DRAG_LEEWAY; double treeY = mTree.bounds.y - DRAG_LEEWAY; double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY; double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY; if (mViewport.width > treeWidth) { if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) { mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width); } else if (xDif > 0 && mViewport.x < treeX) { mViewport.x = Math.min(mViewport.x + xDif, treeX); } } else { if (xDif < 0 && mViewport.x > treeX) { mViewport.x = Math.max(mViewport.x + xDif, treeX); } else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) { mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width); } } if (mViewport.height > treeHeight) { if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) { mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height); } else if (yDif > 0 && mViewport.y < treeY) { mViewport.y = Math.min(mViewport.y + yDif, treeY); } } else { if (yDif < 0 && mViewport.y > treeY) { mViewport.y = Math.max(mViewport.y + yDif, treeY); } else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) { mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height); } } mLastPoint = pt; } private Point transformPoint(double x, double y) { float[] pt = { (float) x, (float) y }; mInverse.transform(pt); return new Point(pt[0], pt[1]); } private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { @Override public void mouseScrolled(MouseEvent e) { Point zoomPoint = null; synchronized (TreeView.this) { if (mTree != null && mViewport != null) { mZoom += Math.ceil(e.count / 3.0) * 0.1; zoomPoint = transformPoint(e.x, e.y); } } if (zoomPoint != null) { mModel.zoomOnPoint(mZoom, zoomPoint); } } }; private PaintListener mPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { synchronized (TreeView.this) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); if (mTree != null && mViewport != null) { // Easy stuff! e.gc.setTransform(mTransform); e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); Path connectionPath = new Path(Display.getDefault()); paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath); e.gc.drawPath(connectionPath); connectionPath.dispose(); // Draw the profiling box. if (mSelectedNode != null) { e.gc.setAlpha(200); // Draw the little triangle int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2; int y = (int) mSelectedNode.top + 4; e.gc.setBackground(mBoxColor); e.gc.fillPolygon(new int[] { x, y, x - 11, y - 11, x + 11, y - 11 }); // Draw the rectangle and update the location. y -= 10 + RECT_HEIGHT; e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30, 30); mSelectedRectangleLocation = new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT); e.gc.setAlpha(255); // Draw the button mButtonCenter = new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2, y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2); if (mButtonClicked) { e.gc .setBackground(Display.getDefault().getSystemColor( SWT.COLOR_BLACK)); } else { e.gc.setBackground(mTextBackgroundColor); } e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y + BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE); e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y + BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2, RECTANGLE_SIZE + 1, RECTANGLE_SIZE); y += 15; // If there is an image, draw it. if (mSelectedNode.viewNode.image != null && mSelectedNode.viewNode.image.getBounds().height != 1 && mSelectedNode.viewNode.image.getBounds().width != 1) { // Scaling the image to the right size takes lots of // time, so we want to do it only once. // If the selection changed, get rid of the old // image. if (mLastDrawnSelectedViewNode != mSelectedNode) { if (mScaledSelectedImage != null) { mScaledSelectedImage.dispose(); mScaledSelectedImage = null; } mLastDrawnSelectedViewNode = mSelectedNode; } if (mScaledSelectedImage == null) { double ratio = 1.0 * mSelectedNode.viewNode.image.getBounds().width / mSelectedNode.viewNode.image.getBounds().height; int newWidth, newHeight; if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) { newWidth = Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image .getBounds().width); newHeight = (int) (newWidth / ratio); } else { newHeight = Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image .getBounds().height); newWidth = (int) (newHeight * ratio); } // Interesting note... We make the image twice // the needed size so that there is better // resolution under zoom. newWidth = Math.max(newWidth * 2, 1); newHeight = Math.max(newHeight * 2, 1); mScaledSelectedImage = new Image(Display.getDefault(), newWidth, newHeight); GC gc = new GC(mScaledSelectedImage); gc.setBackground(mTextBackgroundColor); gc.fillRectangle(0, 0, newWidth, newHeight); gc.drawImage(mSelectedNode.viewNode.image, 0, 0, mSelectedNode.viewNode.image.getBounds().width, mSelectedNode.viewNode.image.getBounds().height, 0, 0, newWidth, newHeight); gc.dispose(); } // Draw the background rectangle e.gc.setBackground(mTextBackgroundColor); e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4 - IMAGE_OFFSET, y + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) / 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2 + 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2 + 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING); // Under max zoom, we want the image to be // untransformed. So, get back to the identity // transform. int imageX = x - mScaledSelectedImage.getBounds().width / 4; int imageY = y + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) / 2; Transform untransformedTransform = new Transform(Display.getDefault()); e.gc.setTransform(untransformedTransform); float[] pt = new float[] { imageX, imageY }; mTransform.transform(pt); e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage .getBounds().width, mScaledSelectedImage.getBounds().height, (int) pt[0], (int) pt[1], (int) (mScaledSelectedImage .getBounds().width * mZoom / 2), (int) (mScaledSelectedImage.getBounds().height * mZoom / 2)); untransformedTransform.dispose(); e.gc.setTransform(mTransform); } // Text stuff y += IMAGE_HEIGHT; y += 10; Font font = getFont(8, false); e.gc.setFont(font); String text = mSelectedNode.viewNode.viewCount + " view" + (mSelectedNode.viewNode.viewCount != 1 ? "s" : ""); DecimalFormat formatter = new DecimalFormat("0.000"); String measureText = "Measure: " + (mSelectedNode.viewNode.measureTime != -1 ? formatter .format(mSelectedNode.viewNode.measureTime) + " ms" : "n/a"); String layoutText = "Layout: " + (mSelectedNode.viewNode.layoutTime != -1 ? formatter .format(mSelectedNode.viewNode.layoutTime) + " ms" : "n/a"); String drawText = "Draw: " + (mSelectedNode.viewNode.drawTime != -1 ? formatter .format(mSelectedNode.viewNode.drawTime) + " ms" : "n/a"); org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text); org.eclipse.swt.graphics.Point measureExtent = e.gc.stringExtent(measureText); org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText); org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText); int boxWidth = Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max( layoutExtent.x, drawExtent.x))) + 2 * TEXT_SIDE_OFFSET; int boxHeight = titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING + layoutExtent.y + TEXT_SPACING + drawExtent.y + 2 * TEXT_TOP_OFFSET; e.gc.setBackground(mTextBackgroundColor); e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight, TEXT_ROUNDING, TEXT_ROUNDING); e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); y += TEXT_TOP_OFFSET; e.gc.drawText(text, x - titleExtent.x / 2, y, true); x -= boxWidth / 2; x += TEXT_SIDE_OFFSET; y += titleExtent.y + TEXT_SPACING; e.gc.drawText(measureText, x, y, true); y += measureExtent.y + TEXT_SPACING; e.gc.drawText(layoutText, x, y, true); y += layoutExtent.y + TEXT_SPACING; e.gc.drawText(drawText, x, y, true); font.dispose(); } else { mSelectedRectangleLocation = null; mButtonCenter = null; } } } } }; private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node, DrawableViewNode selectedNode, Path connectionPath) { if (selectedNode == node && node.viewNode.filtered) { gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); } else if (selectedNode == node) { gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); } else if (node.viewNode.filtered) { gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); } else { gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); } int fontHeight = gc.getFontMetrics().getHeight(); // Draw the text... int contentWidth = DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; String name = node.viewNode.name; int dotIndex = name.lastIndexOf('.'); if (dotIndex != -1) { name = name.substring(dotIndex + 1); } double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING; drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true); y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight, 8, false); y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; if (!node.viewNode.id.equals("NO_ID")) { drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8, false); } if (node.viewNode.measureRating != ProfileRating.NONE) { y = node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING - sRedImage.getBounds().height; x += (contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2; switch (node.viewNode.measureRating) { case GREEN: gc.drawImage(sGreenImage, (int) x, (int) y); break; case YELLOW: gc.drawImage(sYellowImage, (int) x, (int) y); break; case RED: gc.drawImage(sRedImage, (int) x, (int) y); break; } x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; switch (node.viewNode.layoutRating) { case GREEN: gc.drawImage(sGreenImage, (int) x, (int) y); break; case YELLOW: gc.drawImage(sYellowImage, (int) x, (int) y); break; case RED: gc.drawImage(sRedImage, (int) x, (int) y); break; } x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; switch (node.viewNode.drawRating) { case GREEN: gc.drawImage(sGreenImage, (int) x, (int) y); break; case YELLOW: gc.drawImage(sYellowImage, (int) x, (int) y); break; case RED: gc.drawImage(sRedImage, (int) x, (int) y); break; } } org.eclipse.swt.graphics.Point indexExtent = gc.stringExtent(Integer.toString(node.viewNode.index)); x = node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING - indexExtent.x; y = node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING - indexExtent.y; gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT); int N = node.children.size(); if (N == 0) { return; } float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N; for (int i = 0; i < N; i++) { DrawableViewNode child = node.children.get(i); paintRecursive(gc, transform, child, selectedNode, connectionPath); float x1 = node.left + DrawableViewNode.NODE_WIDTH; float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2; float x2 = child.left; float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; float cy1 = y1; float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; float cy2 = y2; connectionPath.moveTo(x1, y1); connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); } } private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y, double width, double height, int fontSize, boolean bold) { Font oldFont = gc.getFont(); Font newFont = getFont(fontSize, bold); gc.setFont(newFont); org.eclipse.swt.graphics.Point extent = gc.stringExtent(text); if (extent.x > width) { // Oh no... we need to scale it. double scale = width / extent.x; float[] transformElements = new float[6]; transform.getElements(transformElements); transform.scale((float) scale, (float) scale); gc.setTransform(transform); x /= scale; y /= scale; y += (extent.y / scale - extent.y) / 2; gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT); transform.setElements(transformElements[0], transformElements[1], transformElements[2], transformElements[3], transformElements[4], transformElements[5]); gc.setTransform(transform); } else { gc.drawText(text, (int) (x + (width - extent.x) / 2), (int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT); } gc.setFont(oldFont); newFont.dispose(); } public static Image paintToImage(DrawableViewNode tree) { Image image = new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math .ceil(tree.bounds.height)); Transform transform = new Transform(Display.getDefault()); transform.identity(); transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y); Path connectionPath = new Path(Display.getDefault()); GC gc = new GC(image); // Can't use Display.getDefault().getSystemColor in a non-UI thread. Color white = new Color(Display.getDefault(), 255, 255, 255); Color black = new Color(Display.getDefault(), 0, 0, 0); gc.setForeground(white); gc.setBackground(black); gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height); gc.setTransform(transform); paintRecursive(gc, transform, tree, null, connectionPath); gc.drawPath(connectionPath); gc.dispose(); connectionPath.dispose(); white.dispose(); black.dispose(); return image; } private static Font getFont(int size, boolean bold) { FontData[] fontData = sSystemFont.getFontData(); for (int i = 0; i < fontData.length; i++) { fontData[i].setHeight(size); if (bold) { fontData[i].setStyle(SWT.BOLD); } } return new Font(Display.getDefault(), fontData); } private void doRedraw() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { redraw(); } }); } public void loadAllData() { boolean newViewport = mViewport == null; Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mTree = mModel.getTree(); mSelectedNode = mModel.getSelection(); mViewport = mModel.getViewport(); mZoom = mModel.getZoom(); if (mTree != null && mViewport == null) { mViewport = new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 - getBounds().height / 2, getBounds().width, getBounds().height); } else { setTransform(); } } } }); if (newViewport) { mModel.setViewport(mViewport); } } // Fickle behaviour... When a new tree is loaded, the model doesn't know // about the viewport until it passes through here. @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mTree = mModel.getTree(); mSelectedNode = mModel.getSelection(); if (mTree == null) { mViewport = null; } else { mViewport = new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 - getBounds().height / 2, getBounds().width, getBounds().height); } } } }); if (mViewport != null) { mModel.setViewport(mViewport); } else { doRedraw(); } } private void setTransform() { if (mViewport != null && mTree != null) { // Set the transform. mTransform.identity(); mInverse.identity(); mTransform.scale((float) mZoom, (float) mZoom); mInverse.scale((float) mZoom, (float) mZoom); mTransform.translate((float) -mViewport.x, (float) -mViewport.y); mInverse.translate((float) -mViewport.x, (float) -mViewport.y); mInverse.invert(); } } // Note the syncExec and then synchronized... It avoids deadlock @Override public void viewportChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mViewport = mModel.getViewport(); mZoom = mModel.getZoom(); setTransform(); } } }); doRedraw(); } @Override public void zoomChanged() { viewportChanged(); } @Override public void selectionChanged() { synchronized (this) { mSelectedNode = mModel.getSelection(); if (mSelectedNode != null && mSelectedNode.viewNode.image == null) { HierarchyViewerDirector.getDirector() .loadCaptureInBackground(mSelectedNode.viewNode); } } doRedraw(); } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000164 12747325007 032630 xustar000000000 0000000 116 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControl0100644 0000000 0000000 00000012772 12747325007 033234 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Slider; import org.eclipse.swt.widgets.Text; public class TreeViewControls extends Composite implements ITreeChangeListener { private Text mFilterText; private Slider mZoomSlider; public TreeViewControls(Composite parent) { super(parent, SWT.NONE); GridLayout layout = new GridLayout(5, false); layout.marginWidth = layout.marginHeight = 2; layout.verticalSpacing = layout.horizontalSpacing = 4; setLayout(layout); Label filterLabel = new Label(this, SWT.NONE); filterLabel.setText("Filter by class or id:"); filterLabel.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); mFilterText = new Text(this, SWT.LEFT | SWT.SINGLE); mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mFilterText.addModifyListener(mFilterTextModifyListener); mFilterText.setText(HierarchyViewerDirector.getDirector().getFilterText()); Label smallZoomLabel = new Label(this, SWT.NONE); smallZoomLabel.setText(" 20%"); smallZoomLabel .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); mZoomSlider = new Slider(this, SWT.HORIZONTAL); GridData zoomSliderGridData = new GridData(GridData.CENTER, GridData.CENTER, false, false); zoomSliderGridData.widthHint = 190; mZoomSlider.setLayoutData(zoomSliderGridData); mZoomSlider.setMinimum((int) (TreeViewModel.MIN_ZOOM * 10)); mZoomSlider.setMaximum((int) (TreeViewModel.MAX_ZOOM * 10 + 1)); mZoomSlider.setThumb(1); mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10)); mZoomSlider.addSelectionListener(mZoomSliderSelectionListener); Label largeZoomLabel = new Label(this, SWT.NONE); largeZoomLabel .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); largeZoomLabel.setText("200%"); addDisposeListener(mDisposeListener); TreeViewModel.getModel().addTreeChangeListener(this); } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this); } }; private SelectionListener mZoomSliderSelectionListener = new SelectionListener() { private int oldValue; @Override public void widgetDefaultSelected(SelectionEvent e) { // pass } @Override public void widgetSelected(SelectionEvent e) { int newValue = mZoomSlider.getSelection(); if (oldValue != newValue) { TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this); TreeViewModel.getModel().setZoom(newValue / 10.0); TreeViewModel.getModel().addTreeChangeListener(TreeViewControls.this); oldValue = newValue; } } }; private ModifyListener mFilterTextModifyListener = new ModifyListener() { @Override public void modifyText(ModifyEvent e) { HierarchyViewerDirector.getDirector().filterNodes(mFilterText.getText()); } }; @Override public void selectionChanged() { // pass } @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { if (TreeViewModel.getModel().getTree() != null) { mZoomSlider.setSelection((int) Math .round(TreeViewModel.getModel().getZoom() * 10)); } mFilterText.setText(""); //$NON-NLS-1$ } }); } @Override public void viewportChanged() { // pass } @Override public void zoomChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10)); } }); }; } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000164 12747325007 032630 xustar000000000 0000000 116 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOvervie0100644 0000000 0000000 00000034373 12747325007 033234 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Path; import org.eclipse.swt.graphics.Transform; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; public class TreeViewOverview extends Canvas implements ITreeChangeListener { private TreeViewModel mModel; private DrawableViewNode mTree; private Rectangle mViewport; private Transform mTransform; private Transform mInverse; private Rectangle mBounds = new Rectangle(); private double mScale; private boolean mDragging = false; private DrawableViewNode mSelectedNode; private static Image sNotSelectedImage; private static Image sSelectedImage; private static Image sFilteredImage; private static Image sFilteredSelectedImage; public TreeViewOverview(Composite parent) { super(parent, SWT.NONE); mModel = TreeViewModel.getModel(); mModel.addTreeChangeListener(this); loadResources(); addPaintListener(mPaintListener); addMouseListener(mMouseListener); addMouseMoveListener(mMouseMoveListener); addListener(SWT.Resize, mResizeListener); addDisposeListener(mDisposeListener); mTransform = new Transform(Display.getDefault()); mInverse = new Transform(Display.getDefault()); loadAllData(); } private void loadResources() { ImageLoader loader = ImageLoader.getLoader(this.getClass()); sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ sSelectedImage = loader.loadImage("selected-small.png", Display.getDefault()); //$NON-NLS-1$ sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ sFilteredSelectedImage = loader.loadImage("selected-filtered-small.png", Display.getDefault()); //$NON-NLS-1$ } private DisposeListener mDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mModel.removeTreeChangeListener(TreeViewOverview.this); mTransform.dispose(); mInverse.dispose(); } }; private MouseListener mMouseListener = new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { // pass } @Override public void mouseDown(MouseEvent e) { boolean redraw = false; synchronized (TreeViewOverview.this) { if (mTree != null && mViewport != null) { mDragging = true; redraw = true; handleMouseEvent(transformPoint(e.x, e.y)); } } if (redraw) { mModel.removeTreeChangeListener(TreeViewOverview.this); mModel.setViewport(mViewport); mModel.addTreeChangeListener(TreeViewOverview.this); doRedraw(); } } @Override public void mouseUp(MouseEvent e) { boolean redraw = false; synchronized (TreeViewOverview.this) { if (mTree != null && mViewport != null) { mDragging = false; redraw = true; handleMouseEvent(transformPoint(e.x, e.y)); // Update bounds and transform only on mouse up. That way, // you don't get confusing behaviour during mouse drag and // it snaps neatly at the end setBounds(); setTransform(); } } if (redraw) { mModel.removeTreeChangeListener(TreeViewOverview.this); mModel.setViewport(mViewport); mModel.addTreeChangeListener(TreeViewOverview.this); doRedraw(); } } }; private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { boolean moved = false; synchronized (TreeViewOverview.this) { if (mDragging) { moved = true; handleMouseEvent(transformPoint(e.x, e.y)); } } if (moved) { mModel.removeTreeChangeListener(TreeViewOverview.this); mModel.setViewport(mViewport); mModel.addTreeChangeListener(TreeViewOverview.this); doRedraw(); } } }; private void handleMouseEvent(Point pt) { mViewport.x = pt.x - mViewport.width / 2; mViewport.y = pt.y - mViewport.height / 2; if (mViewport.x < mBounds.x) { mViewport.x = mBounds.x; } if (mViewport.y < mBounds.y) { mViewport.y = mBounds.y; } if (mViewport.x + mViewport.width > mBounds.x + mBounds.width) { mViewport.x = mBounds.x + mBounds.width - mViewport.width; } if (mViewport.y + mViewport.height > mBounds.y + mBounds.height) { mViewport.y = mBounds.y + mBounds.height - mViewport.height; } } private Point transformPoint(double x, double y) { float[] pt = { (float) x, (float) y }; mInverse.transform(pt); return new Point(pt[0], pt[1]); } private Listener mResizeListener = new Listener() { @Override public void handleEvent(Event arg0) { synchronized (TreeViewOverview.this) { setTransform(); } doRedraw(); } }; private PaintListener mPaintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { synchronized (TreeViewOverview.this) { if (mTree != null) { e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); e.gc.setTransform(mTransform); e.gc.setLineWidth((int) Math.ceil(0.7 / mScale)); Path connectionPath = new Path(Display.getDefault()); paintRecursive(e.gc, mTree, connectionPath); e.gc.drawPath(connectionPath); connectionPath.dispose(); if (mViewport != null) { e.gc.setAlpha(50); e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); e.gc.fillRectangle((int) mViewport.x, (int) mViewport.y, (int) Math .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); e.gc.setAlpha(255); e.gc.setForeground(Display.getDefault().getSystemColor( SWT.COLOR_DARK_GRAY)); e.gc.setLineWidth((int) Math.ceil(2 / mScale)); e.gc.drawRectangle((int) mViewport.x, (int) mViewport.y, (int) Math .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); } } } } }; private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) { if (mSelectedNode == node && node.viewNode.filtered) { gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); } else if (mSelectedNode == node) { gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); } else if (node.viewNode.filtered) { gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); } else { gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); } int N = node.children.size(); if (N == 0) { return; } float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N; for (int i = 0; i < N; i++) { DrawableViewNode child = node.children.get(i); paintRecursive(gc, child, connectionPath); float x1 = node.left + DrawableViewNode.NODE_WIDTH; float y1 = (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2; float x2 = child.left; float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; float cy1 = y1; float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; float cy2 = y2; connectionPath.moveTo(x1, y1); connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); } } private void doRedraw() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { redraw(); } }); } public void loadAllData() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mTree = mModel.getTree(); mSelectedNode = mModel.getSelection(); mViewport = mModel.getViewport(); setBounds(); setTransform(); } } }); } // Note the syncExec and then synchronized... It avoids deadlock @Override public void treeChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mTree = mModel.getTree(); mSelectedNode = mModel.getSelection(); mViewport = mModel.getViewport(); setBounds(); setTransform(); } } }); doRedraw(); } private void setBounds() { if (mViewport != null && mTree != null) { mBounds.x = Math.min(mViewport.x, mTree.bounds.x); mBounds.y = Math.min(mViewport.y, mTree.bounds.y); mBounds.width = Math.max(mViewport.x + mViewport.width, mTree.bounds.x + mTree.bounds.width) - mBounds.x; mBounds.height = Math.max(mViewport.y + mViewport.height, mTree.bounds.y + mTree.bounds.height) - mBounds.y; } else if (mTree != null) { mBounds.x = mTree.bounds.x; mBounds.y = mTree.bounds.y; mBounds.width = mTree.bounds.x + mTree.bounds.width - mBounds.x; mBounds.height = mTree.bounds.y + mTree.bounds.height - mBounds.y; } } private void setTransform() { if (mTree != null) { mTransform.identity(); mInverse.identity(); final Point size = new Point(); size.x = getBounds().width; size.y = getBounds().height; if (mBounds.width == 0 || mBounds.height == 0 || size.x == 0 || size.y == 0) { mScale = 1; } else { mScale = Math.min(size.x / mBounds.width, size.y / mBounds.height); } mTransform.scale((float) mScale, (float) mScale); mInverse.scale((float) mScale, (float) mScale); mTransform.translate((float) -mBounds.x, (float) -mBounds.y); mInverse.translate((float) -mBounds.x, (float) -mBounds.y); if (size.x / mBounds.width < size.y / mBounds.height) { mTransform.translate(0, (float) (size.y / mScale - mBounds.height) / 2); mInverse.translate(0, (float) (size.y / mScale - mBounds.height) / 2); } else { mTransform.translate((float) (size.x / mScale - mBounds.width) / 2, 0); mInverse.translate((float) (size.x / mScale - mBounds.width) / 2, 0); } mInverse.invert(); } } @Override public void viewportChanged() { Display.getDefault().syncExec(new Runnable() { @Override public void run() { synchronized (this) { mViewport = mModel.getViewport(); setBounds(); setTransform(); } } }); doRedraw(); } @Override public void zoomChanged() { viewportChanged(); } @Override public void selectionChanged() { synchronized (this) { mSelectedNode = mModel.getSelection(); } doRedraw(); } } hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/0040755 0000000 0000000 00000000000 12747325007 031165 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000171 12747325007 032626 xustar000000000 0000000 121 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableVi0100644 0000000 0000000 00000020341 12747325007 033125 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui.util; import com.android.hierarchyviewerlib.models.ViewNode; import java.util.ArrayList; public class DrawableViewNode { public ViewNode viewNode; public final ArrayList children = new ArrayList(); public final static int NODE_HEIGHT = 100; public final static int NODE_WIDTH = 180; public final static int CONTENT_LEFT_RIGHT_PADDING = 9; public final static int CONTENT_TOP_BOTTOM_PADDING = 8; public final static int CONTENT_INTER_PADDING = 3; public final static int INDEX_PADDING = 7; public final static int LEAF_NODE_SPACING = 9; public final static int NON_LEAF_NODE_SPACING = 15; public final static int PARENT_CHILD_SPACING = 50; public final static int PADDING = 30; public int treeHeight; public int treeWidth; public boolean leaf; public DrawableViewNode parent; public int left; public double top; public int topSpacing; public int bottomSpacing; public boolean treeDrawn; public static class Rectangle { public double x, y, width, height; public Rectangle() { } public Rectangle(Rectangle other) { this.x = other.x; this.y = other.y; this.width = other.width; this.height = other.height; } public Rectangle(double x, double y, double width, double height) { this.x = x; this.y = y; this.width = width; this.height = height; } @Override public String toString() { return "{" + x + ", " + y + ", " + width + ", " + height + "}"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ } } public static class Point { public double x, y; public Point() { } public Point(double x, double y) { this.x = x; this.y = y; } @Override public String toString() { return "(" + x + ", " + y + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } public Rectangle bounds = new Rectangle(); public DrawableViewNode(ViewNode viewNode) { this.viewNode = viewNode; treeDrawn = !viewNode.willNotDraw; if (viewNode.children.size() == 0) { treeHeight = NODE_HEIGHT; treeWidth = NODE_WIDTH; leaf = true; } else { leaf = false; int N = viewNode.children.size(); treeHeight = 0; treeWidth = 0; for (int i = 0; i < N; i++) { DrawableViewNode child = new DrawableViewNode(viewNode.children.get(i)); children.add(child); child.parent = this; treeHeight += child.treeHeight; treeWidth = Math.max(treeWidth, child.treeWidth); if (i != 0) { DrawableViewNode prevChild = children.get(i - 1); if (prevChild.leaf && child.leaf) { treeHeight += LEAF_NODE_SPACING; prevChild.bottomSpacing = LEAF_NODE_SPACING; child.topSpacing = LEAF_NODE_SPACING; } else { treeHeight += NON_LEAF_NODE_SPACING; prevChild.bottomSpacing = NON_LEAF_NODE_SPACING; child.topSpacing = NON_LEAF_NODE_SPACING; } } treeDrawn |= child.treeDrawn; } treeWidth += NODE_WIDTH + PARENT_CHILD_SPACING; } } public void setLeft() { if (parent == null) { left = PADDING; bounds.x = 0; bounds.width = treeWidth + 2 * PADDING; } else { left = parent.left + NODE_WIDTH + PARENT_CHILD_SPACING; } int N = children.size(); for (int i = 0; i < N; i++) { children.get(i).setLeft(); } } public void placeRoot() { top = PADDING + (treeHeight - NODE_HEIGHT) / 2.0; double currentTop = PADDING; int N = children.size(); for (int i = 0; i < N; i++) { DrawableViewNode child = children.get(i); child.place(currentTop, top - currentTop); currentTop += child.treeHeight + child.bottomSpacing; } bounds.y = 0; bounds.height = treeHeight + 2 * PADDING; } private void place(double treeTop, double rootDistance) { if (treeHeight <= rootDistance) { top = treeTop + treeHeight - NODE_HEIGHT; } else if (rootDistance <= -NODE_HEIGHT) { top = treeTop; } else { if (children.size() == 0) { top = treeTop; } else { top = rootDistance + treeTop - NODE_HEIGHT + (2.0 * NODE_HEIGHT) / (treeHeight + NODE_HEIGHT) * (treeHeight - rootDistance); } } int N = children.size(); double currentTop = treeTop; for (int i = 0; i < N; i++) { DrawableViewNode child = children.get(i); child.place(currentTop, rootDistance); currentTop += child.treeHeight + child.bottomSpacing; rootDistance -= child.treeHeight + child.bottomSpacing; } } public DrawableViewNode getSelected(double x, double y) { if (x >= left && x < left + NODE_WIDTH && y >= top && y <= top + NODE_HEIGHT) { return this; } int N = children.size(); for (int i = 0; i < N; i++) { DrawableViewNode selected = children.get(i).getSelected(x, y); if (selected != null) { return selected; } } return null; } /* * Moves the node the specified distance up. */ public void move(double distance) { top -= distance; // Get the root DrawableViewNode root = this; while (root.parent != null) { root = root.parent; } // Figure out the new tree top. double treeTop; if (top + NODE_HEIGHT <= root.top) { treeTop = top + NODE_HEIGHT - treeHeight; } else if (top >= root.top + NODE_HEIGHT) { treeTop = top; } else { if (leaf) { treeTop = top; } else { double distanceRatio = 1 - (root.top + NODE_HEIGHT - top) / (2.0 * NODE_HEIGHT); treeTop = root.top - treeHeight + distanceRatio * (treeHeight + NODE_HEIGHT); } } // Go up the tree and figure out the tree top. DrawableViewNode node = this; while (node.parent != null) { int index = node.viewNode.index; for (int i = 0; i < index; i++) { DrawableViewNode sibling = node.parent.children.get(i); treeTop -= sibling.treeHeight + sibling.bottomSpacing; } node = node.parent; } // Update the bounds. root.bounds.y = Math.min(root.top - PADDING, treeTop - PADDING); root.bounds.height = Math.max(treeTop + root.treeHeight + PADDING, root.top + NODE_HEIGHT + PADDING) - root.bounds.y; // Place all the children of the root double currentTop = treeTop; int N = root.children.size(); for (int i = 0; i < N; i++) { DrawableViewNode child = root.children.get(i); child.place(currentTop, root.top - currentTop); currentTop += child.treeHeight + child.bottomSpacing; } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000160 12747325007 032624 xustar000000000 0000000 112 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.ja0100644 0000000 0000000 00000033403 12747325007 033027 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui.util; import java.awt.Graphics2D; import java.awt.Point; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; /** * Writes PSD file. Supports only 8 bits, RGB images with 4 channels. */ public class PsdFile { private final Header mHeader; private final ColorMode mColorMode; private final ImageResources mImageResources; private final LayersMasksInfo mLayersMasksInfo; private final LayersInfo mLayersInfo; private final BufferedImage mMergedImage; private final Graphics2D mGraphics; public PsdFile(int width, int height) { mHeader = new Header(width, height); mColorMode = new ColorMode(); mImageResources = new ImageResources(); mLayersMasksInfo = new LayersMasksInfo(); mLayersInfo = new LayersInfo(); mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); mGraphics = mMergedImage.createGraphics(); } public void addLayer(String name, BufferedImage image, Point offset) { addLayer(name, image, offset, true); } public void addLayer(String name, BufferedImage image, Point offset, boolean visible) { mLayersInfo.addLayer(name, image, offset, visible); if (visible) mGraphics.drawImage(image, null, offset.x, offset.y); } public void write(OutputStream stream) { mLayersMasksInfo.setLayersInfo(mLayersInfo); DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream)); try { mHeader.write(out); out.flush(); mColorMode.write(out); mImageResources.write(out); mLayersMasksInfo.write(out); mLayersInfo.write(out); out.flush(); mLayersInfo.writeImageData(out); out.flush(); writeImage(mMergedImage, out, false); out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } private static void writeImage(BufferedImage image, DataOutputStream out, boolean split) throws IOException { if (!split) out.writeShort(0); int width = image.getWidth(); int height = image.getHeight(); final int length = width * height; int[] pixels = new int[length]; image.getData().getDataElements(0, 0, width, height, pixels); byte[] a = new byte[length]; byte[] r = new byte[length]; byte[] g = new byte[length]; byte[] b = new byte[length]; for (int i = 0; i < length; i++) { final int pixel = pixels[i]; a[i] = (byte) ((pixel >> 24) & 0xFF); r[i] = (byte) ((pixel >> 16) & 0xFF); g[i] = (byte) ((pixel >> 8) & 0xFF); b[i] = (byte) (pixel & 0xFF); } if (split) out.writeShort(0); if (split) out.write(a); if (split) out.writeShort(0); out.write(r); if (split) out.writeShort(0); out.write(g); if (split) out.writeShort(0); out.write(b); if (!split) out.write(a); } @SuppressWarnings( { "UnusedDeclaration" }) static class Header { static final short MODE_BITMAP = 0; static final short MODE_GRAYSCALE = 1; static final short MODE_INDEXED = 2; static final short MODE_RGB = 3; static final short MODE_CMYK = 4; static final short MODE_MULTI_CHANNEL = 7; static final short MODE_DUOTONE = 8; static final short MODE_LAB = 9; final byte[] mSignature = "8BPS".getBytes(); //$NON-NLS-1$ final short mVersion = 1; final byte[] mReserved = new byte[6]; final short mChannelCount = 4; final int mHeight; final int mWidth; final short mDepth = 8; final short mMode = MODE_RGB; Header(int width, int height) { mWidth = width; mHeight = height; } void write(DataOutputStream out) throws IOException { out.write(mSignature); out.writeShort(mVersion); out.write(mReserved); out.writeShort(mChannelCount); out.writeInt(mHeight); out.writeInt(mWidth); out.writeShort(mDepth); out.writeShort(mMode); } } // Unused at the moment @SuppressWarnings( { "UnusedDeclaration" }) static class ColorMode { final int mLength = 0; void write(DataOutputStream out) throws IOException { out.writeInt(mLength); } } // Unused at the moment @SuppressWarnings( { "UnusedDeclaration" }) static class ImageResources { static final short RESOURCE_RESOLUTION_INFO = 0x03ED; int mLength = 0; final byte[] mSignature = "8BIM".getBytes(); //$NON-NLS-1$ final short mResourceId = RESOURCE_RESOLUTION_INFO; final short mPad = 0; final int mDataLength = 16; final short mHorizontalDisplayUnit = 0x48; // 72 dpi final int mHorizontalResolution = 1; final short mWidthDisplayUnit = 1; final short mVerticalDisplayUnit = 0x48; // 72 dpi final int mVerticalResolution = 1; final short mHeightDisplayUnit = 1; ImageResources() { mLength = mSignature.length; mLength += 2; mLength += 2; mLength += 4; mLength += 8; mLength += 8; } void write(DataOutputStream out) throws IOException { out.writeInt(mLength); out.write(mSignature); out.writeShort(mResourceId); out.writeShort(mPad); out.writeInt(mDataLength); out.writeShort(mHorizontalDisplayUnit); out.writeInt(mHorizontalResolution); out.writeShort(mWidthDisplayUnit); out.writeShort(mVerticalDisplayUnit); out.writeInt(mVerticalResolution); out.writeShort(mHeightDisplayUnit); } } @SuppressWarnings( { "UnusedDeclaration" }) static class LayersMasksInfo { int mMiscLength; int mLayerInfoLength; void setLayersInfo(LayersInfo layersInfo) { mLayerInfoLength = layersInfo.getLength(); // Round to the next multiple of 2 if ((mLayerInfoLength & 0x1) == 0x1) mLayerInfoLength++; mMiscLength = mLayerInfoLength + 8; } void write(DataOutputStream out) throws IOException { out.writeInt(mMiscLength); out.writeInt(mLayerInfoLength); } } @SuppressWarnings( { "UnusedDeclaration" }) static class LayersInfo { final List mLayers = new ArrayList(); void addLayer(String name, BufferedImage image, Point offset, boolean visible) { mLayers.add(new Layer(name, image, offset, visible)); } int getLength() { int length = 2; for (Layer layer : mLayers) { length += layer.getLength(); } return length; } void write(DataOutputStream out) throws IOException { out.writeShort((short) -mLayers.size()); for (Layer layer : mLayers) { layer.write(out); } } void writeImageData(DataOutputStream out) throws IOException { for (Layer layer : mLayers) { layer.writeImageData(out); } // Global layer mask info length out.writeInt(0); } } @SuppressWarnings( { "UnusedDeclaration" }) static class Layer { static final byte OPACITY_TRANSPARENT = 0x0; static final byte OPACITY_OPAQUE = (byte) 0xFF; static final byte CLIPPING_BASE = 0x0; static final byte CLIPPING_NON_BASE = 0x1; static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1; static final byte FLAG_INVISIBLE = 0x2; final int mTop; final int mLeft; final int mBottom; final int mRight; final short mChannelCount = 4; final Channel[] mChannelInfo = new Channel[mChannelCount]; final byte[] mBlendSignature = "8BIM".getBytes(); //$NON-NLS-1$ final byte[] mBlendMode = "norm".getBytes(); //$NON-NLS-1$ final byte mOpacity = OPACITY_OPAQUE; final byte mClipping = CLIPPING_BASE; byte mFlags = 0x0; final byte mFiller = 0x0; int mExtraSize = 4 + 4; final int mMaskDataLength = 0; final int mBlendRangeDataLength = 0; final byte[] mName; final byte[] mLayerExtraSignature = "8BIM".getBytes(); //$NON-NLS-1$ final byte[] mLayerExtraKey = "luni".getBytes(); //$NON-NLS-1$ int mLayerExtraLength; final String mOriginalName; private BufferedImage mImage; Layer(String name, BufferedImage image, Point offset, boolean visible) { final int height = image.getHeight(); final int width = image.getWidth(); final int length = width * height; mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length); mChannelInfo[1] = new Channel(Channel.ID_RED, length); mChannelInfo[2] = new Channel(Channel.ID_GREEN, length); mChannelInfo[3] = new Channel(Channel.ID_BLUE, length); mTop = offset.y; mLeft = offset.x; mBottom = offset.y + height; mRight = offset.x + width; mOriginalName = name; byte[] data = name.getBytes(); try { mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length; //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { e.printStackTrace(); } final byte[] nameData = new byte[data.length + 1]; nameData[0] = (byte) (data.length & 0xFF); System.arraycopy(data, 0, nameData, 1, data.length); // This could be done in the same pass as above if (nameData.length % 4 != 0) { data = new byte[nameData.length + 4 - (nameData.length % 4)]; System.arraycopy(nameData, 0, data, 0, nameData.length); mName = data; } else { mName = nameData; } mExtraSize += mName.length; mExtraSize += mLayerExtraLength + 4 + mLayerExtraKey.length + mLayerExtraSignature.length; mImage = image; if (!visible) { mFlags |= FLAG_INVISIBLE; } } int getLength() { int length = 4 * 4 + 2; for (Channel channel : mChannelInfo) { length += channel.getLength(); } length += mBlendSignature.length; length += mBlendMode.length; length += 4; length += 4; length += mExtraSize; return length; } void write(DataOutputStream out) throws IOException { out.writeInt(mTop); out.writeInt(mLeft); out.writeInt(mBottom); out.writeInt(mRight); out.writeShort(mChannelCount); for (Channel channel : mChannelInfo) { channel.write(out); } out.write(mBlendSignature); out.write(mBlendMode); out.write(mOpacity); out.write(mClipping); out.write(mFlags); out.write(mFiller); out.writeInt(mExtraSize); out.writeInt(mMaskDataLength); out.writeInt(mBlendRangeDataLength); out.write(mName); out.write(mLayerExtraSignature); out.write(mLayerExtraKey); out.writeInt(mLayerExtraLength); out.writeInt(mOriginalName.length() + 1); out.write(mOriginalName.getBytes("UTF-16")); //$NON-NLS-1$ } void writeImageData(DataOutputStream out) throws IOException { writeImage(mImage, out, true); } } @SuppressWarnings( { "UnusedDeclaration" }) static class Channel { static final short ID_RED = 0; static final short ID_GREEN = 1; static final short ID_BLUE = 2; static final short ID_ALPHA = -1; static final short ID_LAYER_MASK = -2; final short mId; final int mDataLength; Channel(short id, int dataLength) { mId = id; mDataLength = dataLength + 2; } int getLength() { return 2 + 4 + mDataLength; } void write(DataOutputStream out) throws IOException { out.writeShort(mId); out.writeInt(mDataLength); } } } ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000172 12747325007 032627 xustar000000000 0000000 122 path=hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java hierarchyviewer2/hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumn0100644 0000000 0000000 00000010307 12747325007 033163 0ustar000000000 0000000 /* * 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.hierarchyviewerlib.ui.util; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.TreeColumn; public class TreeColumnResizer { private TreeColumn mColumn1; private TreeColumn mColumn2; private Composite mControl; private int mColumn1Width; private int mColumn2Width; private final static int MIN_COLUMN1_WIDTH = 18; private final static int MIN_COLUMN2_WIDTH = 3; public TreeColumnResizer(Composite control, TreeColumn column1, TreeColumn column2) { this.mControl = control; this.mColumn1 = column1; this.mColumn2 = column2; control.addListener(SWT.Resize, resizeListener); column1.addListener(SWT.Resize, column1ResizeListener); column2.setResizable(false); } private Listener resizeListener = new Listener() { @Override public void handleEvent(Event e) { if (mColumn1Width == 0 && mColumn2Width == 0) { mColumn1Width = (mControl.getBounds().width - 18) / 2; mColumn2Width = (mControl.getBounds().width - 18) / 2; } else { int dif = mControl.getBounds().width - 18 - (mColumn1Width + mColumn2Width); int columnDif = Math.abs(mColumn1Width - mColumn2Width); int mainColumnChange = Math.min(Math.abs(dif), columnDif); int left = Math.max(0, Math.abs(dif) - columnDif); if (dif < 0) { if (mColumn1Width > mColumn2Width) { mColumn1Width -= mainColumnChange; } else { mColumn2Width -= mainColumnChange; } mColumn1Width -= left / 2; mColumn2Width -= left - left / 2; } else { if (mColumn1Width > mColumn2Width) { mColumn2Width += mainColumnChange; } else { mColumn1Width += mainColumnChange; } mColumn1Width += left / 2; mColumn2Width += left - left / 2; } } mColumn1.removeListener(SWT.Resize, column1ResizeListener); mColumn1.setWidth(mColumn1Width); mColumn2.setWidth(mColumn2Width); mColumn1.addListener(SWT.Resize, column1ResizeListener); } }; private Listener column1ResizeListener = new Listener() { @Override public void handleEvent(Event e) { int widthDif = mColumn1Width - mColumn1.getWidth(); mColumn1Width -= widthDif; mColumn2Width += widthDif; boolean column1Changed = false; // Strange, but these constants make the columns look the same. if (mColumn1Width < MIN_COLUMN1_WIDTH) { mColumn2Width -= MIN_COLUMN1_WIDTH - mColumn1Width; mColumn1Width += MIN_COLUMN1_WIDTH - mColumn1Width; column1Changed = true; } if (mColumn2Width < MIN_COLUMN2_WIDTH) { mColumn1Width += mColumn2Width - MIN_COLUMN2_WIDTH; mColumn2Width = MIN_COLUMN2_WIDTH; column1Changed = true; } if (column1Changed) { mColumn1.removeListener(SWT.Resize, this); mColumn1.setWidth(mColumn1Width); mColumn1.addListener(SWT.Resize, this); } mColumn2.setWidth(mColumn2Width); } }; } hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/0040755 0000000 0000000 00000000000 12747325007 022753 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/auto-refresh.png0100644 0000000 0000000 00000001035 12747325007 026061 0ustar000000000 0000000 PNG  IHDRasRGB pHYs B(xtIME!"IDAT8˭Ka?h) DhIKEbt.Svh!v)x"V*@"%ܽޛi ><|cW8`;&8j@*87|1y 8d:s@jOdݰH&^Kut/}*8š, W-⬌0`ymԢyb(ۮ>†HfIyקR9/@ VZϓHfF7,:K%bY9Y(.9 of4VVx>?ݰ%"+@.2^c4a0?~-rA <vSOaNe,}5!F5 ht/i6ڣjh.SEF,}8V~ez:I^UIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/capture-psd.png0100644 0000000 0000000 00000000523 12747325007 025705 0ustar000000000 0000000 PNG  IHDRasRGB pHYs B(xtIME $IDAT8cd̰ea````dI B,p֔fc`hNpg!,'bx{7 @s)\: cFB߼`a:^& 00@n3}d=?\oF嚽@c=TK_%cU[`:Y%mEUB"wn.'_k  UsEDZ0\g>^c`*8ۨ!r6  ^~43ߴm}|U9:P= Y.jP,[P"P7>F%dKAsn䊵"wABtUucܙX"2۟?|/~GN۟Ϗ~Cr"vP@k/{RBg4~c9kZ tYˈƂ-￈w/mlL-zQkY}7_|(6> 68->@9:VeusgC}깷x-A f_zFez-~vY{GaJA;-N;E7a?Š i/AWQΣRWl7.ˍ7A[}×/""~]hH)x d[= (k+ 3] 2w R?WƚŽ@ѰlXQMt+wg(ż m|DIkFMV-v q9 . T. .( Ӽ.o>=o u4'xenNi]YSg=N$k}n~6 /4uI3/Սqy6/ąnvE]8 SPzP?s¼}8h׊Zor+" 7xw3.dkaL+?|૏B[-W3?Ao(Q}Wb|UZ_[ /wm[bAs8'+ZtMɅWQ!6n ?7]=z&MsDD|}bku#:hDEBt]}x" 6(.D $@b3V!YE`?4AZsHYL{7p:b.V@uQxS.lQg[:Gn/{%v^-nشz pIk9}mm/&Mxr0eZ"h1gQҜ.U{ -&U>tQ#EQpm !NX@ٵ4Żj6BU,K(\PT.`\x eţaS|v/\0K"zRKs]Zڵ ޫ[к^fnBHbwl6!-ľߐ٪;10Y)0J FABfH-6ǬBo@$rXؗG5R*se-eb-f,Û SU_C܀JiMρyC-r "1îCPjs+Wl@XWJ ?:}?tVWB=k|x^g>џ$UE!m6SvҏX[!_EaJ&48[M+'m_fJdA m_J1īܒ?A-(0bVoZ 2fuԽA$)C\6 4FJ2 IAPB76܄Pwg\xdݗY7Mdq/!PBXg׹ V1qp\j 7FK @e X-զC@ǪL،4ϥt=\\`k:+9&0Z;ٝ7]ME!vY.;?ȍ+C c͐l-W c5E#MI >(bP[#S :#)ODD.2;\zg7 P@SふGbZVb1}S-[O(# $:55\ CM c2'K!V>:<܆l$>3| w{H*xC(VHF//7,ºQ$ͣbe73@nO [Ƃ^`FG#n]r>ZNCv[m6omXb#eǃ4G pSgL%){`zJ5wKĄ v0ƌחbqS aNzdslݕiu`fi,.t7bQˡiqR%6K2.7zaSN5m'g]<(␾<99Ni1E棝/ "^*q5 CPv:g{TwX8̱yv߼n$:$K/_,du"L3'U!41˪>?Rx\l>;P:b"IJC#RNrv"1-&V. Φf0ꦷtuRl %ن4{ YmQwAJI1sfܠEqgQT @bHZo TpJc +ddh"$%ʹ%[j"0/V'79tY:_0RPwdzL%P-nk|ayɇ3V16,]$ n".E·D脧$=x{&q L fm%dAZ&9}vxDS),99ɫ䤖"L*EzUTejwCATU6;4jhK@6حְ]œ "(T|p0!`X-taYkRϘ9Cڈg*@fZGU,硜l'9oOSj:)?uhilaZfDz/Lkt7s^JiifHX4` ד JQ:(bKSlV ط2&PQ^щi>J&6-`I `ḆieCCAVf_㸢ב!15mCKda*W(B[(N@ 5PPes"ڼ=Xut9kMنl}ظ8#$` 8mUE(Sb]|e*j'4 EhOc3;TTrԂh"WBI";*/_U8u\(ZI1Y&ݍ4Ql%PJ4ꙬZo$oGDU"#M(1q) xȬA-|;V%?&.V`Hh8$Z"bL" x{$7,5Ar` S9*Ae5t]bif<͍/he))s"c0}IϚ[ۍ59 @jV<D843o>Lg]$hgr6SQ&EvhN_{.~_FNQ<֨Z%/ #&TcƠ0¸w3UQEƴG˷4Hm<RiH@6[ُIwx8hP[X;+DIGD+/b[`q/դ}I8]#LiNߊOsj2N:UBdԘr"t ;ܷB+b ׅ>p݅J#k utCVw-ʽŜ16 %Ze>(ӷؗ\nAB9Jr&+!pƋ=Ǫ5朒=a A <<@sZhgY6r0i`XhSב>:n݂)A\R}_m\ΨL>E e0EcЬC^ \*p Ԉg H/V=+Ne(NFh³b7{Y9aT*+#EjqiΧ8ț/-`=EН4X0q€=9A\0PPiN-KA3T  6D)~8~u[BY)&<ÁrKLdUtY@fUT~1fVaSNh֞"zVyD9.p4,[fJ\C%|mkD+e-TcsXy=Ly.e-2ޕ6K~BNBH*_M2SG,ϢKqp$J>QbL ~v`;T`-T2,V`әFSUEnoV)2Sa(vf#|=1T3L r8؆ )*0ߞ:"wur39uu0Ss8` pNì |W3 0>1T)G@H-{ !R۵Qo=Aa\tMX9W ^*s4h cGs9;Nϵ%эfUA΢)x (I`Q,մDloR٤t%hַD3C̹K GQpȢ yMlAU(+j)[]pP>2F8SO[!{Euy"D&ϥ\\k}ȩ sm%8iZUK*sJrIt'jkj&-jX{߉<•cLZVT5qE+WԆ7NTx W ܽ}S$ D{o8z1sUʙq2Z`MT/uוa*s&-5cHԱE˜@p1)%i[:QplM;{/tFOm8Cn-U`5UKbFQiR=M d3zEcK5.Lsziռ);ZQ*UpB4ST[*l'q5]ƘJuv5H@9XU*J"ŭךKn]}!X"zfWO|h6j"(H"DC5z̝ ]wrx\A&GDkrۏJҖ Y)Σ>EԔxÌnYt=X k'T)yy:"awU*֩a9oO ]0>D $u,;w X=*M RԗPr|`%z0gs!o+J)h]B<"Hf0©W4t*U?ܟrcCs ,0UbVȀڌ=sQo8g6-i0[q iM$*[RbEi!aJ@T2+.8})xvfU4]o>a@-k#M['i~捧KeQ ~8:cL:,UCY ]M)`**!yRѱ4KZ;\AV3u`? 1R1x>&F:@ŹRk9ik5U5FUL=&&RVёhfn)JKD?-u^ ݊{PLvmkSe,ك90T HFvE\)[ )=~ipC&!(h6Aa1T8'mB kpkD̰#5a;'%䈆y_;֊Ƈ3N&_n{+5oٌ\Ls ֞5xܝ9Ea3Q. :v%3 h4Z4 sBWOd |ץSpHYZ[ D*?XEܮʞ:Ƃ+"aR0hЪO:}n)"0=bP;wr ୯VENg'f,$ܴt ڏ7WHkH}:آ g-kP;ߗ[Ukq:L;u5N-Z@z6($B۞Pg[aw=""~)¿e۟>gb.ܸd<6 Q) vV3}W`8@ 9I3q:n aD&@ k~ՇQDMTVڱiIDATNş $>'8nG}dMCfMC^hx7-bl5܍fl=D *(p i Up->fkWr|К?[2(kIsTj爈g_g?G͈ޟ_DD=ugG\Ԯ\9b!nSVOzW_ s][B<`}0&uBז "؊_J8NZD09JnnDs)#p0L^ᇟo}Y 4 _< MSI i I1͘pn<x)̦qkMp.h%ԸiS$a΁t3Fci̢E/7ĕ3)--)R,.TcNzS̓Hxv@Y}ZLW  o5NJN闃?}Fm1r U9r03|MhcO*lE 9_~JQ/ I=\7Q?|#IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/green.png0100644 0000000 0000000 00000000456 12747325007 024563 0ustar000000000 0000000 PNG  IHDR;֕JsRGBbKGD pHYs  tIME#*tEXtCommentCreated with GIMPWIDAT(ϭӻ@ E3& !#p5nP P1PߎՎ$RB%셳xjaP DJ+aR@~yHT(yCovR [nqu|̚6,k^ RєjLtIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png0100644 0000000 0000000 00000000634 12747325007 027301 0ustar000000000 0000000 PNG  IHDRasRGB pHYs  tIMEߺWtEXtCommentCreated with GIMPW IDAT8c`0"sv,6f``p``` e``8 Av,e``H```pC3`BȷoqgRd} ffexUKtG{d eqZ200200Hbs㟬X7f``gfYW*e``π77Y2^Xjhjf````ذf#d``gX !V@Hl0 a$5 !9` HtVD qfIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/invalidate.png0100644 0000000 0000000 00000000607 12747325007 025601 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs  tIME0tEXtCommentCreated with GIMPWIDAT8˥0 E39 ڬ)h=2w"#Sѥӈ0rt>ҷ,}X3s# 'A%Ap(EAKLbjA^tKgW*N=%evIg,#1 k!m61-@QIT=E>&y 1ohPOnov)Y6\2OZR2mxvM)e>ak K a7BMYELngbk}lkoF)GCNg/*ϴ6X0̯Ox DIlFX7Z:EfC -{۪ΡIa#\C0Of:C Y6~q#@$,B>:dJ2` 5d YY^^%#-{I'H15F|$cDj#˩!jcG L b^%SG&&un!Uad8ҏĘi;( к/W@'5+ѸEּ̊{3HA ݤڶ_a^@Xxsq4p5:f<`:so;] bIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-overlay.png0100644 0000000 0000000 00000001045 12747325007 026054 0ustar000000000 0000000 PNG  IHDR /@sRGB pHYs  tIME.(vIDAT(ύKkQFܙLiBZ1XUU֊DFw{QAD].DPi /L#4i0$su!]Yb[ l4:PJQ*84#98:&Vss KOB $Zi 7NOZ+Ta2;B8`aaW4j6Bmh01)n[ ^cGs D4;uR&L;~FDv{H6uok}-fQo#N3!Kzg;MtyX,mg[A3 Y_7 m}&i6чA8Bu?uNK)2ivI~ӓ':9Msu]LS*Mf2\|Db1ɧ%1IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png0100644 0000000 0000000 00000000440 12747325007 027317 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs  tIME, t^NtEXtCommentCreated with GIMPW{IDAT8˥] ѳ3˺mXv kB'%z@uԏߪJI >0tz5#O;Z⇒<~ۮ|>7Cu#|UyL@޽OSU?|  ?T?|?͟}C/sUUX(T|\Tw?Uľ.O @<}_/ྮ\z|@~=?Ս5~'?{܆5O~~8=WT[uo䍍ﻙ?~"?rc];fvӸO^5}_̫w~=i4o0lAl޿CAv3&a813}Z%k o7ﺙ?i׹^lc<il{EVW 3}Qdaon+#Kk5yk}mgNRo7C?6e3ɧ駿|>%Į>׵``)s CZpfu-N3nE'EMnqKXI"b1}mYh=~A۪_/7tnO>YmmcG;y oRX>X9c/{ _r_0,(yj=OwHÌ'[ri0Np@Jqu' )(^@>.C{rͿ>y, 8.Z|"3Y5b^ϽXPc?ޚgsvuQ^x󦔿kcspY՟,jK>\<uw꬝9W> Wvu5b1֩w({x]vHG ]W"zk?l&G$X8 ē? ~"U%c]?fT/( Zi?KgpZ6~]QwydG9^ݛʐ|VJ5$<|y}ݴ{ֱWԎGU)Y;sn4+VOΏy` >;]ngECM44,0hj=[8<> 9>SqjeXy',޻~g=c}ږcʩ5{U7b=gtƊ,(tkw@j~q@ZMhYn|}υ h[k˵NҲ,ֹ^^?wxI"j룏&MV tO.ca~yݬidOlA`7wհ+*1^:3$!ۣ3>=g qZ'eyJ &"q5'3bz Uk_+ۘ :qB@UzZWP^r6?" K`Ɨ[څe0> T$=v*K6F)Wy= V1`*oOCVYeMvas9'sSkp-Шt툼 ;ﻃۻГՔXrߕ5tq 221=&VX$ehu4Dzgx'D-(i9y@VUlCPX89,%t,v;ƴE; 3[C9ԤY̩85,_cNStG-H.h,ذFc=bbT\˗BMzAh Xt5=ham_ca/xXchb9㭅mŒEFT 7zɑׁH8Z<^SluOzp-XˆJ='>D YX<(k&/:!TV+jd* ["5V%t=I 鹒1 {4@ӿbm+6wC?.RӺv(]UupJgقQW;1a]I%%Eکn\v1 2F#.$Ќں(Ij1Ǐ'sF8]ne!P7y%G=-{ՂQlekZrȑs1a9( ,m| c !|C ݚur2~>4a4 x7ku^SP' Ioch'b "k ,W:ϊ2*-0cFeWjK;nx1󂪵$O|%AǟhAV7̯+Lṳ̏"45-<>ϡ՞88Zk30iǶ\=Q!ߧ*vYZe4zcqƟR]ղ-EjU >T*[( ʂىhS[77O\V*_E%nt.7*_1ӉIǛ[Pm(➙A+hA$s% Հy}9W-9Y W8V⸽H 9`-WYԿ'ߓֈ4aJ|Kq`ܕqs_,-dGJ^:wuJۗtl \}tNvx+)5gMja4*c+91e0#pQR%rH QUD`yolZF@뺕ux/:qez8DKV£tZJ|̤In(hyǵ3L \5hJ1I'W*)Vs`qR^K7U[_DՊT`߯W=<dGU̜ҞdYr %c.|(L4$T>Y_WQCeso<֫aE]q;.+–h֪̔(Rea#JMUssyn(aȀQH-lzJA\~,FCEb%^f^&)jZ^K8'XCLb2W$^0w[dzzZAC笸'{J2Z(GF+F_s0Q]Ւ;/U]i?lZCoY?Fj/g0HTq-=i"& BK~uמF*ᬃۓ8 OZ3;J<j^D*'XKJIPlJ[A>󸻘wiy]|g=V.@ $Vd#S<{LʶNn9CtG2`83$T$s::­O'PwvfMZ}i3n'zR蘱DJw Wׯxn3Fp>5Yr̸(a7@v#p1,g0-Y;c{{NTؐ9*~4/(,Q z?K }%6$ĂWv%Q'wU5@䩰y!Zxu@sbHpꠧ}R:QE;sŞy*9xg s'lg&5a)X}{bb1>=e@w<|)Vn|hVudv򐧖^{gHx7KGmT뎏R- !'v%NȆ*(}5uБfN#BӸsN{2"[u F;iIpVr[:Q%wL:x d ×ub @C!)FFE`Q"r BOtVmT A67U^88We Quq cL2pb[(k|l#jy>\ʛqZ6D*A sVy^km0,!")hV[ ]6&u{;@ I.FTxGqwPyo| <ni"A@57>'3Q—R (U!T(*(bQ¤ ,9!St8A &P-\%77f}` B$x.dY"5@WBT˺5qL1Sqqum'ڰ(G!|L#ͧ*l/pj{n5<9\`&IMVnrPA&8N_ɕ 1>Kdtu蓠|PӾgdT DF<۪ A^+Oy*x T_S1zDSϹj3[ctk%@ŵ 2θZI8 ϪJ>byy6Wz0"VωLdeH|N+hc 帬3tE_-'y]8@zgF(;7G9,U 1yva,qDY(3U.X\KD2JWthebU,*d1ֳdȇ6#)EhY.{Zp=EbCWth»: upg\XWܞQɉ"",A6Մq̭:E5rCm7 =[8a{?}ddy" ₘ7 Y%68:sk"cؠpPj)Arh?tвU>! c/bkZwci黬݊D3c5*5ʸv ߎIO.h%ҟ !5@nZKqr)vVmJe{ݻ{rR("8;{KE5ŠD(hɤ9Pןk8qrayiJmM}zq5\TJcmN=gNkIPRI+շTSP4P#u chx-=Fnfj]iYMʳŹ]OꙈ>4"'?FBkذX> 6kogXy@P1b6Z99U5X-#Õu Cs\$,VgÃpNwD`5WnPØy.i=Pd?@4V~ =;fF!Jl;fm[`-ɕH$ʾ.\>Kl B2wҲ,:zlbc«cU=TC٢sK^՝]6LgUh=&1 Nż|V*zu'd0:4Ut0g0LrǯK4]gM"n8&^D2yiT!s+zhpl:;Ӫ$maI ,~0`8F|ʊw3޸w|,ý{ca/mB-ɜB ߚ1bv6$8ZS*@"A9l/c:]n2|Czr1x 4)j\?6TV^do*[ӫ]8H fѬ)a2)wlan? sJ FJ g{Шd){[LZe_ Es49$0RkFVπP) %ۏ#yҠ\*kSYg,}ws…zY m 8*d;M N3|M#8@A̳$`րf'E&ɲ%ωgٵq;K[ yU-hK.ͲhܹBx'"}ÜH6=FOu{8屰급G N\֐EqP&>`՚П4S2fk۰7v#v2z3yYp'-aMjuYP=_X%rUN_RtY.Af@v5R*1#WԂjfÌ׀h8k'?5 1W }fsaף+ 4V^ZCM)>qVṯF5A >rmO^`jUFź{r e$mb*epUeבwtͽ@%hJx Y08be # B!D()!B"j^]- A֯\P|hBnO=E]):xUiI>NdVǛÂɾh(ϺX"@cr5er(s<hl8g\3M0ťFTQIXtIF~%㴕wrQkӡ #d-Zld濑ިL,؃e j#yeL3eD.tɡAj L?{Մud#ţbFTӕF‰֢uƀj3wX(G)tP]Ѯ+ ԬS&B*)@ cҡ\{Z5?tkm}:F;J':gsr`i@gf!j>3oO2+PvRfv%b%R)$zIwShθ0Vv8T;u q#z \ك8>k|X͊$ *p@M41~R8=WKi"[P2zvƒ->(h'1(C{֦'>EK =/VOzө"J Z]WPJh+ XEGpSL{8B&3=ېq2>}[/ UU~{-K_q>LRn66l,90^0嚚0.1[S(F=8j7hF(=ט}-U`=1n8]'ZqPr5}?:!ŠY ІsPŰbn0A=l eN;zzI_Ee b?\糜%z0Vq~:_+ #YnWJ."/eFspC97GGڄr`m,)g{)iV݋ "7% kU1M:iyGOoh*(5>bh% 歠RZKD&oV>Dxc*: 7_9aǗO8NΎ{å"][(wk[tIYe%$.UCkU.VP._?{]}YW8l%ze=X@ZI;`k1FeU8b!{豉?k7Z>cUO/ҳuT~񽻯5mVDcjԬQOol˜ rUgys0k~PَWM\\[zsQU_p#qa-ӥzin Զ,ZҪKI4XŸNdkȎ=Z ̗_Y{ @ճ<4 ? I{VOrV/.Fe+|҉<E](fV\U#_|w~![hg0Ӕۏ{k{5;YWz41nɂz7]^czmf-B/m3+6Y'VHљ*ߏ PMniT:"qʿRvWޖ|j%R#ǻ|͗oM/cx?ceUѶ[kr~7OR/û1-ӎWMͭo|ۦǦ~H]+XFKT9tvpS1ȘlFԕ@;.ae$efT %@x1!6>(q|T~u٣ bi3ӦۯrcI&_?w!OxgϺ8y{|Aaf v>Y|>MUM~?~?7(- ;B@K̂ºl*yQif7+wb)]!hq6dz/Ad\ QqJ.ᡢ: ts_Ui?|gzy6_7_?|H[>%IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-black.png0100644 0000000 0000000 00000000235 12747325007 025144 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs  tIME.!MʗIDAT8cd``@`b0jM@כeIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/on-white.png0100644 0000000 0000000 00000000236 12747325007 025211 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs  tIME.7"IDAT8c `ԀQF ,k؛IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/picker.png0100644 0000000 0000000 00000000562 12747325007 024736 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs B(xtIME9(5IDAT8˥1JCA$j:+aH<6i,ӥJ)xsBZ0ج㾼G.3+l3v7&[X|tMzlGex: z'@ @=XFb:xLHyфxF@/{ݜ}lyZ i?o I#dxlI# q&= ^\T5O]ϛ m+LZ=uIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png0100644 0000000 0000000 00000001336 12747325007 030766 0ustar000000000 0000000 PNG  IHDR[/iCCPICC ProfilexkQPZPPЅ @1(r<ڡy&dfD0Ӆ W"]ƺn-Pp#JAʸgB)ù;"{n@rԤ4_*K[ p*zǞ4m+n?uKe@Ī>]vA1^1@XN>a|~D>=悰ȖѰ@ u@\AlDV  /%ˠ@|ُ]O8;' uG`a"oz5_7xw_a7>|t:}]ae r5^(p MHc* Y+f!K_k5AaU mW/423lɹ0d]lZ\X^aI5ŀnlop69l3H@Ff"4I/225U*_=VyHNVw nqiүĥQY][` pHYs  UIDAT8c`8}d SFrY`e#???Lx {YQQ ?`$G`4R`4RBu}i*kIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png0100644 0000000 0000000 00000001335 12747325007 027177 0ustar000000000 0000000 PNG  IHDR[/iCCPICC ProfilexkQPZPPЅ @1(r<ڡy&dfD0Ӆ W"]ƺn-Pp#JAʸgB)ù;"{n@rԤ4_*K[ p*zǞ4m+n?uKe@Ī>]vA1^1@XN>a|~D>=悰ȖѰ@ u@\AlDV  /%ˠ@|ُ]O8;' uG`a"oz5_7xw_a7>|t:}]ae r5^(p MHc* Y+f!K_k5AaU mW/423lɹ0d]lZ\X^aI5ŀnlop69l3H@Ff"4I/225U*_=VyHNVw nqiүĥQY][` pHYs  TIDAT8c`016,qYFrY` ?~d Wop/߻w JJSF F #0Rzcj$*aIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/profile.png0100644 0000000 0000000 00000001125 12747325007 025115 0ustar000000000 0000000 PNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8=hSQU$ T*h(TD+4T(IE.b,B s*,k\֊Xj?!դ޻AIL?˩mw ޘ@RZa "6]Wֱ&u@TѓZ& |Tx{&qS_KX\}-ls.,h~!\GVbG} 3A'y#hUtu:7**OŕpKX~:X#R ]['D#HVocD^\$ ܈Z:cC(9fg.\qf @aeTRjm RrƞM=UO_h+,%ܹqp3H [yG@Tf \U" F̹l6w}*9Qiv8Vے,9C`UVnuPi0 I,y0i/zѝ#dkIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/refresh-windows.png0100644 0000000 0000000 00000001550 12747325007 026605 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs  tIME2\IDAT8uSKHTQΝ;s_4|YVˊEZah!AAT!HP$$AE* (WNLsutƹ3[~ CrD%3|(\x`t)Wb>9Y |b5\NVVTD)ˑ\.]rYMSᕪ}Y%I < z,"HK$ub'*zu(ylzCI)6caz,f0M"c xMAU=ʺZhoSE%T f󎴤WK.g0Lr9ǾMx\l :P1F7XG 6+ֶ:a;?\8Z 1HڣFGI`a ܌Gf3[~8 #MrJ&g'_-btwSr9v+4~M÷vY)e`B/(/Y* PTyf*j}xfrgduDE9}I,;ÖjhݗhLU>SA@@ d<:;ҭZEђQ~{XH D@)xZH@i,1k- -|l,)Uz8 &Z;hsPD8QE@63 a%l'A#jh_\&@~KIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/request-layout.png0100644 0000000 0000000 00000000337 12747325007 026464 0ustar000000000 0000000 PNG  IHDRasRGB pHYs B(xtIME9/gRyqIDAT8˵R[ kw }l.`dSi 8 ӻd;"fg0[పƴqmb%/Y#fe:Tx⛤xdI *QEhmU%e 0 шIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/save.png0100644 0000000 0000000 00000000550 12747325007 024414 0ustar000000000 0000000 PNG  IHDR(-SsRGB`PLTEntVmVJWmb{oQa{DM_)1C?FVDߟԮe pHYs  tIME [IDAT]I Ph4 OWXMsAdSZ%1ma k"#(1O_k E@s\,w!X>D(G&<9!tNrfejpIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png0100644 0000000 0000000 00000042150 12747325007 027747 0ustar000000000 0000000 PNG  IHDR>atEXtSoftwareAdobe ImageReadyqe<fiTXtXML:com.adobe.xmp k@IDATx}Wy73;uK-#Y{`B ġ HI Hbb15lH%YV/+~}f7{{' F3;vv}> Ayx<S54 uBD]^=ÞXK#&ʥ (*ϱ#G+2sor3~Zww\@;#iny~ r_ox0N*Ѓ| oQᎹW.Ne›hxI8wt;W[V6]ž\*e`>Wڒ>-7OD+!@~"oRd. s|#2*p@/s\vձX?>gٱE_H.Di6m}L~7b1r0*8W* O=Sp>~۬FHpOhD~oHP:wM=9/~MwGߚJ^F2ph#il-#@UdsYo 4BB TWC}桇3D@6"  /T)hRC}Ʃo:-n'D"]p?3z];P~>f]A3 2d[džK߼G?rO.G?v*2X5@SoAo#R',_Y' )2#pO_|F{U|2>ukntre,s)9U{ͶOLR^P{@KR8qUR9h-!#LUPܞtYGez*'WZ_Z ;wX`i xb9wo]EH}z C&;nb Z jv5\X\xJt6ih Ce Uc3P"LN_7?=MFBydq@F><G*hR}=yC/_G;{2L$QfAǀ9 t)Ԑ}] ph nP*P,JÃ_N~e)7韒V(0])F|6 R>Q}ϷxeLl |_vTrV,O!? 0:] A" u J ks@iKɂ 4'T.Mc p0t'. D>U/Wu B,Lvړ}?f Ȉ#sY'>w^]cȯT "7} f%IK&]C@&Q,݀#:|̀0`&y:B<qe9M M8FYv3`X GQ;c tv)plIx-dZٴ}ZwM{+¥{UӇQKԱNbeoNWp_߾aW:~3_kH$j$&&ޭ%COLQ<VC4N&1Tx0Rv?>tLCl߀grjKjڜgP " #ןwm?}?ɍH@n)DGs xOy>q/Wپo̍WoklX0 8Eem9xH}QG F4 2efK()vRsy _D!d vl$IRb FMaΚyKkp{ͥ16R7JpcU{O1lL=,#7E˥+iQk[fi<w=f+ȉsS8~ԄVo/p$~FV xl.SK8>Gc{6331"uIsR@B7`*İ>_ !%Q܏W-+@y8@HpBrAOsy]0RuWܒNٶϲ޲hcA>'6 4tXad+ɸ{FOw<Q#!,h/>G5@\D@wOXXVCOr5H+^AݽFGǀ y8}ɻO9@0sQW~7:~-ՅRO5Y(4Spf<4x@>+Y'_C/F];|mz1|YLgDw+s27~CH;5y#37* i* 2P5y^69\4w|*}CQaS޽׾7rպg2 Om^[)Bvz`9|lP(%˹a;_9xh;9V(&;py{fBFV` (C[*,BMU%NJwz2뢗i@"cwXpeץ5}krPs\f/fwX2 |YbcZ=nÈFג9ekE XOsJ":A$ ! F2%~xcfENTxKߒ5ul˚>}EvTd!+THD_w Ueݦ% s!NQđ6q7#> nMD*+)?U4bN1*4׹EsJs&)8̹rI@wsm鷱U-ܿ៩"ТieYdY!-,6d1[jvq~pv$J%QDҗdŠ}.~_7y<y1fff? 'Gߏ'h U`]:Hg!xT,\_a}˭_P[A75Ց/'67񣶃Ǩ !OZA:v?3N"V}[M:Dôl}.T?ҘqOQňQkRp-Q_8 % h1+i.}9(hoEpw~6-m˃=~hؐnAnYYz5[ԟ~XElcp&F ARpq3:9w݈e[Vt潩IF^8_Jx^nO!}k`j&ҝd`qiM^&-oK d#wFWvΉLvr–{eކH@hy@֗pozŻ+N>+0t"s71gkmaCeNyropIcƐ,W;2@Ǣ'M"12h.:zb(:ls!Ig$6aEdE)S /?xjA7Jʠ.ElmOЖ%g9== 8H4*{h<EWy k$"O QOIjjD乔k&+%|(:FDx9X< V)KJ4d`Wn/opgA#x.X?Z\ iHJcF"pm $[,(+HT1ܴx2! (L)%)%`Lŀ: k2˃X?=MEtesN 4] lFYr5JƦS*!t~z>t{;#!ȱy^sIs >d'`p)PUP*EƎAT* #@ȡ]v\_0^ wG/'GQC;hc(x,D:-xw9K$Ѥeg"w- d]`r.#WұKl~biɏߝ=t# .gx -@P׈7#d} 'o4V fbNinib`?^5S.Δ;.Zrـc}Jb*pg~mIWOLUX:,_nxHD[U14%Em1S]@kqhe:>+! 2kumw$NO@' 3 7"u_ZxS]*r['0l% lᕒJ9Y@Ewo"pxʤiԡc6zx3BⅺCD!^?WP>=0ruTc@s;g ;qU$#e$1pR@윧-W'\Q7a|M`Ep?99UKN䇧 N Q…<1VdtY ֜ V"ة8}^0bɩ՚3a6*YE #˛ x{y ϕVkh77LG~.E$NH$emS!QZd"ĄܻMG"өfH5ʅͺfkz=Zr2f=۹"篵O1_'J^m8?9XqFƙp D 31<u'-%k +_c)ڒi9F)+ϢRy.RkVhN[WpyH19r G 8?0~Đj1zk$}em=upgiL 4ýg5k &0wcxNSdV`BVʪ<fP_5YxLTa5`?ZhDoqOGݜXY5w~hqT9ƟqM/X5;H[~=u `jx솄8;4H!GcNJe2EX/,]TZ~M~Mtklpu-T RtK )P3.Is$jL5g9Āgw @ @=_B#;h!0:̇!OyTarye8&2%5j _Eggv .ʿWl]p$VøaY;2y|Cex>U!rQjk#0T!)V}gR# pvٱ1(eB<DĄ'~!z M}u$B)0E.*"MIfRhׯv0xĮeK3H^磫/Ga="%`Դ z3[홓ok^=_3 t"/'vD>R'(`tȦKh !r256V apS؊aQvڽ@.3b%/|zHPg],I`7j wG7,׽to dC(V'n 2hgyZ7Foq?[TϝLTkPgF%U2Wu3y/:ND\VB01J%gFh#>J9@ `BYLj@=pSpBBU0ls֚D7΁89k}J3VhD`('i T+YHG RB= yV,yBjvAv d]~Qgv</j¾4Sfm;4cQql8x)T==ŷcNJO%?0tpLĤ[%i_H ;nqx%g,fq7SG/64"XP2QVL#HC@ =/!P:U2|pp(t/ $ϽU>DAZ@%c۳ph -mv y Dh\NWy9g(j[ˈAlܑb|L@apXk ڍɶO-iVs :6}<vEq$Ut S5|;+A[gL&>ܢH()99fAJ~P W3g'C)JkR|=ӿl:F\X( -D@ l _%qЍPy3۵2+[Td/"8]f(>)qUӥ^ilJ:8 Pg'U)@]'5v'0< *уr(Xxp054~ BT!( n&TPfMOeNiL-PkM4o@jj$[ZK127(ןc Xj- =e9>3+G4{skZ79ɡ!>|z"YuBaݠ/Djr u|khX#6 ndKm7]y anM/8!.ۨQMC2= BĹ^$#)NOETPU,?(\2P.@472Y?]O 6zUcBX6cV}18s'3F~q9 \ęMtePx ~\D/J'Tozngy(9"\NJ ͙Hβ@2e-MCF3T@K:j@ om<`No!9_ALC)9Ee kro fY۷O8ξrI\U dxHiV 䘵U7ܩ>k-&7WF^+96 T3]uC;v{Wf< Pl$PT=Sc0o )kv|[[i͡:*4_LѲhT*Uʌ @D`.@=з/kU0~8T Dm jCL~ߑ?gQ_.DŽDUC666pbLa{/%ǟ} FSP7X3zlcBN΀4V=6>s=z,k/}A7HFWX#$W:_I2œ@0Js'8 ^>s6Y P5b@(ŒV+Wi;]Vȯ︨oZ ڽx >,g@ZC @.A:A:td L!7ݛèn%BҪ ƓD$?( "s 4@ j}T:o(DnQ%. RRu]''1|O<"V93Y I>"PJFC"*0Ì 4!F &d 9;*+GϺ2gp=v|!X"# HmMtkfQhX}D^@ UNz+eK4ŦRoj!sI30~/E ԇI}4VC_luVdZ3C=Tf)%)'03RrYԮC"5\qZ԰۔Bλ.xQ:X'~,'BYmF̲]. $p8"~zbq]Hrc2), \. xW,12yk6:99 17qHa|0,PuU9kKfN$sK$RPpK!9$ɁU*&HEx )xk MN6jp3TST?#Xz~l\t"6McQL0lg.H9Î.PTXix<գ~w\IRV3¾UI|굓B!"ݹbED8_ݯ3_:R" )3A:!)Wn2JQe::E+KSuBW;ШI%VMMRGdL ~ FB9^4\$x^R@+K]AzS} A T4EZ-,zi+q[T>魰au#qg n)i4=cA!;#!y]sAlRNa{{Dx2~iF ܔX491ҠwY7d{ C0[/~x,\y1 d>}mޅ#<[˱JբxH  NՔi .K LƏU\NLA} 5l5~琊\*Š C,D6Eu(vBf4l tKpPxgڒl qe`8` ?RD^T.9͒HS-Frh)S|:A!fصmP>X\سb"Őդ6%f2fM13(戠U퀍9]T@ً\Ȗ!=N 1O26y=][gsu:Rk00yN$n2)/@ͪnƓB647[|9R"oQгl(j?:-ѾMz?F*]GT3FFvQyYc C b 'O\>dJJp3p`'Y`swYT'sF dvqL^RFn͓ { (U Hg7osR$['u'z70KyQMT}֮Dn('DMַA }cb7,]{S)A *=DQ́Oñ@J}>cZH7j" #IO5xyQu u-GB+O  ,#ǦE gaQEFm^÷QNbfPNPq[`1^s52] :kRjj -RTb%h!QOBk !siDRcM!pUoב8yBt+3e =6.O⧜cW0Y { V9)Vm6Rd@,#|!7]?$Xhur6PW x EEeTw>ɞ tH?QFǀ粳ð-}{1yWΌ x! ^]F(g򸚳sm^$3uH#;v0NET٬ecX<~X.⩪bp5v׆DpN %-|bpo!W> /Z]uj÷n45]mP_ᚑAuws[(Vy?* _qBA_>GWrv"[hrfډb`L7~کb :7 WZYdDe Jle<NZzTcV`9S5U>7l6f3L2'+06j̠e0kףpkP T+PF.}UN]]THbH28Mkf#|P@q#A''9%Osϖ]{Q*G{ nuihv*/oM$刕|[_~N Voѻ n "RXiC=^3HE!G6=#(CRBȑ:E֬u ;r|nÃܴ/Ês09QGCF[?}ojaᙬ\N{OzT:[EKat}<Ss`dպe+@bvcy5j+[uɎ,Y+\i$ӤZ7xAU9x9H)j4- `@Ns\a`v,Y7+jA_~6a1m"ů?z7:BoPDeMz!jزڷE1nE D)N!;_3Vɱ\~],!^m/ɒ? : Yij5c%_9 Z/G,  p!cWMly^ݼ=0p2/gl[˲#9~^4J,}#WkR {F"`&>tv a~Qă4ga_MrG` 麁.p7㣥Y)mb<sC-"_7M{rA7x2{P&."RV!ц=l9Q#t\EjHsAɣuuATpM˽ fKEG{BMa< YX1GvU$-'[OO9'z;7 r|8BQMՐ2\(Y^\` QWfUvWn[2R\)Br] 97k rNlYyV{q26ecO6x s)&lXtbPUm(}JI^#SgjoL 9 IW0\8|ya"2!1p@Ww%H_A?2(FcRj-ל {EG.V iV0$#_'v(d==Pfa9;fkRm@mI);驅=#;chI&-m4D(|րkw$/@AZ@9^/gcmK0"g#Fmg/|I~ p.=aG@Nտ=)(&YPCewh.XE@~Y>Y dDkX_[6qhn\-+}n6 p  Y)hg5SFsSTnS~8_r`0+ Av̇u8)Zm',r6,p>2bxuEn@y~A"4l='፾OyLXɖQAkh5eDXZ[&)TOpH4KlR2[\E [wiv0~? 7r?}C?$o' ߿ -Xe벚E$(ipޕ02XD5,^,&z],ZYL㛏DöJ}5;n1tūbW0AzxONF\ #sEYVtz,#~eqR`^.xAeԤSḴooOO(Hyzce$+교%GM/D{; B==fɤp}beZ= 8fI.S^ 6UDh ̛r#-@7^vCONNsڲqՐ%"|!bx?e BN2̓A'o43x<:_F_*:Bbץ:iC~7w|;pF$O |vC~y2šXU '?S'W(MMMYȚE+1\| xl>ڿSy -Gソs+SpEd͐'[~@vsZ,0(Gyz-RǙy\Vxz>/A0LNf6|r{+ .ۿSޥ弅Sk..cր< FyRvEl'4)Dug3"h32(KZz-GF١Ĝ&RiEuEvx;U섳.nfaDܧ2"na qyJ(\~䯍_w#~rX2kcӯ3gD ZS'ʥw`Y8&dn,2Eop+M&զԊNe{Tm%YhЂaXs^J(6E,^0!D"vl8ֻnŷk |7CE<x)F H:QȈ5/wײȑA?ADȴ&T0CCPWahElǏ`o rX\U#G,dG z^ItMgadغe r%5}d?:droK6ē3z>H"0$ng-TdE0xɨĈ$;ffZ 4QxV @4YVHVvXk=@}obn꽷ˊ^]y/VP L#BX&ҐM𚹋D2d ,[<֎}}55xvml@e{rI7Ϥ)r tH$2GZ!qɫ`S"!Ӣl G!?G [㙦~KS|\'''=|;ܝ}G>O>i~O A3 "mV/ZjMebo;37BG\f 3 Gv)Lɬެg5O9ςp:bAF0r`}"ؔi Q_!uR|*%^Ac`NzoFO)2WQkcRXKqX9. K11x.} `ϳ Xnd{o$I[`-!$ ì5{̂^]_/-0 /vڃi8zr-%2 &&~(2Sm2iLf-AkCϩ9Z[Xeޜ37twY}IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png0100644 0000000 0000000 00000012076 12747325007 030156 0ustar000000000 0000000 PNG  IHDRd/sRGBbKGD pHYs  tIME4&tEXtCommentCreated with GIMPWIDATxKE#w=GM`؀`BKHL1S$,*Lbe2zr_Όku%yޢO[/z_ߓ>|ݷO8C3m<~{|y_s{6ջߍ]w9/ Z!ڍ1 &޽a̿mҽ~|[DMUL(3$hMt4X!i5f%*DnBTҬ?;%v%S?[dcU}~mDgi~ }Ϙ;jEn!kuZ|NI@; C'j=,ւ ^$`ꨒBXZ|B$Hbff,O6 DR_J?$jBW,$(T2V64mϛ2se}ίF3G sgkdw5j j)/1|ԸR[__:GDijׯ" :ߵw`,v`(i|il<+R&Z <'k pyhV;Bmu~"Qc`^P+( :I0٘l~=S9^r( ŕ^;1C(K wn -`Ƒjdm(֩MlmWGxWGn>zFPCїxTh Zaz9MbMS]>Go_@ Yabw,t" N)ѳ2!AD-164NmۃqDǜINz#"LfZD(Q P1Jv:gTfT:LL* dLQH4YyR˪:IO~_!" Gם") 6rT]*h2ON : *F&ܳQN@Qvm=_,W=R[#!k,MS*w|Q uEHCU:"iZ`TU%GP\_Izip\Q018.RES fPv#"}V!5Y Qp`PJIЀ(֛ͪp?p HЀ> CWHr gЄVhΡ9Dur+Tb#aDYuTeu1&!kzᩊnU@].pUd;1Њn3nJյҧMh;)։yH\)s#b8bPgxd! 'J㺒pS Zݔ0_@AOwNA9r3*B9)  +;/yp? Fb|CWm.` C&ؕa"g" GYqNMu ʫsr=gdٮȥEg%1!`8>nYNr1XWJ urVcE:4 sc;D9P`|ݤkL͉oу K1`VsNJgĐk^a/EdU:UJqf"_3zqʭ] IhׄSU44=V&Bh:2\ުM1sWM^98P@!!ЛB49$I[4ՉZCF~ b/yf I33^DUzf:R-'s9Ni@&$<ZdԮt-7+}HV58Q Ћ7q.G1#퉤 VF! {8^%iKP% 2UEtVӖș5Pti,ZY+2qTQӢvPaDY*|?tkTFTFR~JҶ8Fg:RL59qTHj4!Y.zwaVѝI[:!NG 82E)$KQTe9><]^׉tt]C%'$CdZ,LVuKP+z=z/Naʔ"5;2.E}WGu %ǂw7*{L y ȚPa-t* ESWY(FФO9ۃe#{9\]&ndD[='nu}*蛕T^:L(:ZNԴI۷N3TBTq[ Ztء*׋uQ}e7Y75GMDHBݺ;^ 4+_-Āi 7ZyQUwxb*b476r8֝nmeՐܜgzUl9d}%H!^(fkhbk#ּ֥ZPzT@Cw; (&iju؁u\o35АF=R=\ d; %(#*կ[z|cZ*dWAI4LHf2>\}WuVwL<[Ln7Pj~U#zNUk&xrj (>Yn;`xZdVw]w}ө FtUvp4++~jQX*!* ԥ}5[tuݬWVTc& hb0qW]FRybdTx ƋVLx מɊ J3Vº-&qz|t矫5w$P ȵ%+tߡBY;D`]ޣBv;Ue*(֚\CS%zjDKsQɷm+@R^w 9A2uz11Aɘv|*fld쳮$G:[`.r,j!I1Z5?VY ٽGe|S7ѼAK鿆7V ^&@*C3Mһf Ne{R,qf*FKzdh;.NJ~yfnvAO$ENav=?\ԄKPv_:m቞\$We?8](z+v<\D:UBd+(Z87c%j޹"E6JUUŕ >0o*{1qEJ>ʥC0ǘH`PEMil5\g MAg>^ شoCiZN{g6YH4Db)Ug'kMWe\"PKr\"ܭ~&koRB$LAU?BdI"%m`^ҏJNQ]ɡJ 䦡MVC;H@JpRy.xP6bB\9gɬ 9 %ޝf\w)?:r$B@w]妌BMS|#dWu#j)K dQCO.H]땵0;s’$#XSxKV<+ԇM% w@CK=}wǢ*uk5Fݮ=q0`7@\IhSYuEnN OodO7 'A4\I-E|N20;O+.]fkӛ!C;Z1aN cxKh27gt@W9N4C? hBrg?!y*"}B] G!NQBD (]t_;x#-BltRt$! i{;~(|xvDe}1k~t8h=Z7r&M h&xuDD `$17\Uhwb;#k}wG Qx .xxJ?9G#aWz3./1uӛ03/ID|ѯ?__[(GŕSeHa"A4PI24̖4fEޡ#Kf zd#slaV߿r%%"01ff[+1?{Ysyk?r:(aiIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-filtered.png0100644 0000000 0000000 00000021467 12747325007 027054 0ustar000000000 0000000 PNG  IHDRd/sRGBbKGD pHYs  tIME!5BtEXtCommentCreated with GIMPW IDATx]ˮem%-͒  %'<}%6 KN$F ke{e-ڧ#qػvE../)"տ~q<U__Ns|ןPF͛ϟ"3x߷ nHoZFzQ{Y6m\k߾&hp4Qb{Xnx;dXkخ=f곔gTϽkI2m~3z; u>*_|&C8!g=}iS羧dJM<0GQ7gQϖ90!Ʒ~PUaoϟK7YU Ku0 >L#n T8|RDєnՉ$NJc|Xl t"IRmDzIǧq93-GyS]eaXpȇwթ0qmRϔQg}z&'O~ȟ$~j 0_/v嬻rn3HquL:>/k,M~9.8s#I#Lƍ=J_$"B$̿0W=n({WE\W9s㘌!}hǗ/ u ?+ƼB q>ɇgոQ6iB{I;ce{0w;MM&?uz[—=d~Nn !ր)18淟ԟ~_{f@D,|(zzUޫk bkӽNLю2u!g6EQA.z4+#td,gJKGD̍:)Ŏɀ#P6(M¡c0K *v ]~xxv_Yytz`Vn_s`LH#fWc*e`ǿ7G;_OΡwggMa ; 1(+B<эGA\G=#Մz_id!!u!sXIA줕)Q/]*jpى|wȩp> A [X["h!wndbɸi i&PlBjtyV+AZo)WWL7C*߭WAF<6A72 W8MwH q/枴!MVNl~"tzhV Ύ1N!m^(HAci-9GgP,aS0'բ-*93" .<~}!L߇C|6aݜ ǽ |Q U vNqHRO/Iq/s#cc[i'bnNY1fxYP ᄳ&m+9&0Z;ٝ7]vSr[ h n'|e1i-No) G 4kDxGS9S :#UlD䌼MVϓc}±ۦrh?VNkJ阅eNWHwJۭXz$:5Ց\WQ61{%ʜ,PZqr'(vgɝhbYQBd^ jdav6!f/4*yT;Z_!Ϫ41: 8pb%5+lm(5=ITM8 K v+;9PCƩͬEc[dP󊯒  !`6/` 4”+AC_rUfircpS"Ò_fi,.s ΛCG*6d+(/L&e  #ˆsNXp! 1&-&(x"hCw`M g;/Je B0!i͏78h C%Ez˧T E66q'41˪>?Rx\l6;P:B%E2GTe' bjt W'eH ^̚<vS6@4.{o[UR@R =ܦ7hX茹V}0iF $FI$Ux?qɆfSE.AP`hx쿅drHCB: :&Čha5H7nýh8k^"!` !0)+d&2u"S<{MNPP60T*RQl*ЀUVJ 3 F¢`84C6WK(G$e4X H0QQ^щi>&6-`I pq$ˆ0Nawv:|9}*\6D rypqqB8-Dع{.O/GDU2ZUf(s oV?AMنluҜqq=GId)xiɘ[њ߃S?S-yr_ FAx:2 %BNE]U\#h̍aBTTS&vq (#j{4B4XR\%EFTg$L2CCh\u,:|#bO@$:%$P 3k)F4 zGAxSi4 De"}$X=Nr{gT-J!%jfM?\vzn+Ęˉ{n'wO~ALf8tkj&ۉe*ަ7I)m83ֆ*g.VAidxlA9IA%G-V)p!k+rE˙ !+1 GzǠE))&˄`ѱќS!UT& TՓcBU-ED Y2Ҭĥ0!AdkL|WX^O)Ckp9m1`$fɥ%zUQW œ(ToiO) x |.(s c0BW0!H9UA'9ft64͵PcBumظ@9lVcs/#%RbCxS'L5gMq­v-5+vP"\ƙCcs8U0% \C}f{}$$:9!WoпD?huG; 8Eeg"Icě0G/.Aigjj zk"P񙹸bHOJ58EP,{cm 4\T f\ ʩdg.PCk ɄͬKJ*TJW+[QO^6 s o'W8 0PhCÄ[#li?&rsAUoy2q>X!Lіd.JJ'Wo\n*MҜ %N܎K)S;Jh̚dDk\g6f /X`DtZ74euWҫ[̉Kch3I$D "}hy1@_{a!㕚'c7}v%>xƳZ= &SrС2L!1鄳};(s\a;ڝ,r90i`Xh+o,OhX ⒪}jrFE\dJLW(CI(feP8 Ax6kEDŽߎEcx>Vx  /,Mx7D\ut˜{W*+#Ejqj]Bh͖"lNǜ.`4 À=9@y[X?i1{@D|"ڠ ma qgƎ:2%OQB^TtQBlfRe cR#YE6U܏cF;y-Wm9WLD#fz%!h>[Ɩ5 JwYqs8A}0tMձ&v֮q(Xj$Upz Dً.EHT~FEO ;?39ėY{aˍ YɧdL݇%e[PŌD&~ z6ghLQSxlUX5\:*)PXM,4[4&*Ubumx\j9ʹ>$]&zR$ -HYRuNZs랠0S[}E\&}᤭DY#@1s<Эҍ먕E/_rPE*f:Bn8.k,i_)SP2"@[©Jڦ^Z5Q{b\>u2Aku#: !rҊTX)PeW9]|@s6oe}4u[yJZad`mNkpA$){s2W3g"N8[KmkGO{LCGNJe>J\Xyr E0".'G".59ELDƖnv-N"dwebgOW\dS)\pEiXa6!B'>Hn2K%7` v^ ~\a5]_>-I]e9^CArJ0LKtg5DޖVAS$i>IpD1E7H@NSɔ[3f!f zF!l|nlZD7a|5!s eAҚ$*Z6% 0Eq 2VO>*;[a3,UӮzm×b0 B'+#E[%a~捧 e毂ΥX%9 }/fߧN 1fCSE1O*2#yASHTw4#;AV3u`? 1R1xQ.Rk 滌rlSdMBcaC"JhߧJZ30pM5W_"zԍz)Xv+A19ڵk]e,L|J) ;|Mn̹uMRIm_!NSpTYl!$ ,G"Ήt'T=ݩFȃa0ql.xNrD<sLCWbsui/ G=Ǖ74&M ֞*eiݝ<̡aRYߊ޲R'/A-O}|ׅ.S)RVxw7KbP!)˵YSm,8(B65tyL\pr4pC3\.Nh!a,Zrn(rꝘԚ"pӮ5h?\#iad#uwEo:~Ϲe 85kĿz|ͥ@HP^9 . g!e@(ʳg=ww=izPU q-^s|9( 2:ajdy$m}޾]*µqmBB ;sP¯2E*, mܷD" }u.A&P~k+amMd:NUdm}!43Ja oY~ӏbUN$K`vlAWMb!Xfq$خ_jBTm6_ =8X.a%z^IQԱ ^42sUP('bU&b)Vei0קה0Ӿ띆T]PH)Ac!m Ʀ984v.jca,T`AC ;a&|Q o+$5C G3n毻'=B,Κ$|mOZZnaǴ^/2Z͍@+㦥 g؝\,i#:lzTeUXس6{iM2x,MSQ}.Z${vIDAT9FSlM T}Irl"wtE fWt#%hpGrJE*2puI"~B0+kSlҥ|εN𚭾Y5D?ȠN2GPk爈nO?QdD|r{[@D_?Q(^ ݦx c _o[T$JYW `>ĕ߅WsBO:aFd0W xꉠ]':XՕMt-tHW&*GMF$H1vbS+v?S|Y@䬞Njgwੴ# (g_܍|9NxC_2" _Փ'=9nwpxʞr))BWP:qLmoj;uLG*mq )?l9N u_< wnÇhOw­VjRTOSuAUE_Ƽa2R(~1Ty|o~S㷓XZa+I__(̗U___@ O]I_(?>?%*IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected-small.png0100644 0000000 0000000 00000030503 12747325007 026355 0ustar000000000 0000000 PNG  IHDRd/sRGBbKGD pHYs  tIME#"tEXtCommentCreated with GIMPW IDATx}KvqU-(`A$vH\2qw|$?1gd)HI,l3e@{1x{Uu޷)=ϹKUVU}ߡu[;ߨt?}o|A?۾??Qץ,ޗ ~u]_u^տb~;{?_^e3|]\S x)!Oz='X$J UE^wBuuؕ+A\溥 `];|ghI>W?/_k/lÏ~T_Ge#/P6yP{! Wkly<~U \B|qis &mb l|l7~ >gy- -~~TY}ß=i:Wdje^|c_^B~<={-&>6"eUw=.3DLqb=r ΋x8C9yG7}?*V_/UUW *CGն!њVbk vpW k!:@nvHܽ . 7/7t׆O/c:Ȁ:oW:lǘ#;΃}UU?'Y~þwP*8b`m\XEEe}˧E6=IY4l(ȣxoM^.?l,/}sXKׂ_nm8YLb:F?R+n nڙfu6$v$xĺ,rLD5 Zr)m9AXI\tKWǶ:U:YlړL;V|T`)q9bvܧQ’9S#eAg&|܆S\Ef?=񅏿!>AYT,cIg,q;9$F_AۺV?̀^@W??̓ З  D8@jYZ-sର? ͌PqZ2 DV 8bG߫/bq7dPvK,߻,ەKܳMI\#Vn%9ܶ=,c@ß?Zfqmi&bQz,~X-=qk p_syՓg.[!ޟѵu2ow~K1 q,k…GPסjs/1@qxg77]'H):`!n@`ǰ=dȩE O87 fHt=k-\n^6~koڮrb//}C9Be]uóF_jO`ZKؒS$nM*%^~ݏaO.-Ot&y1GhqaQ,Vlʭ@EjKO5>U^zY6VE^}P/ >Y==utHati)ԗNdxBl?`7ge Krm\\V^_!5眔;Ui>7kǻrNyT/92z&bC;;X[k;l^8)Ne^q4>4#$Ǒ} 25kʈϵrU(AKTq59@@lṒZ+2VFT0^ XWXNBPѽ9n%RWEɌ#Ws'՞x4VƽHdJIws #(vYzbۚX YKXqɾ/4Ї8|jyjlS/)FEމ-9r3_K$1at C,/=^m;rp|Pu]/ٯK'r'&& Y^Lna!Q8yY %HqhOAj[|3G#uQQA5 X%ދLqV.9-hp cd2<~'Kw%,ТTPRwaCux Y?xW{'Ħ FعvRG}'[RMD‚ƒrEqgibKh>0YwO„u Wj28+Gr}t'ߩ0 *;(V ь,HAHqͳMd#=2UuS\Z?Sd%+U.7}6e𔑗ѡD)VECֵ#a:sZvyʄK`Sj!0ی2%زfySsk 'p@wW"Kx UL= 9an740@m@HT)G{$䡢 /:( n 9s'e7Rp]`Z?D u xLYHp,R˨)qFnI @k#z2ד}]|1QG$Lr(\Esv?4o@jxHxh^+/ V'-qk(ӓQԎ N}lYj<-`ۉ@]%W2Q =G"-ޕn:lSN(9ic=ܔy IeCZJazU?hT؏z죀edCtJ<xx~ZmO:Zd%Uٗ>GQJ:DzQ, bϓ֓[Szzgmtb`GNR:8sD?.!Xļ Zxj3IrI g`E+ަ+sJ+Co',:D,9"_JL2@p#ZN*b(N{5dQv 'ah{ve#,A=-dHw p4TS4@Q?%'Se5i,ƚ%(S`Z˕PHHnŃ=FkKҸR4DD)fн#CחcV@&i>4H7:4- HwиC (BHݺBټ"H/>e 9Isx"9bpa7Fb<ZVQ -4'XaKf,%7?_2*b|#P^N*ů]QNx'ba%ZҸ 0/D}CkJUFB=)~G"HT gNㇰۡK>i4 v> p00l{p6¿aZ'{pj ;w9T ILAIXXhh@K/_!Tڇ0)b2yBܔ-?I75-TBDTF9@Nl. 9ZÖ8*MkpH-xC_pG"Dv79 60]ʅR2LNuys̅˱K|63QAQ[[#?Ϻ#Zu4#K-wꌼ:{ 3g/IKŵ6Ήe2NH$aR[7 eR"avO%r.Q2X *-x@,٥0n!EYFf`vR>$%M*%w)P3ˠ# % R:3l]*J jbp߿N̛B/GJ`o^XaҒhuʱmGލo.)PeZa[\ǧ\{M.Y5-d5s-]p'4xKZlV6Cg[7VFjăEcdSl.ƁɜtK@V;^g4S7f3́!*Ń'FPER:c7Y՚n[ T.Fwx Bg(ADol@`y|FaTFq>0c<~Ơ2@Yü1rZLmGoث[RԸtS]لwRaɅ1!^Wu]Bk[O^AbihLw$L`Ѱ 4QNX:x61a39G)N v;\jG*aÑ>Lwvdn zf WVLq(O87JOu˕bY5k`Ty*(=MkaU w V*֋ܺo$p,ZK'*!SkқH@" tz?xmI<‡HѪKEq*=0^jִATm?ueUZsZJuB ܚ!}NhyX2(5w"ޓ!كNvu톮wqrnLgRиVe-Ǘ;0Xl`bhזCI4R[y[ xT F &͵c%GMXgLJR40$j!:D.j$=e6SC 1P@![XW z̘TyL$z\";[/AajEf+zxJbj4Jh5* aU )&LOoLpj7N}jH511]^SDW@&{oIУ3뽛ޜ!glQ 뀱UGG]& HQp=zО.*Uow#$&ຕ385Z* =~vrh`ihl"A"]᳴@U-=dM6EN2+U'<QjYpE|dy=|٠~Er!B1o+hbx,<6oe.(qRlwSM@#٢Ò%PSy!e D\GGK2I,GBz:h$C8j jϒ!\YMiQkI5wJdZl"R_a#6_`&\4qJP}/-Q@B7wr $}n#ٰT̟&(=M63IvkրeS^qJݫ}5XҺׄYQ.JR:BJ,C+X4PpߝV+D=V0*H 叁Eǡĕæl=Az:{} U1+UF) tQM5 I3Z 겇tٱڪw?%\"ٟϭg/iDxZH98#l|4}k6rDȌ nrE?ǴmVyn5Cɳ 2_:AQ KzJx@VgI€Ǐ2^ 겝uFj7U#\*bpsJ$&AP&hpʶZ}Vyi.e]j;,.YJuF {}C(˭iB(6DLP"vWIh!|j3(7d챨|/rɄlv_gm:Ou{7IX9^kU4 [^HĀXj.i[vP"TtB he%N{FYќ)o+S0]aPn[grA:]wѻV5. "U߂I0˕?m!FBU&M׀r߈C >R\{{t{[AaqlD^r*xtɎ`vz9"׈R8S6KD[MyKVh< YچxS9IDAT>rcYw!/Ȕ03 o$V"R vGYo%VJ_E5 XOu %po۷% ES[,C+1ynC\‹疥P#jF ~9_ˀLu'ZZw YIҠD:fG!]LÓN@ W T f[몪3Dkܭs ʶGwRDCJ?S` ־iVWu83e)*QDzlEJ$q>VD>=ª*uԂtLMRJs`|Š* jQX'%<|}(bNN`BrАld;ge" e E1٦"^ n]ఫ;yk240#:wg:ecu v2c(?pjkBQK6er#Zuk֝Vq[iv}0g>)-H@D/]TA[ޓϚ ]9sruQ 8F 9BF1pKuPyAup %b0Jd# hr85@H넇#/tI/5Tgch߫ NKSc.sݑ7NeqmzjdL] ző71"rT'?tb\9܉nx9J/9z'x|; Ӌ읠n.8˷^ ]kۺmϻE:@Szx+:vyyu5lK.g :PCstuZ"7£^~oa ZJ%,]!M)s,FDCѥ?@*(.i~#)bA37aJEߤNAzN16jeE }2Ź]ػ!hajpcP&.7,5uÕ黖Wd;d^[k4DU\[;E)/yfF&Ve]|ir j{(Plˆ 7J|;aFLG=";wVJ8TG|C1M5Yz䜴碊^`Yial7 0m6lbBU9j#a('ʠ- -dl}o9CDOݮ\AT .rM]j`}S,N\5cVV,8<}qsR|de~?yO9Hxq3bTkPC O$׽E )ɗ-0em]Xc@Il2]H)>ƈDBG\ӞAPX`e\cBOh+|-kj/T|oV_Vu ˂z}xޭ'ӁP9*(a=BIF V /|AzṢcd&54Il0مry UjSc>fxu~ߟ~+~v70Yh?e=~Ғ9cښv L<#{CҬyVw/^ܫ{L}Y "EFa#69c|~BPxsV{^ 3~{1u[Gկ7Y>?GE[Ŝ?'2IQh*؇`y|n"xIB;q?H I^޸xFk٘~ϿսЈ Ǔ:?*>=5c-p !|IoQ'Xtc~.S2v#x]I cB֔!|ꛋy\}QcJ}+U znw8arBU$B ЖvJHGXT"I&GC/w÷붻C:E|~ݯXpS } 5:DC/ E%B&Yorg7;륎_RίQY۶!8fVOooy񦮹F`b'ٵ[(I7VϿwb~sAO!?x!ΞWv,"D=[%ݣs)uUb^ ȋsmg]BCB[X0./Yp8GLEDK`يv MϾ2/OJm8U M]np<1t+9Eu`TY C'Jvg˔7zjQ1ho~@o.O[؟|7_kA?۸?J*KIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/selected.png0100644 0000000 0000000 00000027577 12747325007 025270 0ustar000000000 0000000 PNG  IHDRd/sRGBbKGD pHYs  tIME;eStEXtCommentCreated with GIMPW IDATx}mYq\%x@H Bc-?/fS%d!!̞ e+=8gU|"C&}kU?YYY?O{?7y#o?M7ߦoӦ?e3'Xu"P8ooFMHA^{ץݯX Jo7K_鵣3myzFpϮ\3 E}gZuY_/ƻn>?l^دly8œj*v7sHqƴknn'xaد=^&z/tu /mϿ`]r ݊׶ӱ;y oRXnLyۋ]u—Â5S:<>#=gdҌ/\Cw'xaUG]/O_y h3'ׅSXT\XN>n[︭f].+DZת\- ܗZ?-y,Klm߭)[8 `q#-[vٶ|z߯7Z5stnצ޳kUK^MCq}y.Xu} {ӝG*>5VOteoio^}W@ϡZf1?b}sw,?0]t0zᝨ^yh.f >;4C 7Q3p=H,Yi5Հ镇@яGN-b e]!!Û0t JBv6Vwkk2Ҟ,k S(}=П9.CYh;͉T0)Sum!O&xY̫8"zq(\^iu0hMđ?`k-i9t G7|x Iu6 QM/ݣmuvUon֕`-t1= T QSM-!RU3Eւܬ2MƯd? pS8Sw,P|8>匾8F%L+5 O?*ZcŦI;HcZ^LYzbr^gxRpYX:xg1,u`w d<B/!״h`B5s]o_[YC 5IzT4;gOT޸~ hjItUhI^V9ߌ[ca"Ň V 78W#23v/1e4bsҝp@-@`]11FtFdB~Cl ^sF+wm!?r*LU@`y^D4s6Z#^ugtaׯ'R0gnCNjCwp }WfUqhDa Im-7tCz&I,f=dk14\`gbOvk)pbMZjqhYܫdC|a]ͨ( ,m,5 c V@j9٭yzsq%߰_7kE{s6#wEpr ̷Z҈= NwPtZFeF:!`КfCGkǵTE6c-G,_ kq!a%l!4v0$\QZx5p@j)bFn1Cp=á#8oJ)͗Ue[-W: [8Ŭf*`IrGrQwNwzeJ [J|_~ a(jH^Jc~bs)|cO9ء'3=\Ktu|B/Y:`Bib8l:Pl2<1b8& N0W@3Sfxyi(m_p@pw[YgZ5d:lmtCϽq}}|BgWI}#%PUɈ1':Vӆ*o/3 7+H҈ }F,ۆc VPk8ξ󊬖.$zmTG}FD)`xkg%'%úʡc-]omG׺a|o(dC<-Ŧ V37(!h 4D9d(X Y5b)6$WU.RaRc`RPA DY2oifxMf= /L&-2 =e%N+RQ:EM%>V fˤC7 {N. ̤0e0PM:<\yu8t8M^N~Ҙó*+aL4$TǬ/+!bɞkTg,xpy%/$eaK4DkfJkUBtʆE&D%@Cr% MtI6~S Px"Sk P;&J. 4nͼ5ل.wP:׫Lb2p?I0Na>_t1>SSo`i19+ UwYuxnъ'LlW49%X:"Q2\Xڴ~>V/g0HTq-=i"& BK~uF U݊=;'Zs3MfǢ_-ן ӧ Z˕ҡٔx$n*E\Pт򷝒viRݬ4Cpu+ZX%2A}+$ X!#۫ru ;9DcQݏ%*ޝPjлB7.eiO, IZU!A O 78>.)˗OFfB]]!LpFu{R穰!P3VyQ#|hMj6RԱ. 2b|zRKˀ(yι)~VoO3 ڗ4>&«jshS,D$>ڕ8#^#Eߓ}TG -;˵FwE\Vm!(x'- Jn,*nwTdR0Okx*' Iq7-[>K M(IC/;ae9(D7drK\r`*.6 UYu/]qȑ^&E_w.YU5WX>Dюh<}g,Ovj u'ksVTcܝ".ՌsG^%xV4.Jկqc 4$yG\[o Qu=++qr+1q?>QBe7T5!)QŢIXr C 8A &P-\%77f}` B$x.dY"5@WBT˺5qL1Sqqum'ڰ(< D3'ޢY6T"t6̜b4V&m)l#{sJ9]e˜L:$GWigI-k_4`1AjwVŕ;5\r}L^KX)M3m@6EJbjyrMHrM:H :M1q3'W.|Z.9x62am:$(ԴpA#_g&B60q앋oo# ^F")P)NehdZ:" w/'sն{<` <僉N7qJQJ=,+N W|qAHy1ˋe@'+elļȏFJ_t&XnMV:rј*?DoHjU6OղP%*SϔǚL괻J>,qDY(3U.X\oq*K[ƈQ C+bqP%,VYՋ(6r[YAZ cP$&>tuK&CZw g|VʵqIŜ(" en/=> u@Hp@Cfyfx\MmL<+AeK€P=OD\\pEzȐQk3O 3x&`~#8=s$CP˹QCVfؐ@XUIW7\Z.+g"Q)X @ 2]RS ZgǁeHf #[Rfj\`FUۧR<)1t;y)ğ{; Z2C@:N\k~^6P M{q5\TJcmN=gxB+gn1fj 5"CI`a`AŘ{܋hwWcu  z|uXqxгԩΖӃ~=0Cgf BaKX1PADpH(ʌF[1Eay؅˫z- ,cx1瞕.JC\+6&:V5MA5-:ךoͮmreƳ*^ʞI\F 'Yż|VovP`)atziL7Apށa⥙<J_!͗hΚ8E$p$ZMƽdҠC6E5WО l:+Ӫ$maI ѬȡN(4㳕oRd%fCK!2 Ψ¨fcUWNlF[f# oQzpX} Ggq<-Y{ /l_QE!ڄZ #aʚޘ ㌾jM[L? )d ?N blUdžˆszmD5 I*FE/b qUIv.$3h֔`0MϭCU$*X FL9ľw*baN $H "ly2er/t+]I|`Z#f}\V|p)5#+g@q{Gg_WiPxéլ.ǀsrzY m 8*d;/'Q N3|M#8@A̳$`րf'E&ɲ%ωgٵq;u0* sZ"'[4MNzG?D9T`"[̼Uд8-{:ѥb௫.9kx I큞eerMj2S G%sO'# &&WjAJi~4 `t@B E/"wJ,烰89Eǘ՚П4S2fk۰7v#v2z3dOZÚ s=_X%KB%>(՜ҎuFJ%#yx6i+VX5 Zɏn HUdy\no`ʂͱPFtǰ>8Uu)֐F?q~OUoxQ j\r[ ڠ@UQDN}C:S℁]vRu$c7FiU٪Ebu]s`ntx5y֒&]lA01B!,vYeCA;OC.c OG㒰qۤeix714IT"3} i5cJkg8žKP,T0(ч}"0_VΎ>Ү~^1aaq Btƃq:]Κ}髲xǡ"._T W2H//,2LЂ좨BiD:ye*h= ̍V <*OC1a<(G8;b,sxb!:R@gwaƹB8-|-) L(wr;oVFqRџ#rƈŔQC ;"!2LOX~8'ժU)FT,Yd`U\&QZAT!zA.sӐy%Sg?'tQ't N1^FZ:f;8K@싆K;.\-"h4&129\9^4~f q63c&eu@#(z, םcXVEINbO4B2s:FNz2hĝ88: '7`ae(^qA.vG6bZEgC4~΁=xjs K1F*տ ` '[N6[b FvV9@l 5딉J 4Øt<A60ްVt O]Zt[NQNĥz҉ٜ@++Eduy>ǼYLq4qOUT5C=H=JЊ%uL3;x[QP- iK޸ԅƚ&_"ex"B} \2P.|_;??/FnRO<[&A-ߓlAF=\Fs}(kmz" ]xu=Gݯy}%iGuk?x dk0tIEӆ0&1# |&:b&-6=~Ͳ9εZk>uږ*c078zlm3}?!(4S^vָ>w?~1hsPŰbn0A=N^V2y`n΂\^zђB=T]?KW+ Zz,|j%]ju\U։ҲZam߬OJ_ao0a){h~&kcI9K}ηNs]MZJxH\!V 1# -M6%}hG(z|ͼZɩkuy+*|6[崕阊Cb!숓pH]ږ6]G)@`~Y&&- sUdКjU!F{+΂J-E-%!J_f8rM8^_-0*l!αE^m"ևX =?ro\?ZU)2ct+h Q|,Lt&ΡVbɵ@.Px53@f?ïHb?9]GP^OeV]J6O/Dvj`|t VqeWyz֨,^&в,[e ҅Z'9A+xdbpU'|҉<E](fֽWNt_۪Z7`) 9(w>|>x ~LݺS8' Gq^yK{?W]X_Q}a l|^3mU_ QxyhQVFUac(#}9KFµ $>hC9 -gOZ6rd4H/mݕ"">!_ߣZ~H?~ofn&~oXF}"i,˷M x~+Z3^̆G{˴ٮASs/|G/m?O{7W? A> Cj<'vƩU#K#c겍[0\~ zmA8]'^xQ/U ſ%=J=T}p[=J[?/p3i mm$I>]NF| 925{nlW$֟/~Ǔ5~i33?y_6u76;ϿԍmϿM?!3"IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/show-extras.png0100644 0000000 0000000 00000000512 12747325007 025740 0ustar000000000 0000000 PNG  IHDR ysRGB pHYs  tIME ŇڔIDAT}AJ@JvBuF:Ko1W.&Dt%j:y\='`WUefMӜǣ+wi~W"cVC7sY.(5lL )ź-ta퓯 ߭ӞLtu໒%cvaO{Y%m29tr, ~=4ԑ}HB󏴯oظAt#"_8p*dA(rkٚ]!;B>hg)D8ҥr~c $8BE% XGjdIbVb4$.sC{ 4^GZEtsHT5jR>Bظj32PP}KLdiq$;jin 97D-"<nM%ʢ0HPE=;r>B8~~'<4Γ5G $gUh4MF,.VbjQA7w?e X->d֭ynNU+{F{o@] 2A͠Y[0KkRͪ詏V`̇wUH@(,t )dua{6ΙO@"9lll,^?qW6Cpx/"ʒJUIENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view-selected.png0100644 0000000 0000000 00000000424 12747325007 027153 0ustar000000000 0000000 PNG  IHDR.sRGBbKGD pHYs  tIME#tEXtCommentCreated with GIMPWoIDAT8c`*T ϜKĄTCG q~~~8ާ,ɆV J,)M)1aElسVNE6,O)\IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/tree-view.png0100644 0000000 0000000 00000000431 12747325007 025363 0ustar000000000 0000000 PNG  IHDR.sRGBbKGD pHYs  tIME^tEXtCommentCreated with GIMPWtIDAT8c`*T &qɝ9{TC1?~g341"PbŠa8 SRRd +6LiN +bÞt4( {$ .K9)IENDB`hierarchyviewer2/hierarchyviewer2lib/src/main/java/images/yellow.png0100644 0000000 0000000 00000000377 12747325007 025000 0ustar000000000 0000000 PNG  IHDR;֕JsRGBbKGD pHYs  tIME$utEXtCommentCreated with GIMPWZIDAT(ϥ \ >H,ӑ\ҕe(?v< i'ы]w4^J(e +(ܝ Ku;U/.qxIENDB`hierarchyviewer2/hierarchyviewer2lib/src/test/0040755 0000000 0000000 00000000000 12747325007 020620 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/java/0040755 0000000 0000000 00000000000 12747325007 021541 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/0040755 0000000 0000000 00000000000 12747325007 022317 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/0040755 0000000 0000000 00000000000 12747325007 023737 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/0040755 0000000 0000000 00000000000 12747325007 027626 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/0040755 0000000 0000000 00000000000 12747325007 031111 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/mo0100644 0000000 0000000 00000000201 12747325007 032651 xustar000000000 0000000 129 path=hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateCon0100755 0000000 0000000 00000025366 12747325007 033256 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.models; import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Display; import junit.framework.TestCase; public class EvaluateContrastModelTest extends TestCase { private static ImageLoader sImageLoader; private static final int ARGB_BLACK = 0xFF000000; private static final int ARGB_WHITE = 0XFFFFFFFF; private static final int ARGB_DARK_GREEN = 0xFF0D6F57; private static final int ARGB_LIGHT_GREEN = 0xFF1DE9B6; @Override protected void setUp() throws Exception { super.setUp(); sImageLoader = ImageLoader.getLoader(EvaluateContrastModelTest.class); } @Override protected void tearDown() throws Exception { super.tearDown(); sImageLoader.dispose(); } private static int argbToRgb(int argb) { return 0xFFFFFF & argb; } public void testFailsAllCases() { Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); Rectangle bounds = allBlack.getBounds(); // Text color is irrelevant in these tests because it will be calculated. // no text size, not bold EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ContrastResult.FAIL, model.getContrastResult()); assertFalse(model.isIndeterminate()); // text size is normal, bold model = new EvaluateContrastModel(allBlack, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.FAIL, model.getContrastResult()); // text size is normal for bold, bold model = new EvaluateContrastModel(allBlack, null, (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.FAIL, model.getContrastResult()); // text size is large, not bold model = new EvaluateContrastModel(allBlack, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ContrastResult.FAIL, model.getContrastResult()); } public void testPassesAllCases() { Image[] images = { sImageLoader.loadImage("black_on_white.png", Display.getDefault()), sImageLoader.loadImage("white_on_black.png", Display.getDefault()), }; int[] textColors = {ARGB_BLACK, ARGB_WHITE}; Image image; Rectangle bounds; // Text color is irrelevant in these tests because it will be calculated. for (int i = 0; i < images.length; ++i) { image = images[i]; bounds = image.getBounds(); // no text size, not bold EvaluateContrastModel model = new EvaluateContrastModel(image, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ContrastResult.PASS, model.getContrastResult()); assertFalse(model.isIndeterminate()); // text size is normal, bold model = new EvaluateContrastModel(image, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.PASS, model.getContrastResult()); // text size is normal for bold, bold model = new EvaluateContrastModel(image, null, (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.PASS, model.getContrastResult()); // text size is large, not bold model = new EvaluateContrastModel(image, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ContrastResult.PASS, model.getContrastResult()); } } public void testIndeterminateFailsNormalAndPassesLarge() { Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); Rectangle bounds = greens.getBounds(); // Not providing a text size is the main cause for an indeterminate result. // Text color is irrelevant because it will be calculated. // no text size, not bold EvaluateContrastModel model = new EvaluateContrastModel(greens, null, null, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.INDETERMINATE, model.getContrastResult()); assertTrue(model.isIndeterminate()); // no text size, bold model = new EvaluateContrastModel(greens, null, null, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.INDETERMINATE, model.getContrastResult()); assertTrue(model.isIndeterminate()); // text size normal, not bold model = new EvaluateContrastModel(greens, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ContrastResult.FAIL, model.getContrastResult()); // text size normal, bold model = new EvaluateContrastModel(greens, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.PASS, model.getContrastResult()); // text size normal for bold, not bold model = new EvaluateContrastModel(greens, null, (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ContrastResult.FAIL, model.getContrastResult()); // text size normal for bold, bold model = new EvaluateContrastModel(greens, null, (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.PASS, model.getContrastResult()); // text size large, not bold model = new EvaluateContrastModel(greens, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.PASS, model.getContrastResult()); // text size large, bold model = new EvaluateContrastModel(greens, null, (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(ContrastResult.PASS, model.getContrastResult()); } public void testGetBackgroundColor() { Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); Rectangle bounds = allBlack.getBounds(); EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(argbToRgb(ARGB_BLACK), model.getBackgroundColor()); Image blackOnWhite = sImageLoader.loadImage("black_on_white.png", Display.getDefault()); bounds = blackOnWhite.getBounds(); model = new EvaluateContrastModel(blackOnWhite, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(argbToRgb(ARGB_WHITE), model.getBackgroundColor()); Image whiteOnBlack = sImageLoader.loadImage("white_on_black.png", Display.getDefault()); bounds = whiteOnBlack.getBounds(); model = new EvaluateContrastModel(whiteOnBlack, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(argbToRgb(ARGB_BLACK), model.getBackgroundColor()); Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); bounds = greens.getBounds(); model = new EvaluateContrastModel(greens, null, null, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(argbToRgb(ARGB_LIGHT_GREEN), model.getBackgroundColor()); } public void testGetTextColor() { Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); Rectangle bounds = allBlack.getBounds(); EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(argbToRgb(ARGB_BLACK), model.getTextColor()); model = new EvaluateContrastModel(allBlack, ARGB_BLACK, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(ARGB_BLACK, model.getTextColor()); Image blackOnWhite = sImageLoader.loadImage("black_on_white.png", Display.getDefault()); bounds = blackOnWhite.getBounds(); model = new EvaluateContrastModel(blackOnWhite, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(argbToRgb(ARGB_BLACK), model.getTextColor()); Image whiteOnBlack = sImageLoader.loadImage("white_on_black.png", Display.getDefault()); bounds = whiteOnBlack.getBounds(); model = new EvaluateContrastModel(whiteOnBlack, null, null, bounds.x, bounds.y, bounds.width, bounds.height, false); assertEquals(argbToRgb(ARGB_WHITE), model.getTextColor()); Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); bounds = greens.getBounds(); model = new EvaluateContrastModel(greens, null, null, bounds.x, bounds.y, bounds.width, bounds.height, true); assertEquals(argbToRgb(ARGB_DARK_GREEN), model.getTextColor()); } } hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/0040755 0000000 0000000 00000000000 12747325007 030243 5ustar000000000 0000000 ./PaxHeaders.X/hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui0100644 0000000 0000000 00000000166 12747325007 032665 xustar000000000 0000000 118 path=hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java hierarchyviewer2/hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerT0100755 0000000 0000000 00000003265 12747325007 033326 0ustar000000000 0000000 /* * Copyright (C) 2014 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.hierarchyviewerlib.ui; import junit.framework.TestCase; public class PropertyViewerTest extends TestCase { public void testExistingCases() { assertEquals("alpha", PropertyViewer.parseColumnTextName("drawing:getAlpha()")); assertEquals("x", PropertyViewer.parseColumnTextName("drawing:getX()")); } public void testEdgeCases() { assertEquals("alpha", PropertyViewer.parseColumnTextName("foo:alpha")); assertEquals("x", PropertyViewer.parseColumnTextName("foo:x")); assertEquals("get", PropertyViewer.parseColumnTextName("foo:get")); assertEquals("", PropertyViewer.parseColumnTextName("foo:()")); assertEquals("", PropertyViewer.parseColumnTextName("foo:")); assertEquals("getter", PropertyViewer.parseColumnTextName("foo:getter")); assertEquals("together", PropertyViewer.parseColumnTextName("foo:together")); assertEquals("together", PropertyViewer.parseColumnTextName("foo:getTogether")); assertEquals("()get", PropertyViewer.parseColumnTextName("foo:()get")); } } hierarchyviewer2/hierarchyviewer2lib/src/test/resources/0040755 0000000 0000000 00000000000 12747325007 022632 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/resources/images/0040755 0000000 0000000 00000000000 12747325007 024077 5ustar000000000 0000000 hierarchyviewer2/hierarchyviewer2lib/src/test/resources/images/all_black.png0100644 0000000 0000000 00000000222 12747325007 026502 0ustar000000000 0000000 PNG  IHDR[4jAsBITOtEXtSoftwaregnome-screenshot>%IDATh1 Omo7@IENDB`hierarchyviewer2/hierarchyviewer2lib/src/test/resources/images/black_on_white.png0100644 0000000 0000000 00000024433 12747325007 027560 0ustar000000000 0000000 PNG  IHDR%ʪnsBITOtEXtSoftwaregnome-screenshot> IDATx}i\w ; ʾ"Fq+q55j!fQ1xո$%; #"T6AAM`fp~L8,j'3]U}Ω5AQ ,oֻ& ,ķ ,"- .H| , 8oɓ'Z!mmm[ B,ߟCb,kB \/{퐙 VVVS!///?z_ggjХKFZZ R&--SN qrrBܹs]SAp޳gwKU!&M0V;wݻaڵkvȼNիW7AnnnttW8Nrrrkמ>}&>>~ŔJ{ァhBcƌi%yϞ=[vH$ڶmK+[kXׂf#.__`G}H{012RڔVsrr BGBzk׮F>--m„ ŔJeRRRQQMII0`6QPPPTTh\n[OޢE]bXt555!DǙ Z,6l-Id}}ӧOjkkmvݻw :G!:.^2=GG7K*++gΜ |4vvv.,,D]~ݘ{ BHpBPBzlEeggoذ!,,mРA+VwF QSSO,((vjժfyE͛7߷C%&&ieEEGzG1|Y/>ydee?czu:WXѻwo??1c|7eee&l _N8o޼={0`>ڴi ~G={,--U3fC߾}LE=ydӦM#F0`SRR x|ggO2|Mw!Juٙ3gvܹcǎf:( ,֭[ǎO~Is=G3P(z Ŏ?wB.]2[^^m6///' ?0??sR͛_VTTo566nذ8Z/$I?/6nXVVF/]ooo;x^k*jڴiP=""˗E:tk5S$IEEE1 K.t:ZxuqqIMM MbxՍfRBQԩS@}?`:-B1zhD"IOO7o<@ícrFܹuVCK5k ^ Ph8O20K [hRW^޽C cWUUm߾ٰ cѬjwdׯ g믠ܛ8q"}wԹs988W EQ$IϚ5QU;xsi4=2عs'9r$8\~̙3AAA fF׆=z9s*$$$0>ԌE3!r̙3AXnݺEFFvPDDT*5|huu5f PN2eԩfGGGӅ1O_ 3СCZXg_ %ӧ1ƍ>}zpp0A ٳg)Q۶m֎9faܾ}{ٳgϞ6m^Ǐ?0G$I={w|°Gݫ73f x ;v,|>۶mf/EQ;w~ ӧOdС/_"kdҥpkӦMt1"hȐ!<f@W_i(R)3@s8pOLL1}>}͘12322LVs4B :L6-***$$7(y N:M4)22sPb9:+fƌؒ /,gϞ'NtuuE-E%BX`8qbmm^/;;YV[NNr8]v#Iχ'NVIfk\Q$o>N*bǻfAdgg[ÇLrzEn {ÇnYYq/^IOyyyhh(BQϟq.DiVKJJj+ %>F]]ĉCb-XV=wPg@QfH/> xpINNC'Zj)"IO?gϞ=JntǏ e˖T*4$y)̤+W{:... V/]DL5&ÇxB644`ڵ+ի}[]\\Y@O=&!Ծ}G*͛7c)JVt@LH_~%7^zǘ'Oh40aqܹsx nܸWWVڵ zխ[7|f7ȑ#J[[[oܸ\\# ELL A<oŊKtRRH.{{{=cf61bakT+$Z^d bXGS4Q4|8 2^KO jZ =F}# n߾I7nD hM&?7P^^s\H|Zl2 8ŋ \~&H$b<2+sI?,E S&''D"st'N}!X_xjҥzK EQ}EC'Op 1f'LP=RWZbʭw"(i0C;m"Ϝ9xdaaaP`[ǎ._8JsN2*񽼼:СCwwwbxѢE$cիWA5-- &/766bTll, w !b޽'^ rJC6k̑yyyd$IPcGR[nAL6kC[,񋋋!@իɓ'(KO[[ .0]p!$$o,&&sVL 5ڵk pppx"nPbkhnėJ`H$S111Ђ ߼<tkFnn.:\]]&7JtRXK,Y¨&mllbN8aH3Hч 3g+/B_ $In߾ݴwww VUUA(` D?}i߼y3N3B644L<Od[AOnFl>occCQc8;#/^l-̘RIŋ!Dgư6mIRX(?4i\.'b6l`|HNNB 2У!DD~>L{ry;v,;;'OJ'epƍJ߯HHHff\.OII1F:ub-H|V}1Ipihh=zֶ6---<<foMM<1i~-nݺZ4h̙c>򯍍 V2B(F _[[Mmxx8 ús999aaa^""88xǎ!))) [3$A-q5pa0߿SSSͫ 447z]+BUUUbkkk;n8FjG`EEE Eiڜ;v\|9...,,]U*555 Y=1q~ gժU!COPNNNll\.G?~߾}o7n܀ OOχ22JKKJ%=}۷o:uL&[t/B]tYv-cg[,Mf,u06lءCt:&ZfؘQUU+W KÀ;;,B!J+WB, ;zM1/^Ka _Z LZUU%\^WW̨j۲z؅H$3bpqޖٳCCC=jbm(jIIIp1c ,0x{65504OmUU&}||/?%'|4/_oߟaÆf=FR%&&ڵŋ2LPT*NgJmm-0V.kL)M6aCV\ػw#G@3JRzÆ [|͛w&2adO -N0,H$तS"~'ϧgmFQQQzz\.EEEСVRsB8~8cR 0%~kW\v믿...&y,P(`%D&pwX,xkbui1=zyzzB_JJJV'o1d2w;J`jbX"oӦM=z(*55ҥKаbŊ۷oձl[[[#ICCHCglb&33+6lv j*6X fcbb1jmF8X,jCE X,lˁ {EEBÆ LH.772$[@BؔNgzWd0nܸTI&]v-??,bΎf[#o7`uܹ>Zfƍ{!I燇ׯcǎ@T*illԫr9ZD3{'n߾-ɖ,Yr@7e<BZT*M֭޽{ SRRbccNbK___ЖL>e(--qxnnUN:e2A|{MM L/c_)ist???}+zu$igg9S=ҷ csN;v5k}Jk@ S*jnm<<}zdd$BH&}Gobbȑpq97$I&$$\p!f͚_oߎmH*0ŋqoܸA$е-& ;vd447oބwttl'Û>={0ޛ1J'ر#̘tS!I^jx{а0pU(6m2'a#VRR.!`p8JB.߼ySVѳgO.a$I۞J,PRRbL $~CCÇkW_ |dtť(g\% D+}1R_YY'$$$$$69~ق˗a<''_e|MMMv2a555qqq2*&&cȐ!#G$"//"ٵYS?88$eee'Odlx+ƴ$TbBCCm>>>pJHHAAAʁ7 +`Ç&??g5Jw62X%m)222'0.RN8؝8`R{PvZ/"JgΜc PlCI\c~/_:tPHۀh4SNwٖ'''8T02lFF}Ty9H/5޽{zeŠoH2$N{ >-]f1fܲܙ݃@_&ׂ91MMMW׹st7ovqqǾ+WB}wޅsK5\l&t:ݮ]` \655cwy(@ŹLb>UBzBAX C.Ϛ5>ƒḒB[l1G:uJ&:r۶mw1=2oll1|2wLv5URC-]\SSyfpׯ=ׅKJJ`Qd˖-ϴyzȐ!8ʗ$I$a튱ۀp4jXÇf̘˖-[cƌw$ɲ󟸿bnoݺeHEQ;.C?33os?xjyyy,1iiiz\.˻y۷r9Ǜ?P5jTPPPzzzEEVz _~Mjjj&.\XQQm۶=\sss/]t]Nb s=zׯ_T*wٷo7W-z׋̙P(y[eIDAT572vxP(޽{/[>ST .LJJ۱ҥ 7o\[[ZSS3sLӤ+--ȑ#|ѣCCCݻwe_~֭{ .Jݍ}I(8qélN?O> p8>/^,--9rdhhRNNNuZҥK [ƿ}ׯߵkjjjMֿX\\\|ƍk׮zZcww={̞=d׮]Gܹ3yիWoݺhmm|rsb7mڴlٲW^=ztE ^~͛ѫW/HVϷEĤIRSS_ZZhѢQF 2յ.55ٳp ٲe XcǎUUUݸq#&&&22ťɓ'W\sN˨2˗/W(۷o;vFy˗(@ taw%&zfpbhAWW$h!. 1IvvvIIIm=>EQcǎnnnka~䪪*% @ !!!aaǧ~FRamܹscFJJ ~YEF>ofrܹs1:W1ux{|Έw+}wqppFZI=>EQ[޵WWW;}J#…  ;X,@`ee`Xcƌz ("I2)) 'p8PmllN>udx.Q*~!~A-9Iw#IglCr9N2J BUăocc(Fi&}ٳ6<<\pa]$g̘ѽ{vډD" T@@9sL̨Ç%V{addxO<9-- hҤI-Ei4cǎ >Xxxx{˖-UUU**=zH$@ t2k֬fRkćnnn ]\\?cTzj'qammmkk۾}{777'NQ 3,P( _xQWW'mcn݂ PtTⲲ:$I0Ir#""?~cuhh(WE@De}}WьZڵk`%۷J.ߺu  $I>ٳg"}G怢ϟb["0Zj5j$fjH-Lb$sl %%%+MR4//ʪ]vEzÔpuumn96|455IҺ:@ H|||@H$FbjKJJJKKkkk5wsBITOtEXtSoftwaregnome-screenshot>IDATxsSϞ{>5 mDv@pbi 6L>ޘLЗL'C:!i2M܁@ےd~n@Z>9ٳWmGBQvTh;*ڎ mGBQvTh;*ڎ mGBQ-vģp~YN ~'_~OYwP~gIWofVr Ngc>I b2:t芝eٴ1`g٢+/Yy O?!H) ;<_|*4a9q}VnGTj _WB"&l0MQABbYEkz~k\F_7 50BJ $Ȁ5weJ=P#rvDVp9\11_cYL%67R+kp~Yz,>7. ?xϢ0_e04|^(5*9o\i1Oh}7kA$J`*AG»_|+=߻rӎ5=-џ nWl>7M=ae#@{y_]A8gN@Qz04Oy bh3Kg0 <jXS9L;>\ W"NJbJ@GBG Ȇ/ QIي)6cqsG#Yw@zVNM({PTj">;怷v \ $Ih2_lT9JXQ`¹%Qmb2L0kߎqpKfv*];l|xX˥Ai9oցDYӢZyT18b1vG؁#boFl+̬OsF8`1p ޹b2f}<bW:&F1NhYK^_o$_Ok|!/j]z5xͭ/}_^1_0_Cgۦk{ej~:o񊬹\kb!,nׯutߚ}fw# %_^Rp{͞UMB5@:9k* l\ "{~YgOl /uk; ^RY[1a2?]®,j}!q/w[Ҝ&A!bkȆB\ja IDATx]w\m.MPtEA bFƟDT1|5F#FlATDcT4P@AvYXr߼uEyeΜs= ```````````` E}lZ @R9f̘KҥKΝ;\b>"X6mǦM63oޭ[7 Njyyy4sVVTEۭ:tKY,p8mi/|||l6}`Ĉ𹱱q ߪDrA##f?yyy&Mb2^^^FFFRܸgϞ={="W0O9ԩ0,--mȑ;(--U +KΞ=kkkakYY_OCC& R0 .kaa`0,--wzjDOaf---9Ntt_TTԳg>"|t0R)sww755)5No.ۻwoJoggץK Auuwĉs܃o &P^*11_%W0y: _^r%\dI߾}͛#G|D:?"~GEF̞={ҤIƁk֬YxM Ø/^hmmx&&&G>|0N A؁zwv[;T$͟S@`P>(d2Yqq1'\aFFƹs9W_}0--o]z59rA/^XXkNn 0Y*h4KKKr͠ QPPP[[h6"޼y{n̒JӧA 1T(۷/a puu0ɓ'vvvd޻woRfƌ*2##رc İaÐS a?\\\Pnݺ7o}>}z^LMM[ZZ/\3nܸ#G544:uŋjfffv3a>l2PHjС7n466h4:u_~adzjΝdQX!C7¢ڵkW\Aaggw9 W^=z|駾9s͛F ɤI|||,--5M}}}nnnbbbee,k'N055Jo޼HLL|m#G@lڵ%%% p驭ɜ9soooollRTTtҥ7oaΜ9pϟ\.wݻ?`0fΜٽ{w.+_xqĉ͛7?}tݺuuuu#99ð5k燆vҥv͚5}$0`@DDEKKKEEEzz (O@Y,֝;w0 J_}ÇuV^=e ò-[#ٵk׮N:M>}РAvvv4͛7oNLL/hɣGvrrhKJJjllcǢu|FꫯjZKHHj90(Rx;_t3*++8m4T%"## [[[߿PSV_rG}J!\j@Bؼy3@ylJ5|BoƷn݂%K揞EzuURI޽{d7!PSS3| 6ֶ}wrB޽j.'O&uJ:F~XTvZGyS~YBAn[ZZIp蘛 ---ӧOדE:|c,--Q7o&Wx111Q*.]BٹŪUȯ[шD;v]3`0V\ ]Cƶn$H~3fBFsݮ]V>>>P|%%%ׄFsqq9s\.'?K~~a>cFFFPuذa,ڷoԹv%VX1f%ޔ 7)))?~|yy9K0 311ihhK+W4T*;w3څ PT]]MՈW666P(*****PRΜ9C`('SbZ*,,TTpǰi74mƌ_R7662 J9pP++ .@7oDުU@(+ fΜ |(..FG'SN-++JWUU!VUU5o#MUUUܧcǎ-..*J 7oޠZ[[tR&Csjjj  x"^8|}}srr7JKK]TVVIG!1 q!X,WH ?@ݻwBT*U[[ہztr;&'֭["*syH .tuuutttqq9RΟ?Ox=СCGFػwuB7[yxxbz>}@h4.]BOrǏ !!!?<ѠAUWWAFr_VT*PrJhM֌,---oܸ#jҥnnnNNN Du9//VVV.^y„ Ϟ="(44&O8E,ļ6Mmrf;v “'O 6l؀D[jjjPP#-[}޽{ѣH!8yd߾}al{{{'$$@Jڿ?My"}0#0̵k"Y7rHggg޽{ܹsr J;6{!CvO>/^ MMM_|񅇇̙3ѤH$} B*=zG/C|C. OP޽{)`xZĉE"FQ*Ȓ#ϝ;ҥK&&& ;w彡_ ch4#G>!!!h!+:RSS!Gg6NNNO>gfftaaa.Jp\]]{+677yrȮgRRv CG躉/͛NNNyyy&/D3BB3g΄gqt:}׮]Љ6/ΝK~_0r98 -::׮] 7}:ZYY V2r?#??Ȑ!<oܸqgΜ!T999Z f)@Pп˗/_~0ZFĽ Qb~۹~zyy%YpˍjuRR[ZZZܹCvyՂ @Q:D"`/]tVT&''STTTh4???JBBBN:y?&W0ac>,x'Oha?! ={F~_s999R޽{wԩl6{ĈG@ϴk׮}0 +(( 3r(PZSi R֭Fh4C{\vv6"\ Թsg__[nXݻbڊq[q^fFjWVV+h4Xhff@Cf۶m K*feei hll2da< _ڎD1xD9NNlmm\UzBR B3NwښX,:rۗ>}mZVlPQ 4mԩ[l59--mӦM#1 +((@6mnnnkkkff6n8??秧'JB\p8l6`^ D"ar555ihV^ `BjΜ9wޥ -9999Q6ǃ⬬,Hlӱ($ ' NJ eϡٳCh~޽[R0FsԠ 033u떵FYYY8It"ԭ[/>mcqƑSSSn?CHHHxx8D̩T͛7N+ ,rzwJKK?SСCWZecc3z۷Ϝ9a׮]g͚5dkkkd2ujt;M6!ӧ+**7n^666qJ444PPfϞܹs m5іCBF***J%ɴ9h"K?R|B022ʉD"I(|D񂃃,,,0 kjjAfSSS$Rud ]ΝߚؘlDW|24ylEj}}j($Rllllllϯ'Ǐ]! ))S[.OJACaɌPzX|~YYYjjnN`P((C3}„ #G橁x0 DRx(Y,Hvd~|y\.722ruunYYYUUυB!ӧdzݧ#謨x[~EӧOߪӽ|Nƽ݇z[ҿ>**!!aժU3FOD"yͰ&3Ԕ,]tݺu);;ƍQQQ4mĉxodde˖˗ zp={6eZ\FC#F BDboofNnVa') EF/"""TOvzfF>wܙ9s%F,ȡCу M}rjP(K} ڢ~,"CJ|gW966%%%)_R}_h'|bO<uyBϟ… Ϟ=ß4DDDRdbiz[!ccc?~biiiccqƘAa0(WDcc#H7D{={̙3'TYYق @Sr;w|A'oSN/_f'B0횛seۘȩNH{.AXo1m*; ږN:.e=fi4TN{ @& ۘ><蒞YKǏb BPG~/^}"͎ 0l̘1ӧO3ɰ8{yyM4RXZZ] -ZfJebbbnnnBBXxzzRN$ߝixiO'O2dDB}B:={DBE#v ϟ?_SSuV^M)n?o޼ &>IOOߴix셇GGGw K.(8//_ Y%F~TL&sΜ9{{sFDDDDD>4Z~=L[kk_~?>y$ӓ/` ww l֬Y[ƐcW^ETрɄ!89B}PZZaX` w+AG={kj;ePu}=|SذaJ^c:d2'N( 5MZ[[_~ 4/&cx;wdMMM$Et:ĉа}w()wSNqLLLo 44O*N޽;ʫ<>'8u$yh4;wL&#oա{}(ë4>Alڴ JBadd$QݺuCiߧӖ<%%:liiyhϳ~z8U*d*K.8ضd2CCC!H>|Tڢ<`999VVV{X/%[ IDAT[VVF L!N466?Bќw&&xzz:~FLMM;W[.>93 .Dug̘g||<>f"I-^!++ 1 qh4> {K,d2So߾Gf2˖-9u,VVVFdR:qbV__}v///{{{kk˗/'''߸q-22t:-4mܹIVݻIIISN c0+,,$WTT ֯_RZZZLLL)>ȐH$wܹ+Wo߾-y<^HHHtt4dlݺIpp0~2CܖܹsAK۷5haϞ={611NLLLHH}Hh]cd~~ :|aaa---L&{޼yQZZIH$ںuMII5jTQQ0 mllL2|p###H,*ggg6ٳ'''G.[YY92226mbnڱcǺu8Ndd!m.]"##GbrÇ ߵkv 1̑n:v ߽{_~)((P(&Mj]V8p $$$$$2999999==],s8[CCƍ*ܹsgaaaqqqǏ,~ƌaaaJN>=qĩSٳg[YY1=z̙3G[t4"""Pj=AJ899 ֜]\\ Fuuux ؟=L!$(߱c>MB|,kرHT---uuuMMM(bMM͔)Sк2ܸq_T7tk֬Azt:=((%&TbX 477#e$,,`>ZP(Dɧd2پ}(lYrGA=H$x) "--  mi4Gb1Caii)aC#>aÆ CO%T*Bhmm-..v)bnnO ã,&&=>rJh/N0믿UG"DGG"ASUUgĈϟ?G^H$>|RL&|^?%IAA~$''y敕IRx_* qjժsBv}>p@]] X|M>H&8wܷek0611Y.Ma:憆o󔗗϶jĉ:#= >3`?ж~1Gaa!uAA4md'$$:;;|֭[gX9 nL6-00ϯK.<O&?{ݻxg|##jf,ѐ{;zZ93gݛJj1**700߀.C%ޗX,vWچFJMMM:8 )+a"hҥ񡡡vvvFFFbΝ;`aEiׯU(uuu999p@hKu!0`9(!bqyyGt{ >|Ȑ!nnn<OP䀿-rרT*O8qڵ0+^(Ԕxd 0Ttorym^|t|`ׯQVye` cʔ)`۶mFFFk֬#MDB] 0$AT*x"w&..edd;_"IENDB`monkeyrunner/0040755 0000000 0000000 00000000000 12747325007 012353 5ustar000000000 0000000 monkeyrunner/MODULE_LICENSE_APACHE20100644 0000000 0000000 00000000000 12747325007 015473 0ustar000000000 0000000 monkeyrunner/NOTICE0100644 0000000 0000000 00000024707 12747325007 013266 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS monkeyrunner/build.gradle0100644 0000000 0000000 00000001627 12747325007 014635 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'monkeyrunner' dependencies { compile project(':base:common') compile project(':base:sdklib') compile project(':base:ddmlib') compile project(':swt:chimpchat') compile project(':swt:hierarchyviewer2lib') // Specific prebuilts and external libs used by mr. compile 'com.google.jsilver:jsilver:1.0.0' compile 'org.python:jython-standalone:2.5.3' testCompile 'junit:junit:3.8.1' } sourceSets { main.resources.srcDir 'src/main/java' test.resources.srcDir 'src/test/java' } sdk { linux { item('etc/monkeyrunner') { executable true } } mac { item('etc/monkeyrunner') { executable true } } windows { item 'etc/monkeyrunner.bat' } } // configure the manifest of the buildDistributionJar task. sdkJar.manifest.attributes("Main-Class": "com.android.monkeyrunner.MonkeyRunnerStarter") monkeyrunner/etc/0040755 0000000 0000000 00000000000 12747325007 013126 5ustar000000000 0000000 monkeyrunner/etc/monkeyrunner0100755 0000000 0000000 00000006150 12747325007 015607 0ustar000000000 0000000 #!/bin/sh # Copyright 2005-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. # Set up prog to be the path of this script, including following symlinks, # and set up progdir to be the fully-qualified pathname of its directory. prog="$0" while [ -h "${prog}" ]; do newProg=`/bin/ls -ld "${prog}"` newProg=`expr "${newProg}" : ".* -> \(.*\)$"` if expr "x${newProg}" : 'x/' >/dev/null; then prog="${newProg}" else progdir=`dirname "${prog}"` prog="${progdir}/${newProg}" fi done oldwd=`pwd` progdir=`dirname "${prog}"` cd "${progdir}" progdir=`pwd` prog="${progdir}"/`basename "${prog}"` cd "${oldwd}" jarfile=monkeyrunner.jar frameworkdir="$progdir" libdir="$progdir" if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/tools/lib libdir=`dirname "$progdir"`/tools/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/framework libdir=`dirname "$progdir"`/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then echo `basename "$prog"`": can't find $jarfile" exit 1 fi # Check args. if [ debug = "$1" ]; then # add this in for debugging java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y shift 1 else java_debug= fi if [ "$OSTYPE" = "cygwin" ] ; then jarpath=`cygpath -w "$frameworkdir/$jarfile"` progdir=`cygpath -w "$progdir"` else jarpath="$frameworkdir/$jarfile" fi # Figure out the path to the swt.jar for the current architecture. # if ANDROID_SWT is defined, then just use this. # else, if running in the Android source tree, then look for the correct swt folder in prebuilt # else, look for the correct swt folder in the SDK under tools/lib/ swtpath="" if [ -n "$ANDROID_SWT" ]; then swtpath="$ANDROID_SWT" else vmarch=`java -jar "${frameworkdir}"/archquery.jar` if [ -n "$ANDROID_BUILD_TOP" ]; then osname=`uname -s | tr A-Z a-z` swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" elif [ -d "$frameworkdir/$vmarch" ]; then swtpath="${frameworkdir}/${vmarch}" else swtpath="${frameworkdir}" fi fi if [ ! -d "$swtpath" ]; then echo "SWT folder '${swtpath}' does not exist." echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." exit 1 fi # need to use "java.ext.dirs" because "-jar" causes classpath to be ignored # might need more memory, e.g. -Xmx128M exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir:$swtpath" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@" monkeyrunner/etc/monkeyrunner.bat0100644 0000000 0000000 00000003634 12747325007 016355 0ustar000000000 0000000 @echo off rem Copyright (C) 2010 The Android Open Source Project rem rem Licensed under the Apache License, Version 2.0 (the "License"); rem you may not use this file except in compliance with the License. rem You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem don't modify the caller's environment setlocal rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 rem Change current directory and drive to where the script is, to avoid rem issues with directories containing whitespaces. cd /d %~dp0 rem Check we have a valid Java.exe in the path. set java_exe= call lib\find_java.bat if not defined java_exe goto :EOF set jarfile=monkeyrunner.jar set frameworkdir=. set libdir= if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=lib if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=..\framework :JarFileOk set jarpath=%frameworkdir%\%jarfile% if not defined ANDROID_SWT goto QueryArch set swt_path=%ANDROID_SWT% goto SwtDone :QueryArch for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a :SwtDone if exist "%swt_path%" goto SetPath echo SWT folder '%swt_path%' does not exist. echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. exit /B :SetPath call "%java_exe%" -Xmx512m "-Djava.ext.dirs=%frameworkdir%;%swt_path%" -Dcom.android.monkeyrunner.bindir=..\framework -jar %jarpath% %* monkeyrunner/jython/0040755 0000000 0000000 00000000000 12747325007 013666 5ustar000000000 0000000 monkeyrunner/jython/test/0040755 0000000 0000000 00000000000 12747325007 014645 5ustar000000000 0000000 monkeyrunner/jython/test/MonkeyRunner_test.py0100644 0000000 0000000 00000003140 12747325007 020705 0ustar000000000 0000000 #!/usr/bin/python2.4 # # Copyright 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. """Test cases for com.android.monkeyrunner.MonkeyRunner.""" import time import unittest from com.android.monkeyrunner import MonkeyRunner class TestMonkeyRunnerArgParsing(unittest.TestCase): """Test ArgParsing for the MonkeyRunner methods.""" def testWaitForConnectionNoArgs(self): MonkeyRunner.waitForConnection() def testWaitForConnectionSingleArg(self): MonkeyRunner.waitForConnection(2) def testWaitForConnectionDoubleArg(self): MonkeyRunner.waitForConnection(2, '*') def testWaitForConnectionKeywordArg(self): MonkeyRunner.waitForConnection(timeout=2, deviceId='foo') def testWaitForConnectionKeywordArgTooMany(self): try: MonkeyRunner.waitForConnection(timeout=2, deviceId='foo', extra='fail') except TypeError: return self.fail('Should have raised TypeError') def testSleep(self): start = time.time() MonkeyRunner.sleep(1.5) end = time.time() self.assertTrue(end - start >= 1.5) if __name__ == '__main__': unittest.main() monkeyrunner/jython/test/all_tests.py0100644 0000000 0000000 00000003144 12747325007 017210 0ustar000000000 0000000 #!/usr/bin/python2.4 # # Copyright 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. """Test runner to run all the tests in this package.""" import os import re import sys import unittest TESTCASE_RE = re.compile('_test\.py$') def AllTestFilesInDir(path): """Finds all the unit test files in the given path.""" return filter(TESTCASE_RE.search, os.listdir(path)) def suite(loader=unittest.defaultTestLoader): """Creates the all_tests TestSuite.""" script_parent_path = os.path.abspath(os.path.dirname(sys.argv[0])) # Find all the _test.py files in the same directory we are in test_files = AllTestFilesInDir(script_parent_path) # Convert them into module names module_names = [os.path.splitext(f)[0] for f in test_files] # And import them modules = map(__import__, module_names) # And create the test suite for all these modules return unittest.TestSuite([loader.loadTestsFromModule(m) for m in modules]) if __name__ == '__main__': result = unittest.TextTestRunner().run(suite()) if not result.wasSuccessful(): # On failure return an error code sys.exit(1) monkeyrunner/scripts/0040755 0000000 0000000 00000000000 12747325007 014042 5ustar000000000 0000000 monkeyrunner/scripts/help.py0100644 0000000 0000000 00000002367 12747325007 015351 0ustar000000000 0000000 #!/usr/bin/env monkeyrunner # Copyright 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. from com.android.monkeyrunner import MonkeyRunner as mr import os import sys supported_formats = ['html', 'text', 'sdk-docs'] if len(sys.argv) != 3: print 'help.py: format output' sys.exit(1) (format, saveto_path) = sys.argv[1:] if not format.lower() in supported_formats: print 'format %s is not a supported format' % format sys.exit(2) output = mr.help(format=format) if not output: print 'Error generating help format' sys.exit(3) dirname = os.path.dirname(saveto_path) try: os.makedirs(dirname) except: print 'oops' pass # It already existed fp = open(saveto_path, 'w') fp.write(output) fp.close() sys.exit(0) monkeyrunner/scripts/monkey_playback.py0100644 0000000 0000000 00000004161 12747325007 017563 0ustar000000000 0000000 #!/usr/bin/env monkeyrunner # Copyright 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. import sys from com.android.monkeyrunner import MonkeyRunner # The format of the file we are parsing is very carfeully constructed. # Each line corresponds to a single command. The line is split into 2 # parts with a | character. Text to the left of the pipe denotes # which command to run. The text to the right of the pipe is a python # dictionary (it can be evaled into existence) that specifies the # arguments for the command. In most cases, this directly maps to the # keyword argument dictionary that could be passed to the underlying # command. # Lookup table to map command strings to functions that implement that # command. CMD_MAP = { 'TOUCH': lambda dev, arg: dev.touch(**arg), 'DRAG': lambda dev, arg: dev.drag(**arg), 'PRESS': lambda dev, arg: dev.press(**arg), 'TYPE': lambda dev, arg: dev.type(**arg), 'WAIT': lambda dev, arg: MonkeyRunner.sleep(**arg) } # Process a single file for the specified device. def process_file(fp, device): for line in fp: (cmd, rest) = line.split('|') try: # Parse the pydict rest = eval(rest) except: print 'unable to parse options' continue if cmd not in CMD_MAP: print 'unknown command: ' + cmd continue CMD_MAP[cmd](device, rest) def main(): file = sys.argv[1] fp = open(file, 'r') device = MonkeyRunner.waitForConnection() process_file(fp, device) fp.close(); if __name__ == '__main__': main() monkeyrunner/scripts/monkey_recorder.py0100644 0000000 0000000 00000001452 12747325007 017602 0ustar000000000 0000000 #!/usr/bin/env monkeyrunner # Copyright 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. from com.android.monkeyrunner import MonkeyRunner as mr from com.android.monkeyrunner.recorder import MonkeyRecorder as recorder device = mr.waitForConnection() recorder.start(device) monkeyrunner/scripts/mr_pydoc.py0100644 0000000 0000000 00000002312 12747325007 016223 0ustar000000000 0000000 #!/usr/bin/env monkeyrunner # Copyright 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. import com.android.monkeyrunner.MonkeyRunnerHelp as mrh import pydoc import sys def create_page(title, document): return """ page.title=%s @jd:body %s """ % (title, document) BASEDIR = 'frameworks/base/docs/html/guide/topics/testing/' def main(): document = "" for clz in mrh.getAllDocumentedClasses(): object, name = pydoc.resolve(str(clz), 0) document += pydoc.html.document(object, name) page = create_page('MonkeyRunner API', document) file = open(BASEDIR + 'monkeyrunner_api.html', 'w') file.write(page) file.close() if __name__ == '__main__': main() monkeyrunner/src/0040755 0000000 0000000 00000000000 12747325007 013142 5ustar000000000 0000000 monkeyrunner/src/main/0040755 0000000 0000000 00000000000 12747325007 014066 5ustar000000000 0000000 monkeyrunner/src/main/java/0040755 0000000 0000000 00000000000 12747325007 015007 5ustar000000000 0000000 monkeyrunner/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 015565 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 017205 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/0040755 0000000 0000000 00000000000 12747325007 021741 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/JythonUtils.java0100644 0000000 0000000 00000044243 12747325007 025104 0ustar000000000 0000000 /* * 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.monkeyrunner; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.text.BreakIterator; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.Py; import org.python.core.PyBoolean; import org.python.core.PyDictionary; import org.python.core.PyFloat; import org.python.core.PyInteger; import org.python.core.PyList; import org.python.core.PyNone; import org.python.core.PyObject; import org.python.core.PyReflectedField; import org.python.core.PyReflectedFunction; import org.python.core.PyString; import org.python.core.PyStringMap; import org.python.core.PyTuple; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.collect.ImmutableMap.Builder; /** * Collection of useful utilities function for interacting with the Jython interpreter. */ public final class JythonUtils { private static final Logger LOG = Logger.getLogger(JythonUtils.class.getCanonicalName()); private JythonUtils() { } /** * Mapping of PyObject classes to the java class we want to convert them to. */ private static final Map, Class> PYOBJECT_TO_JAVA_OBJECT_MAP; static { Builder, Class> builder = ImmutableMap.builder(); builder.put(PyString.class, String.class); // What python calls float, most people call double builder.put(PyFloat.class, Double.class); builder.put(PyInteger.class, Integer.class); builder.put(PyBoolean.class, Boolean.class); PYOBJECT_TO_JAVA_OBJECT_MAP = builder.build(); } /** * Utility method to be called from Jython bindings to give proper handling of keyword and * positional arguments. * * @param args the PyObject arguments from the binding * @param kws the keyword arguments from the binding * @return an ArgParser for this binding, or null on error */ public static ArgParser createArgParser(PyObject[] args, String[] kws) { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); // Up 2 levels in the current stack to give us the calling function StackTraceElement element = stackTrace[2]; String methodName = element.getMethodName(); String className = element.getClassName(); Class clz; try { clz = Class.forName(className); } catch (ClassNotFoundException e) { LOG.log(Level.SEVERE, "Got exception: ", e); return null; } Method m; try { m = clz.getMethod(methodName, PyObject[].class, String[].class); } catch (SecurityException e) { LOG.log(Level.SEVERE, "Got exception: ", e); return null; } catch (NoSuchMethodException e) { LOG.log(Level.SEVERE, "Got exception: ", e); return null; } MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class); return new ArgParser(methodName, args, kws, annotation.args()); } /** * Get a python floating point value from an ArgParser. * * @param ap the ArgParser to get the value from. * @param position the position in the parser * @return the double value */ public static double getFloat(ArgParser ap, int position) { PyObject arg = ap.getPyObject(position); if (Py.isInstance(arg, PyFloat.TYPE)) { return ((PyFloat) arg).asDouble(); } if (Py.isInstance(arg, PyInteger.TYPE)) { return ((PyInteger) arg).asDouble(); } throw Py.TypeError("Unable to parse argument: " + position); } /** * Get a python floating point value from an ArgParser. * * @param ap the ArgParser to get the value from. * @param position the position in the parser * @param defaultValue the default value to return if the arg isn't specified. * @return the double value */ public static double getFloat(ArgParser ap, int position, double defaultValue) { PyObject arg = ap.getPyObject(position, new PyFloat(defaultValue)); if (Py.isInstance(arg, PyFloat.TYPE)) { return ((PyFloat) arg).asDouble(); } if (Py.isInstance(arg, PyInteger.TYPE)) { return ((PyInteger) arg).asDouble(); } throw Py.TypeError("Unable to parse argument: " + position); } /** * Get a list of arguments from an ArgParser. * * @param ap the ArgParser * @param position the position in the parser to get the argument from * @return a list of those items */ @SuppressWarnings("unchecked") public static List getList(ArgParser ap, int position) { PyObject arg = ap.getPyObject(position, Py.None); if (Py.isInstance(arg, PyNone.TYPE)) { return Collections.emptyList(); } List ret = Lists.newArrayList(); PyList array = (PyList) arg; for (int x = 0; x < array.__len__(); x++) { PyObject item = array.__getitem__(x); Class javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(item.getClass()); if (javaClass != null) { ret.add(item.__tojava__(javaClass)); } } return ret; } /** * Get a dictionary from an ArgParser. For ease of use, key types are always coerced to * strings. If key type cannot be coeraced to string, an exception is raised. * * @param ap the ArgParser to work with * @param position the position in the parser to get. * @return a Map mapping the String key to the value */ public static Map getMap(ArgParser ap, int position) { PyObject arg = ap.getPyObject(position, Py.None); if (Py.isInstance(arg, PyNone.TYPE)) { return Collections.emptyMap(); } Map ret = Maps.newHashMap(); // cast is safe as getPyObjectbyType ensures it PyDictionary dict = (PyDictionary) arg; PyList items = dict.items(); for (int x = 0; x < items.__len__(); x++) { // It's a list of tuples PyTuple item = (PyTuple) items.__getitem__(x); // We call str(key) on the key to get the string and then convert it to the java string. String key = (String) item.__getitem__(0).__str__().__tojava__(String.class); PyObject value = item.__getitem__(1); // Look up the conversion type and convert the value Class javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(value.getClass()); if (javaClass != null) { ret.put(key, value.__tojava__(javaClass)); } } return ret; } private static PyObject convertObject(Object o) { if (o instanceof String) { return new PyString((String) o); } else if (o instanceof Double) { return new PyFloat((Double) o); } else if (o instanceof Integer) { return new PyInteger((Integer) o); } else if (o instanceof Float) { float f = (Float) o; return new PyFloat(f); } else if (o instanceof Boolean) { return new PyBoolean((Boolean) o); } return Py.None; } /** * Convert the given Java Map into a PyDictionary. * * @param map the map to convert * @return the python dictionary */ public static PyDictionary convertMapToDict(Map map) { Map resultMap = Maps.newHashMap(); for (Entry entry : map.entrySet()) { resultMap.put(new PyString(entry.getKey()), convertObject(entry.getValue())); } return new PyDictionary(resultMap); } /** * This function should be called from classDictInit for any classes that are being exported * to jython. This jython converts all the MonkeyRunnerExported annotations for the given class * into the proper python form. It also removes any functions listed in the dictionary that * aren't specifically annotated in the java class. * * NOTE: Make sure the calling class implements {@link ClassDictInit} to ensure that * classDictInit gets called. * * @param clz the class to examine. * @param dict the dictionary to update. */ public static void convertDocAnnotationsForClass(Class clz, PyObject dict) { Preconditions.checkNotNull(dict); Preconditions.checkArgument(dict instanceof PyStringMap); // See if the class has the annotation if (clz.isAnnotationPresent(MonkeyRunnerExported.class)) { MonkeyRunnerExported doc = clz.getAnnotation(MonkeyRunnerExported.class); String fullDoc = buildClassDoc(doc, clz); dict.__setitem__("__doc__", new PyString(fullDoc)); } // Get all the keys from the dict and put them into a set. As we visit the annotated methods, // we will remove them from this set. At the end, these are the "hidden" methods that // should be removed from the dict Collection functions = Sets.newHashSet(); for (PyObject item : dict.asIterable()) { functions.add(item.toString()); } // And remove anything that starts with __, as those are pretty important to retain functions = Collections2.filter(functions, new Predicate() { @Override public boolean apply(String value) { return !value.startsWith("__"); } }); // Look at all the methods in the class and find the one's that have the // @MonkeyRunnerExported annotation. for (Method m : clz.getMethods()) { if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { String methodName = m.getName(); PyObject pyFunc = dict.__finditem__(methodName); if (pyFunc != null && pyFunc instanceof PyReflectedFunction) { PyReflectedFunction realPyFunc = (PyReflectedFunction) pyFunc; MonkeyRunnerExported doc = m.getAnnotation(MonkeyRunnerExported.class); realPyFunc.__doc__ = new PyString(buildDoc(doc)); functions.remove(methodName); } } } // Also look at all the fields (both static and instance). for (Field f : clz.getFields()) { if (f.isAnnotationPresent(MonkeyRunnerExported.class)) { String fieldName = f.getName(); PyObject pyField = dict.__finditem__(fieldName); if (pyField != null && pyField instanceof PyReflectedField) { PyReflectedField realPyfield = (PyReflectedField) pyField; MonkeyRunnerExported doc = f.getAnnotation(MonkeyRunnerExported.class); // TODO: figure out how to set field documentation. __doc__ is Read Only // in this context. // realPyfield.__setattr__("__doc__", new PyString(buildDoc(doc))); functions.remove(fieldName); } } } // Now remove any elements left from the functions collection for (String name : functions) { dict.__delitem__(name); } } private static final Predicate SHOULD_BE_DOCUMENTED = new Predicate() { @Override public boolean apply(AccessibleObject ao) { return ao.isAnnotationPresent(MonkeyRunnerExported.class); } }; private static final Predicate IS_FIELD_STATIC = new Predicate() { @Override public boolean apply(Field f) { return (f.getModifiers() & Modifier.STATIC) != 0; } }; /** * build a jython doc-string for a class from the annotation and the fields * contained within the class * * @param doc the annotation * @param clz the class to be documented * @return the doc-string */ private static String buildClassDoc(MonkeyRunnerExported doc, Class clz) { // Below the class doc, we need to document all the documented field this class contains Collection annotatedFields = Collections2.filter(Arrays.asList(clz.getFields()), SHOULD_BE_DOCUMENTED); Collection staticFields = Collections2.filter(annotatedFields, IS_FIELD_STATIC); Collection nonStaticFields = Collections2.filter(annotatedFields, Predicates.not(IS_FIELD_STATIC)); StringBuilder sb = new StringBuilder(); for (String line : splitString(doc.doc(), 80)) { sb.append(line).append("\n"); } if (staticFields.size() > 0) { sb.append("\nClass Fields: \n"); for (Field f : staticFields) { sb.append(buildFieldDoc(f)); } } if (nonStaticFields.size() > 0) { sb.append("\n\nFields: \n"); for (Field f : nonStaticFields) { sb.append(buildFieldDoc(f)); } } return sb.toString(); } /** * Build a doc-string for the annotated field. * * @param f the field. * @return the doc-string. */ private static String buildFieldDoc(Field f) { MonkeyRunnerExported annotation = f.getAnnotation(MonkeyRunnerExported.class); StringBuilder sb = new StringBuilder(); int indentOffset = 2 + 3 + f.getName().length(); String indent = makeIndent(indentOffset); sb.append(" ").append(f.getName()).append(" - "); boolean first = true; for (String line : splitString(annotation.doc(), 80 - indentOffset)) { if (first) { first = false; sb.append(line).append("\n"); } else { sb.append(indent).append(line).append("\n"); } } return sb.toString(); } /** * Build a jython doc-string from the MonkeyRunnerExported annotation. * * @param doc the annotation to build from * @return a jython doc-string */ private static String buildDoc(MonkeyRunnerExported doc) { Collection docs = splitString(doc.doc(), 80); StringBuilder sb = new StringBuilder(); for (String d : docs) { sb.append(d).append("\n"); } if (doc.args() != null && doc.args().length > 0) { String[] args = doc.args(); String[] argDocs = doc.argDocs(); sb.append("\n Args:\n"); for (int x = 0; x < doc.args().length; x++) { sb.append(" ").append(args[x]); if (argDocs != null && argDocs.length > x) { sb.append(" - "); int indentOffset = args[x].length() + 3 + 4; Collection lines = splitString(argDocs[x], 80 - indentOffset); boolean first = true; String indent = makeIndent(indentOffset); for (String line : lines) { if (first) { first = false; sb.append(line).append("\n"); } else { sb.append(indent).append(line).append("\n"); } } } } } return sb.toString(); } private static String makeIndent(int indentOffset) { if (indentOffset == 0) { return ""; } StringBuffer sb = new StringBuffer(); while (indentOffset > 0) { sb.append(' '); indentOffset--; } return sb.toString(); } private static Collection splitString(String source, int offset) { BreakIterator boundary = BreakIterator.getLineInstance(); boundary.setText(source); List lines = Lists.newArrayList(); StringBuilder currentLine = new StringBuilder(); int start = boundary.first(); for (int end = boundary.next(); end != BreakIterator.DONE; start = end, end = boundary.next()) { String b = source.substring(start, end); if (currentLine.length() + b.length() < offset) { currentLine.append(b); } else { // emit the old line lines.add(currentLine.toString()); currentLine = new StringBuilder(b); } } lines.add(currentLine.toString()); return lines; } /** * Obtain the set of method names available from Python. * * @param clazz Class to inspect. * @return set of method names annotated with {@code MonkeyRunnerExported}. */ public static Set getMethodNames(Class clazz) { HashSet methodNames = new HashSet(); for (Method m: clazz.getMethods()) { if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { methodNames.add(m.getName()); } } return methodNames; } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyDevice.java0100644 0000000 0000000 00000046762 12747325007 025202 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.common.collect.Collections2; import com.android.chimpchat.ChimpChat; import com.android.chimpchat.core.By; import com.android.chimpchat.core.IChimpView; import com.android.chimpchat.core.IChimpDevice; import com.android.chimpchat.core.IChimpImage; import com.android.chimpchat.core.TouchPressType; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.Py; import org.python.core.PyDictionary; import org.python.core.PyList; import org.python.core.PyObject; import org.python.core.PyTuple; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.logging.Logger; /* * Abstract base class that represents a single connected Android * Device and provides MonkeyRunner API methods for interacting with * that device. Each backend will need to create a concrete * implementation of this class. */ @MonkeyRunnerExported(doc = "Represents a device attached to the system.") public class MonkeyDevice extends PyObject implements ClassDictInit { private static final Logger LOG = Logger.getLogger(MonkeyDevice.class.getName()); public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyDevice.class, dict); } @MonkeyRunnerExported(doc = "Sends a DOWN event when used with touch() or press().") public static final String DOWN = TouchPressType.DOWN.getIdentifier(); @MonkeyRunnerExported(doc = "Sends an UP event when used with touch() or press().") public static final String UP = TouchPressType.UP.getIdentifier(); @MonkeyRunnerExported(doc = "Sends a DOWN event, immediately followed by an UP event when used with touch() or press()") public static final String DOWN_AND_UP = TouchPressType.DOWN_AND_UP.getIdentifier(); @MonkeyRunnerExported(doc = "Sends a MOVE event when used with touch().") public static final String MOVE = TouchPressType.MOVE.getIdentifier(); private IChimpDevice impl; public MonkeyDevice(IChimpDevice impl) { this.impl = impl; } public IChimpDevice getImpl() { return impl; } @MonkeyRunnerExported(doc = "Get the HierarchyViewer object for the device.", returns = "A HierarchyViewer object") public HierarchyViewer getHierarchyViewer(PyObject[] args, String[] kws) { return impl.getHierarchyViewer(); } @MonkeyRunnerExported(doc = "Gets the device's screen buffer, yielding a screen capture of the entire display.", returns = "A MonkeyImage object (a bitmap wrapper)") public MonkeyImage takeSnapshot() { IChimpImage image = impl.takeSnapshot(); return new MonkeyImage(image); } @MonkeyRunnerExported(doc = "Given the name of a variable on the device, " + "returns the variable's value", args = {"key"}, argDocs = {"The name of the variable. The available names are listed in " + "http://developer.android.com/guide/topics/testing/monkeyrunner.html."}, returns = "The variable's value") public String getProperty(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return impl.getProperty(ap.getString(0)); } @MonkeyRunnerExported(doc = "Synonym for getProperty()", args = {"key"}, argDocs = {"The name of the system variable."}, returns = "The variable's value.") public String getSystemProperty(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return impl.getSystemProperty(ap.getString(0)); } @MonkeyRunnerExported(doc = "Sends a touch event at the specified location", args = { "x", "y", "type" }, argDocs = { "x coordinate in pixels", "y coordinate in pixels", "touch event type as returned by TouchPressType()"}) public void touch(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); int x = ap.getInt(0); int y = ap.getInt(1); TouchPressType type = TouchPressType.fromIdentifier(ap.getString(2)); if (type == null) { LOG.warning(String.format("Invalid TouchPressType specified (%s) default used instead", ap.getString(2))); type = TouchPressType.DOWN_AND_UP; } impl.touch(x, y, type); } @MonkeyRunnerExported(doc = "Simulates dragging (touch, hold, and move) on the device screen.", args = { "start", "end", "duration", "steps"}, argDocs = { "The starting point for the drag (a tuple (x,y) in pixels)", "The end point for the drag (a tuple (x,y) in pixels", "Duration of the drag in seconds (default is 1.0 seconds)", "The number of steps to take when interpolating points. (default is 10)"}) public void drag(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); PyObject startObject = ap.getPyObject(0); if (!(startObject instanceof PyTuple)) { throw Py.TypeError("Agrument 0 is not a tuple"); } PyObject endObject = ap.getPyObject(1); if (!(endObject instanceof PyTuple)) { throw Py.TypeError("Agrument 1 is not a tuple"); } PyTuple start = (PyTuple) startObject; PyTuple end = (PyTuple) endObject; int startx = (Integer) start.__getitem__(0).__tojava__(Integer.class); int starty = (Integer) start.__getitem__(1).__tojava__(Integer.class); int endx = (Integer) end.__getitem__(0).__tojava__(Integer.class); int endy = (Integer) end.__getitem__(1).__tojava__(Integer.class); double seconds = JythonUtils.getFloat(ap, 2, 1.0); long ms = (long) (seconds * 1000.0); int steps = ap.getInt(3, 10); impl.drag(startx, starty, endx, endy, steps, ms); } @MonkeyRunnerExported(doc = "Send a key event to the specified key", args = { "name", "type" }, argDocs = { "the keycode of the key to press (see android.view.KeyEvent)", "touch event type as returned by TouchPressType(). To simulate typing a key, " + "send DOWN_AND_UP"}) public void press(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String name = ap.getString(0); String touchType = ap.getString(1, DOWN_AND_UP); // The old docs had this string, and so in favor of maintaining // backwards compatibility, let's special case it to the new one. if (touchType.equals("DOWN_AND_UP")){ touchType = "downAndUp"; } TouchPressType type = TouchPressType.fromIdentifier(touchType); impl.press(name, type); } @MonkeyRunnerExported(doc = "Types the specified string on the keyboard. This is " + "equivalent to calling press(keycode,DOWN_AND_UP) for each character in the string.", args = { "message" }, argDocs = { "The string to send to the keyboard." }) public void type(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String message = ap.getString(0); impl.type(message); } @MonkeyRunnerExported(doc = "Executes an adb shell command and returns the result, if any.", args = { "cmd", "timeout"}, argDocs = { "The adb shell command to execute.", "This arg is optional. It specifies the maximum amount of time during which the" + "command can go without any output. A value of 0 means the method" + "will wait forever. The unit of the timeout is millisecond"}, returns = "The output from the command.") public String shell(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String cmd = ap.getString(0); if (args.length == 2) { return impl.shell(cmd, ap.getInt(1)); } else { return impl.shell(cmd); } } @MonkeyRunnerExported(doc = "Reboots the specified device into a specified bootloader.", args = { "into" }, argDocs = { "the bootloader to reboot into: bootloader, recovery, or None"}) public void reboot(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String into = ap.getString(0, null); impl.reboot(into); } @MonkeyRunnerExported(doc = "Installs the specified Android package (.apk file) " + "onto the device. If the package already exists on the device, it is replaced.", args = { "path" }, argDocs = { "The package's path and filename on the host filesystem." }, returns = "True if the install succeeded") public boolean installPackage(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String path = ap.getString(0); return impl.installPackage(path); } @MonkeyRunnerExported(doc = "Deletes the specified package from the device, including its " + "associated data and cache.", args = { "package"}, argDocs = { "The name of the package to delete."}, returns = "True if remove succeeded") public boolean removePackage(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String packageName = ap.getString(0); return impl.removePackage(packageName); } @MonkeyRunnerExported(doc = "Starts an Activity on the device by sending an Intent " + "constructed from the specified parameters.", args = { "uri", "action", "data", "mimetype", "categories", "extras", "component", "flags" }, argDocs = { "The URI for the Intent.", "The action for the Intent.", "The data URI for the Intent", "The mime type for the Intent.", "A Python iterable containing the category names for the Intent.", "A dictionary of extras to add to the Intent. Types of these extras " + "are inferred from the python types of the values.", "The component of the Intent.", "An iterable of flags for the Intent." + "All arguments are optional. The default value for each argument is null." + "(see android.content.Intent)"}) public void startActivity(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String uri = ap.getString(0, null); String action = ap.getString(1, null); String data = ap.getString(2, null); String mimetype = ap.getString(3, null); Collection categories = Collections2.transform(JythonUtils.getList(ap, 4), Functions.toStringFunction()); Map extras = JythonUtils.getMap(ap, 5); String component = ap.getString(6, null); int flags = ap.getInt(7, 0); impl.startActivity(uri, action, data, mimetype, categories, extras, component, flags); } @MonkeyRunnerExported(doc = "Sends a broadcast intent to the device.", args = { "uri", "action", "data", "mimetype", "categories", "extras", "component", "flags" }, argDocs = { "The URI for the Intent.", "The action for the Intent.", "The data URI for the Intent", "The mime type for the Intent.", "An iterable of category names for the Intent.", "A dictionary of extras to add to the Intent. Types of these extras " + "are inferred from the python types of the values.", "The component of the Intent.", "An iterable of flags for the Intent." + "All arguments are optional. " + "" + "The default value for each argument is null." + "(see android.content.Context.sendBroadcast(Intent))"}) public void broadcastIntent(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String uri = ap.getString(0, null); String action = ap.getString(1, null); String data = ap.getString(2, null); String mimetype = ap.getString(3, null); Collection categories = Collections2.transform(JythonUtils.getList(ap, 4), Functions.toStringFunction()); Map extras = JythonUtils.getMap(ap, 5); String component = ap.getString(6, null); int flags = ap.getInt(7, 0); impl.broadcastIntent(uri, action, data, mimetype, categories, extras, component, flags); } @MonkeyRunnerExported(doc = "Run the specified package with instrumentation and return " + "the output it generates. Use this to run a test package using " + "InstrumentationTestRunner.", args = { "className", "args" }, argDocs = { "The class to run with instrumentation. The format is " + "packagename/classname. Use packagename to specify the Android package " + "to run, and classname to specify the class to run within that package. " + "For test packages, this is usually " + "testpackagename/InstrumentationTestRunner", "A map of strings to objects containing the arguments to pass to this " + "instrumentation (default value is None)." }, returns = "A map of strings to objects for the output from the package. " + "For a test package, contains a single key-value pair: the key is 'stream' " + "and the value is a string containing the test output.") public PyDictionary instrument(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String packageName = ap.getString(0); Map instrumentArgs = JythonUtils.getMap(ap, 1); if (instrumentArgs == null) { instrumentArgs = Collections.emptyMap(); } Map result = impl.instrument(packageName, instrumentArgs); return JythonUtils.convertMapToDict(result); } @MonkeyRunnerExported(doc = "Wake up the screen on the device") public void wake(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); impl.wake(); } @MonkeyRunnerExported(doc = "Retrieve the properties that can be queried") public PyList getPropertyList(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); Collection properties = impl.getPropertyList(); return new PyList(properties); } @MonkeyRunnerExported(doc = "Retrieve the view ids for the current application") public PyList getViewIdList(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); Collection viewIds = impl.getViewIdList(); return new PyList(viewIds); } //Because the pythonic way is to have flatter hierarchies, rather than doing the //findView(By.id("foo")) style the java code uses, I'm going to expose them as individual //method calls. This is similar to WebDriver's python bindings. @MonkeyRunnerExported(doc = "Obtains the view with the specified id.", args = {"id"}, argDocs = {"The id of the view to retrieve."}, returns = "The view object with the specified id.") public MonkeyView getViewById(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String id = ap.getString(0); IChimpView view = impl.getView(By.id(id)); return new MonkeyView(view); } @MonkeyRunnerExported(doc = "Obtains the view with the specified accessibility ids.", args = {"windowId", "accessibility id"}, argDocs = {"The window id of the view to retrieve.", "The accessibility id of the view to retrieve."}, returns = "The view object with the specified id.") public MonkeyView getViewByAccessibilityIds(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); int windowId = Integer.parseInt(ap.getString(0)); long accessibilityId = Long.parseLong(ap.getString(1)); IChimpView view = impl.getView(By.accessibilityIds(windowId, accessibilityId)); return new MonkeyView(view); } @MonkeyRunnerExported(doc = "Obtains current root view", returns = "The root view object") public MonkeyView getRootView(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new MonkeyView(impl.getRootView()); } @MonkeyRunnerExported(doc = "Obtains a list of views that contain the specified text.", args = {"text"}, argDocs = {"The text to search for"}, returns = "A list of view objects that contain the specified text.") public PyList getViewsByText(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String text = ap.getString(0); Collection views = impl.getViews(By.text(text)); PyList pyViews = new PyList(); for (IChimpView view : views) { pyViews.append(new MonkeyView(view)); } return pyViews; } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyFormatter.java0100644 0000000 0000000 00000006767 12747325007 025747 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.collect.Maps; import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import java.util.logging.Formatter; import java.util.logging.Level; import java.util.logging.LogRecord; /* * Custom Logging Formatter for MonkeyRunner that generates all log * messages on a single line. */ public class MonkeyFormatter extends Formatter { public static final Formatter DEFAULT_INSTANCE = new MonkeyFormatter(); private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyMMdd HH:mm:ss.SSS"); private static Map LEVEL_TO_STRING_CACHE = Maps.newHashMap(); private static final String levelToString(Level level) { String levelName = LEVEL_TO_STRING_CACHE.get(level); if (levelName == null) { levelName = level.getName().substring(0, 1); LEVEL_TO_STRING_CACHE.put(level, levelName); } return levelName; } private static String getHeader(LogRecord record) { StringBuilder sb = new StringBuilder(); sb.append(FORMAT.format(new Date(record.getMillis()))).append(":"); sb.append(levelToString(record.getLevel())).append(" "); sb.append("[").append(Thread.currentThread().getName()).append("] "); String loggerName = record.getLoggerName(); if (loggerName != null) { sb.append("[").append(loggerName).append("]"); } return sb.toString(); } private class PrintWriterWithHeader extends PrintWriter { private final ByteArrayOutputStream out; private final String header; public PrintWriterWithHeader(String header) { this(header, new ByteArrayOutputStream()); } public PrintWriterWithHeader(String header, ByteArrayOutputStream out) { super(out, true); this.header = header; this.out = out; } @Override public void println(Object x) { print(header); super.println(x); } @Override public void println(String x) { print(header); super.println(x); } @Override public String toString() { return out.toString(); } } @Override public String format(LogRecord record) { Throwable thrown = record.getThrown(); String header = getHeader(record); StringBuilder sb = new StringBuilder(); sb.append(header); sb.append(" ").append(formatMessage(record)); sb.append("\n"); // Print the exception here if we caught it if (thrown != null) { PrintWriter pw = new PrintWriterWithHeader(header); thrown.printStackTrace(pw); sb.append(pw.toString()); } return sb.toString(); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyImage.java0100644 0000000 0000000 00000016014 12747325007 025010 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Preconditions; import com.android.chimpchat.core.IChimpImage; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.PyInteger; import org.python.core.PyObject; import org.python.core.PyTuple; import java.util.logging.Logger; /** * Jython object to encapsulate images that have been taken. */ @MonkeyRunnerExported(doc = "An image") public class MonkeyImage extends PyObject implements ClassDictInit { private static Logger LOG = Logger.getLogger(MonkeyImage.class.getCanonicalName()); public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyImage.class, dict); } private final IChimpImage impl; public MonkeyImage(IChimpImage impl) { this.impl = impl; } public IChimpImage getImpl() { return impl; } @MonkeyRunnerExported(doc = "Converts the MonkeyImage into a particular format and returns " + "the result as a String. Use this to get access to the raw" + "pixels in a particular format. String output is for better " + "performance.", args = {"format"}, argDocs = { "The destination format (for example, 'png' for Portable " + "Network Graphics format). The default is png." }, returns = "The resulting image as a String.") public byte[] convertToBytes(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String format = ap.getString(0, "png"); return impl.convertToBytes(format); } @MonkeyRunnerExported(doc = "Write the MonkeyImage to a file. If no " + "format is specified, this method guesses the output format " + "based on the extension of the provided file extension. If it is unable to guess the " + "format, it uses PNG.", args = {"path", "format"}, argDocs = {"The output filename, optionally including its path", "The destination format (for example, 'png' for " + " Portable Network Graphics format." }, returns = "boolean true if writing succeeded.") public boolean writeToFile(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String path = ap.getString(0); String format = ap.getString(1, null); return impl.writeToFile(path, format); } @MonkeyRunnerExported(doc = "Get a single ARGB (alpha, red, green, blue) pixel at location " + "x,y. The arguments x and y are 0-based, expressed in pixel dimensions. X increases " + "to the right, and Y increases towards the bottom. This method returns a tuple.", args = { "x", "y" }, argDocs = { "the x offset of the pixel", "the y offset of the pixel" }, returns = "A tuple of (A, R, G, B) for the pixel. Each item in the tuple has the " + "range 0-255.") public PyObject getRawPixel(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); int x = ap.getInt(0); int y = ap.getInt(1); int pixel = impl.getPixel(x, y); PyInteger a = new PyInteger((pixel & 0xFF000000) >> 24); PyInteger r = new PyInteger((pixel & 0x00FF0000) >> 16); PyInteger g = new PyInteger((pixel & 0x0000FF00) >> 8); PyInteger b = new PyInteger((pixel & 0x000000FF) >> 0); return new PyTuple(a, r, g ,b); } @MonkeyRunnerExported(doc = "Get a single ARGB (alpha, red, green, blue) pixel at location " + "x,y. The arguments x and y are 0-based, expressed in pixel dimensions. X increases " + "to the right, and Y increases towards the bottom. This method returns an Integer.", args = { "x", "y" }, argDocs = { "the x offset of the pixel", "the y offset of the pixel" }, returns = "An unsigned integer pixel for x,y. The 8 high-order bits are A, followed" + "by 8 bits for R, 8 for G, and 8 for B.") public int getRawPixelInt(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); int x = ap.getInt(0); int y = ap.getInt(1); return impl.getPixel(x, y); } @MonkeyRunnerExported(doc = "Compare this MonkeyImage object to aother MonkeyImage object.", args = {"other", "percent"}, argDocs = {"The other MonkeyImage object.", "A float in the range 0.0 to 1.0, indicating the percentage " + "of pixels that need to be the same for the method to return 'true'. " + "Defaults to 1.0."}, returns = "boolean 'true' if the two objects contain the same image.") public boolean sameAs(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); PyObject otherObject = ap.getPyObject(0); IChimpImage other = ((MonkeyImage) otherObject.__tojava__(MonkeyImage.class)).getImpl(); double percent = JythonUtils.getFloat(ap, 1, 1.0); return impl.sameAs(other, percent); } @MonkeyRunnerExported(doc = "Copy a rectangular region of the image.", args = {"rect"}, argDocs = {"A tuple (x, y, w, h) describing the region to copy. x and y specify " + "upper lefthand corner of the region. w is the width of the region in " + "pixels, and h is its height."}, returns = "a MonkeyImage object representing the copied region.") public MonkeyImage getSubImage(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); PyTuple rect = (PyTuple) ap.getPyObjectByType(0, PyTuple.TYPE); int x = rect.__getitem__(0).asInt(); int y = rect.__getitem__(1).asInt(); int w = rect.__getitem__(2).asInt(); int h = rect.__getitem__(3).asInt(); IChimpImage image = impl.getSubImage(x, y, w, h); return new MonkeyImage(image); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyRect.java0100644 0000000 0000000 00000006146 12747325007 024670 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.android.chimpchat.core.ChimpRect; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.PyInteger; import org.python.core.PyList; import org.python.core.PyObject; import java.util.List; import java.util.LinkedList; import java.util.logging.Logger; /* * A Jython wrap for the ChimpRect class that stores coordinate information for views */ @MonkeyRunnerExported(doc = "Represents the coordinates of a rectangular object") public class MonkeyRect extends PyObject implements ClassDictInit { private static final Logger LOG = Logger.getLogger(MonkeyRect.class.getName()); private ChimpRect rect; @MonkeyRunnerExported(doc = "The x coordinate of the left side of the rectangle") public int left; @MonkeyRunnerExported(doc = "The y coordinate of the top side of the rectangle") public int top; @MonkeyRunnerExported(doc = "The x coordinate of the right side of the rectangle") public int right; @MonkeyRunnerExported(doc = "The y coordinate of the bottom side of the rectangle") public int bottom; public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyRect.class, dict); } public MonkeyRect(ChimpRect rect) { this.rect = rect; this.left = rect.left; this.right = rect.right; this.top = rect.top; this.bottom = rect.bottom; } @MonkeyRunnerExported(doc = "Returns the width of the rectangle", returns = "The width of the rectangle as an integer") public PyInteger getWidth() { return new PyInteger(right-left); } @MonkeyRunnerExported(doc = "Returns the height of the rectangle", returns = "The height of the rectangle as an integer") public PyInteger getHeight() { return new PyInteger(bottom-top); } @MonkeyRunnerExported(doc = "Returns a two item list that contains the x and y value of " + "the center of the rectangle", returns = "The center coordinates as a two item list of integers") public PyList getCenter(){ List center = new LinkedList(); /* Center x coordinate */ center.add(new PyInteger(left+(right-left)/2)); /* Center y coordinate */ center.add(new PyInteger(top+(bottom-top)/2)); return new PyList(center); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyRunner.java0100644 0000000 0000000 00000023614 12747325007 025243 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.common.collect.Collections2; import com.android.chimpchat.ChimpChat; import com.android.chimpchat.core.IChimpBackend; import com.android.chimpchat.core.IChimpDevice; import com.android.chimpchat.core.IChimpImage; import com.android.chimpchat.core.ChimpImageBase; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.PyException; import org.python.core.PyObject; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JOptionPane; /** * This is the main interface class into the jython bindings. */ @MonkeyRunnerExported(doc = "Main entry point for MonkeyRunner") public class MonkeyRunner extends PyObject implements ClassDictInit { private static final Logger LOG = Logger.getLogger(MonkeyRunner.class.getCanonicalName()); private static ChimpChat chimpchat; public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyRunner.class, dict); } static void setChimpChat(ChimpChat chimp){ chimpchat = chimp; } @MonkeyRunnerExported(doc = "Waits for the workstation to connect to the device.", args = {"timeout", "deviceId"}, argDocs = {"The timeout in seconds to wait. The default is to wait indefinitely.", "A regular expression that specifies the device name. See the documentation " + "for 'adb' in the Developer Guide to learn more about device names."}, returns = "A ChimpDevice object representing the connected device.") public static MonkeyDevice waitForConnection(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); long timeoutMs; try { double timeoutInSecs = JythonUtils.getFloat(ap, 0); timeoutMs = (long) (timeoutInSecs * 1000.0); } catch (PyException e) { timeoutMs = Long.MAX_VALUE; } IChimpDevice device = chimpchat.waitForConnection(timeoutMs, ap.getString(1, ".*")); MonkeyDevice chimpDevice = new MonkeyDevice(device); return chimpDevice; } @MonkeyRunnerExported(doc = "Pause the currently running program for the specified " + "number of seconds.", args = {"seconds"}, argDocs = {"The number of seconds to pause."}) public static void sleep(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); double seconds = JythonUtils.getFloat(ap, 0); long ms = (long) (seconds * 1000.0); try { Thread.sleep(ms); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Error sleeping", e); } } @MonkeyRunnerExported(doc = "Format and display the API reference for MonkeyRunner.", args = { "format" }, argDocs = {"The desired format for the output, either 'text' for plain text or " + "'html' for HTML markup."}, returns = "A string containing the help text in the desired format.") public static String help(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String format = ap.getString(0, "text"); return MonkeyRunnerHelp.helpString(format); } @MonkeyRunnerExported(doc = "Display an alert dialog to the process running the current " + "script. The dialog is modal, so the script stops until the user dismisses the " + "dialog.", args = { "message", "title", "okTitle" }, argDocs = { "The message to display in the dialog.", "The dialog's title. The default value is 'Alert'.", "The text to use in the dialog button. The default value is 'OK'." }) public static void alert(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String message = ap.getString(0); String title = ap.getString(1, "Alert"); String buttonTitle = ap.getString(2, "OK"); alert(message, title, buttonTitle); } @MonkeyRunnerExported(doc = "Display a dialog that accepts input. The dialog is ," + "modal, so the script stops until the user clicks one of the two dialog buttons. To " + "enter a value, the user enters the value and clicks the 'OK' button. To quit the " + "dialog without entering a value, the user clicks the 'Cancel' button. Use the " + "supplied arguments for this method to customize the text for these buttons.", args = {"message", "initialValue", "title", "okTitle", "cancelTitle"}, argDocs = { "The prompt message to display in the dialog.", "The initial value to supply to the user. The default is an empty string)", "The dialog's title. The default is 'Input'", "The text to use in the dialog's confirmation button. The default is 'OK'." + "The text to use in the dialog's 'cancel' button. The default is 'Cancel'." }, returns = "The test entered by the user, or None if the user canceled the input;" ) public static String input(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String message = ap.getString(0); String initialValue = ap.getString(1, ""); String title = ap.getString(2, "Input"); return input(message, initialValue, title); } @MonkeyRunnerExported(doc = "Display a choice dialog that allows the user to select a single " + "item from a list of items.", args = {"message", "choices", "title"}, argDocs = { "The prompt message to display in the dialog.", "An iterable Python type containing a list of choices to display", "The dialog's title. The default is 'Input'" }, returns = "The 0-based numeric offset of the selected item in the iterable.") public static int choice(PyObject[] args, String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String message = ap.getString(0); Collection choices = Collections2.transform(JythonUtils.getList(ap, 1), Functions.toStringFunction()); String title = ap.getString(2, "Input"); return choice(message, title, choices); } @MonkeyRunnerExported(doc = "Loads a MonkeyImage from a file.", args = { "path" }, argDocs = { "The path to the file to load. This file path is in terms of the computer running " + "MonkeyRunner and not a path on the Android Device. " }, returns = "A new MonkeyImage representing the specified file") public static MonkeyImage loadImageFromFile(PyObject[] args, String kws[]) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String path = ap.getString(0); IChimpImage image = ChimpImageBase.loadImageFromFile(path); return new MonkeyImage(image); } /** * Display an alert dialog. * * @param message the message to show. * @param title the title of the dialog box. * @param okTitle the title of the button. */ public static void alert(String message, String title, String okTitle) { Object[] options = { okTitle }; JOptionPane.showOptionDialog(null, message, title, JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE, null, options, options[0]); } /** * Display a dialog allow the user to pick a choice from a list of choices. * * @param message the message to show. * @param title the title of the dialog box. * @param choices the list of the choices to display. * @return the index of the selected choice, or -1 if nothing was chosen. */ public static int choice(String message, String title, Collection choices) { Object[] possibleValues = choices.toArray(); Object selectedValue = JOptionPane.showInputDialog(null, message, title, JOptionPane.QUESTION_MESSAGE, null, possibleValues, possibleValues[0]); for (int x = 0; x < possibleValues.length; x++) { if (possibleValues[x].equals(selectedValue)) { return x; } } // Error return -1; } /** * Display a dialog that allows the user to input a text string. * * @param message the message to show. * @param initialValue the initial value to display in the dialog * @param title the title of the dialog box. * @return the entered string, or null if cancelled */ public static String input(String message, String initialValue, String title) { return (String) JOptionPane.showInputDialog(null, message, title, JOptionPane.QUESTION_MESSAGE, null, null, initialValue); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyRunnerHelp.java0100644 0000000 0000000 00000024464 12747325007 026060 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.clearsilver.jsilver.JSilver; import com.google.clearsilver.jsilver.data.Data; import com.google.clearsilver.jsilver.resourceloader.ClassLoaderResourceLoader; import com.google.clearsilver.jsilver.resourceloader.ResourceLoader; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Set; /** * Utility class for generating inline help documentation */ public final class MonkeyRunnerHelp { private MonkeyRunnerHelp() { } private static final String HELP = "help"; private static final String NAME = "name"; private static final String DOC = "doc"; private static final String ARGUMENT = "argument"; private static final String RETURNS = "returns"; private static final String TYPE = "type"; // Enum used to describe documented types. private enum Type { ENUM, FIELD, METHOD } private static void getAllExportedClasses(Set fields, Set methods, Set> constructors, Set> enums) { final Set> classesVisited = Sets.newHashSet(); Set> classesToVisit = Sets.newHashSet(); classesToVisit.add(MonkeyRunner.class); Predicate> haventSeen = new Predicate>() { public boolean apply(Class clz) { return !classesVisited.contains(clz); } }; while (!classesToVisit.isEmpty()) { classesVisited.addAll(classesToVisit); List> newClasses = Lists.newArrayList(); for (Class clz : classesToVisit) { // See if the class itself is annotated and is an enum if (clz.isEnum() && clz.isAnnotationPresent(MonkeyRunnerExported.class)) { enums.add(clz); } // Constructors for (Constructor c : clz.getConstructors()) { newClasses.addAll(Collections2.filter(Arrays.asList(c.getParameterTypes()), haventSeen)); if (c.isAnnotationPresent(MonkeyRunnerExported.class)) { constructors.add(c); } } // Fields for (Field f : clz.getFields()) { if (haventSeen.apply(f.getClass())) { newClasses.add(f.getClass()); } if (f.isAnnotationPresent(MonkeyRunnerExported.class)) { fields.add(f); } } // Methods for (Method m : clz.getMethods()) { newClasses.addAll(Collections2.filter(Arrays.asList(m.getParameterTypes()), haventSeen)); if (haventSeen.apply(m.getReturnType())) { newClasses.add(m.getReturnType()); } if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { methods.add(m); } } // Containing classes for (Class toAdd : clz.getClasses()) { if (haventSeen.apply(toAdd)) { newClasses.add(toAdd); } } } classesToVisit.clear(); classesToVisit.addAll(newClasses); } } private static Comparator MEMBER_SORTER = new Comparator() { public int compare(Member o1, Member o2) { return o1.getName().compareTo(o2.getName()); } }; private static Comparator> CLASS_SORTER = new Comparator>() { public int compare(Class o1, Class o2) { return o1.getName().compareTo(o2.getName()); } }; public static String helpString(String format) { ResourceLoader resourceLoader = new ClassLoaderResourceLoader( MonkeyRunner.class.getClassLoader(), "com/android/monkeyrunner"); JSilver jsilver = new JSilver(resourceLoader); // Quick check for support formats if ("html".equals(format) || "text".equals(format) || "sdk-docs".equals(format)) { try { Data hdf = buildHelpHdf(jsilver); return jsilver.render(format + ".cs", hdf); } catch (IOException e) { return ""; } } else if ("hdf".equals(format)) { Data hdf = buildHelpHdf(jsilver); return hdf.toString(); } return ""; } /** * Parse the value string into paragraphs and put them into the * HDF under this specified prefix. Each paragraph will appear * numbered under the prefix. For example: * * paragraphsIntoHDF("a.b.c", ....) * * Will create paragraphs under "a.b.c.0", "a.b.c.1", etc. * * @param prefix The prefix to put the values under. * @param value the value to parse paragraphs from. * @param hdf the HDF to add the entries to. */ private static void paragraphsIntoHDF(String prefix, String value, Data hdf) { String paragraphs[] = value.split("\n"); int x = 0; for (String para : paragraphs) { hdf.setValue(prefix + "." + x, para); x++; } } private static Data buildHelpHdf(JSilver jsilver) { Data hdf = jsilver.createData(); int outputItemCount = 0; Set fields = Sets.newTreeSet(MEMBER_SORTER); Set methods = Sets.newTreeSet(MEMBER_SORTER); Set> constructors = Sets.newTreeSet(MEMBER_SORTER); Set> classes = Sets.newTreeSet(CLASS_SORTER); getAllExportedClasses(fields, methods, constructors, classes); for (Class clz : classes) { String prefix = HELP + "." + outputItemCount + "."; hdf.setValue(prefix + NAME, clz.getCanonicalName()); MonkeyRunnerExported annotation = clz.getAnnotation(MonkeyRunnerExported.class); paragraphsIntoHDF(prefix + DOC, annotation.doc(), hdf); hdf.setValue(prefix + TYPE, Type.ENUM.name()); // Now go through the enumeration constants Object[] constants = clz.getEnumConstants(); String[] argDocs = annotation.argDocs(); if (constants.length > 0) { for (int x = 0; x < constants.length; x++) { String argPrefix = prefix + ARGUMENT + "." + x + "."; hdf.setValue(argPrefix + NAME, constants[x].toString()); if (argDocs.length > x) { paragraphsIntoHDF(argPrefix + DOC, argDocs[x], hdf); } } } outputItemCount++; } for (Method m : methods) { String prefix = HELP + "." + outputItemCount + "."; MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class); String className = m.getDeclaringClass().getCanonicalName(); String methodName = className + "." + m.getName(); hdf.setValue(prefix + NAME, methodName); paragraphsIntoHDF(prefix + DOC, annotation.doc(), hdf); if (annotation.args().length > 0) { String[] argDocs = annotation.argDocs(); String[] aargs = annotation.args(); for (int x = 0; x < aargs.length; x++) { String argPrefix = prefix + ARGUMENT + "." + x + "."; hdf.setValue(argPrefix + NAME, aargs[x]); if (argDocs.length > x) { paragraphsIntoHDF(argPrefix + DOC, argDocs[x], hdf); } } } if (!"".equals(annotation.returns())) { paragraphsIntoHDF(prefix + RETURNS, annotation.returns(), hdf); } outputItemCount++; } return hdf; } public static Collection getAllDocumentedClasses() { Set fields = Sets.newTreeSet(MEMBER_SORTER); Set methods = Sets.newTreeSet(MEMBER_SORTER); Set> constructors = Sets.newTreeSet(MEMBER_SORTER); Set> classes = Sets.newTreeSet(CLASS_SORTER); getAllExportedClasses(fields, methods, constructors, classes); // The classes object only captures classes that are specifically exporter, which isn't // good enough. So go through all the fields, methods, etc. and collect those classes as // as well Set> allClasses = Sets.newHashSet(); allClasses.addAll(classes); for (Field f : fields) { allClasses.add(f.getDeclaringClass()); } for (Method m : methods) { allClasses.add(m.getDeclaringClass()); } for (Constructor constructor : constructors) { allClasses.add(constructor.getDeclaringClass()); } // And transform that collection into a list of simple names. return Collections2.transform(allClasses, new Function, String>() { @Override public String apply(Class clz) { return clz.getName(); } }); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyRunnerOptions.java0100644 0000000 0000000 00000015000 12747325007 026605 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.collect.ImmutableList; import java.io.File; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; public class MonkeyRunnerOptions { private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName()); private static String DEFAULT_MONKEY_SERVER_ADDRESS = "127.0.0.1"; private static int DEFAULT_MONKEY_PORT = 12345; private final int port; private final String hostname; private final File scriptFile; private final String backend; private final Collection plugins; private final Collection arguments; private final Level logLevel; private MonkeyRunnerOptions(String hostname, int port, File scriptFile, String backend, Level logLevel, Collection plugins, Collection arguments) { this.hostname = hostname; this.port = port; this.scriptFile = scriptFile; this.backend = backend; this.logLevel = logLevel; this.plugins = plugins; this.arguments = arguments; } public int getPort() { return port; } public String getHostname() { return hostname; } public File getScriptFile() { return scriptFile; } public String getBackendName() { return backend; } public Collection getPlugins() { return plugins; } public Collection getArguments() { return arguments; } public Level getLogLevel() { return logLevel; } private static void printUsage(String message) { System.out.println(message); System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE"); System.out.println(""); System.out.println(" -s MonkeyServer IP Address."); System.out.println(" -p MonkeyServer TCP Port."); System.out.println(" -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)"); System.out.println(""); System.out.println(""); } /** * Process the command-line options * * @return the parsed options, or null if there was an error. */ public static MonkeyRunnerOptions processOptions(String[] args) { // parse command line parameters. int index = 0; String hostname = DEFAULT_MONKEY_SERVER_ADDRESS; File scriptFile = null; int port = DEFAULT_MONKEY_PORT; String backend = "adb"; Level logLevel = Level.SEVERE; ImmutableList.Builder pluginListBuilder = ImmutableList.builder(); ImmutableList.Builder argumentBuilder = ImmutableList.builder(); while (index < args.length) { String argument = args[index++]; if ("-s".equals(argument)) { if (index == args.length) { printUsage("Missing Server after -s"); return null; } hostname = args[index++]; } else if ("-p".equals(argument)) { // quick check on the next argument. if (index == args.length) { printUsage("Missing Server port after -p"); return null; } port = Integer.parseInt(args[index++]); } else if ("-v".equals(argument)) { // quick check on the next argument. if (index == args.length) { printUsage("Missing Log Level after -v"); return null; } logLevel = Level.parse(args[index++]); } else if ("-be".equals(argument)) { // quick check on the next argument. if (index == args.length) { printUsage("Missing backend name after -be"); return null; } backend = args[index++]; } else if ("-plugin".equals(argument)) { // quick check on the next argument. if (index == args.length) { printUsage("Missing plugin path after -plugin"); return null; } File plugin = new File(args[index++]); if (!plugin.exists()) { printUsage("Plugin file doesn't exist"); return null; } if (!plugin.canRead()) { printUsage("Can't read plugin file"); return null; } pluginListBuilder.add(plugin); } else if ("-u".equals(argument)){ // This is asking for unbuffered input. We can ignore this. } else if (argument.startsWith("-") && // Once we have the scriptfile, the rest of the arguments go to jython. scriptFile == null) { // we have an unrecognized argument. printUsage("Unrecognized argument: " + argument + "."); return null; } else { if (scriptFile == null) { // get the filepath of the script to run. // This will be the last undashed argument. scriptFile = new File(argument); if (!scriptFile.exists()) { printUsage("Can't open specified script file"); return null; } if (!scriptFile.canRead()) { printUsage("Can't open specified script file"); return null; } } else { argumentBuilder.add(argument); } } }; return new MonkeyRunnerOptions(hostname, port, scriptFile, backend, logLevel, pluginListBuilder.build(), argumentBuilder.build()); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyRunnerStarter.java0100644 0000000 0000000 00000016275 12747325007 026615 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableMap; import com.android.chimpchat.ChimpChat; import org.python.util.PythonInterpreter; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.Enumeration; import java.util.Map; import java.util.TreeMap; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; /** * MonkeyRunner is a host side application to control a monkey instance on a * device. MonkeyRunner provides some useful helper functions to control the * device as well as various other methods to help script tests. This class bootstraps * MonkeyRunner. */ public class MonkeyRunnerStarter { private static final Logger LOG = Logger.getLogger(MonkeyRunnerStarter.class.getName()); private static final String MONKEY_RUNNER_MAIN_MANIFEST_NAME = "MonkeyRunnerStartupRunner"; private final ChimpChat chimp; private final MonkeyRunnerOptions options; public MonkeyRunnerStarter(MonkeyRunnerOptions options) { Map chimp_options = new TreeMap(); chimp_options.put("backend", options.getBackendName()); this.options = options; this.chimp = ChimpChat.getInstance(chimp_options); MonkeyRunner.setChimpChat(chimp); } private int run() { // This system property gets set by the included starter script String monkeyRunnerPath = System.getProperty("com.android.monkeyrunner.bindir") + File.separator + "monkeyrunner"; Map> plugins = handlePlugins(); if (options.getScriptFile() == null) { ScriptRunner.console(monkeyRunnerPath); chimp.shutdown(); return 0; } else { int error = ScriptRunner.run(monkeyRunnerPath, options.getScriptFile().getAbsolutePath(), options.getArguments(), plugins); chimp.shutdown(); return error; } } private Predicate handlePlugin(File f) { JarFile jarFile; try { jarFile = new JarFile(f); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to open plugin file. Is it a jar file? " + f.getAbsolutePath(), e); return Predicates.alwaysFalse(); } Manifest manifest; try { manifest = jarFile.getManifest(); } catch (IOException e) { LOG.log(Level.SEVERE, "Unable to get manifest file from jar: " + f.getAbsolutePath(), e); return Predicates.alwaysFalse(); } Attributes mainAttributes = manifest.getMainAttributes(); String pluginClass = mainAttributes.getValue(MONKEY_RUNNER_MAIN_MANIFEST_NAME); if (pluginClass == null) { // No main in this plugin, so it always succeeds. return Predicates.alwaysTrue(); } URL url; try { url = f.toURI().toURL(); } catch (MalformedURLException e) { LOG.log(Level.SEVERE, "Unable to convert file to url " + f.getAbsolutePath(), e); return Predicates.alwaysFalse(); } URLClassLoader classLoader = new URLClassLoader(new URL[] { url }, ClassLoader.getSystemClassLoader()); Class clz; try { clz = Class.forName(pluginClass, true, classLoader); } catch (ClassNotFoundException e) { LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e); return Predicates.alwaysFalse(); } Object loadedObject; try { loadedObject = clz.newInstance(); } catch (InstantiationException e) { LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e); return Predicates.alwaysFalse(); } catch (IllegalAccessException e) { LOG.log(Level.SEVERE, "Unable to load the specified plugin " + "(did you make it public?): " + pluginClass, e); return Predicates.alwaysFalse(); } // Cast it to the right type if (loadedObject instanceof Runnable) { final Runnable run = (Runnable) loadedObject; return new Predicate() { public boolean apply(PythonInterpreter i) { run.run(); return true; } }; } else if (loadedObject instanceof Predicate) { return (Predicate) loadedObject; } else { LOG.severe("Unable to coerce object into correct type: " + pluginClass); return Predicates.alwaysFalse(); } } private Map> handlePlugins() { ImmutableMap.Builder> builder = ImmutableMap.builder(); for (File f : options.getPlugins()) { builder.put(f.getAbsolutePath(), handlePlugin(f)); } return builder.build(); } /* Similar to above, when this fails, it no longer throws a * runtime exception, but merely will log the failure. */ private static final void replaceAllLogFormatters(Formatter form, Level level) { LogManager mgr = LogManager.getLogManager(); Enumeration loggerNames = mgr.getLoggerNames(); while (loggerNames.hasMoreElements()) { String loggerName = loggerNames.nextElement(); Logger logger = mgr.getLogger(loggerName); for (Handler handler : logger.getHandlers()) { handler.setFormatter(form); handler.setLevel(level); } } } public static void main(String[] args) { MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(args); if (options == null) { return; } // logging property files are difficult replaceAllLogFormatters(MonkeyFormatter.DEFAULT_INSTANCE, options.getLogLevel()); MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options); int error = runner.run(); // This will kill any background threads as well. System.exit(error); } } monkeyrunner/src/main/java/com/android/monkeyrunner/MonkeyView.java0100644 0000000 0000000 00000015667 12747325007 024715 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Preconditions; import com.android.chimpchat.core.IChimpView; import com.android.chimpchat.core.IChimpView.AccessibilityIds; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.PyBoolean; import org.python.core.PyInteger; import org.python.core.PyLong; import org.python.core.PyList; import org.python.core.PyObject; import org.python.core.PyString; import java.util.List; import java.util.logging.Logger; /* * Jython wrapper for the ChimpView class */ @MonkeyRunnerExported(doc = "Represents a view object.") public class MonkeyView extends PyObject implements ClassDictInit { private static final Logger LOG = Logger.getLogger(MonkeyView.class.getName()); private IChimpView impl; public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyView.class, dict); } public MonkeyView(IChimpView impl) { this.impl = impl; } @MonkeyRunnerExported(doc = "Get the checked status of the view", returns = "A boolean value for whether the item is checked or not") public PyBoolean getChecked(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new PyBoolean(impl.getChecked()); } @MonkeyRunnerExported(doc = "Returns the class name of the view", returns = "The class name of the view as a string") public PyString getViewClass(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new PyString(impl.getViewClass()); } @MonkeyRunnerExported(doc = "Returns the text contained by the view", returns = "The text contained in the view") public PyString getText(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new PyString(impl.getText()); } @MonkeyRunnerExported(doc = "Returns the location of the view in the form of a MonkeyRect", returns = "The location of the view as a MonkeyRect object") public MonkeyRect getLocation(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new MonkeyRect(impl.getLocation()); } @MonkeyRunnerExported(doc = "Returns the enabled status of the view", returns = "The enabled status of the view as a boolean") public PyBoolean getEnabled(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new PyBoolean(impl.getEnabled()); } @MonkeyRunnerExported(doc = "Returns the selected status of the view", returns = "The selected status of the view as a boolean") public PyBoolean getSelected(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new PyBoolean(impl.getSelected()); } @MonkeyRunnerExported(doc = "Sets the selected status of the view", args = {"selected"}, argDocs = { "The boolean value to set selected to" }) public void setSelected(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); PyBoolean pySelected = (PyBoolean) ap.getPyObject(0, new PyBoolean(false)); boolean selected = (Boolean) pySelected.__tojava__(Boolean.class); impl.setSelected(selected); } @MonkeyRunnerExported(doc = "Returns the focused status of the view", returns = "The focused status of the view as a boolean") public PyBoolean getFocused(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); return new PyBoolean(impl.getFocused()); } @MonkeyRunnerExported(doc = "Sets the focused status of the view", args = {"focused"}, argDocs = { "The boolean value to set focused to" }) public void setFocused(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); PyBoolean pyFocused = (PyBoolean) ap.getPyObject(0, new PyBoolean(false)); boolean focused = (Boolean) pyFocused.__tojava__(Boolean.class); impl.setFocused(focused); } @MonkeyRunnerExported(doc = "Returns the parent of the current view", returns = "The parent of the view as a MonkeyView object") public MonkeyView getParent(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); MonkeyView parent = new MonkeyView(impl.getParent()); return parent; } @MonkeyRunnerExported(doc = "Returns the children of the current view", returns = "The children of the view as a list of MonkeyView objects") public PyList getChildren(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); List chimpChildren = impl.getChildren(); PyList children = new PyList(); for (IChimpView child : chimpChildren) { children.append(new MonkeyView(child)); } return children; } @MonkeyRunnerExported(doc = "Returns the accessibility ids of the current view", returns = "The accessibility ids of the view as a list of int and long") public PyList getAccessibilityIds(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); AccessibilityIds ids = impl.getAccessibilityIds(); PyList pyIds = new PyList(); pyIds.append( new PyInteger(ids.getWindowId()) ); pyIds.append( new PyLong(ids.getNodeId()) ); return pyIds; } } monkeyrunner/src/main/java/com/android/monkeyrunner/ScriptRunner.java0100644 0000000 0000000 00000015466 12747325007 025253 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.Lists; import org.python.core.Py; import org.python.core.PyException; import org.python.core.PyJavaPackage; import org.python.core.PyList; import org.python.core.PyObject; import org.python.core.PyString; import org.python.core.PySystemState; import org.python.util.InteractiveConsole; import org.python.util.JLineConsole; import org.python.util.PythonInterpreter; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; /** * Runs Jython based scripts. */ public class ScriptRunner { private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName()); /** The "this" scope object for scripts. */ private final Object scope; private final String variable; /** Private constructor. */ private ScriptRunner(Object scope, String variable) { this.scope = scope; this.variable = variable; } /** Creates a new instance for the given scope object. */ public static ScriptRunner newInstance(Object scope, String variable) { return new ScriptRunner(scope, variable); } /** * Runs the specified Jython script. First runs the initialization script to * preload the appropriate client library version. * * @param scriptfilename the name of the file to run. * @param args the arguments passed in (excluding the filename). * @param plugins a list of plugins to load. * @return the error code from running the script. */ public static int run(String executablePath, String scriptfilename, Collection args, Map> plugins) { // Add the current directory of the script to the python.path search path. File f = new File(scriptfilename); // Adjust the classpath so jython can access the classes in the specified classpath. Collection classpath = Lists.newArrayList(f.getParent()); classpath.addAll(plugins.keySet()); String[] argv = new String[args.size() + 1]; argv[0] = f.getAbsolutePath(); int x = 1; for (String arg : args) { argv[x++] = arg; } initPython(executablePath, classpath, argv); PythonInterpreter python = new PythonInterpreter(); // Now let the mains run. for (Map.Entry> entry : plugins.entrySet()) { boolean success; try { success = entry.getValue().apply(python); } catch (Exception e) { LOG.log(Level.SEVERE, "Plugin Main through an exception.", e); continue; } if (!success) { LOG.severe("Plugin Main returned error for: " + entry.getKey()); } } // Bind __name__ to __main__ so mains will run python.set("__name__", "__main__"); // Also find __file__ python.set("__file__", scriptfilename); try { python.execfile(scriptfilename); } catch (PyException e) { if (Py.SystemExit.equals(e.type)) { // Then recover the error code so we can pass it on return (Integer) e.value.__tojava__(Integer.class); } // Then some other kind of exception was thrown. Log it and return error; LOG.log(Level.SEVERE, "Script terminated due to an exception", e); return 1; } return 0; } public static void runString(String executablePath, String script) { initPython(executablePath); PythonInterpreter python = new PythonInterpreter(); python.exec(script); } public static Map runStringAndGet(String executablePath, String script, String... names) { return runStringAndGet(executablePath, script, Arrays.asList(names)); } public static Map runStringAndGet(String executablePath, String script, Collection names) { initPython(executablePath); final PythonInterpreter python = new PythonInterpreter(); python.exec(script); Builder builder = ImmutableMap.builder(); for (String name : names) { builder.put(name, python.get(name)); } return builder.build(); } private static void initPython(String executablePath) { List arg = Collections.emptyList(); initPython(executablePath, arg, new String[] {""}); } private static void initPython(String executablePath, Collection pythonPath, String[] argv) { Properties props = new Properties(); // Build up the python.path StringBuilder sb = new StringBuilder(); sb.append(System.getProperty("java.class.path")); for (String p : pythonPath) { sb.append(":").append(p); } props.setProperty("python.path", sb.toString()); /** Initialize the python interpreter. */ // Default is 'message' which displays sys-package-mgr bloat // Choose one of error,warning,message,comment,debug props.setProperty("python.verbose", "error"); // This needs to be set for sys.executable to function properly props.setProperty("python.executable", executablePath); PythonInterpreter.initialize(System.getProperties(), props, argv); String frameworkDir = System.getProperty("java.ext.dirs"); File monkeyRunnerJar = new File(frameworkDir, "monkeyrunner.jar"); if (monkeyRunnerJar.canRead()) { PySystemState.packageManager.addJar(monkeyRunnerJar.getAbsolutePath(), false); } } /** * Start an interactive python interpreter. */ public static void console(String executablePath) { initPython(executablePath); InteractiveConsole python = new JLineConsole(); python.interact(); } } monkeyrunner/src/main/java/com/android/monkeyrunner/controller/0040755 0000000 0000000 00000000000 12747325007 024124 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/controller/MonkeyController.java0100644 0000000 0000000 00000004357 12747325007 030303 0ustar000000000 0000000 /* * 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.monkeyrunner.controller; import com.android.chimpchat.ChimpChat; import com.android.chimpchat.core.IChimpDevice; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.logging.Logger; import java.util.Map; import java.util.TreeMap; import javax.swing.JFrame; import javax.swing.SwingUtilities; /** * Application that can control an attached device using the network monkey. It has a window * that shows what the current screen looks like and allows the user to click in it. Clicking in * the window sends touch events to the attached device. It also supports keyboard input for * typing and has buttons to press to simulate physical buttons on the device. */ public class MonkeyController extends JFrame { private static final Logger LOG = Logger.getLogger(MonkeyController.class.getName()); public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Map options = new TreeMap(); options.put("backend", "adb"); ChimpChat chimpchat = ChimpChat.getInstance(options); final IChimpDevice device = chimpchat.waitForConnection(); MonkeyControllerFrame mf = new MonkeyControllerFrame(device); mf.setVisible(true); mf.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { device.dispose(); } }); } }); } } monkeyrunner/src/main/java/com/android/monkeyrunner/controller/MonkeyControllerFrame.java0100644 0000000 0000000 00000012421 12747325007 031245 0ustar000000000 0000000 /* * 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.monkeyrunner.controller; import com.android.chimpchat.core.PhysicalButton; import com.android.chimpchat.core.TouchPressType; import com.android.chimpchat.core.IChimpImage; import com.android.chimpchat.core.IChimpDevice; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.Timer; /** * Main window for MonkeyController. */ public class MonkeyControllerFrame extends JFrame { private static final Logger LOG = Logger.getLogger(MonkeyControllerFrame.class.getName()); private final JButton refreshButton = new JButton("Refresh"); private final JButton variablesButton = new JButton("Variable"); private final JLabel imageLabel = new JLabel(); private final VariableFrame variableFrame; private final IChimpDevice device; private BufferedImage currentImage; private final TouchPressType DOWN_AND_UP = TouchPressType.DOWN_AND_UP; private final Timer timer = new Timer(1000, new ActionListener() { public void actionPerformed(ActionEvent e) { updateScreen(); } }); private class PressAction extends AbstractAction { private final PhysicalButton button; public PressAction(PhysicalButton button) { this.button = button; } /* When this fails, it no longer throws a runtime exception, * but merely will log the failure. */ public void actionPerformed(ActionEvent event) { device.press(button.getKeyName(), DOWN_AND_UP); updateScreen(); } } private JButton createToolbarButton(PhysicalButton hardButton) { JButton button = new JButton(new PressAction(hardButton)); button.setText(hardButton.getKeyName()); return button; } public MonkeyControllerFrame(IChimpDevice chimpDevice) { super("MonkeyController"); this.device = chimpDevice; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); JToolBar toolbar = new JToolBar(); toolbar.add(createToolbarButton(PhysicalButton.HOME)); toolbar.add(createToolbarButton(PhysicalButton.BACK)); toolbar.add(createToolbarButton(PhysicalButton.SEARCH)); toolbar.add(createToolbarButton(PhysicalButton.MENU)); add(toolbar); add(refreshButton); add(variablesButton); add(imageLabel); refreshButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { updateScreen(); } }); variableFrame = new VariableFrame(); variablesButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { variableFrame.setVisible(true); } }); /* Similar to above, when the following two methods fail, they * no longer throw a runtime exception, but merely will log the failure. */ imageLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent event) { device.touch(event.getX(), event.getY(), DOWN_AND_UP); updateScreen(); } }); KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); focusManager.addKeyEventDispatcher(new KeyEventDispatcher() { public boolean dispatchKeyEvent(KeyEvent event) { if (KeyEvent.KEY_TYPED == event.getID()) { device.type(Character.toString(event.getKeyChar())); } return false; } }); SwingUtilities.invokeLater(new Runnable(){ public void run() { init(); variableFrame.init(device); } }); pack(); } private void updateScreen() { IChimpImage snapshot = device.takeSnapshot(); currentImage = snapshot.createBufferedImage(); imageLabel.setIcon(new ImageIcon(currentImage)); pack(); } private void init() { updateScreen(); timer.start(); } } monkeyrunner/src/main/java/com/android/monkeyrunner/controller/VariableFrame.java0100644 0000000 0000000 00000012311 12747325007 027462 0ustar000000000 0000000 /* * 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.monkeyrunner.controller; import com.google.common.collect.Sets; import com.android.chimpchat.core.IChimpDevice; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.Collection; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; /** * Swing Frame that displays all the variables that the monkey exposes on the device. */ public class VariableFrame extends JFrame { private static final Logger LOG = Logger.getLogger(VariableFrame.class.getName()); private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(); private IChimpDevice device; private static class VariableHolder implements Comparable { private final String key; private final String value; public VariableHolder(String key, String value) { this.key = key; this.value = value; } public String getKey() { return key; } public String getValue() { return value; } public int compareTo(VariableHolder o) { return key.compareTo(o.key); } } private static E getNthElement(Set set, int offset) { int current = 0; for (E elem : set) { if (current == offset) { return elem; } current++; } return null; } private class VariableTableModel extends AbstractTableModel { private final TreeSet set = Sets.newTreeSet(); public void refresh() { Collection variables; variables = device.getPropertyList(); for (final String variable : variables) { EXECUTOR.execute(new Runnable() { public void run() { String value; value = device.getProperty(variable); if (value == null) { value = ""; } synchronized (set) { set.add(new VariableHolder(variable, value)); SwingUtilities.invokeLater(new Runnable() { public void run() { VariableTableModel.this.fireTableDataChanged(); } }); } } }); } } public int getColumnCount() { return 2; } public int getRowCount() { synchronized (set) { return set.size(); } } public Object getValueAt(int rowIndex, int columnIndex) { VariableHolder nthElement; synchronized (set) { nthElement = getNthElement(set, rowIndex); } if (columnIndex == 0) { return nthElement.getKey(); } return nthElement.getValue(); } } public VariableFrame() { super("Variables"); setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); final VariableTableModel tableModel = new VariableTableModel(); JButton refreshButton = new JButton("Refresh"); add(refreshButton); refreshButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tableModel.refresh(); } }); JTable table = new JTable(tableModel); add(table); tableModel.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { pack(); } }); this.addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { tableModel.refresh(); } }); pack(); } public void init(IChimpDevice device) { this.device = device; } } monkeyrunner/src/main/java/com/android/monkeyrunner/doc/0040755 0000000 0000000 00000000000 12747325007 022506 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/doc/MonkeyRunnerExported.java0100644 0000000 0000000 00000003601 12747325007 027515 0ustar000000000 0000000 /* * 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.monkeyrunner.doc; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Indicates that the annotated method is a public API to expose to the * scripting interface. Can be used to generate documentation of what * methods are exposed and also can be used to enforce visibility of * these methods in the scripting environment. */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD }) public @interface MonkeyRunnerExported { /** * A documentation string for this method. */ String doc(); /** * The list of names for the keywords in this method in their proper positional order. * * For example: * * @MonkeyRunnerExported(args={"one", "two"}) * public void foo(); * * would allow calls like this: * foo(one=1, two=2) * foo(1, 2) */ String[] args() default {}; /** * The list of documentation for the arguments. */ String[] argDocs() default {}; /** * The documentation for the return type of this method. */ String returns() default "returns nothing."; }monkeyrunner/src/main/java/com/android/monkeyrunner/easy/0040755 0000000 0000000 00000000000 12747325007 022702 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/easy/By.java0100644 0000000 0000000 00000004526 12747325007 024123 0ustar000000000 0000000 /* * 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.monkeyrunner.easy; import com.google.common.base.Preconditions; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.monkeyrunner.JythonUtils; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.PyObject; /** * Select a view object based on some criteria. * * Currently supports the By.id criteria to search for an element by id. * In the future it will support other criteria such as: * By.classid - search by class. * By.hash - search by hashcode * and recursively searching under an already selected object. * * WARNING: This API is under development, expect the interface to change * without notice. * * TODO: Implement other selectors, like classid, hash, and so on. * TODO: separate java-only core from jython wrapper */ public class By extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(By.class, dict); } private String id; By(String id) { this.id = id; } @MonkeyRunnerExported(doc = "Select an object by id.", args = { "id" }, argDocs = { "The identifier of the object." }) public static By id(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); String id = ap.getString(0); return new By(id); } public static By id(String id) { return new By(id); } public ViewNode findView(HierarchyViewer viewer) { return viewer.findViewById(id); } } monkeyrunner/src/main/java/com/android/monkeyrunner/easy/EasyMonkeyDevice.java0100644 0000000 0000000 00000020217 12747325007 026750 0ustar000000000 0000000 /* * 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.monkeyrunner.easy; import com.google.common.base.Preconditions; import com.android.chimpchat.core.TouchPressType; import com.android.chimpchat.hierarchyviewer.HierarchyViewer; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.monkeyrunner.JythonUtils; import com.android.monkeyrunner.MonkeyDevice; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.eclipse.swt.graphics.Point; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.Py; import org.python.core.PyException; import org.python.core.PyInteger; import org.python.core.PyObject; import org.python.core.PyTuple; import java.util.Set; /** * Extends {@link MonkeyDevice} to support looking up views using a 'selector'. * Currently, only identifiers can be used as a selector. All methods on * MonkeyDevice can be used on this class in Python. * * WARNING: This API is under development, expect the interface to change * without notice. */ @MonkeyRunnerExported(doc = "MonkeyDevice with easier methods to refer to objects.") public class EasyMonkeyDevice extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(EasyMonkeyDevice.class, dict); } private MonkeyDevice mDevice; private HierarchyViewer mHierarchyViewer; private static final Set EXPORTED_METHODS = JythonUtils.getMethodNames(EasyMonkeyDevice.class); @MonkeyRunnerExported(doc = "Creates EasyMonkeyDevice with an underlying MonkeyDevice.", args = { "device" }, argDocs = { "MonkeyDevice to extend." }) public EasyMonkeyDevice(MonkeyDevice device) { this.mDevice = device; this.mHierarchyViewer = device.getImpl().getHierarchyViewer(); } @MonkeyRunnerExported(doc = "Sends a touch event to the selected object.", args = { "selector", "type" }, argDocs = { "The selector identifying the object.", "The event type as returned by TouchPressType()." }) public void touch(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); String tmpType = ap.getString(1); TouchPressType type = TouchPressType.fromIdentifier(tmpType); Preconditions.checkNotNull(type, "Invalid touch type: " + tmpType); // TODO: try catch rethrow PyExc touch(selector, type); } public void touch(By selector, TouchPressType type) { Point p = getElementCenter(selector); mDevice.getImpl().touch(p.x, p.y, type); } @MonkeyRunnerExported(doc = "Types a string into the specified object.", args = { "selector", "text" }, argDocs = { "The selector identifying the object.", "The text to type into the object." }) public void type(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); String text = ap.getString(1); type(selector, text); } public void type(By selector, String text) { Point p = getElementCenter(selector); mDevice.getImpl().touch(p.x, p.y, TouchPressType.DOWN_AND_UP); mDevice.getImpl().type(text); } @MonkeyRunnerExported(doc = "Locates the coordinates of the selected object.", args = { "selector" }, argDocs = { "The selector identifying the object." }, returns = "Tuple containing (x,y,w,h) location and size.") public PyTuple locate(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); ViewNode node = selector.findView(mHierarchyViewer); Point p = HierarchyViewer.getAbsolutePositionOfView(node); PyTuple tuple = new PyTuple( new PyInteger(p.x), new PyInteger(p.y), new PyInteger(node.width), new PyInteger(node.height)); return tuple; } @MonkeyRunnerExported(doc = "Checks if the specified object exists.", args = { "selector" }, returns = "True if the object exists.", argDocs = { "The selector identifying the object." }) public boolean exists(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); return exists(selector); } public boolean exists(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return node != null; } @MonkeyRunnerExported(doc = "Checks if the specified object is visible.", args = { "selector" }, returns = "True if the object is visible.", argDocs = { "The selector identifying the object." }) public boolean visible(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); return visible(selector); } public boolean visible(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return mHierarchyViewer.visible(node); } @MonkeyRunnerExported(doc = "Obtain the text in the selected input box.", args = { "selector" }, argDocs = { "The selector identifying the object." }, returns = "Text in the selected input box.") public String getText(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); return getText(selector); } public String getText(By selector) { ViewNode node = selector.findView(mHierarchyViewer); return mHierarchyViewer.getText(node); } @MonkeyRunnerExported(doc = "Gets the id of the focused window.", returns = "The symbolic id of the focused window or None.") public String getFocusedWindowId(PyObject[] args, String[] kws) { return getFocusedWindowId(); } public String getFocusedWindowId() { return mHierarchyViewer.getFocusedWindowName(); } /** * Forwards unknown methods to the original MonkeyDevice object. */ @Override public PyObject __findattr_ex__(String name) { if (!EXPORTED_METHODS.contains(name)) { return mDevice.__findattr_ex__(name); } return super.__findattr_ex__(name); } /** * Get the selector object from the argument parser. * * @param ap argument parser to get it from. * @param i argument index. * @return selector object. */ private By getSelector(ArgParser ap, int i) { return (By)ap.getPyObject(i).__tojava__(By.class); } /** * Get the coordinates of the element's center. * * @param selector the element selector * @return the (x,y) coordinates of the center */ private Point getElementCenter(By selector) { ViewNode node = selector.findView(mHierarchyViewer); if (node == null) { throw new PyException(Py.ValueError, String.format("View not found: %s", selector)); } Point p = HierarchyViewer.getAbsoluteCenterOfView(node); return p; } } monkeyrunner/src/main/java/com/android/monkeyrunner/easy/README0100644 0000000 0000000 00000002146 12747325007 023562 0ustar000000000 0000000 com.android.monkeyrunner.easy contains classes intended to make it easier to interact with applications using the MonkeyRunner framework. Instead of referencing a button or input box by x,y coordinate, they can be referenced by identifier, as in the following Python example: ############################################################################## from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice from com.android.monkeyrunner.easy import EasyMonkeyDevice from com.android.monkeyrunner.easy import By # Connect to the current device. device = MonkeyRunner.waitForConnection() # Use the EasyMonkey API, all methods on device are available in easy_device. easy_device = EasyMonkeyDevice(device) if not easy_device.visible(By.id('id/all_apps_button')): raise Error('Could not find the "all apps" button') print "Location of element:", easy_device.locate(By.id('id/all_apps_button')) easy_device.touch(By.id('id/all_apps_button'), 'DOWN_AND_UP') ############################################################################## WARNING: This API is under development and may change without notice. monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/0040755 0000000 0000000 00000000000 12747325007 023546 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/ActionListModel.java0100644 0000000 0000000 00000003717 12747325007 027450 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder; import com.google.common.collect.Lists; import com.android.monkeyrunner.recorder.actions.Action; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.util.List; import javax.swing.AbstractListModel; /** * List model for managing actions. */ public class ActionListModel extends AbstractListModel { private List actionList = Lists.newArrayList(); /** * Add the specified action to the end of the list * @param a the action to add. */ public void add(Action a) { actionList.add(a); int newIndex = actionList.size() - 1; this.fireIntervalAdded(this, newIndex, newIndex); } @Override public Object getElementAt(int arg0) { return actionList.get(arg0).getDisplayName(); } @Override public int getSize() { return actionList.size(); } /** * Serialize all the stored actions to the specified file. * * @param selectedFile the file to write to * @throws FileNotFoundException if the file can't be created. */ public void export(File selectedFile) throws FileNotFoundException { PrintWriter out = new PrintWriter(selectedFile); for (Action a : actionList) { out.println(a.serialize()); } out.close(); } }monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/MonkeyRecorder.java0100644 0000000 0000000 00000005170 12747325007 027341 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder; import com.android.chimpchat.ChimpChat; import com.android.chimpchat.core.IChimpDevice; import com.android.monkeyrunner.MonkeyDevice; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.WindowConstants; /** * Helper entry point for MonkeyRecorder. */ public class MonkeyRecorder { private static final Logger LOG = Logger.getLogger(MonkeyRecorder.class.getName()); // This lock is used to keep the python process blocked while the frame is runing. private static final Object LOCK = new Object(); /** * Jython entry point for MonkeyRecorder. Meant to be called like this: * * * from com.android.monkeyrunner import MonkeyRunner as mr * from com.android.monkeyrunner import MonkeyRecorder * MonkeyRecorder.start(mr.waitForConnection()) * * * @param device */ public static void start(final MonkeyDevice device) { start(device.getImpl()); } /* package */static void start(final IChimpDevice device) { MonkeyRecorderFrame frame = new MonkeyRecorderFrame(device); // TODO: this is a hack until the window listener works. frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { device.dispose(); synchronized (LOCK) { LOCK.notifyAll(); } } }); frame.setVisible(true); synchronized (LOCK) { try { LOCK.wait(); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Unexpected Exception", e); } } } public static void main(String[] args) { ChimpChat chimp = ChimpChat.getInstance(); MonkeyRecorder.start(chimp.waitForConnection()); } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/MonkeyRecorderFrame.java0100644 0000000 0000000 00000035453 12747325007 030323 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder; import com.android.monkeyrunner.MonkeyDevice; import com.android.chimpchat.core.IChimpImage; import com.android.chimpchat.core.IChimpDevice; import com.android.monkeyrunner.recorder.actions.Action; import com.android.monkeyrunner.recorder.actions.DragAction; import com.android.monkeyrunner.recorder.actions.DragAction.Direction; import com.android.monkeyrunner.recorder.actions.PressAction; import com.android.monkeyrunner.recorder.actions.TouchAction; import com.android.monkeyrunner.recorder.actions.TypeAction; import com.android.monkeyrunner.recorder.actions.WaitAction; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.FileNotFoundException; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.Timer; /** * MainFrame for MonkeyRecorder. */ public class MonkeyRecorderFrame extends JFrame { private static final Logger LOG = Logger.getLogger(MonkeyRecorderFrame.class.getName()); private final IChimpDevice device; private static final long serialVersionUID = 1L; private JPanel jContentPane = null; private JLabel display = null; private JScrollPane historyPanel = null; private JPanel actionPanel = null; private JButton waitButton = null; private JButton pressButton = null; private JButton typeButton = null; private JButton flingButton = null; private JButton exportActionButton = null; private JButton refreshButton = null; private BufferedImage currentImage; // @jve:decl-index=0: private BufferedImage scaledImage = new BufferedImage(320, 480, BufferedImage.TYPE_INT_ARGB); // @jve:decl-index=0: private JList historyList; private ActionListModel actionListModel; private final Timer refreshTimer = new Timer(1000, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { refreshDisplay(); // @jve:decl-index=0: } }); /** * This is the default constructor */ public MonkeyRecorderFrame(IChimpDevice device) { this.device = device; initialize(); } private void initialize() { this.setSize(400, 600); this.setContentPane(getJContentPane()); this.setTitle("MonkeyRecorder"); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { refreshDisplay(); }}); refreshTimer.start(); } private void refreshDisplay() { IChimpImage snapshot = device.takeSnapshot(); currentImage = snapshot.createBufferedImage(); Graphics2D g = scaledImage.createGraphics(); g.drawImage(currentImage, 0, 0, scaledImage.getWidth(), scaledImage.getHeight(), null); g.dispose(); display.setIcon(new ImageIcon(scaledImage)); pack(); } /** * This method initializes jContentPane * * @return javax.swing.JPanel */ private JPanel getJContentPane() { if (jContentPane == null) { display = new JLabel(); jContentPane = new JPanel(); jContentPane.setLayout(new BorderLayout()); jContentPane.add(display, BorderLayout.CENTER); jContentPane.add(getHistoryPanel(), BorderLayout.EAST); jContentPane.add(getActionPanel(), BorderLayout.NORTH); display.setPreferredSize(new Dimension(320, 480)); display.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent event) { touch(event); } }); } return jContentPane; } /** * This method initializes historyPanel * * @return javax.swing.JScrollPane */ private JScrollPane getHistoryPanel() { if (historyPanel == null) { historyPanel = new JScrollPane(); historyPanel.getViewport().setView(getHistoryList()); } return historyPanel; } private JList getHistoryList() { if (historyList == null) { actionListModel = new ActionListModel(); historyList = new JList(actionListModel); } return historyList; } /** * This method initializes actionPanel * * @return javax.swing.JPanel */ private JPanel getActionPanel() { if (actionPanel == null) { actionPanel = new JPanel(); actionPanel.setLayout(new BoxLayout(getActionPanel(), BoxLayout.X_AXIS)); actionPanel.add(getWaitButton(), null); actionPanel.add(getPressButton(), null); actionPanel.add(getTypeButton(), null); actionPanel.add(getFlingButton(), null); actionPanel.add(getExportActionButton(), null); actionPanel.add(getRefreshButton(), null); } return actionPanel; } /** * This method initializes waitButton * * @return javax.swing.JButton */ private JButton getWaitButton() { if (waitButton == null) { waitButton = new JButton(); waitButton.setText("Wait"); waitButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { String howLongStr = JOptionPane.showInputDialog("How many seconds to wait?"); if (howLongStr != null) { float howLong = Float.parseFloat(howLongStr); addAction(new WaitAction(howLong)); } } }); } return waitButton; } /** * This method initializes pressButton * * @return javax.swing.JButton */ private JButton getPressButton() { if (pressButton == null) { pressButton = new JButton(); pressButton.setText("Press a Button"); pressButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { JPanel panel = new JPanel(); JLabel text = new JLabel("What button to press?"); JComboBox keys = new JComboBox(PressAction.KEYS); keys.setEditable(true); JComboBox direction = new JComboBox(PressAction.DOWNUP_FLAG_MAP.values().toArray()); panel.add(text); panel.add(keys); panel.add(direction); int result = JOptionPane.showConfirmDialog(null, panel, "Input", JOptionPane.OK_CANCEL_OPTION); if (result == JOptionPane.OK_OPTION) { // Look up the "flag" value for the press choice Map lookupMap = PressAction.DOWNUP_FLAG_MAP.inverse(); String flag = lookupMap.get(direction.getSelectedItem()); addAction(new PressAction((String) keys.getSelectedItem(), flag)); } } }); } return pressButton; } /** * This method initializes typeButton * * @return javax.swing.JButton */ private JButton getTypeButton() { if (typeButton == null) { typeButton = new JButton(); typeButton.setText("Type Something"); typeButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { String whatToType = JOptionPane.showInputDialog("What to type?"); if (whatToType != null) { addAction(new TypeAction(whatToType)); } } }); } return typeButton; } /** * This method initializes flingButton * * @return javax.swing.JButton */ private JButton getFlingButton() { if (flingButton == null) { flingButton = new JButton(); flingButton.setText("Fling"); flingButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.add(new JLabel("Which Direction to fling?")); JComboBox directionChooser = new JComboBox(DragAction.Direction.getNames()); panel.add(directionChooser); panel.add(new JLabel("How long to drag (in ms)?")); JTextField ms = new JTextField(); ms.setText("1000"); panel.add(ms); panel.add(new JLabel("How many steps to do it in?")); JTextField steps = new JTextField(); steps.setText("10"); panel.add(steps); int result = JOptionPane.showConfirmDialog(null, panel, "Input", JOptionPane.OK_CANCEL_OPTION); if (result == JOptionPane.OK_OPTION) { DragAction.Direction dir = DragAction.Direction.valueOf((String) directionChooser.getSelectedItem()); long millis = Long.parseLong(ms.getText()); int numSteps = Integer.parseInt(steps.getText()); addAction(newFlingAction(dir, numSteps, millis)); } } }); } return flingButton; } private DragAction newFlingAction(Direction dir, int numSteps, long millis) { int width = Integer.parseInt(device.getProperty("display.width")); int height = Integer.parseInt(device.getProperty("display.height")); // Adjust the w/h to a pct of the total size, so we don't hit things on the "outside" width = (int) (width * 0.8f); height = (int) (height * 0.8f); int minW = (int) (width * 0.2f); int minH = (int) (height * 0.2f); int midWidth = width / 2; int midHeight = height / 2; int startx = minW; int starty = minH; int endx = minW; int endy = minH; switch (dir) { case NORTH: startx = endx = midWidth; starty = height; break; case SOUTH: startx = endx = midWidth; endy = height; break; case EAST: starty = endy = midHeight; endx = width; break; case WEST: starty = endy = midHeight; startx = width; break; } return new DragAction(dir, startx, starty, endx, endy, numSteps, millis); } /** * This method initializes exportActionButton * * @return javax.swing.JButton */ private JButton getExportActionButton() { if (exportActionButton == null) { exportActionButton = new JButton(); exportActionButton.setText("Export Actions"); exportActionButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent ev) { JFileChooser fc = new JFileChooser(); if (fc.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { try { actionListModel.export(fc.getSelectedFile()); } catch (FileNotFoundException e) { LOG.log(Level.SEVERE, "Unable to save file", e); } } } }); } return exportActionButton; } /** * This method initializes refreshButton * * @return javax.swing.JButton */ private JButton getRefreshButton() { if (refreshButton == null) { refreshButton = new JButton(); refreshButton.setText("Refresh Display"); refreshButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { refreshDisplay(); } }); } return refreshButton; } private void touch(MouseEvent event) { int x = event.getX(); int y = event.getY(); // Since we scaled the image down, our x/y are scaled as well. double scalex = ((double) currentImage.getWidth()) / ((double) scaledImage.getWidth()); double scaley = ((double) currentImage.getHeight()) / ((double) scaledImage.getHeight()); x = (int) (x * scalex); y = (int) (y * scaley); switch (event.getID()) { case MouseEvent.MOUSE_CLICKED: addAction(new TouchAction(x, y, MonkeyDevice.DOWN_AND_UP)); break; case MouseEvent.MOUSE_PRESSED: addAction(new TouchAction(x, y, MonkeyDevice.DOWN)); break; case MouseEvent.MOUSE_RELEASED: addAction(new TouchAction(x, y, MonkeyDevice.UP)); break; } } public void addAction(Action a) { actionListModel.add(a); try { a.execute(device); } catch (Exception e) { LOG.log(Level.SEVERE, "Unable to execute action!", e); } } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/0040755 0000000 0000000 00000000000 12747325007 025206 5ustar000000000 0000000 monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/Action.java0100644 0000000 0000000 00000002540 12747325007 027264 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; import com.android.chimpchat.core.IChimpDevice; /** * All actions that can be recorded must implement this interface. */ public interface Action { /** * Serialize this action into a string. This method is called to put the list of actions into * a file. * * @return the serialized string */ String serialize(); /** * Get the printable name for this action. This method is used to show the Action in the UI. * * @return the display name */ String getDisplayName(); /** * Execute the given action. * * @param device the device to execute the action on. */ void execute(IChimpDevice device) throws Exception; } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/DragAction.java0100644 0000000 0000000 00000004535 12747325007 030070 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; import com.android.chimpchat.core.IChimpDevice; /** * Action to drag the "finger" across the device. */ public class DragAction implements Action { private final long timeMs; private final int steps; private final int startx; private final int starty; private final int endx; private final int endy; private final Direction dir; public enum Direction { NORTH, SOUTH, EAST, WEST; private static String[] names; static { Direction[] values = Direction.values(); names = new String[values.length]; for (int x = 0; x < values.length; x++) { names[x] = values[x].name(); } } public static String[] getNames() { return names; } } public DragAction(Direction dir, int startx, int starty, int endx, int endy, int numSteps, long millis) { this.dir = dir; this.startx = startx; this.starty = starty; this.endx = endx; this.endy = endy; steps = numSteps; timeMs = millis; } @Override public String getDisplayName() { return String.format("Fling %s", dir.name().toLowerCase()); } @Override public String serialize() { float duration = timeMs / 1000.0f; String pydict = PyDictUtilBuilder.newBuilder(). addTuple("start", startx, starty). addTuple("end", endx, endy). add("duration", duration). add("steps", steps). build(); return "DRAG|" + pydict; } @Override public void execute(IChimpDevice device) { device.drag(startx, starty, endx, endy, steps, timeMs); } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/PressAction.java0100644 0000000 0000000 00000004031 12747325007 030276 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.android.monkeyrunner.MonkeyDevice; import com.android.chimpchat.core.IChimpDevice; import com.android.chimpchat.core.TouchPressType; /** * Action to press a certain button. */ public class PressAction implements Action { public static String[] KEYS = { "MENU", "HOME", "SEARCH", }; public static final BiMap DOWNUP_FLAG_MAP = ImmutableBiMap.of(MonkeyDevice.DOWN_AND_UP, "Press", MonkeyDevice.DOWN, "Down", MonkeyDevice.UP, "Up"); private final String key; private final String downUpFlag; public PressAction(String key, String downUpFlag) { this.key = key; this.downUpFlag = downUpFlag; } public PressAction(String key) { this(key, MonkeyDevice.DOWN_AND_UP); } @Override public String getDisplayName() { return String.format("%s button %s", DOWNUP_FLAG_MAP.get(downUpFlag), key); } @Override public String serialize() { String pydict = PyDictUtilBuilder.newBuilder(). add("name", key). add("type", downUpFlag).build(); return "PRESS|" + pydict; } @Override public void execute(IChimpDevice device) { device.press(key, TouchPressType.fromIdentifier(downUpFlag)); } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/PyDictUtilBuilder.java0100644 0000000 0000000 00000003553 12747325007 031415 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; /** * Utility class to create Python Dictionary Strings. * * {'key': 'value'} */ public class PyDictUtilBuilder { private StringBuilder sb = new StringBuilder(); public PyDictUtilBuilder() { sb.append("{"); } public static PyDictUtilBuilder newBuilder() { return new PyDictUtilBuilder(); } private void addHelper(String key, String value) { sb.append("'").append(key).append("'"); sb.append(":").append(value).append(","); } public PyDictUtilBuilder add(String key, int value) { addHelper(key, Integer.toString(value)); return this; } public PyDictUtilBuilder add(String key, float value) { addHelper(key, Float.toString(value)); return this; } public PyDictUtilBuilder add(String key, String value) { addHelper(key, "'" + value + "'"); return this; } public String build() { sb.append("}"); return sb.toString(); } public PyDictUtilBuilder addTuple(String key, int x, int y) { String valuestr = new StringBuilder().append("(").append(x).append(",").append(y).append(")").toString(); addHelper(key, valuestr); return this; } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/TouchAction.java0100644 0000000 0000000 00000003725 12747325007 030275 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.android.monkeyrunner.MonkeyDevice; import com.android.chimpchat.core.IChimpDevice; import com.android.chimpchat.core.TouchPressType; /** * Action to touch the touchscreen at a certain location. */ public class TouchAction implements Action { public static final BiMap DOWNUP_FLAG_MAP = ImmutableBiMap.of(MonkeyDevice.DOWN_AND_UP, "Tap", MonkeyDevice.DOWN, "Down", MonkeyDevice.UP, "Up"); private final int x; private final int y; private final String direction; public TouchAction(int x, int y, String direction) { this.x = x; this.y = y; this.direction = direction; } @Override public String getDisplayName() { return String.format("%s touchscreen at (%d, %d)", DOWNUP_FLAG_MAP.get(direction), x, y); } @Override public void execute(IChimpDevice device) throws Exception { device.touch(x, y, TouchPressType.fromIdentifier(direction)); } @Override public String serialize() { String pydict = PyDictUtilBuilder.newBuilder(). add("x", x). add("y", y). add("type", direction).build(); return "TOUCH|" + pydict; } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/TypeAction.java0100644 0000000 0000000 00000002513 12747325007 030126 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; import com.android.chimpchat.core.IChimpDevice; /** * Action to type in a string on the device. */ public class TypeAction implements Action { private final String whatToType; public TypeAction(String whatToType) { this.whatToType = whatToType; } @Override public String getDisplayName() { return String.format("Type \"%s\"", whatToType); } @Override public String serialize() { String pydict = PyDictUtilBuilder.newBuilder() .add("message", whatToType).build(); return "TYPE|" + pydict; } @Override public void execute(IChimpDevice device) { device.type(whatToType); } } monkeyrunner/src/main/java/com/android/monkeyrunner/recorder/actions/WaitAction.java0100644 0000000 0000000 00000002604 12747325007 030112 0ustar000000000 0000000 /* * 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.monkeyrunner.recorder.actions; import com.android.chimpchat.core.IChimpDevice; /** * Action that specifies to wait for a certain amount of time. */ public class WaitAction implements Action { private final float howLongSeconds; public WaitAction(float howLongSeconds) { this.howLongSeconds = howLongSeconds; } public String getDisplayName() { return String.format("Wait for %g seconds", this.howLongSeconds); } public String serialize() { String pydict = PyDictUtilBuilder.newBuilder().add("seconds", howLongSeconds).build(); return "WAIT|" + pydict; } public void execute(IChimpDevice device) throws Exception { long ms = (long) (1000.0f * howLongSeconds); Thread.sleep(ms); } } monkeyrunner/src/main/java/resources/0040755 0000000 0000000 00000000000 12747325007 017021 5ustar000000000 0000000 monkeyrunner/src/main/java/resources/com/0040755 0000000 0000000 00000000000 12747325007 017577 5ustar000000000 0000000 monkeyrunner/src/main/java/resources/com/android/0040755 0000000 0000000 00000000000 12747325007 021217 5ustar000000000 0000000 monkeyrunner/src/main/java/resources/com/android/monkeyrunner/0040755 0000000 0000000 00000000000 12747325007 023753 5ustar000000000 0000000 monkeyrunner/src/main/java/resources/com/android/monkeyrunner/html.cs0100644 0000000 0000000 00000001315 12747325007 025243 0ustar000000000 0000000

MonkeyRunner Help

Table of Contents

Args

  • -

Returns

monkeyrunner/src/main/java/resources/com/android/monkeyrunner/sdk-docs.cs0100644 0000000 0000000 00000001370 12747325007 026007 0ustar000000000 0000000 page.title=UI/Application Exerciser Monkey API @jd:body

MonkeyRunner Help

Table of Contents

Args

  • -

Returns

monkeyrunner/src/main/java/resources/com/android/monkeyrunner/text.cs0100644 0000000 0000000 00000000656 12747325007 025272 0ustar000000000 0000000 MonkeyRunner help Args: - Returns: monkeyrunner/src/test/0040755 0000000 0000000 00000000000 12747325007 014121 5ustar000000000 0000000 monkeyrunner/src/test/java/0040755 0000000 0000000 00000000000 12747325007 015042 5ustar000000000 0000000 monkeyrunner/src/test/java/com/0040755 0000000 0000000 00000000000 12747325007 015620 5ustar000000000 0000000 monkeyrunner/src/test/java/com/android/0040755 0000000 0000000 00000000000 12747325007 017240 5ustar000000000 0000000 monkeyrunner/src/test/java/com/android/monkeyrunner/0040755 0000000 0000000 00000000000 12747325007 021774 5ustar000000000 0000000 monkeyrunner/src/test/java/com/android/monkeyrunner/AllTests.java0100644 0000000 0000000 00000002764 12747325007 024400 0ustar000000000 0000000 /* * 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.monkeyrunner; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestResult; import junit.framework.TestSuite; import junit.textui.TestRunner; /** * Test suite to run all the tests for MonkeyRunner. */ public class AllTests { public static Test suite(Class... classes) { TestSuite suite = new TestSuite(); for (Class clz : classes) { suite.addTestSuite(clz); } return suite; } public static void main(String args[]) { TestRunner tr = new TestRunner(); TestResult result = tr.doRun(AllTests.suite(JythonUtilsTest.class, MonkeyRunnerOptionsTest.class)); if (result.wasSuccessful()) { System.exit(0); } else { System.exit(1); } } } monkeyrunner/src/test/java/com/android/monkeyrunner/JythonUtilsTest.java0100644 0000000 0000000 00000017746 12747325007 026007 0ustar000000000 0000000 /* * 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.monkeyrunner; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import junit.framework.TestCase; import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.PyDictionary; import org.python.core.PyException; import org.python.core.PyObject; import org.python.core.PyString; import java.util.List; import java.util.Map; import java.util.Set; /** * Unit tests for the JythonUtils class. */ public class JythonUtilsTest extends TestCase { private static final String PACKAGE_NAME = JythonUtilsTest.class.getPackage().getName(); private static final String CLASS_NAME = JythonUtilsTest.class.getSimpleName(); private static final String EXECUTABLE_PATH = "string"; private static boolean called = false; private static double floatValue = 0.0; private static List listValue = null; private static Map mapValue; @MonkeyRunnerExported(doc = "", args = {"value"}) public static void floatTest(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); called = true; floatValue = JythonUtils.getFloat(ap, 0); } @MonkeyRunnerExported(doc = "", args = {"value"}) public static void listTest(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); called = true; listValue = JythonUtils.getList(ap, 0); } @MonkeyRunnerExported(doc = "", args = {"value"}) public static void mapTest(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); called = true; mapValue = JythonUtils.getMap(ap, 0); } @MonkeyRunnerExported(doc = "") public static PyDictionary convertMapTest(PyObject[] args, String[] kws) { Map map = Maps.newHashMap(); map.put("string", "value"); map.put("integer", 1); map.put("double", 3.14); return JythonUtils.convertMapToDict(map); } @Override protected void setUp() throws Exception { called = false; floatValue = 0.0; } private static PyObject call(String method) { return call(method, new String[]{ }); } private static PyObject call(String method, String... args) { StringBuilder sb = new StringBuilder(); sb.append("from ").append(PACKAGE_NAME); sb.append(" import ").append(CLASS_NAME).append("\n"); // Exec line sb.append("result = "); sb.append(CLASS_NAME).append(".").append(method); sb.append("("); for (String arg : args) { sb.append(arg).append(","); } sb.append(")"); return ScriptRunner.runStringAndGet(EXECUTABLE_PATH, sb.toString(), "result").get("result"); } public void testSimpleCall() { call("floatTest", "0.0"); assertTrue(called); } public void testMissingFloatArg() { try { call("floatTest"); } catch(PyException e) { return; } fail("Should have thrown exception"); } public void testBadFloatArgType() { try { call("floatTest", "\'foo\'"); } catch(PyException e) { return; } fail("Should have thrown exception"); } public void testFloatParse() { call("floatTest", "103.2"); assertTrue(called); assertEquals(floatValue, 103.2); } public void testFloatParseInteger() { call("floatTest", "103"); assertTrue(called); assertEquals(floatValue, 103.0); } public void testParseStringList() { call("listTest", "['a', 'b', 'c']"); assertTrue(called); assertEquals(3, listValue.size()); assertEquals("a", listValue.get(0)); assertEquals("b", listValue.get(1)); assertEquals("c", listValue.get(2)); } public void testParseIntList() { call("listTest", "[1, 2, 3]"); assertTrue(called); assertEquals(3, listValue.size()); assertEquals(new Integer(1), listValue.get(0)); assertEquals(new Integer(2), listValue.get(1)); assertEquals(new Integer(3), listValue.get(2)); } public void testParseMixedList() { call("listTest", "['a', 1, 3.14]"); assertTrue(called); assertEquals(3, listValue.size()); assertEquals("a", listValue.get(0)); assertEquals(new Integer(1), listValue.get(1)); assertEquals(new Double(3.14), listValue.get(2)); } public void testParseOptionalList() { call("listTest"); assertTrue(called); assertEquals(0, listValue.size()); } public void testParsingNotAList() { try { call("listTest", "1.0"); } catch (PyException e) { return; } fail("Should have thrown an exception"); } public void testParseMap() { call("mapTest", "{'a': 0, 'b': 'bee', 3: 'cee'}"); assertTrue(called); assertEquals(3, mapValue.size()); assertEquals(new Integer(0), mapValue.get("a")); assertEquals("bee", mapValue.get("b")); // note: coerced key type assertEquals("cee", mapValue.get("3")); } public void testParsingNotAMap() { try { call("mapTest", "1.0"); } catch (PyException e) { return; } fail("Should have thrown an exception"); } public void testParseOptionalMap() { call("mapTest"); assertTrue(called); assertEquals(0, mapValue.size()); } public void testConvertMap() { PyDictionary result = (PyDictionary) call("convertMapTest"); PyObject stringPyObject = result.__getitem__(new PyString("string")); String string = (String) stringPyObject.__tojava__(String.class); assertEquals("value", string); PyObject intPyObject = result.__getitem__(new PyString("integer")); int i = (Integer) intPyObject.__tojava__(Integer.class); assertEquals(i, 1); PyObject doublePyObject = result.__getitem__(new PyString("double")); double d = (Double) doublePyObject.__tojava__(Double.class); assertEquals(3.14, d); } /** * Base class to test overridden methods. */ static class PythonMethodsClass extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(PythonMethodsClass.class, dict); } @MonkeyRunnerExported(doc = "The first method.") public void firstMethod(PyObject[] args, String[] kws) { } @MonkeyRunnerExported(doc = "The second method.") public void secondMethod(PyObject[] args, String[] kws) { } public void unattributedMethod() { } } public void testGetPythonMethods() { Set methods = JythonUtils.getMethodNames(PythonMethodsClass.class); assertEquals(2, methods.size()); assertTrue(methods.contains("firstMethod")); assertTrue(methods.contains("secondMethod")); // Make sure it works on non-Jython objects. assertTrue(JythonUtils.getMethodNames(String.class).isEmpty()); } } monkeyrunner/src/test/java/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java0100644 0000000 0000000 00000005003 12747325007 027502 0ustar000000000 0000000 /* * 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.monkeyrunner; import junit.framework.TestCase; import java.io.File; import java.util.Iterator; /** * Unit Tests to test command line argument parsing. */ public class MonkeyRunnerOptionsTest extends TestCase { // We need to use a file that actually exists private static final String FILENAME = "/etc/passwd"; public void testSimpleArgs() { MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(new String[] { FILENAME }); assertEquals(options.getScriptFile(), new File(FILENAME)); } public void testParsingArgsBeforeScriptName() { MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(new String[] { "-be", "stub", FILENAME}); assertEquals("stub", options.getBackendName()); assertEquals(options.getScriptFile(), new File(FILENAME)); } public void testParsingScriptArgument() { MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(new String[] { FILENAME, "arg1", "arg2" }); assertEquals(options.getScriptFile(), new File(FILENAME)); Iterator i = options.getArguments().iterator(); assertEquals("arg1", i.next()); assertEquals("arg2", i.next()); } public void testParsingScriptArgumentWithDashes() { MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(new String[] { FILENAME, "--arg1" }); assertEquals(options.getScriptFile(), new File(FILENAME)); assertEquals("--arg1", options.getArguments().iterator().next()); } public void testMixedArgs() { MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(new String[] { "-be", "stub", FILENAME, "arg1", "--debug=True"}); assertEquals("stub", options.getBackendName()); assertEquals(options.getScriptFile(), new File(FILENAME)); Iterator i = options.getArguments().iterator(); assertEquals("arg1", i.next()); assertEquals("--debug=True", i.next()); } } monkeyrunner/src/test/java/resources/0040755 0000000 0000000 00000000000 12747325007 017054 5ustar000000000 0000000 monkeyrunner/src/test/java/resources/com/0040755 0000000 0000000 00000000000 12747325007 017632 5ustar000000000 0000000 monkeyrunner/src/test/java/resources/com/android/0040755 0000000 0000000 00000000000 12747325007 021252 5ustar000000000 0000000 monkeyrunner/src/test/java/resources/com/android/monkeyrunner/0040755 0000000 0000000 00000000000 12747325007 024006 5ustar000000000 0000000 monkeyrunner/src/test/java/resources/com/android/monkeyrunner/adb/0040755 0000000 0000000 00000000000 12747325007 024534 5ustar000000000 0000000 monkeyrunner/src/test/java/resources/com/android/monkeyrunner/adb/instrument_result.txt0100644 0000000 0000000 00000000640 12747325007 031100 0ustar000000000 0000000 INSTRUMENTATION_STATUS: id=InstrumentationTestRunner INSTRUMENTATION_STATUS: current=1 INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest INSTRUMENTATION_STATUS: stream=. INSTRUMENTATION_STATUS: numtests=1 INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly INSTRUMENTATION_STATUS_CODE: 0 INSTRUMENTATION_RESULT: result1=one INSTRUMENTATION_RESULT: result2=two INSTRUMENTATION_CODE: -1 monkeyrunner/src/test/java/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt0100644 0000000 0000000 00000000700 12747325007 033157 0ustar000000000 0000000 INSTRUMENTATION_STATUS: id=InstrumentationTestRunner INSTRUMENTATION_STATUS: current=1 INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest INSTRUMENTATION_STATUS: stream=. INSTRUMENTATION_STATUS: numtests=1 INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly INSTRUMENTATION_STATUS_CODE: 0 INSTRUMENTATION_RESULT: stream= Test results for InstrumentationTestRunner=. Time: 2.242 OK (1 test) INSTRUMENTATION_CODE: -1 monkeyrunner/src/test/java/resources/com/android/monkeyrunner/image1.png0100644 0000000 0000000 00000547741 12747325007 025676 0ustar000000000 0000000 PNG  IHDR@ԌDIDATx}+F 0`0`  0`A 4h``pAAA>{-ܛ`Riig9o/yK^QMxK^@fGQng[}nqpϟߞ3cχ{>g~u_:n>cY[Suo=پs}_?ۯv<ϏYx,}/0,o"$I-XNI<;f-<\s|4>m%`f *uD |:w/;7GXT]y`h{~&n^ܩ^ 0у~M> }c 'rcq7VLyoϟ1` '^BUe#Ȳ̾%.X It~xs90g\heйho}r_ruxʘ|o1{b~/%N``7CT]n8@=>6X3YRu9߽7G һ߽Y|ߎg?8(z`yh:(vuXil}t}{a_2`&5aXƾ9?T?mgKv_nwCϤqǎq|1{;}z&w(̢ )"_@/tm->o89x}>sm=O4^O[?b3>Cv18O4?0ط7V}xóLm8>Ǥޮlvtò%/{~t&$,N$҇mMq.6G=ޡQǹ\.5H/XqN +vـ|AMՄm1E0*yl4^"?`2UuVe^߁9ZF}߶~^}Y~|,ͺZg7m\˳\/ |@XkmXT%rYv<)ųCW]s_pNsl32Zk"xp6@{@Ǔ/DFg6_sQMڃYT ;k}nZ/zlElanow؞]ygnܷot[#fm ܪOuS/G7{E\ZYγe {]>4s?B Dȳ7mm1$% uMlX/h-*vM.N7Z 0\ dz@ _q d| ڇ FO%xqaa[|Yxki_{8:|wIc[-pqMƇ{S겨V% җɱoI|V\}a i9?bc?0>o7ugݦmg;ctq?^vEx5Zalo;t=mXjCR-kGG-؜[dlKg{}W$)uveoMȪf.αem 6zf8@qnI .6l8Wu]u Nq0pc?9׾c0O?' |(}6YCߋ*^`K웞e7[\+k7)9Fזw ̱3Tv_m`.`DZJܟf å_ |ʱMLi6=zX>]s4΁k9pg|ikTۚ7?6yOm>5S>MKxn%Ҷ]5`k &W-3p겇I)vXSoB_S}=cbՒ+v}`A0 MMq^1 J m_ӧfV lM56|c5^<3}1]q~KxaZiy})ebfۮkl~Ș|0uG,=s#u@-r=F o.+*=% 6c$T7hxXxPz~l@1fp05 €nAby?ur<VpE Pڸ,M2j^Z%čo;kS&fS6%tI( voyf>==3v;M rOcBu,ڞ^f3AT5;NS͆$j&b{apӉ:LZczaPki3@/_GU wC!g8*#&ޱq;--惕ŶT5͗}lP~5X܊1|)AO{io\}mv19~L1 Havm3?|Иhkv+ЪC-sö9acպO#ɟOmcSӣf7}X瘱ff, Aɢ t.}Nk Cի\f;.cHPa^nqZ+6ov6X2~ /,Zqr/1ӊS{f?guQX}Ywlпw Ž/*Xt u__R`{-xF;mwm?l?WJp<[|z:뫫u|XmѲάQh;o͸sG-X7^sٮ}|~]cVmdZ~1vɼxc-f/m.>4֒uݽonfܳS놮UY-Ͻ]>s;*:9VƖ|ﻗcw?{x~k<6;sxơ?0բFT&W4&(xޜT闎 Vֳg?sAk2_][u~gh3e3t<܇ahmL]G ߿frpTE%*͗ZLd@(4}?T 0'%/݈ .>qß<2?SS1@|I%/,ª)03{> l8as#E&t:<׸%Ta0m lK/yK )>Rւex9ϯ?,,x`#Kb_gG~/yɇ9|  Ԝe9I=~81{Z5nK^]4hzn $- s j%s%P˿4nf?k/yɻTJ$@hL.?l3N`y]V^rZLnzr1T_%*]]+366Йۙ1CZxoJ}ՙgGTv=MP;8@@_˧s5 a1MB(m~ LV,Ym*Ԍ+lΗdHyyIX"]Zѯ8& >X=1eN6qOao}%gz0ضbDc!3#sH܏׼/yFě+`;vU>g@9(V`C*&r%N9]Ϝ`@x"H1RM{CMP@umSU".Pcolsx`u>~c i;s˒3,KP.3 y># ?'SiO *sNϋ#@Gbag(jƾoKu\,*v{$CM~/H"AB O4ɶcj䉑>ob7 We0I̟ ~C}S\4 L1\9A>L>8Xa`պ}~Rh!%/ ;0ު@Vye^$ޱѪg@ &@)8t3!zC\568.Ǎm[,P[[p`Z%SAʗ7&/D/y#O@ “<[8}xp=Pmh(z.\ p0IM_gmP":lA`3^ċi]V]A xk0٣bgY9^g#¥Uy mƌ% muPyBө*,QxV&;O >"D8}֜^L#Zvɮsp^%>cyp(^@&LPa]ClGnU 0 ovۓ{=/yotLe~!Z@H,^b u*Ѹ @/m,i{K~ #)pϫ.ښ(]q>6K^ߕ< ?~S"558 f|2m߃~NZC74)z|L%{_NĘﲤd> ro}G/qS'FK^91Nʧk5!MRvn) kn`uտ/yKxoW5U,̴.jٯ)^MFb5g}/?3aV`apO)Z2坯47cd)-ahK^Eh*0SRL_7%/y?`YV( /yKIKSͰi1,ݽ_$ /yK^{(>'IJ~;U /)3A6PPh|Is~3X>R-7skm?Қ׎vh#AOkJmnrn/߶roD\g _I*B e_SKGMHbZ@_\zhg Ψ;, \O/Q^*䡟zJݢ߷1`ȠO;w'2exݱ}͊@ ݚY U|36A#>mmDK!_U+,kպ_7Uc~sotF7o^K[߶ڿ5zaLwF;Պ8}G}~Wbi$ 17~c?Rޗ9gޗ'kvX^sL`G"-1K׌$wʷ-og_nol㻋r6wI3f-W Kzˌ-I3pew?7_h[IZ9c " {@0PcYdgP.>˒ι!qnt!1ovt,Zkj5Sk_(' W0 =u@A৷[-~T&U n Lk#mC[Lh K{~G{n$\28vGL@@/ٹG{^wisDwM\@?;߽Gvqx;6U W ]j[.m1GmXz RWS>k{AIW%<eAj匹 p4Vsh;@aퟬu[Lor6#`h2Qmk/a( p,lbj5P_ zN pT뻙:@6sOr/J ]3$`H] K,me[h@P%#Gv& !)f8ɍevλmm/ppxXlm mѴ!2Ǿ n}L0IQN[v@H=3A+tG([#5p+۟hS6*gP'풘~rZ30!%D3=Qw LLl3=x #D6AAyrITu6Mj:P\3'maV6Bm3@t-q˛AаFmwENj 6 6&x 6d][YFvړHg1]!\Tv@c_A 7+DC1@nZ`FͨQdAHට*x(w-I=b /E@xF{ `!Nb~LGxZ1Aw~Q\o3>syf- & {_ܲ{^_Tk2ٿ$NS8g記3W:ޏ^(`V2< C1^K:g'ɉ`TY j=>>2=`3-`8krۖiFx᩾QN3@]d+{s9]TZOS2j0 &Uad$: &azֹ?ge#ܞھk7` }.;̒._oh36fP`(FضJ;&y{/cP󴼞ίq類տXelomf=˝` 6aO `h 3!0| 0!QNpBT7h>{& ?) A YyB0~;7Vo=Dϭ?\R\/%Ol}|Q lCT΅ft*)pUl̈E?bZUw`P*<pX @0#X >`fK\$@љ ~*gl\&hlNu\ 5 aP4l.fَ/a~^1g0=kNkbWa6.${g3%&k:9/Q!gfp.Afxtoސxٍ. 旫L(boѴڻZ7Z&I“\8׷]u]9?f|Z1Z H{~"_ H“_i B* V2C(&x1j'H6pB~c[7Ǔv[a ۷mm<]ۛ oVPs*[~֗QV[3|BuWIqEFV_3Tey쵅Ae*>~w˻Tu8?#xKA Ig8ܲnu ~:9:)[`rr+~4<LtoPUmQgҭR ա.K ~`a@sU_M}.Jep^cT;B&C @^!1{LV R'nfws ?s'N{8vapP8*Q/Þ jkX XCcv̠ 퇘X8[3%qtZj4 `؟x P\ 5+D V}Aa&[.g./f /A~(|/cT_@Qwz s1|V^"pCSJvAO0 LR"Z@0;_kjtn vMnONaT^5^*U gCcPY3DrK#t+W/!z8ddЁb~]u& -כӚ^ܚJ!H_νS뱚|a@N^?%Hq~:Cbtڰ Ď#? ] {L0~QXFЌO" iٱ uPb( daqV9xLl$V%eQA9Q)5w%eee&ʔ008Dn`Rpr{Wy-ة`rucZaܟ6W/gcNpLFJFh(Ǹü7pB[W͸E ??g:ϰL0 x+mt-;c0#d_P0d@l .m|pıUbNw*Eo; ~)o/V\3j[@ tZJeU, r+&g&Tέ[LpjU`"4O7?K, 64^ 9ńZyLdK{ֶ= Z/0qd[a01{{Fz eb=ܭ.N"#SUɫ. n~;g r+.q` q0˞+ cЩѿ4cF\5fl1WoOadG1"le ͊ca|&xB)C.(?Bln<|5.@ip*0*Waj̤m# fwn[}#r@.[lC=6!0ۚfM^6\aaґ{ x^`U K Ps2 S%6+ 8k*wY !G v5V=/`)۟^T$|m0>f| aƝ |d8s<`W9P /|w78>˼e~3̯>6R -= P.^n`@<a9a2`W% 6؝U%{7 ļӝ.&8OUA\-n >S:&8F<&dx[eXCᜀRlv3z=S\Bp k94QƊ7_rU͏ |6:_Hp:_rQjK{S=_ Waw޷̐ua~m>7v@0V\`.O-~o b۠"$tWlMm(9B aЇC cvu>X6N7F(0oVilcmQlƜ#L1]v /}d~N+Ziy%zÆ XWhwkq텨e*&`^Ly [/  W{y ~J f{)oĸ'ؠ^ r86myQe`yBA^gH!0Xי!J * m∹xF4+m>cGif]Q状rMoFj.r]聡.Y Zɇs[’6M,USȴ+IOP; Di\Kb.O@ॕB-KVAE2ea x>xwWB;,yٖt? EB#_8j _XV}QwP=1?"^^j&X?krz<{LHj5Xpl_{װ5 ^ T3=ߌ#ǔXw8TY[ܕ!2фJ\9ZMÁ !}yfz#w^~o0ybxLpJ[E'fge@SAW3A ZFiT^0a>0@/P hr7R3>q UR\Uڌ0M^9?t8TQ ~CcΜ%{;g<02@7֟|؉p: pW.!.W| "D#Xr q"6jC$5[ל].`u t v(q#dw9 NrFFтސ Ɨ;\0 ]!Rfi_vHbg>c0D0ERX`;pl\ĸv֏?N\ r v۟IEϧ@ Jb Omw=io6_rxځat|w *Iw L>b⼧u(#\y:8~zܟet-c57ehHk{/RA@'386p@:&SaG gaKb  uW_*c qH{9f5ߍDT%}e JbiɅ|\'Bbh7|u A/h4+~##5vYڳ?RjXNTtʲxwLig Sr>dTjXyf>GbgtuY-i;?$R?f\&Ǡk+ З"C\:bu/EHT moZyv$`BLPx\ !5R7 7Und~j2R3\`O܇.>g\:M&O.Ä l~jA!Kw uxLpZb@aUpd^F_!Oמ|=ɺܸ5HtwxҴ[j6L۾aO:3ҋ<*ߦ 3Wx+䢨H]mt4mma-SjpNyGbI']@ Nko.8+@S,`S1/ 8ͰcI ;2H:gQxu[O ~&hCZ We0 q|`!^@0KB=`4uV2=niT2moZag:Dy $Lb>om|*P9@T5h=v[1@3[M:3o,H Uݏ "dܕ+TېDWDrC '9$ZcX9/*G7N #V5o{Z86Z &oa ~R|V 5+]*8` 8"-75埑)qh0-k"#t&E`P ; D20#r^rM@$c6.t|y8l{"[.e5YoW1`XdO-ss-s6a#;܈ lUzl61"7>3H/8J|j\so^jvPK"c N#U! b`ɂ!0I0bH, ~p,/IVFG&8ϯ!ƌPg 4dIl6@8?R.AV zfe!y30( iV6?Q׬,"2 G,n.G5!*1;>v\X`QZ+k2f_@;u rrº1jf;癶;ikfޣ럝*)]r.Q^q^;%ahUF'x^N6Y: @H,V4hc3*&& Hښb\yz8*Ii{:[ى=p?6!"eNU^Bl~W~A4CTd)As"'"ru*7d|]퐍Lou?Ǧ1"ؑV9ЇhGl|s "!J>`;ٽtfD=%nn6⫰Ś@xS|~jغCD4lሌhwxuUy9G'n}8q!9ժjҞW(lC<.O5:/=e{1>q-}Uf6k+Ѳ UvVꢐ>&)>\yޢ"X9=y,>JWDk*-.I]T-`@)rZ0F!&{b<Rԇikԥ\ rn;\ҝ-qCoJP ݱLk'fixqPtx 0jqj%4=Rc<*&&V{l`c'I5S#jf[  J9L  ~ox* 7$m1~!{ PΛAb;발 EƔ7@N1?~ yP֤uCM 2EǬsYEtOXչ n$"Flp=и$ -N+ F]T|=T\ho0pêpI_b9;į*6PV^jv'mAm1{̯'hwn_Wn_w$70éu^\zUZV:' IU9ZäG2#$ P1>"pbDJ4"ьLRLԗaѪ6.Rsgx*SÈg9ʩ:7H̶@ @3M*6̛q;<7 wt=.u}==]X_q)B f_n[˜ [@`Vr0nNlv'E8?ׯS@1DR Ԃ*/x5bB\ QK['I-B CH \0`O{\76NpԼgWxlF9D1pMfXFfN&` NH9B8F3X.Q5`gbb K+[aP<ҌR;7T -c%/RjtMB)B^ [ ON'M 9@Z`'Y`2 >!LpKj.l}G>E]RHyVe.LT#ALjS𴣣 FG_Ӂ<.A##d1W+3@Ռ=bji@̃X)1P2F [3`tZeC6Hc}6 "aﰙ+XlF(̣ L{y[n!Ƭq(lG_g``}Q [L0u |. idOpŠ.jg ZvCY 8g1!${ITXc%Sjm.ȚPq-`-3>f3-%ih\r SsCu0mU lywWU "T>Lcpnɱ/+蝇 .H(:P?'4u|%Ӡ`Ϲ  [PRfOlՂ'>a~ t{y✞2Ngsƨ]6| 016>9ȘGyGz]fp[8>b<'[P Ấ0A\dj2Oh^y )D#Ä#lS Om 0ؗ< ~P~\F๚S1b O ϛyn❣ EWTdžLbUMApVK1RqaJC۩\mV=ciViPPs4̎NH&H<\EBਊK=1ARWs@e|]@쌲Y~gu`;n2D{zqsX*QۘCHL+Q:P٦_kߓh tCU2D'@ˋd`5lvD!Q ]PQ%X;KGXlfcU #܅T3@)xP:m$?e.UcD*l@,TP< 7~CrMa>ޖ'f;}`&LP GO>sЩG31?U־,0.g.byX<9jI'F8\y~KB_Kfjufg(-~3̯ nӼ®@ h^Vb (V8ih_ҌQxߤ'fah[`f}Xi2% c TR0J=é**::@RAr\Y& #D{EcT2!_ !.}\þ"{/9?GG2Io/ gmrߞ -`[arv}`1#'Gx`Un /+;aO $ S pfVI ;:Fhjp}N5byYi+-3J*[a>[ vu `#д 3V;&t v_s\P*8ZǴ3B 0#N'!dO gd TGG V8E\, ItUU@MV2 $ u梫pD ׹ aQƙVkPƨoVl\,yi x |wð[L_Rٷn;fx+AngA.pҢ8aUbSݩB6Cc~T5aWTn1Ry|9v?N^jMȖZ=p,s-sv`r-`fcf3y}S_fOMf~ce[5E 2Y^`"AҞbђZ8jH`R ~Z̐H:LHdIy-{ Hc$` vsWR~Rs<3O0@L]p~_z8jfe].T;o)zI]nO ~^[v>#T8h8+#,g>>%Aa^*L%-[MˉQ*?8rq g ^Y/we 9w$`| 9) fփz@t Z+, Wk`6gH$UdċA>qpPIJ" +& 1`l``X``(P2ELX9K190in7=*\E=V ib)3S뙎-Jb!SOpؠ@!4۠QeE;_ 鹜{=;<`C͚ 9p S\>{n5`; GL"@I1926' I2()xƧXsYs<<> 70qG_Úۈ `1`79t  ɚvN`<<@X_V2uq`` $#w ֻS&lw 8#؆*C0"^U۰1ipZn3X9=Z-CZièbi%Ɨ(X:%W (\W=9W@xT6_S fF*lNr~g:Y Vx<HUV6@c^c?Wrؘl#ݟzBQ٣G >¬&ۭ #,_2+<1;l 2eٱQ90݁,s ٖWj顼eS[˷KXɑ3fފ# =#=9H::hpPVyn#TF193 TApd p`*%mD}#*^06X:Kd(ǜ8Ik`3m,k~ʱQB_ x@=BB%#*1s  3t_2W+]1AZ̠mpr .w[AQ<"'L"L˙{%^A`v!W˻d> _+/ɦ;  6\a1K.=g5 =pR:a017zKA_ =j7tM;ĄUt!Ry| =%n鋽c8>P2ËioJ*@'ż:t92 >Ni`^yv)W->#&91L\b2 1A`pv]0 X [q lfIfv8 I8(a*3Zmf/Ҡ(`vTqa0(Gn$ 0MtiP@P4yZ6S6~8e (w&+\ʤQjB^*afx g\)%u'وᝊA=ұ`\֜2Y (̙X-3{;[ #z 풾2Xam7} D2=ODf>+qj72<"7ocp*A6,gb8Gmfydt^p{$G, 09$@i pB_ ([ -k12mGT?j5DU0<m6`;[?= -s( (5e:XMt-WѸ /q΀W;Zx GOtkF@'@*F[bY"OjכmkV52 U\xA Y0AF%ÑZj=nQ%:   5@P5Z3<ߗ<_G$&`CLR~ OZPqn ɻZ*͚~' ՗+m 3>ζGwb\$AJ;t m2=S&JT/>bo.= x`df{/<1Pe ~n >r<0bglr 1KNtGЄT vjv|`{%,gbt[K=4v%AN^c3JB5#$$PMi-}ʊa5+jlWo 2*LpH64);/xeFFOU is?qe}Ж<'g  ()u@#SV8NM(#`3S"-q(0 ueUA9FRIb f*+CaHP!h@)^D<1;;$VϚ ^:~&IKC33 qs?)mt63L@ɵ旋n}RR<~%^l~1p|=.>IM5!<%6!i^]0t x~b GLp3M)13&ʕp_xZ%cJJg7#y- Xz"s lOKk,Ïޟ0ܗh+Pa\ {ُ`Iycc~~9*0C}! }2]/ܸ@R%/..=ǻJN =Xi[9 #h:\x;UgDU `oƮ!1*yTͳ?0?pu LULsLql$rZ1í`l@xL\xY:'POAsG}C.#I;d|^=^~U9l@x/x20'ar/OMp~g/v7?~7'&::Ft+m1Bp u\ؠzKyܿXޟ6y&i0A3F o }o`E>Uz{#% t;0j=N,e;\>/s0@[VHu~%1;r/uҽއXcvU Q:{cBIև:*wVi01qFd.޶::`?{ch@#F߇&K%[y%*wFay\F$I23z}` b()2Hod9e)LzSncx/{;y6zˡUz5@P2!L!qtPdbݳj{kl7De/.afx`~yGI:Yx{po.3?Ҿ,@x?.Ǥͧ[k!SVN`h1LA C$y;4e" |;/f5mJo2tgFZ {=!C[ޠ v}ĭ7"2Mos3kav a"%( H$1BIwhyQUBS[M.u{m3혌a1`|2ҝ H/lk{Ra`}e{x30Ʒ̨nVUH\Is[b@ϱb 0LOdɵł[`v w1vw=%so$h%'"d0UV8bdϏɥ+6 FeU>9r/=n?agx\goT$ ;xFN]vb]>`w7dpILûc;idI?[7>\9 ;mf6Q׵FvSFyT6 s|m}1\ח ԢDHVRLv܈J䚒.uWWvapNmF+, eK_ VbvXV@f6ٱ~7Xӝ`R5UR"sFp&!ܑ!g:. u}麰>I6U ; "BOp{y1{߿z!sc+5*g}si_kj}͕]VŝY,SIs>Jev_|L1r|cd/s1‡]A>1S5 `POpٴD耸c`涩۬W[w2 !cƇ"MX~QOAlF8^K0w/Jmmr(S(ՐczVa>Ax ۋRS17h"h[m 8Vetr32B6h31va}HPJzM7vGjt, 6+28gv}J$zHfg*,Kf2OA:3 d cm +~SiPmfXO=d>LtS_@P=?{jQ{f:@d5Xb`- pz&R$frKfgDJe9=`}Ag=w*cx>|7mǝ]ߵΘ}9Pà4?/#zڙ8ڦr7e[g@{ H`?&zru`~R~[cFmkGd, %W+/e ax4m#ƈp:7Xv0 .q X y2tdf l :`q*/ϳ1ELci2IV\Ȥ#l:烔 9㼷/z3x 0HYnIJ) NeQO>nfc {7Z`t.<5<BAHReG-4A3NPΣWPӎ.o&@ hd1-I/[(ͽw7Jp{i}4%|.)C&=F+23pڛ<| j[l]Mk X[ pXyb p)frBY: $0bhcV&W_n%vfG^f UIqYF.3;#|cv-vJMxj٘1 66;ׄXY;g/{)gz'i;lt9C[ɰA@S1kMCY1jL~dALpz?zMp:^#|'LDbI qnj~prr˴TP>c^ W=QW^mmadR\Kَ@./s\wȀ.bӐp쵶尲~k[r4ƘY)ꛯC/#2߷5žހgE0S^a]|`D/k>d[YH7'd]v";ܰfG,Uyty<~`aZ{8Ġ: O oa `^ۄRx#|,:,Nw`~g)4:F?`lUh?`msF$/c|ft+czsM =Q-B1la-?ö)}L#[iz3Sqű7H  @q}.0^O6H =՗#n=/>{_M?33 j_%,ae6PR0Y@'sڑbc?r{v?>.]Cxfj'D\ ><0[<^/ai nIIۇ` Kd pl<IDAT4بŠAngס30lfF*~rNrMa+& +`r#GݎoL9Āإ^.t[Jtzƒ0d1f`$$KIl t}1iݭI#{.lu}YY9 :lt3yT>3O=G b=^ M$;0Jm?Pfb}ؾb9 83QqĖhQ"s%=|ڞ뭱a|;4wrb9L⮑jpd Uc!c})5ѥGG/A] TVU^k3 0V%xcoT])R֥ e( LPT` V>.ǃKe1Se.&>IҠ~ 7?^u:GO>lsjowUѣ#܎>1N0%t~oqS)Weg`9 IG+&x8Xx=NAz .?N#]waLw؛Y)Bh S0bŽC_6HOc`]rbc!bw8cޤ^f,Ց79 n/.}9J@&\}T| ]0D0&KGI"X"1?8HZ+wo7kdK p]foڇ]fX/}"C#uJ2 i8NW2)J1byCMzJwoYJ3ʨV܊–i%̝QPGj+~!5DY}f?2@y=CQϦJoz!~r^C .׻3 p7 y&Ez r-M1`asZcnMӋ1mҗK.='@pݦqTǺO@^ ?5Hcz}CT4w] `)mIc;> 1/_0/L3F)3<@/9\nXv;e`p>1U 3 }:@/bd/8}j0HM|lrJya'7avW@)> ;/|/DŮM6˭.@ITݬ%7V le9vK!P/* %)cld`}R4@=X' ]6S{z]Ҹ{G %F#asW8e@B 3`b ˊ̭ɼ (ƓWZ~w0)#mLѽ7K&f?۞/uϛ$ >N(sק'P|b)?2G%\(CoWӪ*$ʘ_y]6?8`r Fp^ޯ zAz05E(= 4$>` 0fĈ^BkوMk EcutlEbz @^0*'H&HztR%٭eEj8PcQGG[-V =o^ìߑ]&%,e+f  _qa}mE>#kwCRn<$򾯗8IG?g2v7pU,kAOƳA0@v .n-oK\aOoz_ן͟9 񧟿g/A(vWoch"AȝK%fN/\܌,alV"kY QSmeNv][npj@@uM@U ܕaK 2K0k \"JƝ_v{=7}z}M;MGY~J/M匱-vz6L.( p`=PsV ڽ#17 l3/\!wky)|YW qpƟW';?NVٞW\_o]e-ڐ7kz8XYǷ +VB,:{ br B9zOG\Ը0EN)˷r͊0"lp`fȾcLw*nɚY]-J~XX;?08V5ouhGyZ,R˘A e+cX߿hOg8er0o OxYݦ#4&rxY3̠'Hz ruCMC4ǚaٞAR̓}^B.i`{ mb":ўekwn%aO䃐%s( y:}?~2-L'NO6_c/@o/&1.UtS74@W׵{#+CƖd#^"JၱƝ} `nv bm6Ɂ@d~S/qa*/ ?Ú"$=cНAy] @z[wEB5b'4jIUv:]t3" !ak,pFz^U$9Pv;cu_}B/XRAKcV7/r)2n>,z)=߀a"۰I <yH_"BIZAխb%~aDx08%ոTR !Fy0Mf#vk,wqboE\6S4͢tEW'PlRS * dU(ݔRZ)XQ)GCg0$nm=HєX}8\m֪B.MƠ_tO;B@" ހ r XZ7U:ۓ*Vw:5>>Xaō<&ؑ=!iz&S׃$5Q^q޷by}y3vŝʂ%OY$=R–g;țE06Nu/5-(Y}3cf/ΟM%=_eWmnC\=<mL׃%(QZ1Cؠ2>}F?!oEfT0TYnIdK/'X J#߽P"|/N X j#08̒4L`K]UdK9ǔڎb{ 51̩݇PEVF~>usv^9;ypf%0 {Y.2`i}փ9d/ښ*2BT z9'&ܦ'E&!9YyD{bPUSf=O_n| WV8Ƿ)pbGt@Z =շƒץS Ӟ/hxc3h5FJ냽[+c@W&?3,5X9ChEO[!\i2FrHInٞȍo1\ȴ&+(i+޲5Oi(+R<C@ǦÈT,/()V"Do I=%cHN B[Cc@9^+mek>`g:#X#;,v.|J_,<ɀ@y}9I;!4S*sy +n=kh)1[&0 @8@Pj6f8+#LvWJyom4Dh]*'^"A-,Dk/LL!J-8V<a$:޲J7H>mP_xa#qKIOU6:s#۞ᇟ/E^[t}Hb:\o),b,CbtVTbMsmۦ5X/bd`006: 1BڲeZ.UJ' SkHlg!Kd5dso|X԰D7扥D̘K}^n QI)`X$R&@jR_MC6cPH+#&Q>ci$S/ǥj$R咿1dW С/9JwA0[_A}RCDlҜD,0Wܓiim֔>;,`.HF[9.0OtyOh(v_U/ab13c_Yz텲Rރ V oWkx':Ëb+[K&_+Fߧśq^ vW`Y8 gMMx$-nN :e{+bmhhۣloW{~HTaJF -VVŽZ )m w*07"`|^q; {G/dboڙ'^X=C.`(͟Ӎ4|& dy|3(Di %(#-#W8 ep E?G++cMVkb_f`9dy C$wan ԦEG)4}q/C?C6@2Fx12~ܤR σVY G@wE3 `{in>N#krOZ]d;SLs3b '9ΐc, $ϘbMb=}ng'\}g%~3.C~W-L? ?μ| \Dq`lbLޣY|d-3PGaZ{a mrbSXݚ\[^'Yt|zk{%΋#4200dnr<T9r?\9H`AkA_S[>FN}anxM9:@`*(+G'ufFo6aQHK՛rl#氹Tf{,/`ypVrd:,jCpo+yl P\)3dI*~ c1aFvFHI|G|(y}qǚQރ8T+<߮\x} ćLKw $1ïb?}t~^*;~{F!O: _f4/zOC=@?1MHٳM|qD$Vf(6vtSkmX|-Miv{) tX;7HpLLJ]W?RX]IR>9媍&J+cX܃4Q!H%Kʒ]ΧZ  1̽Jv8?9qpw*%:3;òooY& rxrىDavty*VKNR}sw yҶT/Mpoq<Fb^>n4hrd p+_BnԞGf`ʠ,@v3eҋAbBOEsQ r:Y [ax;n78r AX7Lu{Wzuˏx=}8;ݡ?4~R#oFϷR&óOSb~}^F\ e2`l0Z.2|.w:cW\6hx~ PV|{{-2>NVż(XR3`S e%룗64ZRAj!BgUh"DFv*ǬȵWH p/"zb$b1 N Ir}ot:v0;_TDП;ovt3w^N~vsmZ+?l(yK"GLc6 ~5x2c9(L)K6]e(jj~|rWcW^'Y u! P{+r هWoa~gL<#|[?2 xv| 2Op7=ćd]61qCo)xc9+|, SX$+ tv,``¡M]Aj5*B pS['zhw1M=^5ŕ-w;߱#3e8,s:_G׾I="r#א#: 2*״הij֚l&/Pat[nkwkodsy&\c{n6ƾ5p<@_KUcybdЀ6w`;NF#v `:lTj 7Gg`wL?ud9&:d9+aF+N{AT={lIe=r$ a S'6״(x-&hf/0g u=C~~d^gz_0/Q0ן>R{I~cRj2ˬOO՗L{:>?o? ]o)fep7% ҼpPFa- % Wn, VQ,nu+h< d7p{yN[cg_c!C&фS x=63q\QJXC]h^PD2k@> N+S uM6S ܉u~G²GD*} Pf'됬 _؃o$pTHUh_غt[,ZI~ɍ$2*+OM!T47Z6sH'`%Bz&g`ꦯ=8a=)ZS e)aO0OdIsǜܩZS_ 6iR-`P_/aZr8y|sn;hpڍuA5= 1B >2< 9?LѵɿH*W_2V>e/W! tvTk7#p_ߎgp\mȮ‹Ϋϙ],<$1 [V |#WerxK@`6nEOIJϮ1p*Z~]B¾pڀ`F</ Eo7zM %,3=v<p\ޮ6a'f'DS!mܙ:p-fқ],u^q@or(ɷ*?1%(**a|N& ?t#;XV`[r)ÃTX nv}w;_ƥ:9+/{n5^!gb{)5 H?r̒!f9;O&wp:IL_ ;'Z(])2 yW"E}BQ rjrAcGi4`!ekVACnta уL).&ȣd^#nށG{&. ~N_F ׇs1}?}-ޟnUͷpԇa%s#Л}mwBMlq5M`}3cܝa+17"f0𪍐:V_}}峃Yj`xT* 6 KCdRX[(v\WL KTiE$@ÑȬ9*)wrbI7v*`Nx5W%d5Ektlk>b5M{ l_O9^oGc=Vm'oc" f߸eB_ pomZ=gF _0{uAr4e^ .lo p&<36_N?[ix?݅BwPbl#Bf/7|=*0E@ KYore5ɧ8=6ܡ92PMF`oO`7 8ޝ,1&bqÔqBaI/X8šn gX5wFNm!jz3$P\$@@I3ib1C &_!+QWN 0ZV> қuF ;G4 jy8S'=1\e=v-h2~_cy-zsJ]J2CJ}M=%T+r4>beb-knHtO)>n̯Pr~y68<^/`m7lo"Ja*m>ZVmÄ[z`@|a~]wiNI  |F$?0u2y=3\,~#,9y,Gg_c ?7p"BCꫥG/ y(bp`>(lӴgf649-_o7ءlϰ3 ܶ^/hT*GB=`:1nk_Hr^./ձ4Qy\Fuɷ~Vw+q/fMzm{OЀ]`2 C23g[&ة7gpi0G;ޚ!BcCjWL.`F^lt<]>0\HRTF Y6hu eVG2P4%vwl"p *iaer$U,2l92`*BBZ[M٤B\o9&~S-͢ӇY t3l)|tgl o6u׫O؇( w);^4ވ|@pʀpk/NoN5Sοvp `ؽ1Aǫza_ryx^ zwx{^3].03eRܗ/0&aG6\!.4'*Iao0:+7Fhp 6 !=lx8)@uHgdis5$AW\Hez+ LPRF %5kuaz~?1<~_c$/p_?9Cē3>zerB.Ba Sj4 RL}@e-#gk. E@l,BLcY{xU~ao׀xJ)l#.Jh Wfˎot\i0%y&i@uЅQNlk |zzdǧ 9 l-dV"4^r(&> lgMgDŽuZ2QNwCGȤWvXalbB939C <Mj*Fk Av2'a-Ÿ.MmR| s_C## fGw{6J(߾|}'Ց2wa+wVV]+lU|] R ̦mol^aOgXi|}?J>}iJ4ҏ2-$-0(57rtk p47A }R§Wx.?3ç=謞nzZ?1S?L/.?2ƋrƓ 04ZMǣld#wwrv1%m {4Wf(-)I9˫VekiKH*ZV&-`ܚ*2\=*0K_>Y.$ v?@jV1up(&XDk| d/IéIBp>"p!@e; )&Vo4h1GbAk%``ĄXJBp4t @*dJ{ȾIݏ:@SOp ^z9ߙ;̇^ᛞg [# q~e% icj0M*+azȒ; BRRr&`>ӛĦrfx>1mN`tJm=2D;X}}$szP(XXQnwZzQfԘelvl[ /RmSJ`r @rXkFKܝi0S^[ju*0H%n7ܸɁ,.7؏&㑀pնG!NSC$66מQ wÎ>&`H{{Tpi0`/ӃFC =?PB`*~+CJ0P1 iKkA׉U־0@t+ҁvZJROĉNѺNGF=jgnȽ:Q\u8pr;}UX"51S@"E=RFecC2b`[ ̷!zRY#RV9 +c5Sⶎ `IZ*hb+3 )ye=z%k SYzޛa<JazqRc:#V({u*:@@Jn}m L-].WFLDލv5Iq-] G*E6c02r)/5XX9j{ t2Ur^+{O 76{ 5qp2 ؁= Xetvȃȋjd5l* [۷:]v^RʲI2־Krv=Ǿ`%WYVXXbs @X A+I@5񝵇I-/7 |b{9lЉ2mB`aPG=7?KCHOpv.w'F0 &Sf5_2ǞR>2n~~߂ߣ~QP> +})u(i@mqS b B"y<8&Wk # 5 ik)"wKrN2Lꢓp|=Y愔]5xm5VXL߮yήYk+qybf+3~J+r,{sjmC28RFЦ@Tl$4v=ǥObH@[+xch6wCCo !U6W>rzR;Q ۛxq\4%= Y2Mrwa8Z ]WC0D°"2 EHs2FF%@ku]V:nVj{7T^#&Jebzzuv,d\y1VtKIg`R6Fv{8a~C "<ߤrfhzh6bmc|Í3;|8sPi {zVn|;/|}}$ҎZ+^l6O 'NLn20K91`J.;:_~.$10bm`6PσO?VP"9\.A[o#\ÿF~oG2orwګEa}Xh%gGD09)˔cؽdRxu{v9M$A}Uh{Lvb^\ ![{sdEf0`czZ\חL.7.zΕ?PJ^KGvrZ߫]JZ!Kf@6?HwnArTzXo `q}ǖi/C$-}A]zdeH(T+GLT/d ry: &Ñ`q4(^G+e d6zLmbF@G /]CJ® iPopep}?11%ks=n$$xtAp2[ p\ L,JcǖNuLP&g`F룇ׄ 2er[#m@7 AC{^bha $X^Qcft1a}Z`ΐLV"7OL #6?*+ Oz[ z~~{;un!|d_,Qw7+և&e#T +H\}""u J}6]+|tL,[I:dR0 '\(3!CNl}¸)l,ʊl4hc 4;ӭ\A簔wИ]dê M7p}(}Cs\q8JKZ# |F[cg]l'*6GMQU}3%PPJ?.0=v CIГAyƔ!q<viVY0T#gS߯@Q+@*jM;{38 xEh}4Ĥ\Ja4LF^6+t 4:1@Cc2eq[)( Ѐd[9 e߄Wep af S|n{Loz8TuUY߷ߕԴ 0XV$pN4ux1Dt 1>`I@0. z MMy hU<PfRYH~wKdeLRF5?\pbPƥ/ynhװVU,q#qw 3U|[]CaX7ekd28Rکoב}(0LIR7.e<$k4AXt*swjAG gL&U \$!fp[iW`Pdald[RCT6 x%bs9{$LŠv?DS .( i0$vm%CDxE&*c9p ˃(9#RN b>c0aH FV]h5 `Ӵ3@9q[Q ^ZuۿMfؾg\yz顧3؉jC _oF Lp+fZ2 22dˠGy~[d rrq#NwJH;s)@W$wƷ3x>p{*q$N$G&IDmq~4b+Q? 3+R~zApV^?֚޲o\ # NY$4k4}uj ?W_/!}1VdKEFrߺl%&iRXlE:`oL#xPﯧ, I4/a:{s '~";Ln4F>Y<I @=c8U>WՇ3i/0@@ֻ >H(+Ƿ &|;׾]7<`R," x+|ōE7j}f vK?LYU_/WKpl?;2 0̱u?cجha$4͔!F#}^d$f#$~0Ǿ0b1I0H&MƳ9R{=BArQBAg: &d@šJCEb8TZrt+ 6Xr)}xR瘸v{-%btV15` Jx~ .n{ `4C^$u`c4;̠ ;  N-#ip5pR^U4b?盃Ly4^@ph>T<1>20U^?~<_kX=k߂ӹpݶ ,hX6L;^YHddP;Kj[gzv7%vaj@ܘj`AD}X1˕1B"iJTR-0[ I(].qv0d >v2T/Qb/a%N߆i/#@i3Xܧ1_>䋸8fT-W*P|vQtW.[w]{m=Rri0=b{z+]GgnHZN}@}dGH@$vھ5{vM#,g.\&€#QrY8R*c#Im*FVfCtWJVvnӕEP!Qi %~1;RL«vJ ҷ\Moh;#ya 7-&݆r:T,qmZNc80K㡽UV'Hk Yߧd?WL 糀]³< ޟ;<7@H۶K*Јv>F;U-y(HTiNRjNq5Ey%p#A7UfX>pqy>xyyg08B[w#QG r7݇h< :;\NfJ&FڽXkIOvV\_ ,0Z>}h-p_CGz (D9p 72(`"u:<ĠMZ w^+ŀ+ MRE{ Y~w^9 IK47YYcNiRz3'4gw>ո1M2.cߨ};XIla 4Ƀil9ALUPۡszbpgzb ΰ6O l =kLBoU?7A¡pYn}eOj6 A€Czf4I` .hanu‡8j8}a(%(`2zeln)y9Xa9_$Q6P|&;7t & عRڛbX)L~?.}f`wzc A~920(wnAw!dhiig@zsrY :se]ۋ{MH>q]>\ xtn8"̓DOLu(sɮ<㝀v1OZ@δs0tr ,aFnX~dwnoxIne%u`Zߘƅ= 0I ceqlӄA;]0Sh4ДΣJZ$0g^oo0Slo i.%>0DZa=[z(iL59%aE\5r%Yh t3ާ~>ɛOߧL/\Yd,O 0?oq}-hAǦGdu ,/}[?0/, &ljhv $wH9L땙 Q v5Pzz^B|M)Czo t=@qv .@,X`P`pAAd'O8NI33gff^6`ZA2oن3ܭ麿"5tL/󓕖 ~rtIX`4L NH*qA f/K֪L?@ieyȹŇGt++JB`8Gy/;_}LXtO6B*c TسJO`0qҪ .2C`VlPx0 ^y'M#+E]$='6Yx6EQ8)QbW[I7bn`+*貜*q7Yt9r^=w-#9# ډ^qL\`ѼZt\2.d-Bh)s*f OAn徜4 >;= ,~Lo== DZֳQ ;|}Jڑ2E-bWzgf8[eHzL-vw]S}Q~Ij`5 &gme~+}@QI6V ;pWBBav7|ݎ tR8{JE|mo|߫u/\]_R_ᷞ?=077e&)q MVoҕj\fdgDCCyv{*xPa: fY=* ߾{`!!ڽN6iDD엝Jalۇjc1Uec&`ɚ2aER;L;ϜѕS+crs54Ok*6d3`v5Mx%{wm1[4pھQ옫hT339v>-ݔTItb *iO۫t'pf9S5XkTbAj%6;ߘy \jP!߿}04?KjR R|^(ıD:ʰ HF|ʗe}!FƔ 43O c!V;0P*V>9.<8!QbWT,* %W8ZIXI.j MՋsVy/`0W8JG !5tryt;5l BRQ_OSe`g7zlgM@OVYj' I2qVV>ϽLn JϺVB܆$k7J*ZYpk_FOe({1%ᬒ,\;Ǥ5QΎګuY$6sֽ#ŠiJxdRL *o za`;\ &^0] t2syIm-H'z xRw @&KzFR*" tǭ] wOy룗跱Iqxs#Y2w:.G80 <4e;N l6zh}fse>$;p/f7}~~O+αF>\U-WAz\g#~->&Hcƙ&H|gվTsA7($D%>~5u`Nӛؘߋ|7@]"2]G0%CD @~Qq|"y d*Lʧx9^e~"3ŗAwpZz~b~ hs 0.cmo7,XGx~LPNa!Ŕ=# y*?z2؅ί HenYPe4=7e ^=7U(9@d~JL@%!q1 bK)@)<Gp}7cNܧ9(jve FX1k e3vhO G|,=!Ν+t<~ f E]<dn|Dp=)_$D8t7{ P. y0;2sK6 E/" &x}>;2g_2uESo2|`LF1Mr&Xݳߪ b?5$i| ,&yMV2ر OfAD5R밬b@f\jo*~{ޯ|(H[վFOOkw,Y V\Dp:{wfwl"h]:Az~ A][b}d^3b鶌r1 ̦¬鋀nw841\(WaΠm0t?#W'fenaPrzwޓq].2![*콬]h֥.*cu76=ARՍRϞ-3fo2x2y) !ݷCj6 #9]V|t4F0kZŕohi fI2+/R#uL|e`+kQfi1airR#hH{ @*)1 60c.`~a ǞyoKTi֟;"C!$1oHcvzPzP;#b|5P==Zz~7{mm$\ M_r^Y: d |i 4)!td5_**2 ;woLoA ,mwb!cse,8 M)-K_?`"l2XD6OIDL Ԥ;fLV]wZ蓕vc>Z LP1?$جhI@oM5 Xy_6uHg7bb9wr|م~^Hߢbϱ\_L[^e2mWZ= '^(a&Y)vz'7wU ;*o[zUT)Bħ%7$"ɀAl8[c탖]g(~joUi'\%iԟ<."YbN@X!e/;.L.󶭤4~]V^ ^E\o.yaw~VViq)?oWp9l}#K0kx?2 96[T~az]Uz%/Zr<@rf2/-6?|aM |bHؙdYnXۜeBq,*d-S.AgbQ0G&Ye2H .グ)ҫ^Qʩ揵l5X{E#qQHwnTQPƒLiޢ v`+[V(36# 0d7u~A$Zkyؖ kvi(T2ԸeVe/e-*9@5& JgmC^ar'dLvAO}ɥBB;MDg5yO:%rG3K Be̘A0is?0P*򘊯UdYF\z$wb=Q֟$H [=Jއ%Xg_a!Ğ1r&bϋ6Kx?0JfMrnۅA-r 2MZ$PQaJ>~r|Հxolc?1dϷk @0UgPdh@$ ,i!\Gz|X=(c\>Ч0&zh5/y›U: 3z~~d.{xNnik]l%w-JXOQ:0BH%l xUmz^R"EwzuP;m0gΪȭQ"HE8Fy+i.Y2(pu8F'Cp*F+&S9(}c@)&؅3k,|2%_G6" LԸ۱"@b28s&Џrr`JV/{%LQ*\ ӥST#v@SpBqvraKD\2$:Vŗ#vnp?hRKGN/%\u8]ߖ!Iѓy%U"mǴ\H(Xɻ(mV7$nɽoHPa@J^*C; PYK&LfuTΑO&tok0i#F"2P|hˣɡrX*ot[.s7x*ݬ,Vvߵb߉d/a\dMGj|j)Il 'Sad2;V7Q8U+lŖ̓N42f\ ^5z?zlϘf,j x<`B,, [z}k=[Wy/[Ȕ^ުIrC@Yʷ7gXX) -!vq}kʅ9d! avElbÞLL{HfT!pD7hd@\#G3xbϽX%#*;ԷG^3H*^7={W|emZVWq"=haWڲ@5ޛokOS)!sHMx( |r'fpmLQ: >4yq3nπL֖;Ǥ u\,6b(D%|A{>Z7lGcw6YuqCmZcëlpŕߔU7bmK߇F.G %Y m^Jn ؇n}iM{] fdDoqݯ}UZtyH mڰ~SNj~г1t&Zt: oQ2{L)B ¿rljxo`5J_﻾I ί5kٔ@f8y^Q Ę&txs3dL2Lg05mϝuIZ9jq3Ec$4xD61 B"RRV;9Xt{h:F'+$:yJ6M6W@fXz9 N^r~lRO+&usS;­p ݟݠ=G={xHXID<[;ˡV q2Nݷ1s-+|3C@0u6Xd2^o7?qZL@\W5ҳO?C`Rr`cr({YZZs(J?@YHQx^&ϗcVV {c!cɖhuw$T5qwlR7.H*}O `WPQ|5g mgv#·v\ϻY=GZ.6(Z43=mmpHO6_٘ݥLA/VѴ1Eb %larxU||HBY 룿ws{lfAo~t:]kY"]ʾ?\C њ&z"fW M9F}_H`oO_6K 340'n!yQFޢK7IJhGj `ݼ&{<.o4O0zjr}n zߋ`E_=D}OV9?ԫ$pdYxYÑZ'_jyH6 o̧9OR&c<=^A mO٧E`n %%v_g2daQptLlPfLQ1AeM^1jzWFOgh 5"4P٥>)PW9v.yZx>@"?1op]_pCh1 PNwn}3b\XdM}7fkC9_,wT (WӪ\GLpl_2Nr&7ɶzk8K49.LPp!eghDk/eGV=n @"W߃[T X…?I*60ކ)ҟvVNO;Jy{/|u1[(D9H h-SAΒǴ&,+\~g= MJMHk1nj%Včw{SklpVh0Y$ ^vi_Y.,2kp:_i cerĻMlz( 0v+M6$1`J*M !^^wJMFiDаpHsr  k<|=xr^()I]Cm`j[s]yoXm*e{U[%*c8H.@#^v50t?1Bgw8okF`zsF:ّ`YwlH< >mXZ *]K/+%XA;.m/IeX65 {CAl.-| V|0ָ 0=E5PR3Ci**%s̓dZbh&1%7&doz/1qF㽭Tީ+70SYk ?NB; Æ oCĠɛŒu0z@OWFd<aM XGyU`W6`w[1R_O ]gU9XƿߤZ&,Y=~gXi+dD  ?TtrD. vBV]I}P/7AV@0ڨ*TuZG# Gw1縠(tRaBllA.6i| ;Cw<(yLp/LHUr!Z/rz{p ~,,0]ztmUFe' bP;qkG~ v?;~Gg@;_r\(\oI[Y4Er?T9udjcBܯ*! 'iyUc3Jg[:6c%Q#Xd~} IeOFzՉ^>$p,e\_Ϝ5yB}>޲,uz5=`d E)\?p)M05rOgLր`PqAx./cMyY[!gۭJ嘈B~w#_#'4 | .yj72FfOܧ^=̎ߌ_"-YbU_t{u 1Op=ib c8Z),Tev!<&- PaFo0f)U@UN )p.#$] 8*Gtoi1U [YlôO8%VÛ0k){a JX++_ -U2L=/=Hw~V zކH(ʚMk^qߊyn}]AEBnc_lx-h~?_EiP;1 Itd1do{i)|Se]bH[é B9y}ъ& z{bΜ4u}={e5¬t@€I@3sƘ>ͦ7,wʘb'vAW e% (v_C ;d'6bITϭT `9$ [ [#5qQu)8~Rv G rһkФs&q^0RJJ]-re&J2]Ŝ6_ֱ[nZǓb%o#+C Iil 3c3 ŀa}PNq ?08\d26jk/՛   9r" (h2z8`_"J߆Ł8R <};3mRL^;*=-v~N^wSn$lfIHj$ף]uF@:uzwNJWty4Sr  `x\eYĕp,b|e|O'n2#tKa]VLQLR^O{+wf`g1C&oV֩X'Q2:(¢< I| v`*l> Y:K,[bx^#7Uk=֔+ȽEsU RYjO[0&j_D?,&M#^r^ 51>(M@*h<\g=3'"ohǬNodC=8:[ ~$Eatn<___Kf;ɢ#+M[@bi]4y0MzE˽=GY\YC2}}ejR $Ǵ!_; MgL< ~b/ǧtWk:}Wj6yܝ ލ]+ onй}O+.hyyGN0BeR%p1G(%ėvMzaNmFaE^,Atx#!*/&dburL^\㼜-0T:{!j$ yIbRDD {䢐zFU 77-TV[.kUϯԄZ/@!r=|,1}:ʜѺ4tSp#u2 o{ | 0~G! Jȍd%lݍ "+ MJeUVk@0+`-]ŜB˷Rٗ"ļ ,d101Hk cz\tF1466&=rqaE'@:bvz}G%7F}? g)vmFdU/w&G)]ek;77!jCm?Ĺjxs0ұUo`?1ߙL;ZjdQDbsE$.FGfWG솧.9eڸ-uH h.;ljRJ'z j+;L:lTչ2n%p%s+} KQ0ߎV1=}'!xx̾"&u{i!gfE<(J1 f{e?#˝!rq2,..%YńAG? S`)W/jՊA39=-"&?"m~5 _w[1#{2J;5"Yձw-Bz}MQњN)8U@ttvc+Sk`4/nEh} ڟaW}n vG6==y LpOcz|u'&7V{AzMILiW2Q^~w6]|˧I`іl F)!he}µ>YZxǼ\mlN`x ?1럙E/oB;'FմMaPRJ1!dz%05Lm:^;h Orx:t^汄޻4f:Y*1UfLϹ>s_w?0v1ib߹IzmrA,W[&}ɣ1 ]|aɉfT4,ĴipAu}J`m pb{:v_`NrLrFu2αWX@^L}) Vo1H"h4~&//y!t%gg }@1$Noy~y8h؟_Zi)M~篠<ۿ/ O)!'pqqh-fgc(M ( dGnj6T$27@fe/ʭpcVDze[a8$,J /9iOlLc8ĀA$$Y_4w.|zsZut$:~?_1!aIY7`̣${]@Ӕ01\w !٠|&uHR!zlsI6h{XERk)ò૆#lp!vt 'A`@2 Zc#岏oS:{ ,-?ݣ.)/<(, ?c^^\2 r)2qq(F 01S֚(| O 7/ґ3 @ȻOeFD{< e_~ /'pxNbeu)}Ŭt0Ri9a74z'NIP44V)3/k* ^UZ6bԸT~ºT!|:׵p<~ П5Ap,!<*7$6&] ˦GμGmy.m89ϩ㹼wXly-aф@iz4~r}Ʉ:>Lĭzc;_"}O+~Yn']OcHKJ~fpl|6?hSW|&! >h'wDuusA}Rʔa%+mܖc΍%Vx .6 igVzvej7d|DQVGM4%?%At*}mUJPd`S;\}*$wph<5zQ+y|}Yw!em[/@OLC99bhz cf_Yt2۽aiPgrQb&O?cg ~7esɊX('+Rŀ1pWhD9 %"iR a<5LjW+0,Y//vBЋfڻpJ4q~jK%muezJKzƧd*n/F2Pn}cJmtwӴWٌ֎ ԰hgM6Le}HbOFg6:254lv@fKyjׅ@Й{&wʽrI*Z'[o xu@0\PгBZ1P\n5j>keʼ`0 򲴊u6cj)|N>Cy e$}ݷD ML+49EQێGD_թv) oAn8#30[R}[6X39,1pu~;ks~\-,5bkbꨟ+mr }a[+|RE sϓ%#.:0>Ru5>7'.B`d&ۣ``QlNlaNcrL DslϾƇ$-Uw0td|_1~rx| 9$0\vgǰɝam|%m*mJ}g`}DLz' !iJǐpN[dQ]o"X(auL03_彲* 0ԄiEʯMd x/ :(Hʓm1cb9)0@pGS?0WY  e*d tzf2ܘ_Xrqc4vsj-\t#Ymni 6BH-V\(x=\Lr8Ƣ벐z? X %>fǛ__7>F`$s}'n;+/L”^3YjZB kTraI$r=ZoSYKc_qn]x  G.~}N;DW2 x"c5 ]" sU{b"O/W>Muֶ ǝ=6ˆ?6;`h A*$UC%f(\u)SW;݋'x ~[9|! +]E)m~ `IJkfS Xq >WyOo|e _-%F2/U r0ĦlȺ: vɥ4g h c6Dx}7@S*ǤsyȌ8uocB\q_xƄ(Y߶ |I!U^`]%g!d.ԛK%*.lb@EQ,4,1Rno;&s\|,\dez FG+rJ$J .o&iQvJ =3UfŪgMg,J).{o7So,`M?:Ϗ}ZߣE AxF*;vV"Be2>(b6A iot.5rr+Hm*$ʭb)퐤r!qr=HB8GIs!{A8!1l La3M0^KL5_L5C(%<%aS۶[}:L ܘ¾IöIC.r+v]W{ܫnO2_ 12z1vxFS4y1R~XRm ;9becdw C@Ic%Fmj5af7 ^4Qν' ‚@)zyí{+:!u#$/Zb3\pqM̖kn<:Bڃ=/-hC"yaj 25>}.b0<-!&lҖfrv u=X ++\\=\d_b|o9C[NO=n $ư#H(wVQ@܅q/iח c:lS@oȒ^Pjz@ć(s+zPJ]Tb{.%xhC&Ж+D)Kq754y9ȫM$]@< Jm`Rf p㾪3|0|U3p}G[z6LIBcc[EocFE<b%v ]=E)qYk P)%j'񻱚w+opnjyML =FC@蕞Y;ޔ3ZLLL/Xֳ{&D4P;s)3ՐAq8Z;3֋t,{ٛgrRlM 48 )|:b9-n\0 ǓW;Ž>zyGOɥLO)j`[KEt<;~83{6 %inI6SktV0&&[P ߯#_IU\rYնZ>6?J1G<6#mL6e2K֍J5CdA ;L_{xg e%@|+=}!aY=6^EI\IUNe9I$K4puyr~Q[z*[4(Fϱ-'FNN(I-Y\9 +`LX:XXNχvfǦߌ80]VJۛIֿnM)Wo()zv܏vb-҆0M C\?#+ul2KQX@QڳBCN܏ۻ^;;sc贈5вϟQnraqa;d@ laj wRmhZR$@/xNіɯrUasjni|=WPS%Svo8>lfQx5,)4~{ / oJP,wa$)2JVFnp@mZ NhE{>vfX~sTiPko# c o.h^gǖ^O^t׃e3e&2XG2y0G<|b[,ȠfO#!过n_+^_04Iߔ\m i?W[+zqdn@IXgJ@*Ӊeq,R>@@ۉkwƊv22#Qaob'p|py= OwBNir8~?NzIf p*Z fRahZ=aYy]xLc&˄wx̲)Q,W^vU3D +:Rb=[\u ˫-= T^+޲C^yz egb݅E\ ZKӴcJtǮm6ǥs~V}/LK5`ٷr9#3ylx.XAu'(CXA/Ĵ$/hNDJ|pQ9(rbS&* ew")H@ yR&KzXy0AYi# R`` -/p`n7Fo*Qt~$iǸ Kwuh:1qQ&a~'jaDإ3sxa򀫎T wMfn <ۛ8,`]^+m Iu\ C P~x ,_*י|^RQ8} ^珝4pz_N091]y!WcXv9858lZzq򽱎-V+ij1]8ݡ9,*g|<+$A0 =k"= z~h;W ``='b4&O:'+w1E4Quf㒕 Ԓyv{%gˋ+ko/#z\0wsƧ*1&bj6Vm@T6Y] \*e4>F8" fZ pM;n0unpU?҈bekxc/6qAkݤ/!f{lהږE[kem=<4}tgA6\[sg-6=.t[؉yi`fMaKZo;XbuA~]]Sxψ?I>]~#@U/ǟ9 ~iMhڰU&m1a/x"hrry%]ou{N>LG@u2zz:ne21F`-/Yv\^ lnOҋ=F^["qD냑d s/pNsNOor/O뺎RH_6]7Wl3@gR D@GL7p_= 8pÏs/Gu#I9Kzku 4 mlt,rUFn:TTf2z xzg0-J徣|* lK7lݽFUE,mUӏ7{IW=59T%Hޞ7auD*3y=^I"DdR^bwSvP^ >79<#e{Q)%_˗q{^@ ~\/&_@z4p}|^^4LyIɶ h]NRh찞"C^ jj{Ht^\MZ[Y콽:R< ל3++mz,r+ #ξp1u2n`:յ;K4qy'5_cѠv1;םv~oL Q'-`Dzv?rLh^Qi{rM-ͶV=i7oiS.v<*|J&O24'B`:2Fh+THO4_(s$.-c.HERXyop{f[^p_8:`e7VѶ$#hR*Y3|lϛ%qbeo.μHύ [|noe9EOPҫ74Bލav*&7p8X\݌}ZC/Verb?N~;S4ܮ닁링/o,1'ufeY |{}l.)) t5EmF^EOkjoCE-)H;yf_nw ]zOOҙ/4%[\6wN=+ M!iGɥylL1-i Su 1$ MLݖޙą:-;Depo\e=OvFeI4ljbzU5Á/_4y5׆sfY%jV`XM?uybɉ~}.s1h \ܧG(ȋ}:7.gwE0_j?@y9ȑd/cw Fw;eR)qwJdcshЁ͘3{|c{6PXnXn`| PDۉh pVΎmߌ+DyM]g5޺p`vUCZrE`};&Pk!Eu( EBCW>ȱ題ވV%wİcQhg{0zwc'~yd~`mUV!I`n $tlLFL6~I_Ҵ7>vf\+][#召twJ.m:o0i4UL$+`JbiBmS0# Tgq|_㬳5"&&"XCT1Q'RyRE)#q㹒h9=q9XƔ[5hϞ +UjV&z_nkىn;+ߗ(Q,x500d9 W;{e}Q4~GS/mc 3N3Pz5}=|+ә￐̊ģ~''$bQ_o'+ 7Dz? T0O:/3Id %?W;/۲Ӗkm "dc6|tHU˥t20)R?ae:ỹ64a/Cl">ZzR]تdjv c UK/pY3| +3+X554ƴ1,݃52󧲸VXH""g|XHvv[C}41ޔq^Q[UR:m+ 0\h1Hp΃ j3d8KT*W뢓=> 0po<ڢzN1FңΦIcWXSbJY9"ڇj؇'HG^It M`n}@岳;,LE~!}WcxG0\u> Ԯ՘l}׳lC񥿬U7c#҂`_h̕վ=H˸o0J{NE 6Ƈ'm삁W)b!3Dg=vc@<}p(-Y)5|:|gHh>5}ܯE;Q2N\ƴ͓CG`v*p$ "NTgRMQdÔ aPLV'm42DdM&{Kh.8;v d $qmgmZu?ob 7A=CJZV?0@Hi@Ix4py`<@FQx$!0*JqԂI +2|f77vx|Rsg}0 ;v6P|3\4_JÙE c^^ 5H~dVJjo/8>Qk0C}5׋Idn htXd*yh5>?GO>;u6H/G[ijy/T#\Y&VZL ({l[;vTinxMǞa>@Ӯ?!'$`} 0Lez{ywinҐ?\њ](+t߹LMWNLPKƖ螄! O X{v=>sPUe2a^%pb*\GoܔLg;.8fƒ'z7?9 0 vy'qA*,lJME @$oƨIk9!i -"֑ Z=?Ɯ^S`_qaPDٺ7 }Riͯ_N=EoVq^+21G;K"fja:?7K0%ascGcgb`O z^?8r2Ġb d'F-'ƾn U0FKuY{ mF{JVLs̚&~XlJ+mnXo$e>Ō?Jaoÿ{r p,ZN1 ~ld_t?0SM ~̮uWO-σ*E//%VVW/pnK/Ó&x̜g'’HNsW0g#ZNՀ!,`p8.rsN"~<3|6~xo\J)/x-~ꉢU|v-}?w7P `1~ W OdgcL+1)JXOὸ^E=8]bb0.Ʀ><={NV4yQbq\ez>Ibe#Í_o˂bd V~ D ܖ~"Y%iʬܵb{9wav$ev( ۇz0Y@iL魃}b:TNp >4F`5-ÁM">}[JZҟNy?Ҏt { MC".Gub2,Ai6N§ulD؅'SM.S99 f 6.1yLswT~It~[vnzgb!y7 n_ys (>1ƏR:1KZBt~InߞN9O$˷zkeL ȀFǫ9{\8r=ޭhE`f@8hKHؿKr?(կ 1% Sј71>-vј**,@p]i^*04aÒ$|R%'=kj{I47jʭc ӛE -*6/r0x/ w6B0 wwnM>zcZM<Ã׺ҮאTrjW?X.V~C@׮?*s_"ƪ-, |nc&0}-` =Hm1gi~hK:4s4)x3v{ɖmYGk[[>uZi"ǃHgB_׬F~Ɂ yKZc>_$[D<]}[FUF_U^(e`hY_51r7µ:0!`CLw܎&r뽚 #Fr+@ۯ@ңׯƌ&g>;mS`hL)@Ђ~5]1@:ZL<@7ڟ 7愝ȍ [Nhb&GgLf \^HV*C ,3-uCH,,fxfoFl\e9ꒈ/Y'Ls+- VS#d i M8#Ax nL@14ŝ`>4t&nl !Q !e%sr>0mp|Tb+`E-/j a2ǃMLS91u܀M!`hhb6Ĕ}_3W0BL|-r|63Th }S V]AEf`nɌ` :`Z# H<<[YG*V%!,Uu/(`5"kjG-r~\  -٭u,pq<+Fx(KYwZZ7^!c8M80<|#sb)A2?ꋦh*LmM܈;@7C9B7WdL^.Y:WO30TOU_ʄHS@V84s6jfLPKފ)=GV9*:).7=jA~`O;=(b%0n>y3ځywdcGd}5hpf 0ÒE&@%k[>bnAߒ)/4 ~D?#s ?xV0@\ PA Q\( ɳHr#8Y(eRKY}l|%}WpFB{V᷍0A$RZME??( ^R;;Qnwb A@y+ٸe$d ,MSs%cCuK}}hm@}>\!j=vv;e|9q`9-StsbӾ/׻>1a !ӛݔ~B0-ZkK(B8s>?3x}qa5Vrկ v?_4(a._ĸ |Zrl^:pO]r%JTպձ7o.7ĺBAjU4a^" StbuuCFsa 0F~4(xsEvxÌ81QYSuY9bfJ3TV;Z0E41U6f5i6T{^fz<|8. JP2LmJD `.MtSR_(BoBD fUHC? =Wn:BPzw?9_0]gZ]I.0/wګBُU{\EtX͏(?m' ndz% [QGJiO#^>bflVi5}Z܍l.g/ꕘ2zHyOT 1VY*j&[YT/_' ]P?|(#\!a]Y4\`w 8195g.sP_J5J.iG1&%_};?4wr{x*xgF /R3+gQkcoʛ-nN@.GX7O}EQ BX] DZ9w*>ڛbi2@r)fKxLi .2? Wk(3TfVtsYxQgDe!xYngWO(aFG[-N5[1Y+>*m fNƇU~/YC ״,g,`>|rlH}2({ǻ-6>A\7I`$u FA<kqjރE^h8/=\LqLQ2@asKiq5S_<@4fz0R툭->aw deXzG!e//%kRxR6+B<"]X ;_`U, ? /2s XY`l%qV1⒣˹+PC[ Z|m ); .0M8w2Dp&+ `\|ۑiJ\9j Ɖi.ZߣKt] -C;miG_k:cn1ԚhzЦFv/շ$h g7OXzO!^9>YD+ЉKM)LxzsEk0?u*Ƹ*DǨxTdJ(1yIlsK-  _]v43kʌ 7*(NMpM}7#3:pn; KQXE=MVeH09դ;ue&JPE¬P-frEmLsj%a`ErPOAa%me{`~z3 aPA7`B -tЌqnGO 4;R'o)fq'&&0A"l[c7lХ|.m7l| 1doy~=F(8b!Y#Y#n~ȿF{{jRb8 {LYkRsR](ilV19K@.TIZVBpRd`럏㎂|;/Ou .@U x2-7-2S/TSHjP6SdiퟜX'=B\`f>`FL.JgzMf. *a) Va ol5RG_MIw _ar1 0QWh>#A2RF 5L5*\&(ə#?ǥfHF>!okDH&LQ@Ũ3oٓE) "*:C4]0u d}-@Q7F.CaPݞhެi )< 5m\ VFw jFH+|22'a96ɵŮ봩7|7X5<ԛl Ω|) w0qt딟5`c //!ʨVV%B1'b+S:ɟ,'?'@@0w'ӍnqHu.5fX*=S+PA`]nKKq:JHGVJ0'a~" {fR!V4a2QrÄc8PƮM=#U7"WvT3$T h2w>WehՉZ} yMf 0%/o[0Ü^JK kWY E+sX,Wn/oZw|K! Vea!"9xxx;֑i2#ެv=;^JDkCuKn6`~/+?c.>j̟-X<1h:Dgc_0G1k{EgW=Cn!$@.qATآ^@،oX1} G03P\Տ~-2?˟,lq˩*o(݀qBכ"-=d*Z\3vc[#B@0Dm~A @F8B02u SKoC[#X[Detߣu~1 9柙Ѳ2gP65)_o)J  8KAT<5>Caٝ@h3 l+:<" F1]]`/^ %0| FW\K "[z2gOq gHï*wVj7LRg/L.;Ň#;AHZgke rI+[XbZY15Gxqioۣk80'1t>9@Őu_w+o joӠ|Zv0DN̑[xo  'ո`t(ϒɷɲI+دWp Y .ҼM@A5_a#O}& 6yb Kmq!&24 YoTlK48jbj>< ‰aiL?`cͭ-z@' (-ӆ\)K 0 ̼\GLlQ,0ao2S Av* k.L="^`[vw|~ee=M5)0W9  te>yg}|H8 `|k)Xw?ʹ~4FѢݍHJkD\ ËZ@dA:+dʌ M(S #ȗpL8BJ\Aqƕ71!uYx &0+ak "%o|fC> `Uiz ^G>jXQk~n8j+ZXhgP l-*UE!*jw?E6^!u$7ܔ)3o1 v$<"L`a8˘6T9v'ᣰBU%;3&\VS UU4)zڧ dWٟc lhuNpf ' ο[y}HWCenIX>+fhB뺴0WӇKm}?ۻIQ/M<0ͅl% ]+Ib6tf7͍Z .,0,*L/Х0? k0sK!EL)f8:Z(3 0^j=H}'L cu`cy%&>13ֱE4?l|#4"T HUn`AH|#vs V" !aҜe7aLФ4 ]P4OU<6 y=dT`UDMs/TI*GY08O Ͽ-to-.(䐅= `{=X:ˎ@ܵFpNbV<k7 ? ,Χ#-` s0"78Z^"mf ~OO||jٱ{ݹd@xRy[m֏Tkt с)7$i4,rGD3- v5INq-n@ <{5L?؟p!9}"8 Ҫ3BCs# Y5nŒʳ?-([%0aSO[`qf1W!pS q30\4A [>QIXY?_֒%1<J֪Lz_k+f U0fXkKZ#3`cQ&yh.Uv䕩JOu.eI:LQ~/Ǒ]oԄiB | jBܑX^rM_i s#Ť # t.&R(Ejm CTUP ͋Z.v)MYEI ۊ2|`0kOZJ3Eps|>BKXmFF;Yr \_n%?2Ouc1^X +srvy/C dϯA8/Dz`G/;pNlxfe}ߗ(1sjY`L=p#)2Aɏ0oP.XX_% R?FXd \7YkNj1Y^b2OBE#)uV|@/UB*1*^8 րHf(+SIg%SV!z\J>qr7bfWN0 (n~;ùoBux?j+ !$`Bcrև HjAWsfӏg*=?#{@!b0E$}<MI/EB@~FPF_Zlz_zȲLt8o 3+e_ȚMq80@k94d "X}UD?ŖUn:s[9Ƶ?0}Y5M</;WX'a~Y8kF>FwCs"WM!{R\'ȴR$sc`j)l,΁` _jEIU_}(ـ^9PVA[(kPQ1'ը!J <4S Al:OXzx iC?Dؑ努OH/A0RʱZXbqkCIZ*af)!a?0?z(壀X'k;\  .[_]!Z[Et.@EcqD(.4j\7 xi0 zlncᤴQ@=l^Lؼ kdty|̅e* G"RcN[B|[j`fթ/&f*]eNDkSWe vMdFX"T&~q{ݭ~39aqCA\=LX,L ~1>S) `GKNf23}i RR9S{4xe 1ϒ~-)4r:Kf`8ا(o}fDm_\R Jඔd_A82)Y)n͠c !~#O?6 {P02(/6W7olS ?#{9w9x؁%kA#`akm92%$ `YOX!@Mb9t 478G7g{4fZOH 3?vmys9-bH{J[ʋ;D#RSZa*VhH\p4sZD<$~"I8-~> +oO2 Ϛk _Ti;kGv  Ludd.i䆟K{ci!"\LPCAiz4 iE{|_twb`)0mtWh0A~zE _r~vQwl9AvbceM;fDXHޓ_]$F:̤`Qc9ZL'M9=\Lp8ad)70KH6>'0Tp pc:+ [BF, K4%\#ƃ 'V,l 5kT9a'@cE~abo9 |4gKmڼM*0DPͼoPs+K]2 8( YH1gP9*-hn~DE}W_s&loʾ^ r7&iy/ϯ[m¿-#PgZ'hߤ1t7ɇ @p4R6@ۿF~>C@췤o <4!|o.}$cJ;fX86hE@0f4@NUL$;]YzKVCi"&zf{s(iDgccهYSc*9`:s4GD= Z@KcLFT! S+a2wKd!?qoEB:0@L N߉ՄeR1cgCX(0  WܠgLV! NEײod;lB>G  MG O)H]|܌^#??G^ۊI(.~cS w=V-VwS5 6s4(4T"WX-yh_<٢@R n^!=GVG1.~_pۺ_W7W5onT9S3{#L؛M}$BCUzaJRb4`!% Lv_ٮW U}!cTc?50+b<FQ"&1q>=Q၈?>ҵ#p)6 ۦ.naZ{1C ~E01gF!ja3ѿe\[XWmG*!;><_ނ`-*Amhf?ߵMC0|B>#j[dIwCtt`2q_p/_g} ~5ɶC/aO‰' 'C$& 毰V>ߑuw~OO  "l!:&|]o}Z9] &% /gVС*` TPKݦodҽ-s]>(/ 19wecF,|]\3ϿYyaHp pc7_r_8 `dž]L=K1w_'SDAK}li XZmoe& `,"L<c[M1Lm,͐b~Y> +@8H8ƾ,GHӋyxi帷q? &֖\ ̓[yS. aݡ7K 0sn(4*6Y/kJ@l{krخY&I5|)sZ9*1*;F>FowYǧ0oe4?>-?>H<@ע%^Iegy#:|CYZ KnhQd0_߰H>(p e5o*2Żrڇ |iʍ}c<̒c(@J5ƅIgZKM"H1 'G\[1o\|ca|>To^[U!ǾȺAI[!@s(\ ? b.uV T (h6ps ߏƌ3*sӷDZjv;<CI`(f u iW#("r}e޵L>糈]?k) P-#ؖjwYܿ3Ԍ}"bʆIHߢJqUYmWT!L 4 G0䏲o.ď{/Jp{Ď__pJ9ߏ}oᯯb8fu!dx̶^}~_˜~9W>^MIEJ Iˊj1N-> ֯KR-sj/I(`:Dt EP.1z~y1 #W0F#ւJOCu;Ay|P`Bc!|#*bRr΄hkQXf%@ 5P!e=_GK@mˡvU9D`< J> s A:c'Ё^,hAa\.xNf\FP,Q[jS_0:F+ SrQvQY ce&J8NV /5'YFH!Qpts06hbB) P}qbZ56h.e"n/"K #E5c/0@4Z_Ηrh}0U-B[T@^Bl9Km8V77}^C{B&]Кy~d, /'Ckߥ|ٽ0i:VJo(T]\Fw~󞦁ܺ!/PF g\#_AdXebJ/yI䜫S `;KN v $6h^8ba:kõAp~T ~'>vl&G 1O-A>ϖh#ll/?]ݻ9y-@-јoa۔\C.%+LU¾L}uK2!r+@l0B;yXWV773XhUkp3Oq 63@ 9mJa @A_] ڢ)jK5 $"3D$0wndkofDTM#{).dDM'@~+YZxg돾i/k`8=9ѱs8r7%v–X"Qf攭aoo `g+C;F2|{uobKCۧE\+72Η@A$fMn ׆^QUS&"+ɜl2uئ 㣻A\KhA0)25_[iNJ{xk`(Lh2#xyTRuUS6hc؜‹죘)X1PBfsc [f4.| !/b +" ͷv3\BO"籶ׄ9ܡvӼƱ"el)gB~ǫumӓ, Ժe?J |\s#Lg5-&cN,^gV}5Ӄ22DDk[SsbB$ѳDfVm(~$LlF] Pmeg;?mv~q,w-Ý]ߌbfvJL,ꫵU$bS ё1z]P;10 Fc6/?A ~v5VD|,hycĘ) * H$Hr^g!atJiW.M-}lyE;Ͱp= {/"|h?9`ZF^33-Y%2w3-@>RLeݸJ$zhˀ5)}|0azb `87 f3~g|Y.j%A4-[ &7$+I!Ъ>H˦`c{&;Ebsy6&XRc+M -A?ax]hh*Eڲu)H KBP1`PA% b ̛3S;fa';q$?˽"קlČ\ (GndS06>cuYٟHq37|G8~j0*>77Y Sy@Yr'w,s\[m{C$9Y1PlIKnH%D%\^/1dy1TyM}{A#nkǨCf5Js4PtZdS&`X9ꃫlx?}]#Lqm|?2@t>~QV e(-tGmD&kS"X"(uLm|x̱?WZs!hDRZx)p;`@TU)19^Pd{s,yqEqE^1 KE8ldLIo9Ns)Vy!)`nK*Lǂu(+51("G2hhRM|"#0:.BFa^v%n }kTw(2=/~t`c=M0~ cZw Ru#7}M_ :4]˵`vK"( 3Kͮo٠o(:arcokDFg%rx &ڎl,J0% a65wl9h^Ap)7oTUdl/gt5n;1lm1`Jqn8WF(u Yc|-P t:u=Hb.S'GO 1Z?V4EO/ o [&W9pc dZ@̑[Ce/]c%'n|Θ, &=\27ًmCA ؀d!lif;|`&²_ L Yi#% s,VAt5 QI!;)"G:4`؟`hT y81u[KL~$BDӁk:Gh,Q7#m(+l݈ŀGJ .(j4uCN 14G(ރ!`jƪOyyl{!M"m w X*꺚zL+Pf9YAps͛ ݥMjCiy"`J3kd)l(s,2HhV;fSduà"`Hw0UfQ,`^G8 &ɶsLMAMK!@^O3N޶;r%-B9Y@ c'nS2:_ EDRXS6p` F@d'KU%^lO} `}(gf{fwԪY V1|3!ud6~0}mw9xQcÙ9$E'@NlQksgX8g21xn>ɐ@~v fS$;C*QDpci4`gHOĈ"KzZF d6n2ꋦmbDam,д$E[w- 㣲ge ub6= eqNxyFȞӑ~ghAh]ĎYZ PLfhj-Bkꘇ>PgߛTyz|:q><`fH E.J e4smxzd#L,F7616m-GT浓#(6w7"jN{#nL$^m5p-.Nkfۼ\I\2dz#_kpˑn>ֹqLc:c?4)8>D#cDBs%*}U RvjIZAdΈ@hi`RglMH0?1"cop%A%?8([dH!xہ>5fIfOLt?j\L?%֬l}Çf" ׮|Ufd^3)\}{0wo.sw4:N] oOp3|y$ N\5ۻbGa|K1fdT+Ʌ0 HȾ|lQX``z l1pjȁo|w[n[R>o15ld>2k&8z=y;s=S W_3Tzym77F fY^/dxJny e_6bd770%fsd zTQnEW`H/P6f,mG0T%A8@Ib jH%>kFxs3#%N,Ld z>bx a Y6Kqk7@ z99; }kmh^mدx; NEؿ`'!``j0aGB6v0kbBJO> D%NH}UT|?dyh-;t[  17Q&2975AƁK;fpAϔMRpp& thYJU 01VA#0{blndF+&EpuGXЃIw0w/Lh"^?,UE<+P)"LJlVl7}yƬlc,f7$n'c\N>?1cWfjTہq2<ܣa^,V%U@[#}I+  EO6pnw/|pTou &W.Zm(ne4X}~*p ~y% `^UwFD8d !D5  e?|-r 0/# ;hrndg+[c36df2ЂE'G[σq>oq nl˜GvdG8Rkog#rril3 >00 ۫\ҵ*'hF"CTCs)*DR6^fU<ז8)F4sF$&3J.0\m2{+s#%C?ʹRs-l S.S Lڵ &Fn_n͞5ˑ›&l7z}}#pb/yQyg8=Ϲ]}֎ O7T]ɁKvC{-7_d?H;p/ygE~8ߏkȶii&Got09a>ȆxlÑ3ev,5F$;51 mߛ^td_w` ;#s627bCG'`VHF>$. vsKPH1SW^ h:_4_'~y,T_{tˆFg|uAKMqf-"`pR#bĴ@[m0nn}V2UavETQn5tSCmM9ȃz7-OGscYHuZ,dj*?ѭXtC9|UlM ࢿ-=;68hfN~:q (3oDaB򛧁83vƞ5} x- b7b6y}r}pSݍhp{M20S33K`t7uL |LAa# /WzVmcer!X7_y`4`kf; WMe 7~j 0\A +c\_27 sȭs9VtTmmLۓEcz!ë >s$o_2/ץ[N4p'y"/4,̒eR`f3  m.Gf >Tax cXyY8UށD[:t5ed8/0+c_gDJM '{ kjR_nH\GL7Zj`o,VXsTJj2D3\6R٢18fN|d(*?"bpI_STZ_x¢>7 9GJ)j&X8~U-sPQ:PL/1y_8;6у_2z|Wc0d, p' _F@"ǚޜ~iގ}7 `FnPی*`2&LjW mjm{2} u9=G'"A<ȥLٓ&Q&i$MCXFiz@ /mЋ' hlޓDcc}¤FTС%V%2gt?gK*KFׁN  Dcs`冞B֞O Srx?al]EwJރ~!{U<5ȓmZSW,e j22p- pd7oeiq>[;|ؙ 3F cvo7N}+Bف!+gbN1uf0ώ)6708}"D+-~8scx(nu;ֽ@ ÁDu͸33|80Qb./r})@5?ti\.ZXkcrYdvl11y561z췜2;b-C6 H})Sb] )i[la"U?τV}TWwKq~ 8W0Gtnn86z)5C# 6Mg,Gzʍͷh2tk͂@eF~0M,Ş"] ,>vCz74[&5An\oL++J25?)n7zLo#>o$y:/ؗ˿k u 3M]H=yOE?3_Ԑsם$iQ?gw@1K9<2oj2fHWYn8%r+5]>F_kآX\c,CMPT68Wp20L@H81xY2B}tnQ րrlSZA0V斸("pV99H8ؠ+mV4=MK ZGr0;CjJ~m^xֱ\/}kZ2 YC[+*eMé߮F~ RJ|1GѠ(@9&. (?PXӣMxu//kMwyNY-ُ߶ݫVZ7 _W| (Ǩ=f#]pݟ%4H<@ek(_^ :W,q~sNį9U\N|~F՟1&w" 80z|EF<t$ľvqd_Uo2sn[\C~9380?HyVK^C^.-g@d8*J$sh 'LN!y21DwmGcGKuTw8]8Ьs(3s`a:qP<] -\/HeiOf1%#!Y~$fC#a"h#ΠD؋5Z}9ӒcY2.&3!.  7.ff9KГy~7r##Nq8D ";52{# f,'2LQcn\()Z^߲ԉ8JpW8ga(`S 9 ^) Jz2psNGV9t .7jEG>ÐeU„y^c# Ɖ fd>&f7o?o<gDsfdߴ7|&4uJ΄i6%+I-{|l*1J=Ҳ[~q7Rs(VAI{&@lnF^CM\M1`ZgW -;T\(<`0g|Km]5OE!$QG99+q+ފ:V @R4/ˁb1v~9y|tcR0'3!ظ6%ZWȱ<ܷ9GR9# @H(LY!)m6;JXmWDS(oU^Qds+1m@/z"LVnRGhF\R/XLP2s^a?2EOky*J)7\4 Pc\&\c/F0 ]&JoMw6$pkf--_Œ_hmk\cE")k{0'9f/}{= 2͗j/;fIZS%BzK~ÍZ"TUroxuuubz[ߟ!@G)؟v1>+3J-fv.@HAM/-FQX@7"@N80'Z蝤~tN[%!wx ܿ텮08}rn5wgpLc*x8~VB]蘖(:CA @9\ d>I1f/"`< Փ} dv0Z|Yv9Dӊ<y`(.2=GnNd%rq=~{ӘH`av/k9yr/μVd^i0H ݢP!EED r9+ٮTFޯWN%,-3osI ͹3{'6s|mi*[Ϗw0? i@c3Րbq`GiY\hփKUt۳ nj sNPz =0VEGFo9$A#SS=0kً " #[kV(XulHPRk4ZЌ|)>01¼D ]|`P!|̛0K@mgzA&4AI\+@|^]m+ LlHOhqօW[muʗsFmk71FQc X c`h?s29y™db,5J<71MEWcP^  `% XhyI!::"/RԩLH~ni Cx*}x4G%^1CKy`,39n')9SpCL Mu2y$< }[aT̪/e/o<>DJ>$ Y5/cL"ׯ27,ÏhH_qI?3k?:Aޤ ғޤq~ #Nm,CT!&wifNQ).Vou+!ХhmZa"(o" >4rdRx4 =&Lm@Csh`>~9w+ͮU{Ij˴z$0uѽ%Gf吹t#{6 `\PpA/~?}sffWM_[̜9%=49^p젂{1IS;8U`U/=)__som_s8ZnxZz!Y4U4 f _ber⢁}ƕ[)N|1U]qIݰ9˜[c;DǘQ2r ]XkkY>'/RM?jP>`.Jv%P*3P9IfXF#} Z~HS:9( c`L$#4mr"K+t\<SltU*nBĽp0sQo otrx:D-t9$ԕ礞fW=<1:<OfJg,ORL~sƹ~Lۑ~EP D%6iS p9VW+^bIL> MnP]hvkTslw7w궃_$s WfʋN+弿+Ob%=nY{SD=Pf?:3~ȸzmk f)\Aた~`#P`_ʬW?*{ɵ,Eiҭy,}aTɥMbk_ptPdczGT흌/|1 /2 .HMbj'b.{)06ݼ#s&e;k\?s?f1t~0l#˹gVOX(p,ؒ#t_5:sZko}i>8Lwf2O{4 $9)9 N[?\(˳l9#cY&}U3kK[s^I ̱ZJ G'Hˮ01 hc4!s,c \6jLxjw@{ sU=]}e~ĥY>\zuYo!rns}*fIP4 &$W` i!0K+njMAHؤY>26䩶9M{u˕"=rʍ\!;*tǚF5"^ߥ2?fyemŽpXN*/tOePv2!K|neVoijs݂҂W\.} 4t,#,oPy 7#Nf`satd'Um8ZC^ڗkyØoF"br~sୃvZY:ȝx#Mtw& s-9ZU ض܁ ӳ̌L9>uqrTIذrFR#L;S'\^FnsفD׆qy(n@ V Ɯ3NZ-a%"nVRBv^Ԯ᪱gƌ-1ne4Z dUg^-aiYYFyxSpQCr"{c1x >v\yUŠٴh1t=] Xex&p}9h5AI8mLXu"kGt WLK\bks-Ţ>QHG|qfΰooNd^psqf}G0,spl݂4]p]pz_rgTl֋aaN8hV]{*>]p1FCk}ljصN. W c,ϧ3bm)\Ti;s@0pUa2SJ|tPsYT1Sډ Kce@NsM 8ЪedɭN\y% w `t`<0@R@aG߿R жz,ѠBXL[''8I0.w8x @PЌexߑc};LthKLϫl=v?9Y.ƴ rv |~$`Kݍ$1 ݙ\ ٹC=7bH™1;7> {]t309U&W3  M  7DfOk[3]TV.*\[C;ƦT@- /TǾ~ysJV{`"RCVzv85V7 zl_V3.ho(F3lrvI>0ez7aBɞpp[FI:Ve䤚l(ʉ:C;G$a4M/`v07Ow8L1[-5sYv33'jl'e]0f㍆7u:י˫o/ff4wڲf8m|jWiƶ;f(@`0 ~qqӳl 'V[B+c mm5%:Duz}LJnR*`s>^\C`q``oAjNU9̂ߎH`w_+kO"eP?TڕL7wnN gsdbRl#>^ 'jaKbL4d I}{:A~\ҤXouNcyxs1L'Ld4̫WDY /ݜqy17uskɵ US8 8?j h=cK';Z>.Yy؊ kFZq g֛A7D"1/ 1D 7X),о>1 ʉZ`Y!<0s: 8mp+I\a29C5yu>0όCs0G363J"6*80,a$}"Tw )"vef26,| i{۸X]_ݔK {8fO #] mNj\L6 YGĸ}f3+83Rf}Z =T3а- s2k̶VY̏e>E&-D+6'XAQ-p pg׎Z-׎0OR,6Xb 1 je)NzA S4kCCtR)a17ױޕ@"hGsdbu\qnf sPlFد#6 r-4:Ix0> + c@Ͼ%{̀faN#(XxMBtdp]ZS=uf79Mo;#ۗm7KM!Θ7q{t'r](TXD̅\A #!@ia* 17m9?HLTg|1}2+"kx$"Rt3xq%.ha vpmC<~`+."F;asc޶R=Inba[:D{|Vkjf0Ơ p;Hq<޹'1ׅzC;o=N۞L!`HaR#~~} 2ٍ!ެ{ps1&;-#u񙟛 ncx%bS5*GTV aɲ+=}8k*ɵ a!e\Bqjv% sNBs `d$}r,P*'QĘRW3 JU[AEYXSE5aϺ04 ^\aRv7 ;SUx$#ts|I΢[] 4әz 7AB /rF t"zδ$`"Le_Xʖ@ бt`+*ddE`k%J $1Vۡ#0 l2L˅bVbDL۴C9"J!+"9œ$4NP]3d<Y= :J\0n7Au̵RݢYk `[ehhV.wSY7zϰavhty ϭ6{?oK>fbֱἏgzLxpDݑpͭ*:].ȩo}8;#(UPO->R , ,(M"l28dr1 rzX4mEbj@)SЁTcK!"`1TP]+eCa/nr;\;hYj_LaJoLuZZy\ F nV Vo9M&;8943E$žN~9tHx46M'NVVCD9ʓ: MTa2 {C^ 1an7>ĭVzHga5*a c5,] 9mmngab^fsk/6wmrnP;>tq$1/e{mp*57g`* # TiJȻL<`jAP $Y"c-"Ku " fikc6׶J ZL.l8J0nXEyjsR8MND6 z*ab෴éΆw:4]?Grnekmva Lzϥ.1wr1 NWUfanZ9S\FkڼZ$2$_ۭ̬:Q-JgB궎&.1~*cY06_2.lrԚX:565u??&v3ULg2pn=+<./Lټ>s5^nTۊo̽^N5i-* Gn.dY>9f |&&Nau^6:Aqn.7& )KaR.&AQ؉1Wil)T BbbbxAiCi[=X;tlK+VtզOr4/4G0:GVŐԆSNp28/X KSm8Palwx2.C&4 <ll0#thYsݳhJ;=1 ~=^emА?C/='h'tas!2>$[p=@~OXu@n]茉Ϩ^Yuqs%Le3̰)712f ojs f:\0cq1i$M 1 Fm6Q 6Bd\ *}]2[PkmP9jS؃|aV82>3 M{wfL; ~[psNU8 ;Ḛ~֙zXr=Kg"h/z h} N1=W+N5 #C=h-3R1.*NxV8཈YR\kMez~ݘ ߹{NϞ)`M?fx"UpZ2`ܟ;T%,B-GŎ܀gڰ;Q s%, A,wj; }C.a"b-rܱk'}eV9iKءN*Laded7KkcM Z)Lb "?lgX &SdV0b2r+i ͔1oexN2Z\{?g32e&Y6w%"I1eu{涸6wi7笙 F]35umOqD=*I)=9Atie JBB] v|ϘԻ:8/b:U-^x0u\@873$QncrӸ=kO@qfz.%JZִMҘ $)xr; 5gP *pn1`a/Z`ӊ=Z 0Eʹ^0?'\XSt  e2~"٩y-;A ܞ5^xǘ9DB0j82D*\ϙ\3E,vs#1# cȉ:E'#1?~⫽?jr(" ̦ ]޾vpܽ`ǵ2vz~}Ģ+2Kcfj{cSJSKF.8nBergؘK cΨr7ݚɒ*w:>KnrWj`&6b7IgVlf33ViPRpLg !J}SQY!#O9U99\L_i5aNetz39vW0RpW C]vUqCÎ9z2sR.ýNgVC2&BՂk9f~L3^R"9p,CrgZ6LQ',&K)V+jV) vVXNqILmJFyԝyb~V3ͥaq)Tp{. ab ٱ;6hЦ<mud_A¥*mp B԰$ 4zHu!vANr/I%HBܙNS.g$]<"t|mN.!2-Xl\d˜z9EWcg%a4 ua?[y?Wps|3IӫLʆ^;޴x5xQ[{t~0Xy# tnx,z-.3/wut^6 L:.2`E'#F.yæݎvz=:ajq[1I5] 0Xd/ er\3ւL,EF0ɤ̕ch)n%i^9SW'iC=ߙwDق])NĂVbpx6,=W!&QW K^ SݪޮSXt,𷏷zjsNqu,wڝyэcu47T8RIRmg7Y. XjJ]ܴw0CL+mfMs{vY[l|5|ւ[b5 2!Nll,|V|X8- e/`FT~N;1UN1Bf9r,lycbӴќ4'hh'cFrgEQ'ꮿά\]]n1_µ[ ?Lqf^VWӏ֑#v׎ PT}Itwixn-XY+,K' kt u3=)Yl&[{:ʰ%U."dqN-0I+XvT7㋝K^߲iܪ)aEblf4(+0I+usNNe\ eWU-Ɲ `nRG 1კa2?\dsb0Gf4#Pl[Ea=c o}IDz 0eV:&;NIؖ pWbSm50ݜdUYo/D\j־>q;|);V+Ss6Gc5[0*{g6۴""/0@o'mpˬI߷,=f] .iyk妵 F~53S rSm y(z9vQ[/|b8_׾1$zy+gizŐݝ*E,H}6.2ŬnL5 PBj1w]im1!>`W 2AW_ HINs^˫u>߸Ӕ;\u&.B^LCY S6^LH|`N7 5!Xhw3'5Aj: ϸπ722^.6^xqv{WCZ ]ѷDŽmNjbL99 lsn9Vטg ϸ$Nxk2:U$ܹ_sLS":M kMINq']d юey+fޛn6=w_SZ-y0MmJK {قuxTF0A8kn/hdL~H=N?{AB+΢C2a`yY3x|f*sA};@X!\m6jӅ Lʖځgf mamK#Vj1C`Xa /#127 i<8xrLLquS.0\1,d%^؀}50 m 69ȶߐ+,܏hA MXCx[{ }';]D01f=2Tg:t?F`6' 3_#:&E8f O輮rĔ03p"I[Ark`V zvj)R0Iڴi/pF@p Whsu^ǏXL;U=b(:Î0lnzk5S;HN \S菡[6 "ؖ}?" _8I`z!p]=*9fU<_sYo6;Z` ։b/3Nya:.`9%fN1%!2@6#4"8PH{R(:)=:3OhN3x,cV.i|wk[&4th<=dtu. iyt7S_^q&[ЫjSf<|ۃ2:~y`@Odf8gN ] tzpItXU9O[k;HFtyv~&G<:nyپ+<\ϓ_3 D@|q\v>yl 1#&ywcu6ocq\nP VUSX y}oO˞>:+ ҌK\lhgT/R#~)rORmh̆s yVqU,(ryONEaQ8T:f{E +dGB UAelajm{8p.9Gw`"FρU=]eq˲?X9ܜ]M/GY:`#TAǀ*s{pmUGX7R Dz1{ӖL@O_ .ԭpE&{ > Ob܂Q3GsΣ4Vpfx EZD\)$xHtұp8po; RеC-t{x!n?d@܎՛㘾_ױDo{@T #xp=1>Xi>>F"BVر=2ǃ 0x100Bٿ%r4$gKRz7pKYEH6<$Wpv4'Ínf+@ Xq(P ,rs +\IY"NU(YObfSP F]ӫ}!6wͽKEWt>w/2Ī~]2uqyߏc.2(4^ieESa .Ҽ금B/' ?ȖgXx?0>'ڏ_6kMNo /OeM흞"VUHhsJNwzc5)Y{  &f}(CUǘLb;6 ]-sp%qoW>S4%+8v$HvzIv}Ռ)?\O]l/ߕ~?P9T̷rnC E* צPB@N 0A]z?:&3'b1{?Ƃ#i aQ26gO}|ǜ |v``\RC."T"PM50Qmp%X,;1 L'2bBJ2#tn%J^ {>F'˕\7G!Aax 賿 zgܽOV4msj (5#8f/U7DF`.!顳4xw:khQ4PJlw +]>z;T{z Õ0~9 (bP$л 8 eJ+t<>]l> 7K|Nu;c*ݙ Rs; &+v\Fa|`όpf(= w1}{qE,0N`BX8^~hXy#Ft7o/m܄->2?`@XGWj *n4y! H``͓ ۫aF 9/V$ c&YA3Ɇ vH؅o~dx? v cXKT<_=ɮSL9=W9>G뜞#οm~}SXf`N$@j0_b.?Z_[^f= <@nq{sʛ!@̟s iY+i0B"{$z|v%]eMkˌ@LFG^α+]??@|<7NbPxe/::d: gۉT)j$UN84k(, |HgBtG(P0⟷897Ȉ紊͘tƬޏyK՟erpA1>aE>?e("D_MLP)P.vat"b%Z'YvZnBf;\)n ڼF h;w7Ѐ2n&>z-`fm![?&0}Z~5KgA0"nR2)$ MwNp5np8}%]%3)Ytgڷ/ tҚu:>\}:z$i[7'";Nќ`{4gStQwy뗿m˅v}ގߎ,]6 0 /\霺йuG'bŧN%*' ]4Φ ΘLL.KNFB+2-3= 4:h_ E8\ƴ'z@\`Az,=|0?cj?+/Wzݼ28q{}  &QYg:ЌEm|!TnCY_Ƴ7OL/= }zۉиTA\xsPlEWNVйaxܾP:ˆ[f0㫆xO0a='h<߶74wIcAK rhjbu?Ss‰NsCHvMœ [3Zrn;K/Bssh^O쯋8a~n ɀYO&ok?K7+M_06'9Vi%v?\e q8=E,j|z_O@_.{kE񑎕xs򂀏ΩSBϒcYїYх0 l3e\˻&]g ,KsR  ʧ@B'ׯ5fM 1qcCLmɄ7 t0omܾ7>7?7 "t*ڿ\<tٟ,1a^ץ\ 26Lxb.L~E{BXh#bBnG 0Yt|!N~0Fn7x/^WYL 3*s/ OŸ Ģ ,%&Rc& {_9/T}"vB;$/@ц8i7 ~GOqfղ@ D0sàt>Ik.;X8֬M+f!PۭvH> Zt>5h3ddaYzpe}n|_XL] /"!rFz"Ot_Y䂓6f(TEձuVS:#3?ON3G\|Y j99r`k(Ő捲@'v33ll̙sg3G|~ ;Ol4YʓI- 1|$i >&EX !rqΛ+1fqs:'E.l&@(I~"ܺ122]  FJ4qժdq1\% [ucA.iO"J01j5X !/x-!& :_GQ'a[k?P@' O[, E)ͪ&F[$qd0H 4%[8Ԭ;)eN1w.}ق@fju5л}awQcI bNSF|t 0sOMz;YtvGpossv߷j~C`{ G[ԞNQ:|ˁޯ`ZIfm"n090V09¹;"f<|}F5WW~\pyAc/k;u|Gm'hq腣\b=f/XQ|/O3ņwH3oQ;Hw9Nl:+R5I_32n|V h|V[kmWa`N@? 8CRc=@6C8q~a+шO $60 f,c04n%@>.nJ/ry+`׫;Z3zdJ ˑ bm,EY$,Û8(#@<Mf0$F[ PB`>ɯSNo Arj.nD)̊%Fo GDRŕն- #ݖ#8H-b-=O'P"v.iZ z; ő` 1 3^\c٘EVu ։99cemod78,mj7 ~tq}ď[pןx; pCؗ܁YsE4n*s>28*9:n~h}CM,<*Á6UaȀ-\YyɬWsg³0 Np臥;WR/-ul:p[BK9\QF,<S8r+ NǻfAb#w_9B "Zk:)_>&-y$ "c-VyEW")D'Rh$Pd:>%-:@``z wlۥcy? <;E(k>!]΅۞~@E!DrP zbi酎3ǽΐί#3',Km:gcÅpi95vT޶3Ʒ'[4~ޣ>g `N? 9I5 شY8%֋Ê㲭J #f QL.E`YiPX͚/rĪMu,.U *ҙ&8s}@ DC- WTn# 9@ /XQ^N [%7eO`d!.@g]Dh3:0'٭Cd`&^Z\maBVZ@X J0G !yHd\b y?`l}rptzLzg`,>@ W?oN0!rtA_t'vudg?I*ƟIb,h΄_JY q Y ֑t{ <?\|`s˶_u['^s"pYzy9]4NqrqL퉀 <~ho9?.lUx z`+9Ӆk:Z]LҹVΌ{#*#Bۿ6L40`x;1, 0b93#{=-n8_7 ?/-K E{0.Y#`:-ۃAqն⦌J1l 5?Z jP}~盘|{+e؝,"q䄿gT κ?.Bf'2`3[+tp ~ jkT%X:3# ,/Qґg_ʼnс[*GD^54NvVbbR`'d\f J0Vja!9D(Ncʚjչ(Cl ~Dp`]g{"hkyON!oMtM4#|OE 3H>韙bGȠQ=^6 McG-8&~akX:ߘ<~ϿO Y_;5vpf7E#5bQF `%#%WR f[UcPޘ1/c0kzQ7ָ]n\ @ÕXNcP0%+~L'!.0T?e>@'8@Jukchʎ.+6'AX ,.W\E:ER IG.gM@0m<=IB_;cf#{Ag-ŸyI}ԥ0]2È`΀[g2֊1k}3U +38Cg?=mjKzW36?ramqMa -q>t~V_9'3#o{N>0D3!rNog]^'6w9^JU,c;մzF>4c'/s6߅>sM /a9#M#E3,z2qں:^z\sv68TGZ 90,Guj?@ :p̹?sUC4gxN;Xr[<!~!:TjDZYRZR-/R݊f   zS^CݖCB } ó.:ofTcf޳z;o-T̤#2_jip 0zLTMWv9O[ vtX\i3d`sP.Ci["OL^9A6:W#\O?.{ Q`cK v]A6P0=_$\?tnuB/[&:p ;"hLdCLJ~Ǭbvo,<)V20b|ք7c%=23Z+3A:t@A2_L<ׄ'tk˳GKE'0țG}"W*/)Б q9`+E.CXmv&;w"pض!'OL^NfO~ȸ<> X|-a0ؠaN8q_2vEݬ4&yezN;W=E>g_Wxq#18\ K.cOV{׀}n sQHBKDxFfb0 ],'xoBq@?6H>6 O{Wì/%Y~U"U` qcmaq@=2tX^c!V]ɋW jG?#40Q*'} v;TcZ`pO3 3VˋEA2hryb =D}RMa깸Ȱ^0iכ'魈AW"spVi#,}ǸeSt爙z/cmp*Y\ 4eRq77iySJ.lЎkpv7J.(tXOu#p-xVߦs. pgz]/l vss³pVX`r l7:Go9VAG,9'C-[ĞAho)pP{wj 9*&aѕrJ;rEVɍ}V +ˈ\cW|0fab8ÙEж!Ge1bP@ $EZ"p!/+o '`#e5za`3A2[W)0ɈΆGpB rp ;B&;@o l{VW d}5_qׁIjऻSnzo:ֳtLAׂ" Ŭ0x<؝@Z N%gb ~Wox8 ߳s @N$EL[`VF`aWCP/ABea~>0Ķy#] 6]D(-E8V6~WP WQzu~w])TetD' ]Hΐi 4HGǡŐ6 S4wp@Sǯg{zz`q0{~m;+)Ek\[wtD#c~SNUa`y"A0"Bkl AL1{7胆dWnbzkWHs$ʼnji56IW9^0+QX+|K0ڼ`ct[o2t!.:Js0c `!0@4'û]$bͅSp˔=<!y0vH%xofʚGzz͙ Uw)[pkmTΨMA2дr\oߝxn@()Gt2AZ"K-*#_@sC,ڈLvYa?7Ds m~Ng? +ߕ"+ YrX&L@ċHaB^9~r}o zآ3_\_?wfbB蒘1 N|AeUS1(~Ե! w!;&rjU%oؽ$j2, >;VSҜ'|011;At,=c]~~|?(|~}c6ǚ6g蹙afYvu`O y8>Ü X~p*& a`)(H<+q:B2q)x?eΰP3Zs&ni.-kJ1lo5e4ĵnёŸEQnZu1?. `V[L'?+9剣I l}m39i ɡqb,j8'A޺(}[#҅?rh[7jr2-u1dȬe&~Ja <2b|&@C{Q?4"޿c}@ p?dӌ:huJ|-u|,q.NA+v:@)"f`U`O+5Ruj'=]p9 _R3_fmqF :J?34t|,6Okt~ww?:a*/ 3?x ۏ9Mǚ.-ވa[g$|_$\h;;wf0yWDXqh2ƔEaC\uB#O/ϔHY( |oe!lZt/|&H!qql eH"X TXi @;>8 - N'v.-gBgF1CfDlfNEW'\+TsAzηl|<ûTnc;ZHf1^:jo{)hQD.|FK%? eɹJ;期pnOop49O/X; xXxqdCɯNkinS׫rqg~h &SP| `S4`eTnKRfCHP {3?lS P4|! M HNL虚Ă83sG`Tu%ptY PTo`{YcZ./hm{|0 RAI )Ss&~J'H`;A:RvL*0.>t.39bz{. O /f $և :? 1_5/eRlqNZr6R0}gvS|RfgO#jz;GIhpZðƊ.ou{k21BNa9{teNڜKuռNcQAh%'0dz_W\,]OVms|,O _8OOag"#5,榅vze.X4MW}+mo4:<+Ko__({axʎf!^zV *d::/S%yQP_﯒sIc_)}$&|CG}bu1Q_G)Lkt/GDXm\"C&9DW ]499W ᕠe=,<] N>c𖫑n:œv!>g]~e^ ؍|Mwx`(_R-qyxevGuAlG;2-p#[a$Z[#eRv|!Ocܬ}W.3 AL>[ʟn _ӗ$ ϿIPK!(.|Ƈ: VFVbN"\_ C4Ɨu~R+p;s =ɵ]l1*CsZ~[ ~K[MB؟߯wV^S9+_FBA$ֈTcL{:5f<CubZ!*S eXFB0>wnI& ^C}7dH)Ù^Ν e4dݨ"3>shsQ "Bf~ΐq1Ǩs_Ny1z1CS$x: OY]Iń`S Vv# c;tn8 gnBW&r10@61Ϝ!,%Wٚv5kf{faıwb YL]UFˆ ܧie[k3B+,ժ6RӦPkO=h0p.3f2_j_$oB<_Kː@nLK_?=R:cX`=3 (Hd*ޜa"V}~;ݞ1@,qzQ}KQ8nϟgV#W 9@ 0Ka,< z6Y; GaaݐA}w<>t" Q1̫y|?'vbzlu9q.2gÐ,|Dh s|ŋΕa[agVD g2i9#4P]UKZrC=k 3QgM%3\Ow^ .Yy\Lb-,rw^X$OڕUw3IBl0ȶA1ʽZ|aP@2>>_[獌&蘖@ja z82s4Mq_Kwiyo}H@7*7/oN/'/(ICtn5HK)`wM ykzJ˷ kxOsA NjS./t N!J7F Rk^cksm?ٗ:}?] Ga,Izŏ03ZV-Bw Aswx1V30.!xv Y ۝֙owqR-qx+풘~pLcK۩nU.V! F |'m2bJlٹ BicaNp9ATPIU TN' [KE4jCbW d;wBŗU3;a6`h4mvKml9=gr m&U" ϐ|ȨdJcQR= x}qTNt`sM$;ofY<0@0p*N,wü#19 1^_`e|O0eo)ᰁ ~r1N!dAZD',P/6XHϬJ8+ \xurˊ.d0HW# >Nn0T *L6"im$Ԡ`!!2]y8i33]IƧ f&+p.X;AثF+Qrt) vK4]wdX:3PWst1 ]. FuS5%L70T=&whv0 c.r fXi~/^RNϰ\^`TXL*`\޳_9^ >k3)m9E)W9-g`Xjz ,1b5y S*m9*94h9Eua 7S=,: 70ۄ]rR=rE򙳆'TjPҙ:L]k:bjQ*0Ak,tMRt2Nxwmɥy)h[kWZ`ȰX3,EV Tek av.GXGp';Cx)~|p<.## 1qULj3>)?Yɘ^ϕdL.'x8ŰcAf1?  y/(. CFVPޅyڄŞsǽ(_cgG }V$ơ򴬴:i(j5΍MCos[S o?o~\70 mU]zyQ dAad̽1νƓ!WSmW{n1>3Lv08Ͳ+v9m^ܿ~sw$C!dWk3ð*fXչ3F8@Y'hAP9ł#'$0X &sr-GV]  !7Ěb ^7턉n5QE.ld[BF& Wu<#tg嶼O'BeG, [u񡳡mqK*ʐ3`ϟ /oYA)] L:cºT=Iսu4&`RnP2+XMf ۍ``i}`%n)uԶȰk m:}ȴwsc0o_/{,'ͽR'f`.2;=b0`{6K,|TQG&1oκexgA0~yٙ_,Ԙt#gNbʹƘ{0N-n{ \_()V"< ';1i "`\(rGs'm΅Q G 5HyD^d &35o`AvX3lp0Dt[GcE48>JDp쳴vEWf銩sҜT,(@6KGh;^SQWX^c7q":C./\cEUXs\4,8cz89!ǗA/}Aj^gr7SK@7qoˁOZYT2Z$aTURY㱙 o .<龫Y7yܟeiw WbjF0EPת](t3N#IA.-{PX]# ?`!nȑ$JPwvǙ#/ҕB< ˰VM~^l>ߩ ~oy^zes @V0}k]th: N6w6[ :sAp1øN+1ְuX:1=@]wW< >扠 SԠCa'kuP0g[s.BqⲤf^ީށy~)hUǫ ' ( 2i-@k}[,iLdEUf^IeH~(@xϟo !%[ r+3¬)7|C6Zx1h20ͧzN rzU]c9:@y|gy[~ļcvQ:0l|2ϫʇx{wov@GU y @+Ͽ1X~WYoWׅv L/ O{7Qp0 = Xn-u.Rb &VfyVBmrP݇0?.3{~UFT f7Z1~.F`./E@+R%-߈!\ YJY aMfpX-4&Y"GMBYx{={(L.0bKÒB 8so";4gg p(šȱ$ޞ^8yS;W9N=s_o6o_ 1ukݗϏw0}[6V|0BaTXz17:aC q`J҂Wz!oHQ~z%'0Οf TrXX Vdox1$rxb9]<&k MoOy'YmlE` w: 4%%776 b.ʒ ^̪4`F}d~/ $9_'kUw|Fi) I2_c=#WAm"[e[x֜sl1?<WX"Cx sCEU#Ӟ D;w̐n -5g _Co Sp-=۳j[,tVX+|.èV?=xQG!V'A/\`j" \/6+&:{tZ2 ?OG~PyL< e 9?A kxUWwFB`0RcL蜀B`MϽy@ɖ5UZf.UE^Jj1=TM]":GJh0 ߵn ?-72Nenഢ 4w>}I? >8qw< [99q#(1mys@[IvV1؆y :2)Uܜ$<{e"תi\^r\\u?ƅnaZN) eDY_3[z/9fs魽'#0 !pi1NE |$W0ox$r҂(f&˱\al2qNo= 8Iy9B|~=?鹔cӵؠ}rG> -`^scBi5B10@\ܢG\SR020tx ~Ŕp;ߝ1Kn4yڑҤ$ҹ&M'\t=n03P3H#6S0HF%~@SXGM5~ȿ`E`0x9MKqȥ#)QSzގ7O7e7]Ε4>|ctK߳^O={;o]SGlǁ/;/sfwuz#Pwf[^:@h`>hZ۞'ܪX y?~cE_o\9 &-CHV 7qƒa#3t~< q 87 D'8[0 yVmHNv\`p=mvd(9D :5KQgo;KgssM/|IM@/@8Oö9@TK6`~X!F?oC X>`1H8jMeaoO_-ɡ8al0Ӎ@n8<?pA䐯Nߕ7Vn C}3\@ q1\Ƅs(Pw\U{m;V1ь פ0@ _O Gzc8`:WK~N8/d>3lq"NtӶ}X8 ڭI W:tXsJ;_Su+&}1ً`Ubc_l̀ 1 K9u,$> tx xVEowXGE:i<&|K˕9Bz›0XmnCu޾T《9^[ +qyhg~Q=| eOCb|W8[ P!$Z:/z'r l@ M?re_Mt2>&^Nk1燿9^ڇ@-8^|pdž z¬LphM<[Q% O|.T;ly;v;hػmV]6Ã`n.d J% S֊xq@n;|jTz`x?n `jG~$lc'q_i{D_s@ƍ6O`"aB 0 _%+i\:,'o3=@Aמ;Åh+A/.P2|2[>3:S?mcBML[g2ͺfpnhNђDBjT)0~a87V'g@pAYP~r2c cCa%o t:X+sQ,9*0;7Bc_Ha*P ?_zm%OpN!~WW/'Tk ?XĊ>L=~V>0X~VZёyNG` ],R 5emz7OE}!t@ r!|M  Y7\]/I>wSz|' Vz)td."5 8x|N.BxQ 0t`49] !\^֑ ~M2(8 +]g@ t" C#v^>d pVCIp>z89tvf`Xr/93ӏN?*]@3MR0gaaH`aMޓ2\ĸyWjx}\')+mx<&|h~SxXC=`5\hJtqA73X5oL^7,cdzH,2sSF{AY+VpTdg=r~Z=<`B~^'`m?Mf`sZ(yUb  D:@3< k8yLCbP 8(-=Xus hU!|_Eܖu '4zfU^(e6p>[ҩ?NKGx-El]?ho|+b7ES< +.}a^/\~:.LYC[A?rVDo:}`%*"db+ ]KCZ*v8wzVAs;T9@^UQTcо[Ҏԋ @,|J&ƭbBp"6ṇ̀ҪVDB$&QoKಳ 3W𥤈A]Px"4gZ>|#y]RQ͟;6KdrMJ.$M 8v)ޞY1FqCq 4bpnf.s6sI?:'ha o2GE^ດE^*%I%O?&tQN ɷզ &<i Q!3K FLKY%b@zBŎ_6}'mr-|E@4{G+. N0q>K-TZ/~?blZ7<+W]:0/s{l /JK:?FHTtRP̶/r8T%Ρm|m -LPLvl:Y'7jTئ}`tT1yd40:&tz^~gM_7` edUL7SeZB]W9߾nD~\]0ZC]2:9Fװ4jЩ\CaGRX6HtQV t@L:qq WI} IUqζOU"}'z+X"a78(]m@L\mzdǩw0C2sQ涥* B`/8?_]HR((vcA'ރ|Wf>{1xEn&ћVE ^,ᮊvd`#-cg0%:>2|_ZhV,lƺeLHs: CyN;vu(轏-UѲN?^ЋTKe'vu0wr3O34*vԘX\m¬l6w8{ΙOVމFgR@j Ԏm;<`Do_&$h5E!9C%d~=fb$3oIzLƃЎzϪ9MzkuiL}"37δ |1 5{a_.ϕB+ԕtztj/v񍋥[|oG͟"vQE mNmVA߾\OY&>0= .pS߸ھ쒘REպ0F9/MD:B;%K`=^jG2hI>,H}n^YF f^}=E|^g珽ĵy$PyS0LKw0=_5;/HCXފGuQܟz7x>- !8pSzV7QyTZBH,R~Ŭ[Ƞz1)ZFV+2]DG_,lEtC>!NҖ? l+]u#h=;3re**/*akZ\\ 1CS)}j 30RPOb!6jҽ8Wmc1/e߯5Os:!1"_t[ҳnE'ə)9Ŀ|~J-AuX:v'6 /Yo+[`zAgw!:mtώ nd/`Y3 &kՋ.25i]U 8Alf{@0>87 7v^o=8mWa#ƐyUO7KI؀3δfǍٮzܫm\4u;]#q߲&'"2x,7Qtܼ1ۆ1@2G|n@ Gd]ڦq,~4iL}ˣ?9;\-OsWΐ9p|S>ÓE-^mw[M.b&l]APA$69Q kd c }ϬdD\fa+cђYRV nc;3l)-sGSr_"CA:\ YxQ![<\T-q0F`!0Z0]V)x<=m rh,8b=ݯ ȾPsb秅:1P@qָą=tܠz 9:iVikSȱ2%(Vc~L%aN|0D xrJ*+a/*i4=#$iA#3Mtm#1@Ӥ@fw1Arw`9M}~ڃncAqP@1ST_n6YSh5)y:Խfo6G"MXO:Oـ49r6ݦ6@gc~vs0 U*KvR,-ʔљ\ƾˊb/XX*P"I,[v0g`W9BGS`*Y IBa2m;U|rslͥe+ W2eZ2I܄G:[kz0Z76QVp Μ \b1Hx?86'2wYk:ˬRK|_ܿ˯i%( 赆 Gybzs_M**._Vʌ*/0/wҶ3pud^-=1& iAsV  mtl v `ent7 ds֛?/Ufo `KK˕||P]R]wΓ~2%imZIn ۖJASRa= yHi70,בG9AIq1E#Kp^jS.]j/2i}837Usj^԰Ǹ$z8fZa~)jM[ `T96Pd WJ5T,^gL-/j Yg[I*1f~v<2̳ [+aV%’+1?2uB r Qqvf^5[Z3r&9AIMۥmz9`$@|,9?0GC]93}~_Q #F9r^e91te[Gk-J(O%*gjÏ]`e?XH 4̥]7k.2|$$%3<20AF 62G Nd~n6=- +ݺzX*5M16ȉ кЛ̐k pPOOf>Ĕvnβ+lf2cф-ܔAP-D6d`;)Z@Il8Pxqfi|d7/2cc~V]<<,"jSOR|ZopQ): LN,h]ШLi 1P =+ɽb?<),ElhAb,DT6Tw~"k 1fiV>}O۝^<Izhzdla PZyP(Y_Rc|j__ Iۍah8z: wdbXhEoW{Gd%tjXYچ0Vsn2c:n/,ik{nзh)*Msn̫{6MuUz-ǫx$ߗ{\{~H4D&(0VCd~3c3ͱt29iՙ# AGAjcqfg~Ba{< |Ao`VX.68?k.,l: ؽM=J+*3!"n(#/akz> P .Zv' >4vXYmw"aX L`)Fj\YZ#zR&u[Ó4#5Dp)1;mR@NfUv<Ē 17xGzh}2nDk}c6 Zpm d!I3n,7jػ5c63;/ 1+^>+l~o66 kX$Orr;p%[!5UJ1A6}(V֎ s`〫W]Wm pw y,2FYI"tFA3x;r8Ix,lgkaqIƼJ8I0mNa@0bAN-v:=A' ~|8g`y!d?KbN5ϣQ>ٰn&TiU!۪`ٲ@,}~F3&l9A 3CГK G%$C(s;u;Щ*Rx1-~hk˳2XLjY!'h%ZB~QJjO߬3H$@~3HȢ#79 knRkR֋7&) (ëfXesUB:LecJAk0\H`9~oeO/ \<'!X a9@Ⱈ.ߪ,ޭ~&".f} ߗ6fe~.R%%6P(]O 8g(ٜW_B:#.QJgiIQ,T< 8EE5a9m,uН:EDfʢK*ڱL`Up;^ª8?5U8 Ua+W'LtRފfuAZ"ae%A䘤GhjDaEY !9ή TsU2O%g죑mZlZV1\D=*upjC$;֡8RC,5]ᏧFbZ9ܺ>oQ&}ۘTFJhgSqIEvv9 ]Pzn(L[WVW)b`{yCػM2Z W"D]\9-HeL ՔKct!L6'*L~@'if@"5\69{U|Z3Csǹ$̘jAsP6 HkhV:0ZQS9,XzQrR0@X0!Vh2WGhg9fMƫ8ĠH~|^_rҋG~S洺Yf!T*pA6ph !'g9s~]`M9"-/&vƮǛ1CdNKZ(BviPK@`4wj ׼fOi C7 @ @`Ucu 䭄Zuf!3L11`a KLߊ,Y*ʤ9߬ #;P h$ZڎsJ(ByAQ>kF4`eay<Lʴ* u-rs,I4k m`pf1HAruKSHr 0uJcb6Vf 9*_q:g-h N_-'Vbme:A[h[0$BҕN H% J1 a3LퟧndHa8t by?ρ1'&U~ c.هϘVE KcxEs r^UFKL lW'Ukyl{B4hJ+ܬz y˽DZ/y鸟2EUnKI %xQzLT x=lN-e2&XU[4~ƖF:E*.Ld}S;*, -!Hj,bg Ci y|? ZTҒRSy[@P'-sXV`TΏ,PDȗya$<MLLF"ch wمõ Ven/7/20JTȯ=UAh~^aV= r?!gWKRfrz掏SIPhK6k6z(5ǂĦLr. mZ V&y;ʿМM^Ab3ur 99]$7kZAd3PF$~6+`d_ v0dn7 =NQj;UiS̩*\]k 6Sz rEQB3AH􀵆lF2Bza۲IUZHsqn49h{iaR`-BqZ 4N&Yl3b08̢K7ÌjD9D MzZb_o rs'H2(Yn-p3Ó ɻ5*iPM]B' ~Z]ۢ1mwIqKC̰֚ױ! И]^PF7*iҡJLЋ%*j`֭ &+Wk-Z}2ϧ{aJgiWp^ 8Hz\L3;m9." fL Kn:ڣ7 sث-kAs! FԍZV}޲ӹ4ec]΋G\ sQ"i"vxĜWsCNJ1BY%>fpCkE0JNk3x,u0sXӧl%=PksS rviA vyh8t5tAѤwy X`ߕRaT??Rr~VjRClA, yDBkm6H%f\ (Go5{ L^7_aD+zK*2YgmBkj7"z[@vu4 =Ohj5kZkX=Qiկ]..$L: V듀s"CŅ=V_ 7viV6<2_ث~g2G"HӎWkU#@vW 0Es)($+Klu q *. :/YkkNT`l'UA+D& 'r8+D-{PU/^,%-T&9Sл"|_ PÏ\UO9WOZE/Z_Aq4¹K)Ubׄ.n֔L`0L-5z  $n5L02%dGQu'9L-UL~:Ej%YXRk2>ߊL 9DR0r97 &%\9)ol* [ 2|9FR& S05KcKsbb2U0+HQ!Rzh 1iQ\2Ja`Udf]%mo/GX@"9?c^h.{$ءܨ,5ZN z -k&&HXa 'guV02YOgt2ga+̘ZNr3ū;ëhnmmg/|cVuV>9 ʨTq9™@" (WXOUI[Ri<چ̒}\6(⅊9V%%ׅZ j@Գ}R;=d6qL@y5{[ SXnʘ&$ʇslCW:^p wN|1S~|\^8yjyM"J^f_حaTP3ƶjUC%3J靳Lq5.pY%hIQ0o<,ƹnpV{7?,6P7v#`}qvxMeW 7 tu{aGk薼`X#eŪ¹Ef]-jjp*Ρ##T*6M[Gq$wqܥ2KV\ >TtxϚ(bOdHYO f XW-2 Sױn]/y.})n2L:Y!Te2k"R2;M ]i\*!l9vx4/K!Zq=ai+28QK(@V ^u9&͡iFJg|?ɏk qN@Ӆc~z|<|>Jkn|$t<-#}f[nrU`Wza_?~:gaؚ%?Z}60~ٜ:c^сx|pB xL] D=_yCMk0au(ИvtY3g2< ebxIk5Eי%ɫF̬@D~9Hj*+XvgNX*%FVh1:ddTƬ =-8cp)ʪ>'Nà<0KxZ5Z.A23*zYEd֙r.f0/K $Tie^2TaÊ*awU,09kJՖV%jn5côiu 7b}pICz {~Qa$eZc"8fGG( ?N&YxR]ږ^8$VeV'`xrm\T G]4&=Ȭ4u3Ӄ踱*/dߧmmd;5P u0w\B%JfYZ "*`%ZUiǹ`ߕ D628 laq L%wP-5ܭb/\qqyH>Bb3,cMMc"\@w"6Y)L G~$xqQL߆H;L:Hk[m4AFr%#Z01=.9rC"Ufc ºęR$LF˪oa!-Zfmt^nmG4IMwQz}_@zK`bp.Vp b8L ;Y XA`̑0kj$l fj?8adڅP =N2??H@}V}%$NJb[ez q*]xL 5 )*єu2M~:K` +Z' [3~'oh~ cڠA_ `HpNuajngzA~FYqA'cƶs\1}?c).j^/= Cou: kӚFocmz6=_SV+cءC "\cBKl&d-<*w mΧmafʦ pl7ϷRs4Y d^}XB5gw.s nvu}WB^t,Ow{`BJahtp0h&$ H*ZWuKy n[sS.* CiÕ9gc֖Jxaz6 ҁHe;QNZ>B}cvfZMa+=E'a8txgJ j0Frvd\܃N{a߶{|c.$#hd? y_;ݡ9 SwQeNi?m*t{|,qt|'Fpݨ7$~7tu`wLI a<IW0&_sV#A]tz=GȮWƧ>gxZvZ\.gX˭b9:sBME 7r<|`2S7R;8ixsg^R,6<{kL2/W[s> J2pik3;/̻2r{  '*#~̵ccC~p֠&1=Eoߘ~QF\K|.]++Zk5w͝!وѦ8mJL7{Ϛ?<˹K7Fm2̯Ӈ㉺qB2AvTL4 j*eH01=.l-w.ut`Z\v|,=)}fs FeAСsI@^ZlÖ>3~0b>X}_)#)M2x! Ho=4<6VMP>^tRٸNMpAlI ^vj\:tj8WkZdE&!7 =瞮 X:S'9>2@ON'QzƅZ*VZx|:*&ׯ;O>C #xn05ffb|biLH ѨTqeNrj#E:-XrC C[- 8k/6`Ui~copUrVҴLV1fxu[N+ȜaqCXi"d_,|C.̙^C=W-:B|2C0"`]n<~Η,mm׵x|o[a[w̰}S6=<4Zig&m`_+(zT8[sю[7tDh4âݐxEs}H1pY$lN(f $0 |0~'jwhXrGyg"l {~B2Mr'ufUj{E~9M!pE5~ eka>djE]١XwZXEhRDw.a?4s:GQ rgȔn( VGFC%t^-Zy۩ӊ >玒Z+Œ˫ G`ao:I]QPV%-^G 4塂 5} V pEá—/rmK|^_嗇?9?{_|rv{i95qvޚg]gBsimgֆ\aV[UfgZձʼB c Ȁm.6[ f`->DZtP*.s5 ޏVa@mA.13c!P+)(ԕXbQRJ~Bh ,Χmp }h |Üli.@La(@_;+8tS9(,'m,Üĵc1BPr}`n.8֔O3K, %DW Sւlce5  -VmAFքX:~x?+}߿ܟ|Iiydz>Ǐ9|:>h3ۿ}tn?gWYP2glT?L'x g#+bfevYsܞV!|u_BӹvpBH8@R-Ս@KN|MW*e&lvqg,pEl52 gu{(xZΏ;Ke7*boWC/v昻+UNbMݐ. ,4mbՃ-3|+Z}^NOÂj+1lkZцd Y`<a 4>1Cñ-LWɌ0a^`k72BcZ`o k\gE@y>b2\?|_9~žόÎG+r?v#F Pⲫ*lut\5A:Rm5wyMc`ns.(M*2FW2j"jܪ\Sxռ5\ƙo|edz'-G]\h/6Jg WAw|)f $<n$/R蘘 I gJw̕ .Ȗ/sRRpAN[guXv8x`TRk4W{nV9wcQ[eXf4sfm[- j!m=DŽD~ Y ZE.yϟ??fe1ӟ_ 8ϟ)֯G$AjuYت9bADtϑ`S,n9Ngͥ1/}k3hr!]B9ai7/ڬaY?jnS6.qqɋײ_*e*3.©nd:<~h#X0ǸpPIGX(`n/ R1J8"7d4Qf,aJ~F7 8q&vAr<]z%3Fv*Ė MgC$HPl@Pw hIˎ {f"Jf}QO#m&*h=W}o~lP7ZJhh@moDkb)C Ֆe~kǖY0alSBkpXAΏYV_ eo[/NVUhTvM\  O45 MUٞ:6 ޫaCX̶6]8hdaY\Fk:l>}qDܜP\"?'|N`0t^0|pE輥{,T\,Z'L\3 cC^%-HlkUV~pQu0 {;N?{? LjADYsy;e;#%uf*;@Ag,^٥{ͺ:@.sZ;K s MN5hHŎJk v ~V8lf&J.OЩd*>G&݄)Z5WHoD0hwSfzbk@^{O¾?ܧ?_}+nP|?67M)sZmE+/`gT!OG5eXz+^4Ձ[ 0N6LjR`l1<'1[7]e>Kenl&㫶'341VM9Z `vs)UY:J\jhI[= #0nh=w0qzWh0lPaL4KX8`W)O6mPXv!$^+yI@tY.u8AE9<:aj.-\HQN0̆@Ws.Ov^rx߮_WyG`gy{yG~& 0;5 !5zYEF ͹Wy.uFYeI.*Z3X*cM֪/yH{}T)w<֛5^EvPS0*,᱁wmg\;m \5Lj"Wi+4tL'DX &Z,Z2eJj'^H]% >d9#Cod|^!Ktfo8IsU]' i y^)@!5X"KjzIMWɋ$'D8 )!w F_ׯ챾?nq_Ws!}'H.{u|mOO}qþ{787 󳀑!QI+f^IC#8J?\6i HeUss}K| ݊F|jN$9jϠ(hEr}-[0*/;:#_:%:a6H) 'H[XЀUEsv JXUb [2pxdIAt~܎V(i*izK\3Dme}$W0CiQ0<24 ƅ,9NDg-^.V:yo)[\3ǰg|{ >}#|zyQ?{(=1U1$BKliN" 04=q;pW?/? 10g)'5+jn]Wwybuտj%5 n!o!$6=nE/9"a/Mٽ*:%x S*_r67-]C[ͥ3ꢽ^EF^ ޿TPuyR@Wj CKay(8L=YuAvW}b]OiW%[FF"H$b W ^1"bD#^1rgOթsxH'} n*NUK1 VW51 9Ӷu=A0EC>"3 04[>"<24NQOl jy3E~1Ӭ B {+>j}mۻJKef>-t<oN_G@!x>z{ǝN/7{-$:o_um3RtIJn,S!#=Vé(K'q,4~[/"ҪS\-=s`*u}eYyCDXNs!opUڬna$3Dr,%>ZwVck8że]W9.ּFй}3UdY.WIzސquy_+&\$fJQSD{1gKsIqq8_0rL fSG͕SI@Y|CAތ'$'@SԜ^~o7W< 0F9EfHۑn;j|9Kydjп6s)H>kO{"t RF :Pzv{)g}`L.o(/xoǃ.s=~u @W_F(rRMϹVOZ|Rtnl`28lc8q{n=>":A1~b^_1o+֯ i)"`Pja*фAX41)/i}ĈYddi@ѝEFBy]z$x[&"Wtj:TR\aԈ6hsż-@I$GF(NIuOw{g"=ڞj/wM< } Ȯ, e ,*G͔yE.0 EӤdߎ;9ZxJfn4 fޛBGЭ Ux]"AHZdxjE{}fQ40(n { rs|XZe |fG $+|i7Cf;F`HL.T ћy?1e1>\וֹI \v>!ED>s(#)5>2cYi0,ii Yfi[|`kﳧav"ksC{$씙2]K.g(1"tWx|˿#5U^U.T]JMubOgV#rv>+֙i8=4 xSTMfe7r#%guuwh4tix!)1}"4I {;>#>Bjxs.ns`ů+akrlPX@8ҴsC3EL'uQs Q;*gS8@33Fnrn y\37ߐ^/ã߼G7zC۴7<}tD#M3?c)PF&~3 kFvzЕx`PU4хu$TAuc2R$)0R{Y ukS^32.Mw6 F6 #O8pn83֙ >= v4'5Q)% }͵47Ps"Ae^؟nX/c)L<DfFAG/t߂OQNXJI~t^oo[|X]k54VM]w?Jϭ=6&U)D-n.Ys&3E'6d}KLĕWG2SqDv5$Us7t`.ЂWW2S似6`nE~J/B6(4@^&FEfHPAZZV@F7( ]obim)N^CNSC^Z[0kl*s(/Zl0@ؔYJ+] V=@KBч ST9-wCeY{k| $lDMFCd]\Oo=e5;Cߏ0Pamg}! ¶n ;Q?_|#?$D1:G-7? j(~"d}tKo^dF4fy ,ݰܶPQ(&B7 yH?͸7X}kl ǀ5uwzc7yQ:NxMyꘓE^K4y].%}\+2jH5ʨQ)QkNy ?%|'`44hRVuYƇIF4!̽f\Hiz{]R\hYzT9;S9K80DoPM)tuDZXS >ްZ{vrGoX\'31wuKj0;DB<#u`Ci8CpJM0i~v9ߜף>7 k@)%߼aXM7%ܰI`PI(veĦ1ƥ4Q*$Itُ!Il)ǝ*6G) I-TPzq%oKNkIvӕi5A6o0qR&')v@j Pbr๲r6R>V*p_D j 11v!%gYFHHE撀`mc&Q\J _H9AbNxYG xS|5q޸Z &tYM0]x&O9OWG=#`y{ 0ہ.iX{&&gvڝ;vCv]kG-}WY[YY&gt2UKx fVOp嶘HY{d07iw.>µ+3'wJ`3MdI"e>#,T,Y=ڏ .|WJLYs0?Q,I@.el4a4D "0.qYvdnpHpkP@VDSHQ_ns}iϿVx.5tU<pd~:G+55\n'4Χ)= -moXr\ib%A}?ތ}"bM7K/Gp?x>p0ow^#s~XXSv.hg_+cb]~|&>@Јhx55hĦ01m(1]n VF[>ɚkq':0>b Pj.zTs{8ZJ,nDW],J\e&npA~vb©==>,m2 ?*;IA:2)KywFm:Ws)D#6$ Rj' AgB9٣D ͿՌHQ`.StuMsf7!cfl[S@M>e 2w!L ޮA!L>\^sΰq hJ2@<=!֏Z~/M@yM pKU<1}Fs4%>x __6 :  );۾s/(~8؃sʁ8Ze1d]G 3_ݨȻ)Ԋe9x0[5ҦI /6%NZD6d81e2}gI`AOtS[bx!GT_Ǯ,!6B6=nan:ǮuN6+ ' '5#LBRp++!ZRbR\%4ؔH] *ޜht59aB6LcWÏ @`J e7=0hߛE]2?E^ #!-yfIz0Pw|J5˻ǹ &pS Q7R\Wd EQo!Ƣ)hʺnkDdTGזh,:~-#!=Ypa56-:6rOafNa&n]l!2U#ev|.H%>["S4XѹD\o]]HOpuAJwFP8vVxExXT)gx$} H Zhb)LͿY#wfX3Dm8FFDcp BE&Es MM\inrX:(g$x%)1"#~[wo*s?(Gz,"=\׿޹9Yd4K %urTÉ>J2Z8d]f5"0S> ^ZǨoILAUoimX{eDXNvqgakVɊͣMl4#d*4꺐//{ۡdHn^)2hj8| rJs@Q֙7>'r̜=ɳ{5ps^19شH֙V7~' m/9 j -nKFMa#4_͔?(k[|/^l`\%_GMta9\n'u nsSv@ ]$HCo:4i 3sGD*Amk vܘ0evm%׆®FA4U%j.Z7 Q >bZ+Ӯ+-Ue>]w4ѷ; voyV}(?F#}@$HAq]vG{mu"@r&LzR\ i5BU DWcd9Qz6}Ik9׎KOOCGxu7S ER-CӁ&^)ҥi{<-:j JS [sC 8e~B,Ah\z:kQ"@,XŌj |"E{ۨzטS!XxkEڞ*J3- ХT0Km`7)B"C(c!ڠ? 2r^ؼFx|\/wⳏy:q=r 1Ҝ'\~[YQZll疗`mo D ִQ.@$-F-zF}?.א!TJ;Y{5uI џXQխlV-ʋQc76~m?QUwל+퍓JiIz~UL)SX*Y]d#=H~BG&@O?i+]M.%w~* cHN7:fiN0=bANx움hbIԵ zL 0l=2cip4!Un` NmWk֢ &sbF5Bz€ 6| pI3򵉯b\ay7:khy|8o&<.1k}Es4kG4o&HO&\Afys4zrÛx|0z"1: [؈d_ T9 tGoAUIZѽI?*aS P5Fp$2(V@w%i Rm}a6xZ]E WvRiv5HuO8ZjqByw_x_W^kEfR햢ﯪ-VWSԐ(8qTtd˺֙\9ԟddEɜes4Fq19‰ +wFĉ@>n 9K#`Cь3"2 MofZ i8%0Ewi^v6IpZ7@(I^tG&AZ+YMi ;p2#Px`>.F z^ctunAdo w@{HOi/fsp~>{!D諌w#<9FuװEzfsk9 |&=yCui}΍]bSU+ nã2ގ{gFiw#ѐA׷h׫"4i,$]ixyIN] բML[$I膦IH?7 "9C'5LQihaXR5BNx]ꋙ!U@Mt|45ԐHPB=t(!- b5JQ;DTX~1J[LiTU`{bvB7{ˮ'eoL;X `~Nv,F7$ReSgs)}0]ȷ`PAgK7fBZlGf\KK-٩̨VE%Zng-?3/jGqŲ226fCXMl KͦL<J0Az l9w1Dc9Wl5`4W(?t!`#su !t揂xٙGǗT{yb2eaҔRɬ"ި)7i~ޱ#1O87sss-?'sZRT^/gXCdz_V2bPj*wt"+* | EOqXOM( ui9HJ ?:Ldl4fi',"2,CA<p05 STRYN@YSé0g rh1Yig m2mkWfݰ6ٙgLh:j -:~ިfص3u@!6H ՛f.4Ez9ȭrC$YD7V?vLMI.bM:_hXZ `t58˷I@92Qǧ f@ξ>]E/4Dbѷ̀^ֽ=Xzx5mfp$t$aA4ؽ3J@Mܬ;X Ή ;eʍ{jdxʺĦQ8E씖$ּfvƥ<v 'N?YRuKeu4B_Ė]b u}?!i4H93̉ .S32$vU7q*)1j1k6Ƽ𴚀H78hoI2-EF:=+uٽMQ":՚w6F># D )6!Quzc"%Rp]/X Y!}cg, nkmźk`t TQEwmI`FS^~E`;弛q'R E>̕GΪIΞ,,f'Ex Bd)Tx8*t.2#L'BGH؈oÉt#,u!@kܷ6:H=ʀj=?8yaZk Z74.U)ϔ9m=^L~k)b"]:&[u<1&HZ&`ۍu.gY`ʝu4Pv}G{|zNEi.&- qAC6v[*L##/fc)q}_eJF@M qs 3jGke512=QtVHa9͢hh4hi{ԝTjL"LQ)BWO({o* OFxP hۓ:gIɨ!)\ XpR-ϢJ-KfԐG.L\Z\WMcQ "7sd3A+;PaR(N9Rg.ЅJ3#> (5Fo(8##up8J(ވQd ݠz٣=>ĭ}鹼=mEŴul3덬N࿾F5<o>je6{Ki Y*ïT42y ϚLbtcksDlUIG>Q Ӂ}=v7ss5ƴٍ}P sN*6&ShJus*L0dA|"Q"dz"+FTzR)H1|GvkLOA4IhpQ(s2KRhBA$HѰa^%kHt si:>Fr9r.WSHa{5x}Y5 b0xo""ÍA) |.?{X= ?N tk u&)Kh l˹gCGaBOiD ,DM}{jX9o1Ɨr4v% K.[`LMgۥfTIY*ɺwvJsqHtnpDwg5Rr62wj>8OC@|̎N'fܮ؍$iCAg Lv"UH ّ\z U:R^LљUxLp^F@((4R]f10\WZqLSz? >#xr 퍝o2c~HrÈ{<&?>"(A~hH7Wys,F9R͸ q5,D26RkPZ*%yYy8h3W<]}}}}}}}疵׽<}}}}}ޖ}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}u}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}׽8<}}}}}}}}}]ƶU8]}}}}}}}}}]<0ӜӜӜӜӜ>G7<]}}}}}}}<疵]}}}}}}}8}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}u}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}ﶵy]}}}}}}}}}}Y׽u<}}}}}}}}}}]<6O <]}}}}}}}8u}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}yu}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}ﶵuy}}}}}}}}}}}Y׽׽}}}}}}}}}}]<6?78<}}~]]zޞ]zƚ֞=֚֚֚֚֚~֖ƞ]z9=׽uY}ƶ8]}}<8q6G8]}疵ޖ~]~ޖ~]8Ɩ~9֖~֖ƞ=~ޖ=׽u]ﶵY}}]8Ӝ{{qq{678]}疵疵z֞~׽9Ξ=9Ξ~׽9Ξ֖=֖׽֖ƞ~8ƞƖz֞֞YΖޞ}]8ZR{qqRZ.? 8]}疵8Ɩ֖YΖޖ׽}֖8ƾޖ}׽8]׽U4־u׽<}]8}ss{Qqqqqqq8Qs{&'Y8<]Yξ8888}8Ɩ]8Ɩ޾}綵޾֖8ƾ֖8ƺ֞ƶUUYΞ}Yu׽޾]<8s0mk]Qqqqq]Z{0/ Y<]8Ɩ]־8Ɩ־}8Ɩ־]YΖ}־֖8ƾ֖׽ƶUUu׽׽u׽UYξ]______________________  Y,* 2K:)EF!G:RD XWX WX  WXWX WWXX   W      \ \\ \\ \ \\ \\\]\    \ \\      $C[As{Hd^ {@ŀÀ̀Հ@Hb [ cIJKcIJJJ [J [kUYƖ׵YZ׽ߚVQZ9ZRIJR kRRRNsZZ c kb{{ӜY֙ޚZYv______________________________________________    #* 2[)GG)) kQT|V$ X W  X  X X                      \ \ ] [\ [\ \ [\ \ \] \      \           E[BcMD_c@«@´¼̀͂ՀCɬIKkZ ckR [sJ|Ӕs׵UZƚ]YY]YU{IB JZZ Bb cZZNs kZZNs kNssΚޜΚYΎ[______________________________________    T4"1JBE!F HRLRSdXX     X X                 \  \ \ \ \ ] \] \ \ \ \ \ \\ \          _3 D Të@@@̀LJ ck{J|sOcӌԔsXY׽Y֚YYYY׽M[JJJ cIB JZZ Bb cZR kMkMksЃQVƛޚޚZXޒ! ____________________________________________________    4*G!bMk9 HG)1rLXW                            \ \ \ \ \] ] \] \] ] \ ] \ ] \ ]          _Z _Y%A{@A@@͂݀ՂGLϔNkєsOcӔP|׭ӌ׵֚ƚYZؽUQNcJBkZJJBbRJRZ kRbss c׽֚ޙޙr_____________________________________   =N3I:kJ! FF1YքUYV   X W                     ] \] ] ] \\[] \\[] \\ \ \\      __D@@@@ŀŃJO|Ss|kӔֵԌU֖׽YƖޚYY׵\ԌQRKJ [bB BRZBZRZNksZNk{UZYΛ֚ޚޚ֕) ^____________________________________________________________________    MX91I:IBG)I9E!FAa լ]VX WX X X W X                        \  \ \ \ \ ] ] ] \] \] \] \ \ \ \           __ U@Àք|U|UԔYΗ׽ؽ֛׽ޚ׵ޛ׽ӌU|ZJJMkZJRJJ cRJRZbR{NkNkYΛ֚ޚӜ!B V _____________________________________   U<Λ!!1ГE)YjWΖBZRJ :NZ1      T:1) *2J!!G1AAGbrɊt  V XWX WX W WX W W W W  W                     \ \ [\ [] \ \  \             T-sBB@@@@ ΤUUUӜUԜ׵VƖؽӜؽYƗ||Jk SՔRNckRؽZYΚYYYYYZXΚXֽUb)  !bsTD__________________________________________'ZTRbjscSRNRMbb״        S,BD11IBB9F9G19IRrzMRV<V VW W X VWWX WXWX WXW W W W W X W             \ \\ [\ [ \ \ \ \            I\{@@@A@´ǬάΜќӜUӔVUV׵VӔ׽ӜV׵ƖY|NcNcԔԜ [kԔ|J|sؽYYYYZX͕IR  HJ{[___________________________[>uSLZQIbԃRscRZRORLJZ{     U,LSF!19:IB9G9E!1IZbz PLVVXXWX X W WX WX WX W W X  W W  X                 \ \ \ \] \ \ \              DC@A@@AAAȴͤМҤҜUUVVӜVU׵ؽӔؽؽVƗYsWUOksWԔZkkZ՜kصؽZYZ֛ޛޛ֕ӤF1(  HJsO{_________________________________=t׼Z9NBJJ9bTRk)) 2JJ BIz     SG)!9BE)19IIFZj HT$X  XWVW VWVW VX WX WXWW VWV WW             [\ \ [\ \\[\\           sBs£@B@A@A´ìʬФќҜԜӜӔUӔ׵׵UԜ׵׵ؽQQՔRUBRc SԔ{V׽ֽ׽YYYXΚX֖b  D1{jNS___________________-sS 2: :9AbNkVcPJBBM*)bU U T U U V V V  eE)1)1LRR91AAIER s{IΣ U W V WW V XVW W W W W W W W W                  \ \ \ \] [] \ \                  \sBsB@A@@ȤΤΤФМҤќӔUӜUV׽ӜVVӔؽ׵U׽U׵RӔؽ՜ [ӔZVNcsVRؽ׽ŗŖZY֚ӬJB  1sM[___________________________\Ut|cBBM:NB 2 :LR{]QkRJJMJR UU   E !)G1Ak 211AIERb{K|+X XW WWWXXW WX WX WX WWVX WXW WW             [ [\ [\[\ \\ \ \\              {BcAA@@Ǭ˜ΤѤќҜӜUӔUԜ׵׵ӔV׵׽׭՜RԔTRNc׵OkRԔU׽׽Ŗ׽YZYӤ) ss kP3______________\d R[jRMJ B 2N: 2NRs{jMRLJZMRkԃ   V UW V WeCF)G)G1RR19AFRJGbrǂ ҃\V W X X WX WX VX WY WX W W WX VX W W X                [ \ \ \ \] \ \ \                   GdkA@@@ʬҤӜVӜԜUUӔVԜؽVƖS׵ؽROk||s׽׽ؽŒ)( 1 s{cZ______________________'U,k JJrMbMR :NB BORRRk9 )9NZ Rbc|     W~J)9E)BJHB9AJIQrrՄ< VV V V W WWWX VVWVW WX WXW W W  W W      [\ [\ [\ [\[ \\               5lDs@{@@@A@ԤӜԤӔӜԤUӔUӔ׵U׽ӔRؽ׵skRصԔQU׽ֽؽؽ׽ֽ׽{ F1{Ѓj,_________WTtKR RNRL:RK: 21 J JORB *I!J1A RMRQkQk    U  u 2!K)919 :JLB)PkU֜R 1I9JA)H! 2 BIUUH900Szr҃)ĻK͉IL Ԍ I܆jsrrHRFJ|IRGR΋Is{rƊFHJ ȂL Gđ̌LM ĉ ՊHԎȳ Lč μɣF FȫKL I ϬδKK H ɃJTD [ [ [ \[\ [\[\ [ [\ [ [ \ [             ____________________5J9K19) :9MJAAH19׬ RA !1)1) BTUH)1990YO)!1H9»ǣF̉ 勼̋MI ʫǂ zrbbbJsA9 kAHbF{jjEFȓ˛ȚKRՏ݊H K˼ J  ͋ HˣʛM ɫGFDNϴK˴̴ČJI ɋ | L|tD%  [ [ \ [[ Z\ [[ Z\ [\ [\ [ [ [ \             ______________________^.\6JjY 1! 2 : J!1ZbQ{R BA ZbALJQc :) BLJ1H) R{! :I) L˴ Kǻ L̎̃srGb {мR0{Ib͓DbFjzGGʋ˓ MGLݔ GćȳʂΒ J J̼̼NʫHłGɳūǛʛȣLOĎ ʛ˓ ǃʳ dDY  \\ [ \[\ [ \[\ [ [\ [ [             ______________________._ߦJ* S!)) *!11I1c՜LbG19IQӣ{U{J :MB1 JJ:IIPKj[))ʓʹI ćH܊̌čPÕjjcjbb { ʛ9I{ k 9bBjssL ȫ³ԅ̇E {{ EFG JdzJMLLKĈKțțJ ɫGɛ ̓GH L K Ǔ{ M ϤdM[YY  Z [ [ [ [ Z[ Z\ [[ Z [ [        ___________________________^6P3 JHA :1Is :!1)1!!Zj)!H)QQ Z Zzқ[ JNJ :A9I99H1G) 2 B Jʓ J ʛGʫ ̳УQrr { sjjCREbrZ KZD)D9 9KEjZ{GKJ ԆBFJLĆ ̋O LLʫ JJI ͇ ʴ˴I IJN KҼ ͏FGNRM{k II N||U= [[  Z[  [ \ [[ Z [Z[[ [ [ [             ________________________&_C!  1A0rJ9!9 *)!19{ :H91A9 B BBKb jQ BLBMRAI9 JK2L*K21sWJJjsI{ʓțIJMMM{P͓sHsZrbQQHbGZjFZ {jA1RJA0 rGrjrEŒƒK˫H݃DԉDF̣˛ ˣ˛ KNL K ʛ˫ƫGJER ͉ͼ KQ˳Qk HKIII LʋMdD  [ [ [ [ [ Z[Z [  [ [ [ \             ____________________________^_oOSDM") !H!1IԋKI ! ))1(jPc11I)I)I1IHQQJJRYMR :9IAI9B2 1{zʂ { k szJ G { s{{ sσJ rrzFzIIbjJbZGbI{ EjAjDA( IKjÚƢȚͳ̴GDĄ̈ȫG˼Kɛɛ LˬΣėݒPs{Γͼ ͋  ƳԆ HYO݋PRϓˋJ ȓKLLKOtdD=Z%   [  [ [ [Z [ [[ [ [          ___________________________V_11H))J!1!HS *J! )I0 ZQs!AJAK9J)J9AHAYYؼԣrK)cRAIIQ 1{Iz{˃I{IJI { sЋMk cLkJs {GbHjFbbEJJRjEbjbEjrGs)A90FQjEZBZrzD ECǫGIɫɳ KTM z J ˣɛǣF̈ij»ED ݉̎ ˤŎNݔѬMJHJKLK\5Y        Z [ [ Z[ Z [ [               ___________________________^^>_XJ! )I)H )I99HI!9b9M2)JS!)I9 1I!!) BH){פ J œ 2L2! RR1(׬NR) MK ȋ L {brj s kIFjaiɂjFbZbEZFjFjzY rȋjRsA1AGRsZRCjzǒLP܃F E L J UP̏N˻ ̊Lȣģ̇EDHIIONݒ֓RͬK˴˴̴ōMOT<%Y     [ ZZ [Z \[ [ [[  [           _______________________-=E\Os!H) !J!9IA9I)AT)9WIFJ)! 2 * 2A jsMRMR RzՙH1M2 :1z9:JIɣʓMc [Qss ɂHjr{EbjrDbCjjjbHbZYjssID9 (AJYQYrBƒȊлLIDŒGFȫKGIݚLIʳȫR͐ H ĆJȣćųE͊Ԅԉ  TMPRдάʹI ʴˬˬϜdD4$        [Z [ Z[ [[ [\ [\ [ [ [ \                  ____________________^. 6UD :0Ir9)!!J)!19H) уQғ9rX :1! IAzLJZkk(rœ{RkRWcH1 J1ţɻʳ LjK[kLssIr sjj FjEbYGzEjEbrˣ{jRL {0(9rZIbbz zjzɊJŒP˳ L{΋N͐Q̬ ĻILL̊Ԉ̊J ԊԌPOKǫ̊PѴдPQ̴ʴKLŋJM|D=    [ [Z[ [\ [\ [ [ [ [          ___________________^Z%VtRNB *) : : "1I19 B J J J JG9I R1OB!LB JbA9!9!!DG)֌|I)I)AZk JIQKQE(1[RJJrMbj J Z ZQHǣHƂFjrjrrIbb ckZIZZ z krrdzĒ{Ekcbb˃ZHY ǂHAA9ZzG˻ȚŊFĻIʫKJ L ɫzj kZKbbK ɂ ʣɣɳH ŋS^]͓V\Y֙ZQMɛJάάk< TV            Z Z [ Z [ Z[ Z[ Z[Z [ [  [ \ \\ \ \ [ \[ [ [ \\       }{MjcJ::L2R)H!G)AA1LB BQzj ( I!IH :LbD9 9(!9)JAELRV1 I R{ ZA9QA bP{1KBRrbjKR J91E) ̼Gąs{Errb sHZJZHRZb {Ij PKĄzrErz{Ūdzɓb s˃{AAA b9B9B1ArGĊ ǫƫŻCŠI̊KɣNJzr zzrrj zsJʛʣ̫K ̏dzTUXY LN ՏQՔT՝]մWKͼPdL$UU          Z Z [ [ Z[ Z[ [\ [ [ [ \ \  \ \ \      \ \ \ \ \ \ \ \    ]  6T BARRZLBR B9I19G1 *M2)IIMb Z{cOKPCL: JjςK1 )9 )I I1TjI9H9rsII) B)9ALJZKbj R JA9!F9 ĉijEʫƂYHZbjGbbjGjGjHzrHŒĂzDjrłȣj jCRʃLaCI0R̓AICYDQCAZFFNŻCCzĂEDȳ ƫ r {j˂j sjrr {Is͛NΛMQ KMOOSN݄ ͌ԎOP]]YլQTXޗދ N̬M|T#T T T U              ZZ [ [ Z[ \ [[ [\ [\[\ [\[ \\[ \\[ \\ [ [\ [\ [\ [\ [\ [\ [\[\ \\\ \ []   [MVd1 BRLB 2 :MR 19I!! B *:)9{kQ{ZMR1RN{AI9 R֬cH)0( 1RG !)! 2A09A bZZAiiIG9I19Yɴ FG ɣJrrzNJzȊǂzƒĂFǂHHƓǓjJˋDZ{ rjHj0 FzXEqQZrzγ݊īšDqzƻF Kǻɻɣ rJ{bZb s { ̛̣ϼ͓KԌO O ɳJMLMNݑP\\Λ٤TӜҼQ͍ ݊ʹM\vJ       O P OO O Oz֔ *=0[kU}\[}eeEDE$$2K            OP OP O   }P"[8ƚs89D!e)))))B!1C!C)B!)!)!B!!1)1)1)1)1)1):1919191A1A9IAA]}<}}}sIC9!1CIAIQQIZIQBAIQYA88CQAAYZQ08IEZJRBRQCA9JHRQAAJAABB1GRR1DAb(HJK{ BF  FBØ(   CD1͓ {GbRAD9IE1IRDI r(9HAAjGQפMJMJLP  O\yΚκEeNeEDD$$a%rt kq        O OOP   OP Opv}^ׄ~yY2 [B!))1)919)1)1)1)1!1)1)1):)1)1)11:1B9A9A9A9AAA1y8Y]sARIRZIZQQCIQQYCAQQYACICIDIZFjZQ0QZQREZAERA9AIQIQRGZJ0D(CDC Z kbbjC1A(b̓kZKsIb!HBpĸMԎˎˎc C 8 D(Bb HjHbE9IR11 RE9 sb98IIAMZқҫ9 BPss3   \׽ƙ5n^}^$$$aaANK JP         U  P N   P  O N#K<Q! JiII""I2!2)*!2)1):)1)1)11:)1)1)1)9191919191919syuuU׽Y]]| c1I%RZRI9IIQIZEZEZIIIQICA1IAQRCI9QARIIAIIR19AI9bAAD C B 1 cRREJKsB)kRL{IB(B#HB`c"BàE )D( (C Ijj sjIDAb(1AA k΋A)G!)IKzĕLZIs|;O OOo2L|T׽}$$DdƸƄaaA@b*   OP PPP P NO  POq O/ N+KtuΞ "Jk /  H"HI"I*"2*2*:2:1:::191A1A191919199B:EJBFJB kUvuUUUUUU׽Y\y0B99ZFJFJBFJEJZZQQEbZZIYEZGbAIIIAFbYCQQbRAQYIFZRIC9HZ9C9GRj0BCBC 9RZRJKkIRCDC)HJ{IsEZAB D(C((B &ap"AjrB C((D0 C (bYIzYQEzYCA1(j ZF91!JjSrLRrsU"  O O QP sD*KMY΋M##CcCbaAaA  PJ             PP O O O OP   p OO  N;QdUΞ "k . /NON     II IHH"H*"G**G2*:191119191A9B:B:B:EJBEB:EJER ksqUUUUUTUUU׵8ƺ]u|9&:J22H"*:G*IJBRRZQYYFRJAAQA9IQbIRERJ1YbQIQJIJA)9RQ(CB B!BRBHRZ s1AD C HR˃ɃI(C )F!QY( ( @@EQzFjRFrZA E9KIYbrL{PNLbG)9 BI[;P OPP P P  lT"!!AAA!@  0} l.o P       P   P +#S4S44$  N .  N;rluY}4. O P PO O NP P  PP   KI"I"I*"G2*G::B:B:BBJBB:FJ:EBBFJD:JBJAA9BRlsUUUUUU5UU8ƚ}֒lk&2h2"  "2*H:H:JFJGRAZRIIYDZEbQQQIAFbRZIZIIAR:JHRJZE1 9:R9C AAJRk9 C Aj9D(BCE1JBGD @zΛ( 0BBE(D G9IBHJIrZJ!F1j͛{b911H1J]   P       %BBbbMTKP PQ P          Q,S,LmZuU]DL<4$,R,R4$Q$O N / - M+S|}NN O O    N O OO      K J  J""!*":2::EB:DB:EJBDJBDJ:B2EBBB:fRZs/UUUUUUUTU54Ӕ* k        ":GJZQIIaQBIIQBARIIAIB19Jb1E: [1D1AZFB@CF1{MkA@@ bKkIkJcR1IHR sbZIB2": @ j{D1G)!0II)*BIEQ( H)KBb9 B9 B BbRC )@H9}   P    P  PMi!!!!@@a`,LqO     QS,DmU]TDLTUYZZإƛ[[ؕmLQ,,R,RL\QL4 P.o OCnCk . O OP O   O P O OO O      K J I J JH"G**G:F2:F:GB:FJEBFJBEJBEJBDJDJERBEJBeZb{PU5UUU5q|  O OP O O O OOP O O " ;:GBIIQEbAIAIIQIIAAC1AJ91AJIJZjJC CCBIJs sD9(C1 kKkKk{sMs kb1F))G)(8A ( RIZ11(900F19I 1G) B1 !( 0H9ӓs A9Am            O @             M]ۦYY۾W֭׽Ԝ\\YlL\uZإd#MP O  //    NP OO    P OO NO O        K   I J JII"H**G22F::EB:EJ:EB:FBEBJEBFJF:H2G"*MCS0ddC PP NONO O OPOO  #G::JJIA9AII9QIIAIAZFJ9)E1E) :BbC1  C B kbbIkIkRGR1)BBCC!)F)!C((9() B BB9!D Ib()rNk\Q Q      P    J      P  P   ϛX]] {PT{sQU[UZ_]ߞ]YQd3NO PPO OP NP PO O O P   ONO OP OP O     O      JIJ"I"I*G"2G*2G*2H*2I*I2I""POP      OOP P P P P PP  P;BJAAAGbRRRZIQIRGRbJRAF11RJ)BC(DC)Z k c[Z)D(C( D(D 0 )C)1D E(D(CC FARI D  1 As[AH1Y[Γd      Q      PQ Pq      Q Q Q    ]ߚYỸ cUϬGZKkP|Zֵ֚YΚ׽cK+NN O ON M NONO N N P    OP N     N N              PP  O   OP O O OO O  OPOPM3G*:9QIERJERIRJJJHRJGJGJGJ11 AbZ B B  C C )RR cGJ)AC C (B0C C((D CCBBFAB  B A  (8 )9DE D C(C 0(AEAj{k18l        P Q      P   P PZ]]]ߚΙ_{\ގ^X֑σ[^Y\TXTt3+   N  O OO OO P O P     O OP  O   N        P O P     O N O   O  O   OP P   O      N "BFBQBFRERRRbFRZGRRBGJFBHRJRBFJFRbR0000((9 cjb(A  !(B((0C AAJZ R9AAAG)H1H9I B)D ( ( (1FAIQRуRI1HAbC         P        P         PQ P]ߟ\[\[[Q{UZ^]]WΛY]ץPdM3    O OP ON O OP         O OO            O P O  ON ON  OP O OP O O   OO     O O #:BFB:RIRRFZEJFRFRGZFJRFBFRFZGZBAAjjHZ A @ 00AGRKsJkD9 BBA 0QJR: :!H!)D)G!G!D( BAAIIbZAF1A c+                PQ P        P ^]\Wƒ l{҃M{ӤP{MsWXƙZֽ׵ƜΜYޗS  O  O  O O         POP  OP     O        O  O O P O OP O PP P P O    P P O O O P  P P P PP3M;JBJFZIRDJZERbFZGZBFJBJIZA99GZ k c19C1IQA(D9Bk [9C0AIZb k1CD (09C D ( D((00YYQIJZAA) [          T           PQ Q   P     ΚM{MsKkމ{ЃV׽YYֽֽWTYߚl3 M  NO   K     NO            N O O P O OOOP O O NO NP O  OOP OO OO PO OOO32GBJZAJRbFRFZFRGRBERJEbDZQ9B91I ssJkbZbZR1RZ sFbI@(0(9C1GR cKkRHJC)B (B  F)) C((C(8R9ADIQ99BBQIb       P       OPP OP P OPOPY֌H{{ cZ [ZZRiJiJRAd1ARiR(B(B(BRiJHJHBhJ(BB(BIJiJRRiJ(BIJJJJRBi:21)))2!eeffffffffEff     O     Efeffffffffffffffffffff  O      O #; ;RFRZIRIIIZZbZI1#)!#)!#) $!$)#)D)D)d1D)D)$!$)$)D1d1d1d11e1e1d)$) (C( (()0 (C (A(  9(9AZYYIR9FII{ U TT   l  P  P P P  P P ][ȋsHJ9:iJiJRZZRRBA(JZZZBe1B(B:iJJHJ9(BHJB9IJiJA9BIBjJJJiBJJJBiJJiJ:!%eeffffffffff )         f feFEfEfefefefffefEffffffffffK P O P    O O +:REZZIERJRRbBd1#)#!C)C)C)c)C)#)#) #)#)C)C)$)!!#)$)#)D)C)C)c)d)#!!D CCCBCDC (B )(9AQQbQYIAC1ISS U  K PP OP OPPO P PP Q PحYT΍911:919)JRZRiR(JIJ(BhJRhJBHJ(B9(BJRHJB99BHJR(B9(BRRiJiJRJRRR(BAIJiJ)fEfffEfffffffff        EEefEfEfEfefefffffefffefffffffff  O P      O OP OO+2RFZbFRbbb1d)$)#)C)d)C)d)C)c1C1C1#)C)#)#)#!C)D)d)c)d1D)D)D)D)#)d1c)#)#!! ) (CD CD CDD CDC B )AQHrYYGZGZIQQRZ3  U * OP P OP PQ PQP YƙŧZ11999d)19IBJZZZRiJB(BiR(JhJ(B19BRRhR9AHJiJHJJHBRRjJIBiJiJHB(BHBJHJ9B(B)efEE%Feffffffffef  J   ) efEEefefEfEE%FeeEeEfefeeEfefefffffffffffffffJ OPO  O O ONONN*JFRZRZ#1)#!!!#)#)!#!d1C)#)#!D)d)d)C)C)#!D)#!D)d)e)d)1d)c)c)c)!)!)#)#)  BBBCD CBBC18DQaYYGjRIAIQ k  K P P OO NPQ׵ZΙ\9199A9911:RZZRZZRHJJ19119IJRRRR(JABHJJJ(BIJiJRJiJ:9(BRRRiRHB!eeeEfffffefeffffffe       efEfEfffffffEEEfefED D e E E efffefEffffffffe e E E E e fffP O    OP O P P N OO# 3JGRGZI)#)#)!(!#)#)#)d1D)!#)d)d)D)d)d)D)$!$)#!$!#!D)d)1d1c1) !#)#)c1")) (A B B  ABDB (AEQCIQQFjiaQQJZ S P O  f  P  P P O QP O ZΙα(J999d1d1d)d))HB(J9:iJB(BRZiR(B999iRRiRB(BiJiR99(BiJ:(BiJiJiBHBB9)(BiJRRRiJ\kEEeefeeEfefEfEfffef  K    fEE$ E $ % $ % D E D E EEEfEE \cE D fEfefeeEfEfeffffe % U% E fffffJ P OO N O OPOP OP OP OO  N B:1!#)#)! !! #!C)C!d)D)#!d)d)D)#!#!C!d)C!C)C)d1#) #1C1\s)sdQ {DAR}9Ζ{s JZjJRJZkJRosЃmk(J9R9AZZ BA9 JJJ{̓Ls{|RIJ [s [RR cZLssKkFJGJGJJRdOC"""J"K" JUyΚ]}YƶU4U׵׵vC;KKK  ""      L   K  K KK K )+ ]m2ܾܾKJ     O O O O O OO NN     PP O OO P O O O P No3+OO    ȉfȂ֑֙d  K      K K K K K  RRc[tԌՔSSktlttԄҌL:=f=$${>lQO~Q [(B!19 B)E9e9$11Z C1199AAbssR9JKkJGBF:GBJ [ [kZGZJB:s{QkNS:J"IJJJJQt8֝}}}~ߖ4׵uu|KJ   J         K I K K )IhTmmt=>>>>>>=hL    O    O NONO       O OO OO  o#|ŵ4tQ\K;.  0D "&؄<o+   J  K   KJ JJJIJRJO[kS||tkkkkc|T|Ք-^f=,,-zN]}}}]]]\=<49( *J sZD9(EA1) B||LcJZRHRBB9HJZLsZRJHR1ZPtQttMK:* ;22*2lu}~}}]\]QuvuUU5541l   #               K J K) *)]muuڶ>>^^^^^^^^>>=u ]"   N O NO   P NO O O     O O NP OP O  OO  3Tޘx8սS0lS|ƫҥȅdeet       K   KK JK J J KRHJRJ [N[OcNcst|tR|sss|3dz=n=$$=-=5l]}}}}\\<<<ގs  G1 csPB110 !1RJJG:BAA9A9ZbGR9J):k{R [[cckM[c޺Yƚ]}}}}]\\]<<ӜQuvuUUU45  K    IJJ      L  KK JJ J ( 2Iemuu vdzl:\~~~~^^^>=}ie)]*    O  O NNNOO      O NO O OP NO C\xx֕޹޹xΘ\׎%fCD9ܾܾטqt     KLK  KK JL JK JL KRZRJJJOkOkssRR|SRSStLze^>z$,-~-y=N֝0S*UK;Ξ~}]\]<=<<޻ֶf1B F) cMsA(0(1 (1:2G:19199A9GJJBJA cRHJJJJZ c{|}}]}}}}}}}\]\<<4|UvvuvuUU5544j kk K K#  KJK #L   N    K  i *Iemuu v)vlI~i~i~k~k~~~~^^^ٮRmIe\*   O     O N O NO P O      P OP O OQ\03ޙXSPRޘ޹ߙ֎fIe!"zǼܾo;          KL KL bGJHJBIJ [sk|RQ|RRtR>ZLV,$,>-#5=Ϟ،P S"+3d}}}}84<yU9abF199 9DAA)0D1C)1::9B9B9B19C)9RKkRBBGBF:HJBJHJR]<\]]}\\<<<ߺӜ0Ӝ))Jkk J             kh"]iemu v)vIvIvi~i~]Ǯi~i~i~~~)v nmmie)eT   N      O O      O. |WrkxWQPvޙ޹ގEiɧZS K  K    KKJ cHRBHJBZkQRSRRRd_.:\-$,=->-:$pQ T"3;9DL}}\qQq! A)9 (9YZI)(11)99A9RGZRAA9b΃KsGRABGJ9B9Jf9׽Y8YΚ<<<<<]>z$,-^-#n~[9[6+;9DT=}Uu89Y׵ֵ1aaa(D1 A((RB!1ZJJZ kZbJs̋ sbR sO{{9B1{]޺׽u8<ֺֺֻ֚֚֚<׽{0F  J )KJ"      N      H3)]iemm v)vIvIvIvIvׯi~i~i~k~~ n nmeie ]T2                 N+T\]v(jwQPޖ޹0F#b"&<\6Ύ;  K    J K   K K K  cR9JRb cRB SN[Oc [kR9O*>.%83n<-,-^-y<^ƽ:=߽}<璌vXYyκyuZaA 1I 1((91AZ k{OMДМФOOskk{sJ1BXY޺ֺֺ֚ΙΚyyyYY80sQ!F %% F JK"        )TIemmm)v)v)vIvIvIvi~i~i~i~ϐ nmeee)]\K) O   N N       N      S}\6>.:y |eF,$=-y=Nqz}癕=}}Ӝ89Yyκ81@A FI bY RjA1js{Lck[s|O|P tkR ks kB)14ֺֺֻ֚֚֚zyyyYY8Y85k1Ӕ!% %E F J JJJ  N  NM   N jH3 ]Ieem n n)v)vIvIvIv:i~i~i~i~,vee]I]T)T*         O       ŝ}\<]t(I(ޘQPޕQ JNZܶ\}ޑt KKJ  J KJK JK J K JJMc Sct|JIAKRZsj8K*.>.}-x n>>-,Z,MUXyƘ{}]8YYz׵AA00911E1 D)s|scK[BJL[kss΃{ |OJcGRGR{ cZZbu8ƚκֺֺֺֺֺ֚֚ΚΚyyyYYY888vmk!% E E J  #         +L ]ieem n n)v)v)v9~k~IvIvk~[<~; nee]I]T)ThCJ   O    O O     O O   Jս\}\]j(}uPR޹޹TTxƼƙΙ\;|\WίK     K   KL KK JK1BccStSltltNcRKJN[k|Mss...$ 6 "eF5Fl{Ӝ<纭|=||y}}}89XyΚߙv,cA@ a C)!B [JRR2H:2:FBZRZHJRR csKsFRAKsҤTu׽8Y9YYYYXYYY8888mk|!$%%E  K            "T)]ieeem n n)v;IvIv\\;^^ vee]I]TITCI      O    O   K ь޾}}\t(I;ޘQPޕWֵw;\\<ޚֵ8\[;|T #        KK JK F)RRKBL:S[cckcscN[N[kk--.-|-x :LUWm{|ξ:[~dwl[~}}8Yκ94AAA$9)((G9F199:9RJA9HJRRBJBA9GJRsJsb9jOsXƷ4UU׵׽׽׽mk{!E E F %  "#K#  N  K     O   H3T)]i]eemm n nL~;IvҖϲ+vM~ee]i]IUTILC*      O   O  O O O N   NKWΞ}}|(<ޖOR޹7͙WֹޙֺXֵֵy[]޲|       K KK LARIJ1 BRkccN[RRcJR Ssz=---->>9$87Js{{k:[[}}}<粔׵89ƙӔaA(#1) E91F9E11C!1:1D)HJsO k9C19)11GJZ kC9AZBHBRc}}$,$s{{{P[vt|ڥ}}}Yβ8Yƺ޺֖{Ab F9QD9D91A9F9D)1!HB)HJsP |{BD119199RJkL{AABJ99J<yξ}ޚ8ƖU󜒌4ksr`c!% %   "JK" ""#K      K HDT Ui]]eeem n n]_mee]i]IU UL)LC3J      NP N ONO O  O     1dxkI]ޕ/Qޘޘ޸ޘtTt򜕭8tY$$,s{|s{ƞ9|~}}U48YY4)B E9IZAIC90 9)GB)BKcs SZJcR)D1)1C!19R{ sJJ911 c4444}]yӜ4{sr`"j F ) "JKJ"J     hLT UI]i]]eeemm____^ee/~tei]IUTL Dg;';        O          }ZR@I(WXW֘ҔC ӄ          RbbMkZZJRR [JB9KRJA) 2---->>-$,d{|s.SՄ[:ξ;}}ߒ׵8ƙ ($)hRjQFbZC9HRHJR cJGRjA99HRAC1C)E1C!D))ABKs kRAA9 kLk0454545~}}\]<1Ӕvssr@& f  kK""K "*"L"KK   N  LT UIUi]]]e/~=_____________Ǔr>mIU)ULiLCG;'3K l    O     O O O    K ޴ba(a7wWt}d Bj^____________=>>uIU)U ML)DCG;3e kJ      O       ;b(|W56XxWֹ<{ @ fյ  K   JRBBA B1 :J:BBB1BRGJ!D1#---->>-%-|{q[tXz:}}}u8YyUb (I 9 cM{GR99J199B91C)1C!111!D1D1D1)D1HBPќѤ FRB cBB}445444}}}]\\<<r41srD AE Jj """""""JKJK N   ]}L M5~sP~>>^^______^^٦vvei]IU)U MLiDD;'33% k Kl         O    O     ֵvHA((s4ΘA!@D޺\҄  K        91AD1D1)F1)) *JB11JRB))---->--\-\~Y~8t||~\Ƴ׽8ƚBAA#90#9Q kK{99)1D)119C!C!C!B!E)C!))D9)(C1ktssќNb9J954444444֚<}}]\]<<uqPsr!`%E j KK"J JJ J K    JlLqmݦ>>>>>>>>>>>v]]i]IUIU)U MLLHDCg;'32% K k K        O      Q\5s[[b((}7իժիKC!AB;\;WK K J      )D1C1D9C11 1F!1I:R19BB(194--|--><-\%[5|\;=֝5ӔT9޻ֶR:iJ cZ s͋NK{͋ {I()B1CAC)1D))!D)!C1C!D1)1)1)IRk [ [kkN kA9I445454\ӜuuŚκޖQq{% G *"j"K"""K""JJJ*"""*  J ;}u|ݞ>>>>>>>>ڦe]i]i]IUIU)U MLLhD D;G33*Ff J       O O  O  OO    .#]9<իիBAhյ KL        RAA1E9 ( ))9GB9D19))E1 JC-|%-|->-\--\{Qxƾ}U׵88yuR)JiBjZ k{{J{I{ {AIbjAC))) ) !!!) )BJGBGBJKk |Hs9B1C1Kky544448y8ζ4ӜӜҜy0󜲔`QE f*""KJ "IJJ"JKJ"JJJ"+wu[ݞݞ>>>2~i]i]IUIU)U U MLDD(D6uK'33&"F I         O    ޾|||I\7);޹\xΌ  K   sbA9D1D9(0(F99R1E19JRZRMc:-|-|-|-->|---1t{1Sttמֵ8Yƺ8f)%!9AAIZ΃ sj sKs{ZAC)E9(( ( )B)D1C)D)D!)HB19C!RNѤN{ZB)U44544y}}]]ޚ8׽0Ԝ`ࢀ$) ) ""K*"*""K""J""J #  #*\}:{ݞݞަ4IU)U)U U MLLDDHDLLtd(32            O O     CW\|[[]4et\75}<\t        {ZI1D9D1D1 )1AB:) R [kLcMc [C|-\%|-\-65--=|QӜ4׽ޞ}Y4׽89ƺ׵q)RbEAbNs kHRRIRbR9!)!) )!D1C)D1D!1)9BsO |ќќsJX4444}}}]]\<<޺ֻ{Ӕ4a)"I"JJ  KJ "J""JJJJ" OLxuڅ;{ݞݞަ4~ U M MLLDDDHDCdddT2F  J                t=#jJx5W76Ι\\<Ų|-#      LcMsbQ91!E)!)1 [RI JZKJZ c kIR,\-\%\-|->>--.zE|Rtlrt|8]}U׵׽9Yzu c@e!99FAF9AAFA(A99E)D)!!)!E))9D)E)D19A!9sP|sksk{R3~y4־ﶵӜӜuUYy޺ֺֺ{ӔUbi))2-C.C;** "J"J""""""""""  #KJC7m}};{ݞݞݞޞަ6LL UmqmLDHD; Lddt\TT2f  )                  1dx|]E(x5XWWW֘};҄o+          cNScMkZ !E!!AJB [L[ZD99RMk SJO3\-[-<-;-56-.>.UF5?>=NlqY}}8Ʒ׵Xy89!  E)E9((((9:1)1 ) ( )BF1Lks1HBLcK[L[K[ cZ [RGJ{44u֝}YuҜ׽ֻ֚֚Q5eYJJ'2RJJB2IJJ"" "J"JJJ"JJ K  KJI"dmUmu;[|ݞݞݞݞݞݞݞzY{;tuH<;;QTdu\T\Tgfjk            N   +5j}667ֹս3n3   RJB cZD9!11 BJ: SR99 k cZJIJ,;%<%;-\-66.V}M5#8=U}}}8ƚ)"!D)D)AQA9E9 kb11(0   9Ms cB1BBHBH:JGBHRGBHJ9b4ӜӜӜ=<<]<ޙY׵ru֚ΚΚQn(B,[[RGBRJM[ K:J"KJ*""""J"KJ"J K " oC3;L}~;[|||[};;i;\\T\4TP;ff+ Jk               N- 9<)IxִŴų6Ŵս3;          N A1 :I2RGB(@ !911I: c9ARRHJR:*;-<%-%<-6,TU}5-^--{,=N<]]]]<<ߚ֖nk'21d)E)!jL{Ib0B A k9)(0 )@ E119!11A9:)11919ӜQ0{{{4Xyκ޺ֻ֚Κyy-ay(:JkcJFBJ SZ:J* JJJ KJ" "J"" JJJ  JJ!)33+Dxuu}~;[\|||||||\[;څ}XuC'3Lu\TT4TCff f f)K                  ;7<;սtսŴŴtC           E1911I: S9( ((0)9ZbAIAIRBB1#-%--%]_>,-]--#{,<^Yƙ<<84JJ1! JJbJE9D)bJ1!) !1D)( A(A A )1D)E))99A19119AA5y֒/+hfF&&&&&&UyΚκֺֺΚ֙zyYoQ1HBRsRJB [bJ "K"J"J"J"""""""""J""" N  K*""3h3\Xmuu}~~;;;;;;;څ}xu8mdsTCuTTT4TK "% F %  )K               O#U<\(kŕN; +nCSdqttսt1\  MN          D1)1F!)H:HB!1D1A99D)9RA)E9D1199D$-$$, X3^-$-^--=5xeU8yκ1!D)1{REJA)JZJ)1!D)D)) @BAA)D!D!C)1C9)C11B9R9A` #``@`@``@@ eIӜUYyzyyyYҤ`#11:hJRZ9GJRZB*IJ"J"""""JKI"   J*&"2I3\mXmyuuuu}}}}}}}}yuXu8md\\TuTTTLK2%% $ F J*)J            K Qdw\\[Sjս-3" M# ;o3 K   K       991F9E!1H: B:R [ c [J1RI1)D9D1A1E)$,$$:$V 5 m>>,$-]--?>Ee9ALk|5׽׽4-c(JR{PNJkZRJ9919C1D1D)D) ( AB A D)CD!B!9)1)J c̃ JsA``@@`@```@`@ `@ K򤖭YYY94`C)d19:Z kHRBHJGJJ 3 +""""" #""""""JK"J" &"*J;\\d7mXmXmxmyuyuyuyuXuXm8med\TTuTTT4LLK2E f  ) J K    N            -#\;;\ƎC"N# #+    K KL K  O O O  O    J9A1D1E) BIBIJ:JBI:RJIJ1R(0((!D)C!(0#,$,$ #n5$,,]--$,1)(e1ZMkP1k! DH1ӃcOkkZ9AC11C)D111 !! D19A !C!D!1C)C)C)9FB c cJssZ ```@`@`@`@ `@@ˠ糮v88va#)D9d1BZ{ kHR9JGB:JI ""JJ"JJ J KJ j"*n;LUTT\\ddddd\\TTTuTUTTL4LLKK2E)k* JJ J JK      J        ||;\K".# #-# J      K K K   O NO    JBBGR999 J991 :9JRbHZJ9 ( A cs cbx,$$$ Dn5z$,,]-Z>6 )B 9")D1E)E)jMR1JB BD()9D)D)D)D1!!E1! 9Z FRB19)129)92GBGBJ: cGB @```@```@ `@ àÀ`m30$)#1A9R k̓GJBIHJ2*""""*""""JK"J"K* "K*JKJ  NK "Kg*M;KLL4LTTUTuTuTuTuLULULTLTL4LLLKKqC!f Ij   J K            Lƾ;\S. ##. K           PNP O OO   KBBRH::2911E)1)9:RJA@ A(@B[c Scc[$,$Z$XXDn5$$-Fg!@ E1E)D)G)G1E1JE1E9FRA D1!D)!) :MckkFB1B)2)1)A:B1A91"Z` @`@`@ `@ ˠà``@ cǫઠ !#9C9JGRj cb1B1I:J*" "JJ"JJ"I"I  "+ )"* 3CKKLLLLLLLLKKKKC2g K   JJ K   J          S<}\S        N     O O  BJRJJBJ:B1F9D)1:R KISRIZRJZL[cLScctcT$$$xLnF}5-]F(*  A@D)D)1!!()IIIIRb1 !)  !!IJ ScckRAD111:1AAI1999)1&JÒۀ@`` àÀ`@ `#!d!BHRR c c kZGR1:::"""""""K""K" J"""* " "K""KlK K "" 3CKKKKKKKKCCqC2 ** L     K  K  K              QdW\0\           O       O OP OP  GBBA1GBJHJ19()) BBSN[k [ k [kS[cccl[3T$$$:9,enVNUAB D!CC) ( A9ARjbj1    )1HBG2BJGJC)))1)A99B1C9B1919)2)E)!jǻ` ӀӠ܀`@ ˠÀ`@@@8D)9RRKcKckJcsJkB*2""""J"JJI""J*JJ"J"J  j*;QCqCqCqCqCQC;2 "  j  k     J JJ "  K       K K     K K-+ս0\  J     N     N NONO kskZ1GBB9 !1 : B BZ KM[M[NcLSMS KJB[lPtlL$$z$zZ:Z,<6<A( !()!DE!!) F9JR c [kkZGRC1 ( )))))1!D1191B9B9D1)1!C1C1D9)1)9C1D111)11BBksm*E`@ `ˠÀÀ`@ E9e1ZJ [R [Jc{ |{LC2""""" #*"""""""" #MN#kj ) gF%F! "fKj  O N O K  "" K    K K    Qd1\  K              O OP OP O O {kOLER)HJD))R [ZB K[tcL[H::1I:JcPl|OlPdW4$$[CE)!1 !)))) R{c [SR A@@A:9)1)99AB1C1B)C)! )!))B1B)C1!C)!9 BBBO[k|k{P9AYy` `@ 99RGBJBRJcs S2""""I  K"*  j) jjK   K   N     J J K  JK JK KJ  Ct; J K     KK JKJ        O ONbLRJJ΃ZB119!D19RMk|P|ҌO|tcGB)BJ S KclQ|Nl[ CnKCTD4w,:  C((F91E1E99)!1AAAjkRHBZ9)9GR kJ99HR:911B9AB9C9B)1B))  !E1)ABZR9 91ABIJBZO{HR))1H:A"AibxyRA19AIAJJb cZ** "J"""""" #"J"*K"*JJ"J"""K""  Jk"*k"k  #"  N       KK    JK   K       JK       L KK      O O Z J9ZKkB11E1BGJ kZIR JZktsc tJ9:BBMSMScdl[k SRS [JcJA (!11ZR1 F! :RKkFRE9IJRZBJB!Z|ќNJkGJ1919C)1B1C9B1C1!) ( )!)!9B [ksB)19F)999ZQ [91s [)BsLcbYDaDYEaEYbkR9191IAJFJZR:" #JK"KJ""J"JK"IK"J"K"""" "JJIJ*J           KJ  J          J  K    K KJ      O O ZRB [s {QIZJZbGZ9JHBRRLkck tkGB:BM[ccLSM[M[s|M|kckN9AB F1)JBckB9B1) Bc|sER)AJZR  kPtN|{NR119C1D1B1C)91D1 0(()E9E199JJRZR!)E!1999R SkQ|s1F1E)BMkMkBRJIRBHBHBHJIZ [ [ [Ms9EA1BBRJFZFRGR22 # #""*K*""" #"" +"JK"""2""#""  #"* K""""" K*"*" "K""# M  N    "K        K       K   O  ON O    HZJJJkKcZRbB19GZ9B9AIJbLckN[RZlS[JK KBJR[d҄|sZLcd9 @019KB [KJ J11!(A S SZAD9)A c sBHJk|clPlфόZB9(D1!!1B)C)) (!C)!111*:1:)! E)F1E191:HB [L[kLc1C!11HBBJBB9A9:2JGBRJZb9)99JARRQIBH2* JK """""JKJ"   "JK"JJ  " +JK"*J    N     J  JJ J     J J J  M   J IJ     N NONO   NONHR R BZJZZZQD9C)GRFRIB9AAGZZL[ctltN[ SJ SJJ:RM[c[|O kGB Scs{IJF1E) J911QIAE19IBbR9)ZkQtcB[SS[|S{8 1 ))!D)C) D1))1):11D)D) 1E1E1D)99B:HJJ c:11919199JBJ19ABBBAIZC)99A9AAJAIBB"  K" # #"+"*" #   #"""""""    #"K*"* K"JJ"K*J ##              J        K JJ  N      O O O NO  O NP NO ZBRs cD)AR91IJA(AJ cks[\\[KKB2JJBHJ9ABRLS[cPsLkK[tcttRs cIJHRD1EADIDAC191JAA1Jt[K[ SRH:**::J Cd|єK{J IsI)9C)))!)!119C)C1!) ) ()C1A9BBJRJD1E9E1E1E11)99I9C1B)AJKkRJ9919199JAAAJH2*     """" # #""""   O# """K*J"J"            K    JK  K KK J K     J K  J K N       O OOO  O NO OO OHRJ Bs|JkJI9jKFZIAHRRsskSNSBB211B9A9JHJRJM[kllN[M[M[|QPP΃bR9RAHRZsckL[[JB1B9B9B9JJ KcSRJ{AB19C)) ))1C)9C1C9)) !E1!D1)))D919AJGJJD1D9C199I19D1919D1A1LcOZ1D1E11A9JGJZIIBI:J"    """"""""*"""*"*++J"J**J"* + +""J K""""   N K      K KJKK J   K  K  J  K    N      O O OP NO O O O QF9 J:RLkb9R kJ{bjGZHRJRL[M[ KRIBHB11)99AB))B)A1BBM[ckcllkJJ Sc[lQl|JcZEZGJ csL[RH:BJS c919BAA9A91BcQtl|HkRA9((( !C))1()A!!D!D!1D)1!)B!99ID1)C)C1B1B9AC)D1)D)C)99B KtЌs )999B:Z cGZ9H:I*"         #""""J""""   JJ"J"""J "J      O     J      J K  J IJJ  KJ  JJ  J    O NO NON O O FZRZRBZHJIRHRbZbGZbRRRZR cRAC1$)#!!!#!D)!!#!D!e))e!e))1e)$)$!))))))119111sK[HBHBMkK[KcRB1JFBB1919) : :JSdlkP{bB9@ A 1))1( !  !e))111#! !$!$!$!%!)199e) #!$!#!#!d)1D)D)!2 # N#    # #  # # # N##)efefEfEE%EEFEfEfeeefeeEEEeEeeeEEEeEfeEefefff   OO O     %E%%%%EeEEEE%E%E%EEfEfffefefefeE%E%%%eefefEfEfffffff k    cR kJk cGJAD1AAGZZQIJZJZZ΃K{AC)#!!e)11d)d)D!D)D!d)D!$!$!e!))D!e)e)e)e!)f!)))19)$!)(BGBHJKc [GBJ2BGBGB21B!1C)9D)1JRRLS SkNb0@(!)   E)191911D)!!$!$!$!$!e)))19e1 !!#!!d))e)$!Ef    "  N )ffffefee%EEe%eeEEeeeEfEfeEE%EeEE%EEEEE%eeeEeEee     N      FE%%%%%E%EEE%E%%%EEEEEEEEeEfefEfffefeE%EEEEfefeffffffffff j   Mc{{σbIAA)ARYEIY RZbjj sA9d1! !$!$$!d)1))e)D)D!D!!e!)))1e)$!e))1e))))1:e)$!$!1GBZGBJRJ1919C!D))IBGR:BJHR:HJRk tNϔ D1 !  $!f)e)))111D!$!#!$!$!$!$!D!e!))91$) !#)d)1e!e!eff"#"# #  # #  # #N+ Efffffffefff%efEEe!ffEfef%E$EE%EfeE%EEeEf%%%fefEf       Ef%E%EEeeeEE%EEE%%EEEEEf%E%EefeffFEfffefFfefffefffffff   NcMcRIBBBR909IAIE9AI RQbd9d1e19 $)#!#!#!#!C)d)d)e)D!$!#$!e!f!e!1e)e)d)E)e)1e)D)e)1)9!$!e)IB [FJ9A1C1)9)!!BGBFB:I9:*99GZ9IR [Lc|Ф1A $!%!E!))11d)#!#!#!$!!$!$!D!d!e))e1 (!#)#!D)D!eEEEfe "     #   # )efEffffffffefeffe%EEEEfefEEEE$$$$%eefeeEEeeeeEEeEEE N       ) ee%%%%EEEEfffEeEEEEEE$$$E%%%Eefffffefeeee%%feEEEEffeffffj PPO J J199IA J900EI9DABA99DI#9$)$)e1e1e9#)!D)11111#!D)C!d)d!d)#!!!%!$DD!e)e!)$!f)f)$!E!e!)11e)e)!99AJJ{kER9A9BBJFBB1A:ABIAD99JGBLkOZ  !$!$!e!)1d)#!#!$!!!D)D!d)d)D)$!))#)D)D)$!EEEEffff"""## # N+ # ##  ffffffffffEfffefefEeffffefEEEE%EEf%%EfEfefefefN    fff%E%%%E%EefffefEE%E%EEEEfEEEfffffE E E E E fff%%EEEfEfffffff  I)( 991F1IA 0EAIRrbDbQ0 $)E)f1D)#!#d!e)e)D!)))\sd)d))1#!!$$LLk ۂhA t:hA t:Eƺlv9zުԹ>o{;9qꜪ}sjA1tabbbbbbn"311111הMͷȏ4/~p \ *?e/{ˤ 5q*ȴ ރV,vnTL7l:uy_n~//H¹q \ *ӫ_j>2!DsZ&]v=(ormviT_w)?<ҡCy'KkYe|裏.S ĵ#NE߃1'Ny9ru]??^:s]7oJ]r{#ڭtqs jkiW`)0{|J)_+R8\ ׬Lzիj٧h٘g*<+++Cy||~A؏Dq 򕯔c(crO''Ϳ˿6??a>?9.菚??cGnUW}Wɀ/ &7aTZ6 W&ŶqRu}oecڞ3GGs3eKKܜ9w^c@ưXS.oo6KKK浯}ۿ[y{׼51n2> !3_E_d>,e9Bl黾z\|r`i';;e@wyti # /c=V8aD3 l+9pͺʴ.맟~Z.\ lL3}< uQHUs~WUrl///Pz U.\Woo _tF gP|7}YXX0nn߾-dĔR$}~+$??79S3?P|07E΃kQ&Q6gӯy>l V?(Lu?Sv`_%QPEۧzJ mϪʅ(k׮ ??rQ\?z,~{''?!iwwwo A_Uҷ}۷yMeOOc^3fwcl _{z#HcD,>><#ӟٟøVX\2a es a[6g4:|yW||M[[be "XJ`^҆[jD" f;w/a{\*ׯٟA$1ho~e{1'cIϗ&6ֈ͛雚w{w3;~VX:3{ cM # FQY0G=L)uHF.7}86yecڞ~ް Zx<7< l#XxJLe 05n7<8'+.- B? fj g% p`7-Dc3hw+D'_aNbe^\P<L8'ڪh`@k.﹞sY1MKc N&RO cq~SZUجs畍i{&Cǫ$ O> Ao]ϔ]ZquT\b__N#]X A2vD 5S [70_c0/;A_mohwhsY8WLZXkϋyCD^^"Z\D^a0q޽{E66, 4ieVz衇zr P+#"څ{,YLY( u1m{`UFϵ=MaNϔ[..w^jPu }5̅;#Xw<55%S\0q~ɗ|68u_e"m@oP.*R0!!G}GCl]h_OhX&wk^+}nLLL?Bx`Z}> oo6o#ց41pens > #`>vϏk-NJ±Y+LYԞM¶3+"1 YF>0*Bg Ŵ" ӛ'I&:"ªbІyfulI2_wrT(~b_J:~9&?Do+,z ooM3QP_y] bC<{.ԟl #ʲ4Hf'{]SO:7$\Ll? ) 9p2ecڞ XDz+W?Jc+̄@ѷPEυ čhc_GQ?ڗ}"] (AZ|땱_O7U7`ՄU }I sw<ᖰ K;2U%V;aB~s u7S&! ;a{]S쿣0' 8p2eBd)v jGs4*L٘gnBK‹Le -|yrx0?Sg!b ]apW0|ˆ.rt qP` -/ˋ}WAGṁ2JµpM\l=bʑK\)`JIx1P\*ʏr>x`5@Pf{+灚1J h)Ir7a$ko6|p;7YLpֹƴ=S{`'ߜPPs^3U.{Nr`^\B"6Ps+X yF. pqNX͠rDj7$("ae>.X9\xyե$\jX S3Zdzac|_>poӿa"1+WV%u8Md4>>(8ō6 ,S&,9`.S6MXh^\v0VB ʅsiD8Hȱ( PvJnHY}~jO_H𡻀2k~~al(i槟+}}mJn|X_ڞhh,ewpM.zU]nG0PӵD|7( *Ep ٴo>Yŗ`vVo9x4eʤ\!,0Ρq2ecڞ)=-ȯ(*{r!Yg" iB䫾y *+T SqV@b.1)0'aE#2#:KV 1cCIϾM~}J`oVo%:(,!]s32U]&`eoicKțH;˻K-؄m1jB%oX^ Mc܄HC3|`s1P.Z_crU/Mф'kHxk).= ,]lL3) ,3~0o|cbh]TPt,0;""=J NN#a C(Q}׉y*&rL;eߡVѩLj .B4,̑E'@{}>\<7XPE t|;cK_&-҆XZd;;,AL0у8M\o7+lc rl/] owb li< eʄ nYƱY+L3g"uȱ %p]T "wk0-#{|P _)۾o ^A\ `XThEb%uEǷ>';Ҏ@La ŻۚWXzs=@U\D>^  6ڦa=NIL1w#Y&U$=g*1!K 32d/(W2x!ī KX7J 0%0ֱ!Enާc ,Q/o>_˂) qPp`F,Z{nP` |p.9`o S~  -Y 0^Ί$4RsE΃kQ&T"F6`rW>ط1mTͽnT^@` u$O *:+(Eć ZѨtvR Cg&at|PYY[?vIJ.@;ʑv,t{.[_ ޷7z#oRk|f|d>6S(h!/\WOi0Vڱuz+bRWDˣKcX0lz0 ٛY hvq-\2Օi{&Ap~ 借GL9r(Q=4 jQ؆r>(+/HG03>@+| Uk_ZQܤ},<Նձ`r+M#j" %0^ Eǵpͬ2lG|s֕pM\J=2mT=ATh{U4u:Q c]aj"1 j&`쇈T(9J@9cB @GԳcaJG6~G0i1S6A·`>Tф(ܸkf /)+a#DsZ&]v=yߊ媚0 yij5-p F%]h*^:#d|e҄kᚸvzdiA}+k;N?LLLLLLLMcox pcbbbbbbn;3ȴA6mnv{lʊabbbbbbn[j pRebbbbbbnLLLLLL#Sebbbbbbnr`LLLLLL#XA1t Lln%3wbz9!3{l,[ȿnMnsy KrKf憘rz`x~lȼn 6nmq>,0w|,_Zkߺ]>">z Ή/]0g?zc^s_R_LPoHrwkP?~<;_5/^6^8y=VL ;m3r{>k.366L>idNsbx`Œ?0. We>7?}~s ן=:+q= A؝ XI/ڻm3@Aݻw'ʂf$|An{_oW@Vyu ]3wrN%Q_y{ap}p>)7I+SM%:q˽gwիГ)3 018(}/K)ieTVq=AԣA>PiD}{@94ش@$yyQ> ,YDU({^.SVuA *΁LY9p^<_ ggd@@$`bwߋ&ۙ0:ν.w8G{rI*<\bZ-zFA>2 H 2|?$x `AM<9x=}(Aº} (yi3us9["=^ϭXUc\ 01raEg%Ϝ{_ov}KHJ&Mt C@0HϽ!01.SP>}a0R˾{8(U3dg#b6aֽ{(y״ obu <Y$}bN*Asӫ ;b%tn0gH5spLRSX&y&Kf^{9G+&tL/ڛK%X(:WK_V1H`RopPi[*_J%4RkYfn[Il(>X9 %wmr'013rT=Hp*`1;$*:?Cw͸vQa+WW}׹= pɗOzoe1K#`O\Kk@fYAIR6QGi[Olk ۮ]h9x`@ۍ%O$`b@:/M̸)`(A׷ Sn4|s3u*/;|{>_*T`%SUQGy߮YaP'9 vS$`Q SHt}SSOyE{a:lPtPS̢sQ ۽ m߹5p(?u\>Fp\zEaAEպ&kMA+?knJm0A& Dk~-"r{"`}aLE#s^=g"`("eR3{WWE+H6'`;M h$Tv^g `*WU X撦: RBΞYlI{HYN E&GBȊks<: v$Uﮠ L/ RMۯ eet2:)Lx!lEI$ _1Ri[ rϩ 0#asc+}:S#thZdw*R7:Vx//_ >gfniKiڰWݚ|($Q$`b߄jrM23]Gen#2h}ftLL)𕧠i]g2FA_XvSQ8Tfu gYE=ZWYtPV] fn{BջrC[}P o"T=dq/iƶx'iШp{0} 1pWߜik}A2^4H+Mڊ^hB|):oKK.=tzΠ;RZ;"qŏ Z 0#F6PxLjDv^M "!#F: K;㕾d @^Q>yP ྋF0_ۅa++` 0!Y "OE(0ή"tOM_`Je\Yy=7#mkֿJq.MX^CUJXT 1H[)3n$4jY;Q{xa{ ~TұQA΃Z C *(F; doG"| ~X|o.E|2"g1{&闎}(9?EL0A&v$t c~]~= }{zm"pM_f}#WLpMc]^dn>u(:{,.̱|ѓaL0/mb;`ȅʔM2@1ܤYk=|57Z,G}Y %$`bK~2:ktPn R~d:1*HT#bY6T~ezg?|0=|HP~$D#+&S/dגF8'&P(FQlpOK2}17r[i|L!h/);G: Ho$ *@zE 1@OX i_9B9õ6綩o?I$`ZS~ W 8my&Cy:i^1[]~Xn{5اS S28ȋ|iuYv&ez#-Uv0C$`(Ln;!NpOsf1z(?/$-~A/Ld˘ 17A Wk9lWwD=+$aپK"A bd H\2Do.l9֭ -\bvGﮐ #LC'`r Rސ!f U' .y 0AEFDlbl Ά_ aЕ ::BrK$`!iu-5|7P&zO|Hjδդgsq* ў =F ޘg AU@E"pF4n{*zsA|]-s\ݡOXMOiW8M 0A4Մ{' +J.A5PƝt`*aG5 tm"zhKG|A&N_a^<ӎ2ֿÿ su@~BhbN{{}cnD{c>$Lr>?WMAYWAXXnDN *H̀XO{VƶzFXC5CG ߐ I!nF϶#fPZ%0AlE|Z"@Uy)o_R7fbSsp|}6 SX">`]l%fC@ 2gY;ZL { HM~[LXg;mfqs lUۤOA&cOAT0cЅ E| ֳCוp_DU "aA&dZ ~t;$UA& 0AA$`  AA& 0AA  A  xȲabт9S:LLLLNC1u4(3gΜ9s_1LLLLLLLMc'WUÜ9s̙3^>v9xz R}(g{]Sx?o<ء3ϮDY'?).9 :cXauv^?s53s>9yhss:;s} !۳939۳}n3SܥaK̙3gΜ9y"//GAZ6-rۙۙf=*mns5lIEo9ȗ:c)Lmp{jKN:6n)//Zm̙3gμ9yO=EOx2[pv*"Hvj.iJӍjmlĉ6oo6CzZn[QڝNm3gΜ9s[1['[궒z@ī7;|L6@noI;wLLLLLLLJi|li=am|jc]3q6T~E># ΄2gΜ9snwL[q0AAqon* &NqsAQN*" KG;X0AAQtS5$f0AATTT~Nc%*` [Y@ܴi?{3EAT#KX}גwdsTa &);&I, }' RJ:ݓ$x0A c]zH3KV4jKB,ruKpCւvmNu;k+ӈ5fRWQM}*"<1*yٺnQ8{0+wA]uKSׂv 6'`D D d9!$rQdτ\4njr8]34]8 b+|%TAi8a<ׯ}*γeT}Kg:T.x8svO9=jS=''bݭ b(F|L{LͶoYlC:!:Ys}P|Y(ZGt.TA_7{n̙J!9NY%{K77Ak[ =zy6N]虞VP }>_jm>ApMuKpC|>5'aE4kh=EN.Cl(蔯7RE-=@nQp#|B6\y^BE"IW[H Zr5XU<pmuKBAU {ɢ99+7#aЖ!ԥUh%,_P_)L͊Đbybpu]z$b)Msɾ\A[( :&7g͢c(JYK9[T 1>`!TϢdBSIYq{ 3t|S~XU7}:{3Ek_[A[G7LA;I7LA;C7LA;I7LA_LA#SAP0}A12L0AA ULA#SAPpC|тNǬhKkfš9$̙3g [;H}Bwᳫf%3ȼ3gΜy#y3yd>jm" Q cĴpeL]6{Z4хr6pOY+-~@>`!f戒9s̷d'.kҿ \qJzm4ǣ/r6G7b迃|e߉^1Z>ߠ1Ѩ,'Qԙ%3v̟[2 /3gΜ9Q SfȒbBD!;`0I.AaAC_X1WW *is̙3a$Uxa̜Y6SA ѿ #z6_ P0Zڍ5qc#on3gΜD?O/]Z~K5C_GN4iz릎,ʈjj8ڒׇ H[-̙3g>f+E%3}l':h؍Dʗ˜9sce % to<=ltv>,v8dVlm䭖9s̙(w0kۇN.4ٶnnh` :\+[PF;طI`g@!-ynmns#v~+ʱzmŴ\5saʘsA0xL# 4-c7\ b>`U,#0OKiI0܅ct kJtyQ].H${zc̉={|15{ɽ{jtZ9qa_8(!c6h$v `{9s̙/WF_~rGX_2]APXAX@vs4JG9ұArs!| ڍC;|,T0ig|oo梀o9s̙0WQfh:R }'Pk5WE񊂅ڍ5lyDƢ'4 %׼ޡ> 8F.=H[6lGd͖CܿXMΝ볡|[&?bZ3v{- pXxNn3gΜ0s }3RWBV,,SgM725S%APPHԴ{ >, VnR4~kya֡x|j e@pʴ|Kqtl~n @,ۇ4Ɯo >>H _:e6 hp}Ґsӓ}gΜ9sc몀1y:^$woF/HvAXk~F/]4Gg%g}0g|`kpE]- k Q%!mfs(`kk.^0s焈f͚Sf|Hm{BLF8H˗e}rNvHxScS{C!+_7ƴL(XB0?\0`|LiIlGz˗zEozeΜ'Эp.ps]_y3"SЕ tdF<(o{T/Y`frT1) {/l%/<# Xop\w}6P|:=O8# >++eթUZ)g+V :aI#qpdxNigT|Ʋk+uG|mDECe\[_u  i+`A uv *WvN/6w춥 L9pծH] rk5H+/$~r_ݡ/)kэ``Y_!%b Qr5wb,_239}?SıƇmS8MK:,o^$(vz^V~\^~s-8-,/w7!e@~&irmri F(C0 'ٲϭ6:Qyf. gʼy5m[F3ZZQ_Lg&m$;v4"Oõp;I2e)[Do\\ZBYTfw*={>vB;?GKl=i-ΫYٻ u}_@LdSupbɩ6R/>y A ڕysWu%z>γ@llb.NO ˔q2׵0G3}vϮDĨ:d QlRR RGb Z51tZ߽vV\Uʢ4mõ~AEwϯe@PVʘ@HR&Et7[6tp?918jNR._\9?O)B'\=nI>Y &^Pzin6 nA=EeVeV=|zܧISa+f(`o*b>`,loWqQ.}yҴE9*]g" 8FnPRc,K+Z2amR;+NJ>isШn=;8𑠽G5AY.Udݮ7x_|V=FEӮ?W ]6}RFXsS}9P-l}z">t/LLpFԍn엹φ>,= ]͎/lnzynϯ/x^tr~tY9?q~v]r$N"m!S] kAzmcpE ]1:M‘x <XWrrx|n~( ksA[|b)=n2s#u!\%NHcc>ax_%)r*ֿާF`ㅋsKXr-sQ6qEvncrXdW ,خø:ڕs_R|^C?/]#L7J\]C]G=weQr0?Z 36,1)}EZ濾NINHӉEGIc;{,Y7=\09,qn[-奘8J-Q__c+%&]kt:Ci*0vejnIXo*`dIEzu6TY'x Y ߍ>kS_]7|"TWy Xmwݎjԇ1;";mZ*]'V|E磌Ϛ2G9mT[DG|l)[E|k\-$)>lIf]Mo?|\}Ҭ&Y~~;;lסs}= "Y^W)\ J;>פ(%"Ip\lRW r9"NiJR`E>߰ڨ33 X|% z}Ɋv)I>NU0~"FAHt|˙>`{$ӐFng,e9y#sv'tsdmec+q)_Ο0Ad+sA*%m_NU~\2;=oR-y4j^sڋ>ø~,T S>v\vlOrpp\<`HA[;aplf}6?M;nfRګ(UUR6kpQU0(u: KRMROն=)Z6B2gfPCLpJXW‚9Oɦ)`wK~)m] `Vڲ>`]; ᭍xUMӔfrPk#T'l)1}:Gܛ~fQۖxEsR}ɾ&8.4㕣9(^+~{f#3#+{ ~8 {Y zҿuN/|QJtm>vG 0HFtWH:{-= XY *<DzJc*`7Իp<8V\-E:HTM +o3#JXTi#>A0}y_Cڸ>5$lA5]_C>!mY0Ѕ80rG%cd'k^r,y|akYwE GCiTQ+5&_v5HΥ;#PBmp6|2NEYeA<ɫOAf<3JS; N#Jcxepڱ߽_Qr|`.Kkfbbbbj$ag}VvsF&ˑ99+FPԘG,Md+`]2복~p6mn7D@D4tD¥~(J;mS'v<`&&&&c볛b2k?ם_@FNmnsnVjMQvNeфLZ\id>`7" ]-VJu.=5|76#eLdydT/|ЧiGk_5yk~Xsa̙3g>.^T <+V9.$*Sv"e>*`efΜ9su=qU0A>]]iXM0'o|k2х-_!/6=mܫ}$ c,Ca>.<\_|i:TC f%a̙3g>-#}O-(jT&FU c`ƶVw̙3g>\ozWo+e3̢& X85#yb~s+fYk R_cΜ9sͥ_?FB_}Xz#y߉]5G<5h>O6l;/B’|E߉Wo5i].mns[dџ:";`0reZ1أ=#)̙3g|^O.Wҵ 2l}Vv+PWԱeh,mns!lέvDLA4_~-@8<4kvlV…5ABO-3ԂoJ{2AA4C7|u#V =/ͮAȼ(cJw׃bY?k&_0c!. ϞYa AV#w3\%!e$goA><'*5 AD3pE,@PcbZMӢnCs~Z4ς?LD5ZU;~tEzlf/mH 'H}P3'C}pӏ BzKfN~GMCS4౥>(쩓f3Kt@.\0X^[#Dۺa=[\7 AVP PP V4-z>Rxiծ\έ//rےcfG0N# ]Fon<`Ri[}>T+/jP # $*z%>_-U*)g S V'Zǐi0 Y5qlpܸlέXg{2./煫0}GVSFF!ϯ/a'w̚l/^kvZOp^\焊ھ8pMv[M](WZ;펙>8*qcuwrPQǾ /Z_Y5?hWϽ|+-y'< y(s_e6ӫ3Lk~߻ZM+`)FD|N>`uf,\\m!חJ| X[c^fATa%YU.MKhAPFD4y|% z*ܗQƝ!/.헶;=4Zl=(ʡe(q\ѡ:,JZ8׈->PQvYɬXWzTu\} q Rui>.N}2UnmwgOm~޻2{AYLa&ֲUGI<2 SoN9H W#߰:ELY"WyAН>YM;![5atwй:=/4vxؑҔ^8"Χ\I(} j0Hgi@y?Cp Z}<v86P4gRD=ڝ[ѽQ.۪ݺ{q%oeމBw}ۼGۤkY ]?mf˞6" CǗ% akoܹ+2%iv, zWd}aHe.N3ͥ A>hߋFJ*(krtԧWvvGTwfv]-333!Q}CN9SdpwP}"moW {W9m#>`l@,!6}q5͖e4}+ЉC#tn'/ovHiӮꌂ7WR LW8o %)ˇ_܇^Ul'~@.y*qĉ}сMe߉BUnok{>U-6"}f\D'l<+!ABC #)vy~휔 ui桼wa0:.1!`Cv>դXNkYymjھږ([cm.N}꼯"u[vPU߻*GUo e*˒>2@@W]lw>_FqiIGʶKzŨSD2V% X˂ >: w BݣE}EEIV֨>o_pV㛆T(,y_L U*GUOvyU0~IlNh2m#<3d9+*aFgP&vZiºN@Rf* xm_Tf9ӧN{꼯8bSe*VNsTi#W7n[ĀX͔vv@E9XWU$h5)wT~sA>ҏgT?^j`&Dg}kk{#짍>`wGӐv5h ݙ+]ӔmQsN؎6iGP3VťSA0;wJ^ռWL5 :7](,y_u/Y!ʽU=g?mD*1wX+VYzFO7v6U/xݨHgwGx9A /feGyE%&`ڮJR#`t\n]ً7_\hάy<&ǞfM\ݾng](,d}AriˬZ笣U }2T05 k ]@_.]H[!ˎԕlQbjJj WCByt^aijO^n].0^ȷrWͅt%$:a]V²ö4Y~b~v}sݕ}Q˼E`]G+]G2_ |78Pӊ;Ƌto Gl!:1\/tRkk}./)tv͹ut{:zWBMn2U^Ț %%ϵg,N}CWr됧}]6C9t rւ.zκڈ>`ipn-OYT-_0V"AGƷ60 4eLQU7,NE ;;|*4[O<>/DsiT-$`b x av5 >°K4;[^X'k$`b >`@ļ1. 4:gMݒ' ZO[v:V>Wì3CDn}zJ%??>gÇ?;-pZ'P꼷N|D:ovpt]I?ב\k }Zz x>,\_ dZ&ΕTV%z;k¡GzZ|]H6ކ ?5?\4093^g$`"_7Fk(7zOA'4,sV#øvoey X*zoxV#XL/Uv"`)ɉ-Oyj[JݞegI-+_LԫHHw%%F%w;>#\j\xU!`\G:q9Ox1s"cܤU|ܑݛJ +,Wi"Q-oAug DYHX[RmXnYSRgU28uSM07m*~#ݫ>Kub mm~ 嬲,8*N2;jyyA/N>Z+<FPIo+>/ckQyHI;8( %|tEDm{p~u8VIKXY~WQ*~@(s?W>6hRqG4I7JE)OYS:< *4,yOUml%l AE>vY#}K3qy|bcߜ] |S9i>ecMP-p\ ˺m(GJGVW\/P}¬BKy8 kMO{?R?牧~VQt?y 1>`1wj TV'RE>i|n:sBLE8:W^[ V?f9FM i\.yoYq)ǷS-,5kΰ#1UGFq"vY<7#&#{{#Vt:uK S|e!L(NE9GmLM~U[˶EZV/*AIrFyϲ=vTcr3Kk;o,Ui] M"RSm&9 Xz%pڹJK4*D443\ l[ x|*`uOU~` JH|TJ8ٳO\׳O  ˘Z2fُnZq/&ug}+J䍺 =jv+;K 8.ǢS5y>`<;{Jn J=S_h_\ӳTG o }*Ʈmô*4 ڣ|U#bR:}$e,TŖ&(%`rAE>F=EAXT{Jx h9Y4sݏnZvaOFV+5XʰV*8V_4UOǴT>b_a\p}m'Puv:=b02+2)~gy=X y0)<HpSr 86a+aL." KH+;xcr_2ƫ E輴&༯!s( ²ޣ8h,p6KgZuPfV=>r"בHe]A/e>s\׳T2Nu xXĀpf1 f?j>q ++[qD6^<3 L;OEgR;AeĈzS+ ќ#ѐ b*XaCD SLjcXyfZcդ5]ͼe}˒Ca)Z۳b NJr/طgw*DE}hV3EDspc|y 5WsT3*:%HбSsQ+!8'~k!q aoLӀz1*  8W6h@TEvu66$k QmB癨b~ BSMu#nj{b``UU*^J뤎g *1G HU,2;jfwUV͵蒑v"}::2zW/HgVEvo_a+,v)] B(jyHSǪm" 5QEy|ٝvcls]VQ}0WǚGR+1HUXT݃krKY7X"?qMۊ=δƋgl{hҮbpAUÑ=2*@}i<hTlEg=%e;<DY|i>`f]uWTs5#ElvK:8P"+3Q=ϗMuRZ6kg`3Q=O:X3EDspVvi*|kFML#`:&ܼ(gߩ1z/ sHy)rep?AMT[dBL껳;* X&"dm kPZ=,ݠ(%/곮Ty&ʶGl%H T@3EDspkoOEZVyvgWL,Ev&`N<=i.Vªc^j|/AϟV4,27gl{}S%"f2AMT[NK RsՋt/j[C;fEc!5 45"0עK|l֕k]v=uJN±٪DsJձKu%ru`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >`   & )` b >` Dݽ ׍پgz&M& V5s >`   _Z^LA(.}AQ+wvLNǴm  SݸankfiIUAD][_3˗™Et~Izuլ^_nn >` !໑YNGu伙=6kO͛ B+WL0Aq/g[I[!v#]8`=uz9AsӇY3{|VwL@"\& v.Y:$D;)3q`xsw!ကg̘]<&h @%ݸ2ǵGI3^/&_CSBӏM ;1'Nj>o)kADLk7w 3a%b2<&X?q<;cf&Exn3K& va/,6Bf* ]%[,$;,?!'G 9! LADnE-H*$*ʢx%׿G8T Z4F0A 6jFP><- ج*PNIϺ?t' M"4?Qs%Ӑ8 ؖĵ.0Cn*dV4T 8AR>ler ^߃wFK(Z9"JrMouK(gGJ_ J|܀V.HC>񗎛}/d;~G ?AK!V \m8P^4}A0 ʕ S~@B˝$eHjA˵o@r{VžmUSDcR$W|gDALA [횵kk`ҤlmssU>Ww;K0H5 z.|Y Q(a%[|5( \b(7}J!YoDkxp}(Ӹ-9yAj$p I27opmkڶOx|/WQw7<ܾ,ͅ&hZAQFq xWTmfq]|܉m]}[{k| u-hru>_cĎD-k?!zԾ +>}'H7';<뵕ӵ%|ZY>`~(ά 2#̹s/~)M0>`ǽ")UOt}&=~³>വ=|k (VmV.zǛR~R|=VdP;O7H|ƶ"wov}1CO+EF276ZIעP[Z2 G2C pũ'>٬\cm;n)yxk ٝ=Z*5I+aPoh;IBo/HZ :ގu+hwl;q +Y/].nRKS~6JʐPգx2OW&y&t{|Ў:?零wׂB }^{J1`$ĺ! ېL`01><7ٽb#e +N]]̷SUTF7{| p?>Si}k^ Y Z3"R0"ܦpPA2JCL Yï\Aڸ rBķ{.R1QGM bK|Ms*XK>(]^C Ċϓr{&lw̵{|m3ǷS-E-RLC \H"cb fSlSL`%l-O1i3ŀS (E )TPB{}dv^1FXe{sN}8t?倷n%mHau>s|e5+ÑpϦaДD_mcl}-5[w.n'yo$&jJvqurէ{'WCUo:qR\x{a~檬ho-8ox>Cva[b\c:1ˡ9}>6~~jCֶH"ޅܑp_vsxTӟb'=_o,r; fU>TI뎫0=z~=~kW֎{WzFDvF~y?>:[<^q pn-\06ڇqM }- e޶a=jKϙ mH2WOtSo>3t'y\ѯm;0}xPc"^Owȳ%.wOrNrE=/[RڢYyÏkbV]M¢o"t=Mmq('{L*y,aX;tniPJ{k_/ n՗^OZ[0DsO \  Ĝ]WD|;?t8X0q2 8\;͞voB%w;sDv]ذpev!rv$bϯLD[ܦ{j.|cසуQ;)F`ȏ.5Tຠ"*Sm| }3{O )eC8>8ޓqmG)ZCo~Uʕ,7^כ!h'O EA\;QI1X_$d+]?Cγi6ߛqN I8.GЉ'^Y@=K;'G] ޡH?Z&6DyݎaAw ,2o^L泎8eх Ed)HKE@2%r]}ZZF5 $r)!8>[֐{%˹t|Tκ? g,i͂F孯oioWk#8wd[õbː):o;kz9qR?߬PEp@9'=3AVw|o׺{i$M.Qk}!$7+yv-_20//Qcu*yOW' 4Qx]r͌{2q5}DߢgqZ5_t% c]nIIL O&后Kη ~鮗*熧%P˥z:{=Ojq•+vaпUM j 8`G! =WAwYп)ȡ[n%$'nj-I"B/j@ư As¼Ӭw[q8y~9"8j ~w _i8Zlh|\@%CPeTe85lN2Tӈ_w}Yߕ跃 k@7<,n[o}7_Ylg/Dtz[><~T͌u87\Ý86 %>/ 9iޥЩ<7-kwߑ~4*%v/%Tq 1`H*URCTzrbFT@a}؆"~ yRjݬ}rq1|1{?ʿ;r-=8;J8C9"׷92dul}sgEDO!] [YM7h w}owIhbD,!A`[om@m{ruXqR @>  [ ^cNdW8YGΙN%a}08٦+8'i~\l2syn.psrdbSvm׶-v4sݤ ѽfe3t(|ԑp ف/x=&nQ$Ga=q6H׸\]rl;7.gyG_y4 3C2MMKGT-5?Pyl"1  =mu'lc|~J${gH" 츗]TK:LUݗme8^Guv8if{S:ff6 ˊ/ t䆯N> Vg?G7T:-jZvth|Z[::>oS瀃9k88!up;J~UqRHzqu5!W X Upxx|X<.n-=ޕ$.'CCZNG7_ڑTo9 l _lwsE- +\#2M^"K" _E,s#&w̡at_]]C,sY˶e1ܯ?fA?!aYSHx9M"\ej~/ps}O_˸^z^±_x# \+TБ#ƾ>߰]"ي8r } oct8D!`Wqa dN^NSȶ.hU~Dܜd8":Pϧs|,SK%8SE0q6kΑ{ҧPwC0crLX>-]\N*5pVw-WL ޮ)aͿПix||acI}{B8xi=X$rڶ2\sSb C!D@]k.]Jp{ײ՜б طyVDUF[r+Nz>=i~]}@\I:gޫp]tU5"O|YdWq58\pP'oEqq?ȍ W4aJk&ѷ9B|*9 lHRmJ/C̎/HF*hmqyRUPcM (XҎ62h]J''Lz D[Gھ-_e_VZt}""؊pt=ejUܖ"ZP 2ڀ:͒Ld"(q'awKhHT 0 orTm?*]~唃?7y<2=>x^ɑeA?)kj$,m[:*eqtN-'x h=JUъ )zPI2+z~ YI<ʤnp?r)*CEU?|˃z7+?[цt;KºW쳗A9J/dq a#?YH$[8),/sԪk\qKEly؆ՄpU-}^x@ͿR [)8x2qih50+ Ɲ9[v5C. ' Cf2sTM:_/\9J1@d.47w;2#{qy?Wz7=x<I۠^os/'cOדJNZo;kymEO0ԵЎ2:fMj_؇b鶐ilg 6j#"j-" ~oN8/(4C xlЏ[11ۻfI=J8(;嫦6+S.^Hw!ⶕ/xJ-?]/ON1!F= l ʪ(^0px!ɓ)ێ+wV9ЎT dIv$!>|v,Ѕ$_tt{`%H+9Ǽ՜Ā0t.ӿx#^{`8CVpp;5Utv(oT 7#2tV!ߖzzP HBM]f/Y~ ]7rқ:w}rԯߟHOmCY|v}rj֒&OqߜmHDX|3N4 / )SW7FK˹gI`gׯ `DθG\㄃LrrekVS*omE=lHP_Y4 LzؑTh!bw !ۊ[rUOBǮexs~a[m=G#BN*jZ~$fLOr'ms S!pؙօM+q, ھH*ٖR bC ɶa\ƌ ["m u-pC+k|ޯ'\ \mE*8A_dGtdIYC*0\Ln< ڐ.xJX7ܑ-P՜,Ǒ*?pP&2ۅ x]7 9px\Q֣YhDV`柍,+%u䫱5_/S!%/+~涶l?8A̒ ?χn1-JdOUt|/<^? 0L*W+}7"835q ]k>҇{q"qp[YϫmQsZ8(*q6tgSĦW۠6~Z]QgTܑ ~׸PEy;~eǭLRo8j%#7 &n!^ݱmٶߘqȈvӃQ+-9_$Ypd9)ˬjllGh/m[6 8 OهâgekVhF //0֤^03_D-K /.p˄yFr#\7⿗ȸ[kS Ko[}v0VdA?S'dvp+"eU  n툕' O4Zѷtg7 FEr܎!\AV /\䥨m:G2)*{3_x_|'86Aqʼ,­,)vxncnj8''\X㎚ⳙZs#pk-/!=P/ ۍ'v{m+o?*Mk¨ފDAR?_+88Pu؋7r҅Z{ڒ D,"q>Ae g>zsVk(d.=hO>XsŎO 5'O-GՌJQͷr#],'.pu[N(t@LrdEu>vomvI(W|@ma"۔ߪHLXVk{M1 xrk>_'ɟ"U_.,v;O~lifF{qEͷ%anR]_=ʻƣhqTe"e2׈wO>nH,SL }$C—Qi)3oL%Zeh#GDoogj@+_pv {sjf }Uy%(ؓ#5|ߗS -Gp!#}gq,ZS6C߳30e$+p ɠXD&%kۿ} ȇeJrRB"Yf,(cF}]dp K:/_zkY1똈wmu-|c6Y΃),r19vy;{^v|%'윱ŇJօê2lM<.?*tO_$WQoȿA .hY5VakwGiam\A Gh:bbdCh_8v|N/c !ܼn }cV}S<?&b `ݍXk,A6͸ \0Ӭ6kȸ 4fIgJe_o[T:G?.U-仙kmJ7=Ȓ2phC56h3?Hƭ Lٵ@A{?|g0Y; lnDUlWexh'b~⨶nqyjR/U#IRCNӶI?44pr";G+?zmRDN+(g_BmDzI6%kQD@8Lu,^^NCZr\µn֩ e"dy4dacnEq}ޟCCÁlJ*}&"嫥KOZYϾ_|L Dl᪾3^˚ OAuutxvjYV%be^+1% ZTЫRyqiCw;T/]}i!n0+.k-0Ė6rEh3+lҭW>}Yp_k-TAIۀb}ɹ#RW ۝?%ϯ(&UŽB/{3XYWeqI='wiCW/  {è'] `9>sIbyϑ²Ӑ!]`m)9fR #c1C sXyϭk\>m%Zڡ/^OBOvrĤVYQ4YR@>|?kHSEKN2a;? EYLYaw,9cZyhgA%ȓ;*8}"7=_8}e@˓93ⳏϟ;>NʓEY(U`4Oi19b<gjz}ڮ$Ɣr.R9a Ѽfa }uTA/x|n_;G9 G†FFmn $[AYtkpS0><3VxQWO:!l/tK1>~o|h$]w=)H8МG4{rr|Amĭjd[pkW)*'8""]w/OPf],>ɜM92!8 CLQri@Rα}RԹ[څ\7мƶ"O:L֤{VfO?1yfjKcuQ.SQ['by-D)[[lLIjgČi+>mߕ%IәJj;:vWUle ޡ v$l-Vzh=Z βWֲ7*Y녚$ۯ&_3#W; ݾ.8椪²3 p,y_˂2#>w0JQIQӾ!Z= 涳XMSZe|D.-'?mŢ.jHVC<"euD4!pxqi;Y8KJ ~8mX5| …[ҿE\ TI;qď '~տ7d+7\YA`;>9AUŧ ~s, >Jv,N *ڜfȇge{C|}6Eba1Z]ݾ]aSC_.8aQMӂ:r1+:+pKۙ/jV?罼jjJoBȉN~Ԟ.'!L]V-Q@4Y}F9`ٞ9@xl'c5f}HAgybȕ'wK%|%[hP'z,K%9L"y>g|[9C[qzKs#cegJǓT0 z&Du#"bOK"lֳWw^OI]\ jP~lxa|?}خwMH''?|i¬}V CȊ>C(3F5TNL5$eYѩ1Uѱ}WEYR{/t7z2]+}gFǸɲ/;.Un%hLºt~Ilea)klT)WUֽjop5t8&a}[/+э}obrr8ϓpjL^ӰT6+IuĭC̯{.yn{2|j$B &+hu3l;a'Y=CxS_t+|P0W8*5ɄZ\ς9^^n熄)zoz_JȺ@q0-tTy~߄3-[Йx<;7p@X)KmKPVtsAy,e1zu [TbQm^Pؑ-I]n)+Wm[EBIȶpG;ig~XdT=BQ6' E*?x/  pQ1V"kI^lR|4l۳y}HBO\FZ 53u.|xoΩ+ţn /p -kzj4>t;ʝx|#ji[ clXEܻRGkUt: W9x\:B>\<)] v{zv["ᄎg>`A }{8 s+ v"}[VJk8|yN2'aՒNpH$C9Hwdét3$ҍu[o"(EiUo#بcE-*h▻>ef=S ~_>j*K>g\tbe%M}DqIPC"=LU2m[p  }儛nxD&e/8_UG眰 gOZTz/2}%̟}>ޚʺmpg*iWKW]ɂs#N,ǞrEFX֤/h% #j ljH3rD+Bwq8nl:xAQ%6 |AKjz G1TcRz=%A#L\L9}H>.7V3}QrwyŸJnrw}Dz%GmI{ވD)}ŌQ?ٿ_4,s08 #ompL刄Ta8SesKu\ YA\f4g`oaI+ #b~.z3St>\=;g}W$Uhs7w );+-p?%QbA?Z}KJaC9a7pCT=,9QNs¿ owP u|/\tHrens!2zVԀ̾hL5Px&RF8G`UAopQ* %<VFok IV&% XJ~u&n~ 7\Mȶ6&W"}fIK1],U|l-8z`3ċ _<ۢLZL@꧁ P5nz>5%?1ϔD뽘ϻ!>KMן.oua,'LV?;G6ȅ\ЖzQӨۢ Tђ:l D9؍XO\lzy*ل[/>H s\amjgsג 6MM|Щ0l" Z 3">IB=RG6^PYi㜔pNߌ=Շ[S'8BQ|Bra=@Yo:)e&/yoルظ<*- +9\q9gC6$/|PG/Sb'~R.,]U? q->˔EŴKPE7C@8ʘYDfMZZalNi7jùpUct֪t ;> ΞO/xeY* rPDaR}vx[rpr )1"UfPҘ̢tZ9CP'׎2>N Qf塰Iw|G^Iȓ$*)*+ټx$_:}:&:4/X+e`e(E[whˎf6%6=+Lgnj2N˳n}*EPX8o'*zFU'ђD$Lop^J?WO*~@SΛ^ǻYAn{9>ۮNxdA & 21@'Vs#;-X"MfD J޹ ogFx|oQdm~Q1qʶgض_ x4NpQ{"yvDJSr ua'qIȀ.a. 4T?SZF4s10~I_q̊>xh}Q * ۾TɢqYV4[ $Zd>Hx-+d(ܘ̶n&\+kP̶0OσJnE*88">0Y|: d+xX+Bu]\!᷊~QOIjwT 5 c(Vl 禟leɬ$lz7L`Y޶d5Xޏo5LE*Yˑ#X Pķ'ކ跹LlHؼX/113 ?%g}5es? pl ,+CX=d5kjIH(9a~Y>`׻Qd5>^=Aknsp;.zaB)rpIC>C/POYmxUy怋 KK TA6ls g֢BCRzwg݅XAUzT֡WmNB<'Q ޣwP;VuJy.>I1ǯohCEh9NlU~!ߏg5{ƱڱՀ  gmxZ̠ڐ ɒeK{5"OUU"0%;epD:pՒku|8`*e2K2"ߣ+ϧ=W&]s=ۓ`5C$ ۔@KYzp'ֳ?KU;էBKߊ$\`I'DḞOynbBlAvN‚Ĉݺ~#5tjhOZ34Zʾ̲Sr%bms/q y%=YOpO9KŲ?5) V=u{[W/9J#̒PUOW IZcrzyv|=n?'?ppz`o+B5t3&gn%fxgj[dqzc/mOJ_~IPX`RSsn8mP-Ԩ~R='4gxa=|c{B+e4fMϜ^k왉={ۣ_#/X/$$BE?^^t2W!I;m?.h ܜ[Njh"s*`k8tCߨ[>Nx>i|P- |-{_c/ZQDkq t֤?4+Sݨ|h78îYϸmC:W# vV2GmY:jbB -h}݆8}a-i*++Fb1[F9g[/;'v+wb|'f_?;9'o%a;gǕ GcG/^G ۱q,0 'yC.rDaw C\8=OB<k', flBYh"+d3A a,t?CٳCopt3$Zp̖ΐpθܨՀ%MDM?O̖<^r35{YfݶskqJ)C87-ȇwt/G)Zgn3Ng)պtMW]F4{f^ģ\ר}$l G^7|<ō#4=+:J5{߱TgcYss,Ex; i/,R?lL&0(kWCkR̎h k@Z&m8B=UD^ eV5kU8B*fAmm;-wbKҮu@~JUD|[ORs r{19q-r<|SF͕;5elߝ`5gY0XaSlRmlV ߳E}όe NXVq-i]bbEb}!9;n6'I&E4v!ϩdjM$d,ov CU_0DUt; $E(l!/uEu )7}i;{B!0D #kCo*)f*rPIt炍<8d݇(Yv`ڙ?Hhwf1{~Aż * &ϸkCvMdalTL?%U0-d@ٸ{橆}ul3\#RwuCA#1/f0]Y2;LWǝBd» /szs0Ec \09),r$/UY^tyf'84%[傋L*PlD19, 'L$ݼ~M!%aڐ>J5D2|x :(>ĺ$ 4gLj9jp|C=e/W;i7<]` } xb3>L7q\'`)/yn2nK~金ԝ/eeTHzuX-sr`ʳNJ~9A ֗[" 54e-I>|Ұa[~Scs˗[pmư8 jF p8E)*;چDd;mK #K;_}m×1wkCL։JtHZuKmN2ՉlW~b ;^w<M'kv tmYu8٢cnB*o:=(o_ݚL*}$t'/;Trw ٓ.<哧IÁM<{{!u/6xL%j6)0<׮T8КĬM0"͍Z7,%2]e^@]8٘TUuhBYG@OЕe6$͂Nu><_,apZذ"߀v^%glɉM*nNtb~˙ysaq8E!Z yevמlJg+cUµ^wY=<䫱[L*s϶psc ad4rn KTw9#Ø_!Βg"kMl znfY¬h>`Y= DHGd8.["Ƅog-M!ȺDϧ†r .ڙZ`U޿ %nrZQmL# a.3xNsG)Y$E̷,H8xwuļ-I /^?ƑGܾN?ia4aC[ y$ՁDdb'sEI?8-9OlH7's p2~/KߍfŮYwfEcz0 dzȊ^gaCχZ neG8>8_C28af>+E;'ZТ䃵.3n>_+}}* Bhm{2!K"5rɭ!9+>~V&uu>e>36<3tm'eD3b@Ab,;b=ʭ'BQs9b)'y?fCK`|FN=Y1V>mAw :z9FN; .Hܽ3;{-: 1#zt= %e}KmF7]W<(⛍}3,}7ͼG8 z/ψ'yV.[g ^U?0qEyp6$a.n4>Vҹ6 ʌC?JJιPq>W}^auUo ę3dj*`}l%.Cdf/A3N®l&lf֚~ʲS?] ?vbya6!#ߊ mymBsm(~eYQ aG_02d/˾ y.YRVOAy2V+kS \ _ro>j͓NQ !py8өqQG\"clڶ2n,5 3)g\˙sDAȉ^\&xߥ8ȭ_Q& ~7t8_l?ֱ i..y!1[4m)/"ʅ>Ƕzz!nG6[w'\CDY9zҿ'_%({4rznDȸ*/@BfoϸV_btdzXXw[ uC$psX^ye4%8`oLZKof^d@ 얠BʈMMDƽ`Rz+;6t-Xcx@uᲠH#lC1A qSЊg@O*UAh'sĉf6>N~0=|-Ffg`=1NZ^qg¸ֽu}w~%nJs߷|`gBVqs(b*u(VͿh.!_2 Hd?g&0ϗ>6JmǵadAkWAglAx,3}VCqK͋<.YFtEG5| ~rLPY!*gmk QQ{{S0> I̲߳sCDPphb8^s2 )vx?-LȈ])bST/{prƐ^oGqLpoߔ%]W#p6ujN mk{i9w]CMAkeA$s}>Sm ]VlLcUt<-/6#Lzvo7'CM*XZPtYs1\򓖷-Y06D3>:>kmZ-n; ۪EpeBė'GyolW[$`ez- WUKڏ~ߠȎn!xڐ6t\/Z,@lkhWpD bV6C]z;X yb =xL?Urz~ϖm=PlSѿ! n"d|]>P{)+d>3+}1FN߸aጟMssx ˬ- H@pbYb?j!^U>2۹Pӧ;nOU- []njپJ9`AO*< kER孷cfa'9C! oGy:1k_`dk!d,`324< a@r6.L:Y&-gT *4@rYG)93/E"4ejЗ|Eo |55tνjnޯ;\~Hl d2OD5r%>`Qs? 8OfY2_g-D̡IS^oW3kK]FFe #`X !6zUTAdQ>cu_Ԣsm\{ YxmB%YA-Q 3ے]Ɋ.z,y+Rz6[HELlxz6-L 6 pS^dAG^ S{h6;+ЖLд,l#j]p^a+xKb/&́&$lj/ Ӱ]Ʉ. l6הϬgB N&^w;(#Bw63U4ȊV-r;CYp9\fES]&Y4oA`-m8+2*皋~%kz%!'>) +"yq4409R=ˬdI մ-AKn?hEcbA~w XRR Ld.^+nOrßI)x1'^ 2~ڮB([N ߸xy1  owdo}u#{]T5r v}-Z8z[BU/c%N8"NHy+ jD_렂9Z$$ Q&eͯJZG2D-V8cW?bPU\S4qc؀$# _%&aMNj q:GlZ9oωo&;/L3b"on>X=TKػS,\"_\2γYs|{r8w!*r.cvs o7}Ń8a- *h&a[>#'1jتIm&>͆')r}' G߰=/je{Qhe"p{.)0ڙvq"-+;I*WS{ҚqÒe(E*oHYЭĬ~[5=X+}=Me wy݆t"Bse7hN>Rğ_^ao0QPy. 㤼 _A N%4b*-#:u1'4IސClwtC߾ŋjRmGhZ2KmHw/_-]WUT-ܕH`d bcd%۱hx:C-0ˆneQoֳjU}|N:P%r9nm{>NVgZRWAjR}@} ! 533M-g4eI}p|a[rXdQ,+ ߇J/Qy}Q(;?C >:b}]!&5C^UE'NWnL K|0ktϓK*~}eVp^+;ڇf'C48` 8`fAoggd:3gD'pA, _j0bchFv H"^_3#}0 mG:hct"Q.ϕPPeP %KY C9<ɊiR̊3/KȲ8^XT+m}(W9~U*k{|=$o|w8ߙqҋgUK_|yϷu;pr2U΂ODҳ,jg'5=oo sSEK0V0s 29};da'To0{1 xξQExFKj_XV#¦=H[׶d"+zhAżЎVF T`flBk~ Q+,;'y¡-?Ipl5bX,/\~m7{$",Y'.ڐ 9SWvE+~;po7w_n)(dҕsO.bxWǝ̊蟔m`rYzulKh2Lc-E-گpũ@i —|?Y = *jQE6 {~8OW&X8Zp\L(q8]~U_~~—G֣(Y $jg ]9S3Nx}c%C7"YІǿ N5Z&rHv~:oV bT7xmIҁ,$?X/Io0p̊vN8M)+Rs˿_ rÉCn#_C_ 72_!`#~<η]/I8бw2\<8^}E~r./bbcȃ*!OH*iqL*=,/b3%964wypun9z9xƋ.o Z|7>VY@{O[xA UN'x'j96 dEڿX/g`%4 p*6 y0n6I@ƝR=o'|.e.v X}Y,iȂQJ8GoybֺY$aL1p3.D†|S`^E++}ñ7n$.[;^[H(>\+"Mq? ;(L1Ek_ב/ЍO^u7vp8V&tNCqP\.$[t۞Y\j-,6î]bcŏg"Cf1N"ϸͭC]Mmj+zSI_ͳ|gi~*Rц׋ܯm1jBfx +b*z;X>0NYb% F=/ؼ( WipW){ޟcWEC ؉̚m5K a3!zђ>ބdRu\f7o,j6߈cAV޲>`ܑ6~#˂C+ ]Opz;cc!9uDN3/p}ړwx|wGguINLJ0"5}gX\=cM>dz#]qvAѪz*ڲ%gˊnGސ|p̊&\9׋^bTfwX6ϺM]k,}-E!>|w>OȉaX\GAnZQdrY9 C͵8j8'ɺAg#648ppCmf G$5 5N>f3ܝ?ej,`\E 1znsܽpxCҤc܎\{LK} MȂ*zpo+`cbmGy-V)ͭzXw?WGWoۀvA9qgnhl3JaQɮ[ i!?>9e`^-!-LL!bU9N>aVv>^p$b.ArlbY(Z<3Ttb%S?NB:M]~y?X_{ Ьh$+z#W% k9d堖}m\SA_Křϗ&uP^+|SvnPA_8 Ezkz.!"#wh涨%h= ٶS ˢ 5=Sl =JW27qe ).;ČZ}t!i 3ng_'eS%9?Fιnje_諽ef;gړJoTG3޺'cyQq[<2NsV8ጃvsw, ޷,h8s ~:VI[0`HSEPTi A5,6E _,&n͜:V37SONJC[f#|C, /oX<-$RNRo6f/g`DPTnD/pzb&u9[H6L8wv /ޭ!H"^siNPOFvrh?w:쮈5xd,V'^a*ײmF77?MwlBb 6΅RZ/&_q"(ñ` F|eb!BCS;32FCS=0G͛XvdߓmdBU_ YW#K-+:q`=pOH˭m Y#r|tkXa(0' R~1 c|PlYLTvx_ kQ-B3GT;l y.:<2jh,P% iS>eXLJ-:3wwyfya%;-p3k$&]lxק?g/s]=ߊfLe-p {L d6z9=".S} \"}mH?v}۪"愄amIaq{ Z7lOnYvDqXE +N 07w rRm5|Y=Vu&|]o8s$?snק r"HUEFҟؐ|B8"_/̰AFOmYs.q\֧B-o#,*AKU=ٞtrΑCU|vcvfRK;|-+Ed Cx+1 ZqG8 A dɤyᱵ}{ϳI~WHȎKp+aȊܰ=jKmomhAʓcqY[Ua,XwRh E^_ uIhxޗnp.)l͚f݂|ZIJ-IBop+)p95ЎdKt},hSA~G   @13l iY?Oֵ j'5 +0> siu.bo|AUۀsX/; KUÑ0 nSîUjVl7k} XpL(Ð-"P^L¬ÔLͅ e 2e|+Xj v_Ղ f8<㙉X6$^nǤ[JF<, f;wE8Go{۶!u[؊^\BD,'y<a7NXOLzs>ap "[gTFcT63Kn ,Gg^wCvNlU!;k I)jYYqF|?,1' [ Ns# L Y .rBYpK8f?Gvݔ9<%a@@{w$q~f vfD+ok<Ŭ\{2dN͢kA2})9_IgI-aj%{̓d%l#Sjr}WM\be!U5fܣn-nrFW+#uљ͝-6GIpeׇ!`0!C<׼ίM9SUӖbDׂ~/0CÌ9`W;Gt51ZPGG+E1 Z:Qx˂C$\^ '¡M̾W򈇛=bn$U=N'#,w-QNhbu^ߴ V;C_Y͚ al{bd=77oT[3e) H=]jČq Nj`DM>cc-O[҉v3 0R ƮlGiz dDyw09`YeQ`j0*fjԘ!~y:Ky6_l+::A^.xAw-HxfeDA D8pi'- h2꭯o4??p2›7F=fF[ # bS ZΫec鈔jh ( #u;x}KDz2?NC\$Hx/ݖjn@S_$2&!pTI3!KBRөo㡻|ůUj̫:4܏ a;IM݆ꍪhoM IkOx,ls0taKMR-׉>) ::]$8G-`#IE1J0mwdwDff1NuvWWZ͗?6+4{|QɑnǕ8nml_M "CdE{o$-Y.So0kv||L܃I,)X( L%z~ދ4)YC, ЇG_pD1!5υ&ڥ(Fp[Aռ5IK\qAYK48࿮ȂquR~Cx$Ļ" m*N^`:0Iwf[Kw} :&?:! dgIU*`- w k|JVi c%HkNڬr";Oӆw~'0B: Zj%߮W1 B-l/f JcR3t!j=.B'/"'|IV9]LSZ$0Jߴ{*]>5 㼧CxWR[Ɋ^ٜ$9E"Uj%JgV]xpTAbaPN20U vRR_L{A IjNbSz>T/&熌A\ǫmJvm{uO/ۀXN'‰ CX%1U:4_U[r8`lCAO}(d[hYRy]ԁlǠD2C[vT~*)hc3UΎ|gjgnsW"&PgKKB!Rf^25t f+R >VKRȒ}-fk}C݇%a :GĞ$E|A@@!ݶso=61X $Â۪prOx勌|+\f-RWE5 T cͳ!L5Cԙ7-U%1e [W8?͇(wƏ)ԅYO"_eل\LV#{#ч!d:,J*&wڍK{5zT[3?p@[9?˔vnd䞷^`Ϡ>w<0`(hg)|AټYn56r&HW`^?$,)hhNR(n`"$ ,C.>-(XB8Cp kx,$ƄGe\e(t`/n6w9|&1? *c]s[6$Ij4=ϐK6:-G5Ww˧ގTQ.vePpYozf6rr 'vp I!W opSVtlYTgL M8. nG$*nahjOڄ&RejQ $-S.92䬄B G KjՄD$? kZq,-sl媣){z~d9.mG;9s"N#udk2fBz{y9  39 9E`Xv ( | u]Jµ/s0$v?^v.ckQ>_tsr59zݕ}lH;8bi6!pl[ A˟L7 3GbnaP*[g $\pQl;Y}/; +"N70\T{/^6ϟܹ'yR܇ZBي 3yB#T--nflK}9.gs/-`hYo`U{u;zP͊Y f}BԊ\q[vvvmVjjQAoyeA!VXv,>U / P2q ΤYAqylۢjpaϻy @eSp[^!bb|09asC,= YXǎx')CdDe3יC};޷c*A]9XͥƤٳY6C.ThRڿ](8TJ VTfä+ZU'oqs}҆d*hUL+CN25$!Nn;V'<>0d$Nz3_g;̲D*ۑdHK}mIXˬ," d§0FVB5$ٰ':Muf~gr WE[oC!!E6,#Y=;:Kagkxk݆O> rFfe8c〥 @ ;0{y+L$vtv3VNx.x?]¹Sv YKOhҪG{Do } #{Ɔd[Ը~_LφWx[R.Kae+V% ;6$1Pmz㋍ڢ&4B2)-?Pmѡo*[3_7YdZ^cϐ֝ _z.Ur8aPeȓuz _ɑ+)βG  .9˚&,hpڐ|*~@_$^LIY*5xRYNJEnC825c;Q#a[$~w,h*z&7m.5KG6El=Y40BDղ,R,^XycD+vwk:fj [s?_[L勹{%fc% 9aLXzb娊)&h_+.r"FGKk c:w۝-b ard4/_-Li{|,/o{%iEh% "S$T@j Ɏ{7zuGTº+ 5T*Y?1ܞ45:)->zE\-YbNYz2[Wvv^0nd[Lϛvb]TZVBW͐0*!$Xp+R8?+&a7?{kɠm_[!81_/9DIOIr(mgn]8w߁0O ֒dne?C<yV1 b&haXPEIMLU\cX{0{mH22[pbbWG2=ʸYsʶaw{{1% 1rR:MV[B|ketNd]r4IYWSqC®0k-o5tL6t?B8 X(i'a>`qXہ`C%;.x8r4&;D5&laWu<0-<]LJdR}<6al0w0Db;]gq޳$Y7ۑ/ˍw5{W{Y;N(7z6l|ce a-|[sELp Dٚ֔Wz=(a%>bApV d!r@>DzL`5x09M5G qIibl} !bnO*Rm2E2V2zinD%'θJϧ.szwedn J 6T-׆tkR/26$<$f4W`nOGW!]nosmH8`\QWK+ˡq`uǁRG»^껈ə5۳؆7v岬|o-K t,KJAL22yWn3v 9T~22=G/-E/u8m+c*p|՘Ty(qgǥ~[P[hGJ|a`qOb0*21aGa{|)ڐ ZjkBF} qx| ;"\ek e6G~edRP;{K\1 6"{wqx\"ƽw}rT\BS"Gg[g.Sss8RXF$MUnCU^&")k[xH(:գHv߂SvglV8@5l> "qPZ.cRk̪C{s-ʾLus|,/m"nN;n[,gDܳʻsi1JDؒ*W7rm(\˯E;g{-7f- 0T_e (iq|〻g9Ť,f86+W{ W$cVT!{{P%Ɛ^yG=JЏ{0H>~Zv ~ж_bHRĐb )&0 b+&g")BH1)IpSƘ8E)NP*RH…4}G\EgmS6u(=ZUe\t 7DE2q@ ]FZ*gwS{c'4|ߝ:dn&,6W?sV dJRŤy=_+}sV~TWK0D#jfXt% uWapC A )V{.ֳs-WnHz0CP9s_l Hߜ4eUV 鴧k۪[є4WdZƭJl jS|nw;:SƉKZf1 UAo'/h)S,@'@#IDATw/*S[R.-+VN_荬-kE2/eK>[Q5 /j 9bF$ h_E6OZR8v?̣,=/w|%mUVu]%M!6ߴ*opS]V#LyM[ldt~)|^/3Hmd W{[rV?)Fq>쫕j(Jߚd-E+WCK6elv$-1[ Z9`vd +r:L:BUt ?frǎٟp !j2q\4i)jfA)BYK!ր@e^fSgsޏF?=$!ċF GhCN"NV ~+ڵ%IUtuY\.R]LKR_4x!rd9iI #",G8cDF*}D&(,SSrgZ@U}1,tu Z5GA{ ]ki\oC1qz}}^q#TT"c^сͼm(A%|M+}C?泀ou?9ɦ)r?[2x%aAf0Aޙ*L\p)VQiKڜᗮڜWr|1wS4GׇԤ?r);4ɛ{Y^6o8fZGJU'~; "FdUW&sK֩IWlj[*|u#* Zy<0sI8ܙn}L)kS x=jp5.cop)_ wӎpT7K?`垓fZ,GlVzu3ĩ*a ^V\o0j-l`^K1);kM*_0s} 9am>O˜&I whVEoH@: 鹫ƣ\=)FQ~nnZ !M[ԮtB&yL A@/1 %*zE?v^7$*Լ2ͼ+p\p>8 VN^(W+wWqj fΦ!&S +yAf0AQ L |`h 0(R*V%$O1l~_>^wtv4l^*zV3(|zsӋTHa.*QW{ K%0B=qZRP|݈+)\ yY†2S^6xMsqsSyU"ۀ]m뱼'zSBgj7V쳵\trO SK+l$3 IWze΋r^Qe~^UE?ҺJ[xXс\Hh]uY?ӒJ/U0sQOJ *U>asjVҶ1%yQ IZٞ暷s t^/`W mϱnp&F^fSiUO~0ǭ0 g^EE X>27-lsCyAyq0YWvaGDs-S_D#y9GPa;~0Ej~Uϛ{7w:g#™!hG7cU -ղ]U huU]Uttf4eB-1I9ࠀ7jA#0q \*a vm/:0;XY߯)^)4&hQrnv;z0K9%_Wx ? Y nJQܯYrnaP^.>\f &w k f6gW66)^ڔ"Te9XUXUlVn奃qHTf^+qW/b_p._ɧ%-\ŋa?r̾O8`-ԺSs^&w6!(J ~3/>^n`ѓY#+@+8j*RT2K{--y+Z+јL43K-$ұwc{apl ^q;slɫP 3[>psT;O ջ4&Otm͓3KZO(S3(rL <`6Gך传. +a(8?򐚶sVS˭X]% xpÝyA3L] @~VdRԕ9T#s2g! MтFh~?3+JntI'`q9>8C~ڒԂ,a9(t,W{C;e:^б?o x!sę"yTlέxp,Ԭ"bT󨪵6J~.VD[a# JY#VJ<{KcC=zqcμwUo%pCBі= .Y/󁾞ܶ2ohIx2, NX'h[9FJ'2J 4 ۵9G?y*U\mV$(^l100-kJr=*]bq#a^:AE U;/vCUcJX5\YB:ŁiK~QiL7e鶛L/h~$k9+@2QfV}/Kr^~_] kHj~W],^k "8Salt3ƛ %c1(c>dK,}57xK[`ThDL rº~.z[,FE1#sDӰp?vdbd5ArvɢdZLdd-8wΦD2g<ϵӖ UosT{M6 L..3v^'Gvu HN;ەuU/g -b#h6VEWD^WiIJb|1 Cp| õTe}aơ9`kMl>d*TB9ʯ>l,T382vBj($U6m|"-$;g (ƜV.U3SispڍU!ܜz-|MC\'', %fGq'$rt|\!I7s_T#k8cJב)C9TۑCuE~ 6v-7}GX?"JT1o78U2O!]hHTm*w kS6@ G䂯ި(a'Ѧ,,^?>ջ+wWG:\LPlJ'!]GeQŒgjN'~uP[,'Y 9PMbj۶jɓYi8:{|c! ;HGׇ- 3{J#AgJٿ42~ @XĠCGZ.7D/ Ӓ+ZRTkr۱8BO*wL7fdۍ6M'\mڔl)[8H7.?>&6ecRsvա* wS9Ox1P\_ir慲52f2AG-bMEG :+:0rpJ9./jn~tOaA*h,[8B\UYx$)dċ(s!Y_w9blK<ݬZTV\9ROQ!OUP凌<{L{򽟯qrJ{Ectp5iC5M3Y)UԤ}+{Ak%4JuK+rÄ9b3r8NцK `>U'm} ۏR}^.d@ѾL~Os-{.KKS!a ߳Wіh]vY>\zE9gԤl~?n m%)<EXUA3|N^,*>UNջ+-o*ݡm5*=xڇ? ۚ">دHjLZu+mT=s׍ټVl9\,:[A C5Ƚ=ČăܬrUяCWr>|IAƕ9`?5b/IHcC޶ ?-K#k3(=N/{ S@QL a 'x&N6(I[ ]1#}~Nk9~5%k\D`' ,wk#$,rCڸo_QǼJ9*_nё f?l`Wt6~5^[ν+c.6$?juJhԶ/)EPߺկ9e89T N$(EqN UҢVלLq[5te S[6H!'@dM튺}#"u\<*. bNuއHW4Wh CKyҥJZL8V zMG>Ԇ]-Xh{ #-v.Pf1 906|tL{eUuck'=Kˏ㤶kx]OѳmA{8:~wv bnްԤ #*^њ FHx 1̐FsU^B%Wqcl^PVTrfVb@ۡT YH-HR\酂&7Jpu-v=r%.g?;rʾL>CL&}Mp]ۑ )8WeӒ.zE2x rYhC4TAkބNX3Rv?*a 3kγ$WU2R È=ٞwdTnAtc{Ew|]?ֆ%&T1|F.'sSĻ;{95v3A]PH7+3LM5K3+e}u@4$#:/h?®B^UrE>W¾EKW–/jPә{:g6_SV۫/6EFv:u 7NniF~L`z.oѳ$3[vtvHK:;8_]^R3d)p(𢜌TkLWx%}iJȫ}V72yݔ6ŪČul^ҍscth悔[Ť9bz|>yKGK 8J!mj^ g+]aY.2 [ގah u+[XPg_8qg)~eXi~o˅tf*]8>Y&`sA|L-Iedܰ.[ ӗPeŠa|J2}-8v v9<r6w S@m=ۊf !y2y39HJ$#?&k^Ϣn~>_/A[S@uCzEGDh))G*:da=9ay˱/+`s’>X튰893=o7TNF(hPGUqVmqPruӇl{7/CKUjw>Nޣ:%b1 KڷR elb݊M6bnTAd2zAoeӐ>N 3|ƕ%ٗPW; {[oo~A/\ e6SZqnZ‚s~<};qB\Ϟ׿+RbOU7H~<' b! Ņ;ݑ !`6.iSzyM/zE/_I9`(` CgEB 8F*`> ۤ"dÉtYܩH!X S@z6=`ϔkR23W\H{?Wmg/uloc+ML"A[A N@$:W[C,䫡AK?OV ӏN%KUejtouy;;rQJvՎ}ܯZ]߿މ/rRlUTAdj?4+$z t9@򁆎3G^zro*g (B:P^nR#dí (IGf \-ևV*BU r- msjpҒ>`LCD$x#YQj a}QWxWR1U#KHZ~ZK) V]vl-٪cw/ju3P|{Ces2 IXBvSU4iw4 ?\JfsBH::d])- SBwVA3|f 8csBNUr>[Re-66ZnۭS_p^n>JD* zE @d*k֤J ?8+ĺ"`G) X[q'_5bmtyv}"3?x^s5|l=?N9jgiD< 8A؀|MWVGб%|59eapxU#ߔgU_|<`Z-z<۴-d\8[IKrJ#UѶӔll`BDK)pOkdp3hq$"*s-,hTG_I&WoD\BW19";A%9 viD QzEKTn=݊ET`?|ܭ4BQnw Zu={n+^\ًKΉaQ[^0|T`!4?8)RU޻ ,0u𢟘?Aę}MuBNI aJ ^ TMJw_;思 Gf XX.Qe^␥`WYKq_ x-m"[U& R ^1,jIQ^ % ,dAbt儗/A 6,}ORtFPm E'07]07lzev%eZ&2]XXb@Y+VA в0A`T cXԚ04kP7VB=ƼC9`NUA %զk'!7CE06Qu}9# 9` b׼YUYZu_w~p|2'` ET-=\$ mQ,6$igz&"T 9` b0V M-aGRa X, eVA#,Z:af XDxEN^PP2QFr,ݗ'QA GTEժI^Yx7d5=X ]{9` nkU,- WP=[X(Z C%0LAp3͵ Y%k(Br}%(! 0sADMMLl*kA@ \`0 tr0A$|Xj/(`FnX c", b2^W4-ڒ\,(`(k sA1UfS+eiMz5zo5FA$\TEKkTL ,,A[(A4$ b<+ѪҚd  F!(Gy8#MAD֤89H~FA֧AJ0ps'(a"YJil7ÿ_ߟyl<@=dvݗ+)7PP[ . ui <}ލn6۱`c/Gm`[J<?~ocXmp+oN<@+ 嵇|_GKv!s`رɍ3F95y:%qG&&lb\Lq! QXN, SBjz! .ߖLSkE4ۏƜ>ÿ +Fsa>~"x|{,7'v|W}q2?ֱx>7ΧwQyɶ}}ox TflAe_a@C$X\훚).1OYɹCA$uq%r |>a[~G%`QxީiScY;w93b|űъIΩ{qkz [E؈_G/pEUЍ@CaD\ 6dT%yr%V HǼj?kI]Iu{T"qߗ:wjB:&LzN59O $)@: ]cT³P*3#Uah O6 ?O7C `#F|޺܄k<s<b{G9UQIA̿y8* Rpz % -g9bEuxuρj\'qjnOՃ/i]x|$9qMܸ Q7+<9<:G #b^chm2k@yع_OV3`€Uӓ8=E脚-wK',-1^Ц|EjayNX6$ ? ٘vz{"k Y5Ks5-iDT4d| Ιq87qxK?ؼ{ x!4[G苠rbX d`^#Ƥ.;=~I|~* 5|OV*/hg [OrNM|04A;}L9ap"59`rAL 0_oH ;΍oHHw@;QIAq ;_k|m7kJhɐ b%IWQnv;{w1xJn;U-MFeq $ߏ  ` #a(qUldbۭNa;?ԓ&z? b2-FBXQS0*HuYUl*rm*/lOO ^[a"_T&"ͥ4".Cul ar{niIA19LjdAiWf=9yABظ*7\Ed8p梅/>J ( ׋(k ;NYqJ\Gd \iqmG=IAQESTI<` Uã䇐hYA Aߓ'+๝IFDNLC"AAAHO *yM$T/Z<<`0A'.B%ji8Ah<}L8FYX`k  f)$'9`Z@ľg @e 8>9` 8[ ,*s& Y  Βf  & )` b 9` f  & )` b 9` f  O8p… .RO,̃̅ .\4 ` 9` `}Aq }A1U0AAL3LASUA0sA1U0AAL3LASUA0sA1U0AAL3||n˃qFsulW~=>/1l7/ݕϳ})oZr:v&s{_:re]kONƦxۖ=~׫ҟ{B+U\w9.@>pC wy< 9wco< ?w 5v/jP8f%fA7=> 8k-un5"zsR3,%I?¾q]YT–QQp}sՀ/Rl㸔Vw估e#3<_ 9Fl{ bOy…BV>ByE?^Xpȗ``|}F\mv?.# |~\Kv_y):/ϣfx !BOΪ EE+]. k?ۗ2䆋?*^m_I57UQ߽{+oyQ1:Ԇ/;. ?GJB1|BMi֊ߛ}e4>|)`+ON;BYX.\/wVWۏ( ;qRׅ`ơ.f3"?p,lwӸiˎKYsPu>) 4Rwux9q>o*`)3#;KOǨ;zy W+` *[pcT`Y8ٶQu!Iim,PTR*TЏDžpJ*ۗ.{W憚N뢆ǫƫm6wsԋ}M.M xܾ۹j2%ߡ@x GW08QnAf8*[WUH؊UpvbNBPF lw\tT<0U.GY=VXs]U x q||u}s>c2TMɇ;h˭7uY<]݌kTU8ھccPFelձei^6nIrz;NA$iJkt?&!` , i\Et5,ЬwX;S;l}Iݟ󨀙["J,p}N| `!:wUmHv?~%KTMVgc ^ZUw/wCI8Wp,oS\qkpݴeJ5wf7$I?.[fMMcRciZAe˹q7o(c*Ye:қ }LǸ< 9s8]&d7KQ{ yQ#t0sX?xQ#I*`*'LF\%O3LASUA0sA1U0AAL3LASUAT(9`pד a! bބOñrB=*a h| rnlJ0lzf  կW[$^3XGc뷻=ed )q$mʷs[>=z8J'lTEі׹usg`n]yWw s\:׹Y:׹us맳?:׹us};s\:ק0׹uss\:׹0׹us\?^ DiIENDB`monkeyrunner/src/test/java/resources/com/android/monkeyrunner/image2.raw0100644 0000000 0000000 00005670531 12747325007 025703 0ustar000000000 0000000 srScom.android.monkeyrunner.adb.image.CaptureRawAndConvertedImage$MonkeyRunnerRawImage7t.I alpha_lengthI alpha_offsetI blue_lengthI blue_offsetIbppI green_lengthI green_offsetIheightI red_lengthI red_offsetIsizeIversionIwidth[datat[Bxp  pur[BTxpp>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>>>>$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$>>>DDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDBBBXXXdddgggddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddgggdddXXXBBBDDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDDDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDDDDZZZdddggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggdddZZZDDDbbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeejjjiiieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbb```YYYeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeYYY```bbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbbbbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbbbbb[[[eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee[[[bbbZZZaaacccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaZZZZZZaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaZZZZZZaaacccccccccccccccccccccccccccccccccccccccccccccccccccaaaZZZZZZaaaccccccccceeexxxwwweeecccccccccaaaZZZNNNeeexxxZZZaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccrrraaaZZZXXX`````````````````````````````````````````````````````````````````````XXXXXX``````````````````````````````````````````````````````````````````````````````XXXXXX``````````````````````````````````````````````````````XXXXXX``````kkkiii``````XXXyyyVVVeee___fffsssXXX````````````````````````````````````````````````````````````````````````XXXYYY$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$GGGVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]rrrppp]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]qqqqqq]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVVVV]]]]]]]]]]]]VVV]]]ppprrraaaiiiVVV]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]VVVkkk;;;RRRaaagggggggggggggggggggggggggggggggggggggggggggggggggggcccXXXAAACCCSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ^^^\\\ZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSSSSZZZZZZ}}}nnnnnn}}}ZZZZZZSSSuuuUUU{{{kkkbbblllSSSZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZSSSGGG`b[}0(SVNGGGPPPWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWPPPPPPWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWPPPPPPWWWWWWWWWeeeoooWWWWWWWWWWWWYYYWWWWWWWWWWWWWWWPPPPPPWWWWWWyyypppWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWmmmWWWWWWPPP```pppYYY{{{```\\\oooPPPWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWPPPOOOWWW!%NNN|||MMMTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMMMMMMTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMMMMMMTTTTTTbbbtttTTTTTTTTTvvvVVVTTTTTTTTTTTTMMMMMMTTTTTTUUU\\\TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT\\\UUUTTTTTTMMMqqqxxxUUUgggSSSGGGMMMTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTMMMSSS]_ZVVVXXXlll,,, !!!]]]kkkbbbkkkbbbJJJPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPJJJJJJPPPPPPPPPPPPPPPPPPPPP||||||PPPPPPPPPPPPPPPPPPPPPJJJJJJPPPPPPyyyPPPPPPPPPwwwRRRPPPPPPPPPJJJJJJPPPPPPPPPPPPPPPPPP}}}yyyPPPPPPPPPPPPPPPPPPJJJ{{{~~~NNNuuuiii___JJJPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPJJJOOO`iLWWWPPP444WWWGGGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGGGGGGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMGGGGGGMMMMMMMMMMMMMMMMMMsssMMMMMMMMMGGGGGGMMMMMMMMMMMMlllsssMMMMMMMMMMMMGGGjjjfffRRR{{{YYYpppGGGMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMSSSdddMMMMMMMMMGGGFFFaoATTTMMMppp&&&  BBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHBBBBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHBBBBBBHHHHHHHHHJJJNNNHHHHHHHHHBBByyykkkIII^^^hhhPPPBBBHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH~~~HHHBBB:::666HHHq#OOOIIIUUUOOO nnn nnn@@@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE@@@@@@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE@@@@@@EEEEEEEEEEEEEEE|||EEEEEEEEEEEEEEE@@@@@@EEEEEEEEEEEEjjjjjjEEEEEEEEEEEE@@@ZZZ~~~EEEoooPPPRRR@@@EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE|||EEE@@@///???Zj5|JJJDDD>>>RRR999OOO<<<CCCxxx<<<CCCxxx<<>>@@@555888================================================888888========================888888=====================ttt=====================888888==================================================================888eee<<>>???:::OOOFFFyyy555444888888888888888888888888888888FFF888888888888888444444888888888888888888888888444444888888888888888888888888ppp888888888888888888888888444444888888888888888888??????888888888888888888444lll<<>>!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!)))&&&!!!!!!!!!!!!!!!!!!!!!!!!RRR+++```zzzGGG333IIIooo<<<)))IIIxxx!!!!!!!!!ooo!!!!!!!!!!!!bbb!!!8882>xxxxxxxxxxxxxxxxxxxxxxxrUUU'''jjjQQQQQQAAAiiixxxUUU333kkkkkkgggGGG$$$111!!!!!!...###___###<<>> Pj^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}^}AT11/HHHEEE???&&&^^^AAA,,, ...ZZZfffdddVVVWWW... JJJqqq    |||XXX888&&&$$$SSSccc^^^ {{{___ QQQ      }}} xxxVVVFFF}}}www ``` 444 nnn @@@  ~~~uuuooo^^^DDD222ggg,,,LLLEEE  EEEIII$$$   $$$IIIEEE  EEEEEE  EEEEEE  EEERTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRRTRZTZRTRRTRjqjjmjsqsjmj)()1,19<9949141141141)()141)()1,11019<9    949 {}{949141949989 $ 101  )()sqs RPR101IHIbeb  jij),)A@Ababjmj RPRZ]ZIHIsqsZ]Z Z]Zbeb $ Z]Zjij{y{)$)babRPR101 beb989 {y{ susIHIIDI jmjA@AsqsZYZILI)()A sdkstats/.project0100644 0000000 0000000 00000000557 12747325007 013132 0ustar000000000 0000000 sdkstats org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature sdkstats/.settings/0040755 0000000 0000000 00000000000 12747325007 013375 5ustar000000000 0000000 sdkstats/.settings/README.txt0100644 0000000 0000000 00000000203 12747325007 015063 0ustar000000000 0000000 Copy this in eclipse project as a .settings folder at the root. This ensure proper compilation compliance and warning/error levels.sdkstats/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 020362 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 sdkstats/NOTICE0100644 0000000 0000000 00000024707 12747325007 012372 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS sdkstats/README0100644 0000000 0000000 00000000704 12747325007 012335 0ustar000000000 0000000 How to use the Eclipse projects for SdkStats. SdkStats requires SWT to compile. SWT is available in the depot under //device/prebuild//swt Because the build path cannot contain relative path that are not inside the project directory, the .classpath file references a user library called ANDROID_SWT. In order to compile the project, make a user library called ANDROID_SWT containing the jar available at //device/prebuild//swt. sdkstats/build.gradle0100644 0000000 0000000 00000000441 12747325007 013732 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'sdkstats' dependencies { compile project(':base:common') compile project(':swt:swtmenubar') testCompile 'junit:junit:3.8.1' } sourceSets { main.resources.srcDir 'src/main/java' test.resources.srcDir 'src/test/java' } sdkstats/sdkstats.iml0100644 0000000 0000000 00000001712 12747325007 014020 0ustar000000000 0000000 sdkstats/src/0040755 0000000 0000000 00000000000 12747325007 012246 5ustar000000000 0000000 sdkstats/src/main/0040755 0000000 0000000 00000000000 12747325007 013172 5ustar000000000 0000000 sdkstats/src/main/java/0040755 0000000 0000000 00000000000 12747325007 014113 5ustar000000000 0000000 sdkstats/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 014671 5ustar000000000 0000000 sdkstats/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016311 5ustar000000000 0000000 sdkstats/src/main/java/com/android/sdkstats/0040755 0000000 0000000 00000000000 12747325007 020151 5ustar000000000 0000000 sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java0100755 0000000 0000000 00000026646 12747325007 024735 0ustar000000000 0000000 /* * 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.sdkstats; import com.android.prefs.AndroidLocation; import com.android.prefs.AndroidLocation.AndroidLocationException; import org.eclipse.jface.preference.PreferenceStore; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Random; /** * Manages persistence settings for DDMS. * * For convenience, this also stores persistence settings related to the "server stats" ping * as well as some ADT settings that are SDK specific but not workspace specific. */ public class DdmsPreferenceStore { public final static String PING_OPT_IN = "pingOptIn"; //$NON-NLS-1$ private final static String PING_TIME = "pingTime"; //$NON-NLS-1$ private final static String PING_ID = "pingId"; //$NON-NLS-1$ private final static String ADT_USED = "adtUsed"; //$NON-NLS-1$ private final static String LAST_SDK_PATH = "lastSdkPath"; //$NON-NLS-1$ /** * PreferenceStore for DDMS. * Creation and usage must be synchronized on {@code DdmsPreferenceStore.class}. * Don't use it directly, instead retrieve it via {@link #getPreferenceStore()}. */ private static volatile PreferenceStore sPrefStore; public DdmsPreferenceStore() { } /** * Returns the DDMS {@link PreferenceStore}. * This keeps a static reference on the store, so consequent calls will * return always the same store. */ public PreferenceStore getPreferenceStore() { synchronized (DdmsPreferenceStore.class) { if (sPrefStore == null) { // get the location of the preferences String homeDir = null; try { homeDir = AndroidLocation.getFolder(); } catch (AndroidLocationException e1) { // pass, we'll do a dummy store since homeDir is null } if (homeDir == null) { sPrefStore = new PreferenceStore(); return sPrefStore; } assert homeDir != null; String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$ // also look for an old pref file in the previous location String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$ + File.separator + ".ddmsrc"; //$NON-NLS-1$ File oldPrefFile = new File(oldPrefPath); if (oldPrefFile.isFile()) { FileOutputStream fileOutputStream = null; try { PreferenceStore oldStore = new PreferenceStore(oldPrefPath); oldStore.load(); fileOutputStream = new FileOutputStream(rcFileName); oldStore.save(fileOutputStream, ""); //$NON-NLS-1$ oldPrefFile.delete(); PreferenceStore newStore = new PreferenceStore(rcFileName); newStore.load(); sPrefStore = newStore; } catch (IOException e) { // create a new empty store. sPrefStore = new PreferenceStore(rcFileName); } finally { if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { // pass } } } } else { sPrefStore = new PreferenceStore(rcFileName); try { sPrefStore.load(); } catch (IOException e) { System.err.println("Error Loading DDMS Preferences"); } } } assert sPrefStore != null; return sPrefStore; } } /** * Save the prefs to the config file. */ public void save() { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { try { prefs.save(); } catch (IOException ioe) { // FIXME com.android.dmmlib.Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage()); } } } // ---- Utility methods to access some specific prefs ---- /** * Indicates whether the ping ID is set. * This should be true when {@link #isPingOptIn()} is true. * * @return true if a ping ID is set, which means the user gave permission * to use the ping service. */ public boolean hasPingId() { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { return prefs != null && prefs.contains(PING_ID); } } /** * Retrieves the current ping ID, if set. * To know if the ping ID is set, use {@link #hasPingId()}. *

* There is no magic value reserved for "missing ping id or invalid store". * The only proper way to know if the ping id is missing is to use {@link #hasPingId()}. */ public long getPingId() { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { // Note: getLong() returns 0L if the ID is missing so we do that too when // there's no store. return prefs == null ? 0L : prefs.getLong(PING_ID); } } /** * Generates a new random ping ID and saves it in the preference store. * * @return The new ping ID. */ public long generateNewPingId() { PreferenceStore prefs = getPreferenceStore(); Random rnd = new Random(); long id = rnd.nextLong(); synchronized (DdmsPreferenceStore.class) { prefs.setValue(PING_ID, id); try { prefs.save(); } catch (IOException e) { /* ignore exceptions while saving preferences */ } } return id; } /** * Returns the "ping opt in" value from the preference store. * This would be true if there's a valid preference store and * the user opted for sending ping statistics. */ public boolean isPingOptIn() { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { return prefs != null && prefs.contains(PING_OPT_IN); } } /** * Saves the "ping opt in" value in the preference store. * * @param optIn The new user opt-in value. */ public void setPingOptIn(boolean optIn) { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { prefs.setValue(PING_OPT_IN, optIn); try { prefs.save(); } catch (IOException e) { /* ignore exceptions while saving preferences */ } } } /** * Retrieves the ping time for the given app from the preference store. * Callers should use {@link System#currentTimeMillis()} for time stamps. * * @param app The app name identifier. * @return 0L if we don't have a preference store or there was no time * recorded in the store for the requested app. Otherwise the time stamp * from the store. */ public long getPingTime(String app) { PreferenceStore prefs = getPreferenceStore(); String timePref = PING_TIME + "." + app; //$NON-NLS-1$ synchronized (DdmsPreferenceStore.class) { return prefs == null ? 0 : prefs.getLong(timePref); } } /** * Sets the ping time for the given app from the preference store. * Callers should use {@link System#currentTimeMillis()} for time stamps. * * @param app The app name identifier. * @param timeStamp The time stamp from the store. * 0L is a special value that should not be used. */ public void setPingTime(String app, long timeStamp) { PreferenceStore prefs = getPreferenceStore(); String timePref = PING_TIME + "." + app; //$NON-NLS-1$ synchronized (DdmsPreferenceStore.class) { prefs.setValue(timePref, timeStamp); try { prefs.save(); } catch (IOException ioe) { /* ignore exceptions while saving preferences */ } } } /** * True if this is the first time the users runs ADT, which is detected by * the lack of the setting set using {@link #setAdtUsed(boolean)} * or this value being set to true. * * @return true if ADT has been used before * * @see #setAdtUsed(boolean) */ public boolean isAdtUsed() { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { if (prefs == null || !prefs.contains(ADT_USED)) { return false; } return prefs.getBoolean(ADT_USED); } } /** * Sets whether the ADT startup wizard has been shown. * ADT sets first to false once the welcome wizard has been shown once. * * @param used true if ADT has been used */ public void setAdtUsed(boolean used) { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { prefs.setValue(ADT_USED, used); try { prefs.save(); } catch (IOException ioe) { /* ignore exceptions while saving preferences */ } } } /** * Retrieves the last SDK OS path. *

* This is just an information value, the path may not exist, may not * even be on an existing file system and/or may not point to an SDK * anymore. * * @return The last SDK OS path from the preference store, or null if * there is no store or an empty string if it is not defined. */ public String getLastSdkPath() { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { return prefs == null ? null : prefs.getString(LAST_SDK_PATH); } } /** * Sets the last SDK OS path. * * @param osSdkPath The SDK OS Path. Can be null or empty. */ public void setLastSdkPath(String osSdkPath) { PreferenceStore prefs = getPreferenceStore(); synchronized (DdmsPreferenceStore.class) { prefs.setValue(LAST_SDK_PATH, osSdkPath); try { prefs.save(); } catch (IOException ioe) { /* ignore exceptions while saving preferences */ } } } } sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java0100644 0000000 0000000 00000017632 12747325007 025753 0ustar000000000 0000000 /* * 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.sdkstats; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.program.Program; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Shell; import java.io.IOException; /** * Dialog to get user permission for ping service. */ public class SdkStatsPermissionDialog extends Dialog { /* Text strings displayed in the opt-out dialog. */ private static final String HEADER_TEXT = "Thanks for using the Android SDK!"; /** Used in the ADT welcome wizard as well. */ public static final String NOTICE_TEXT = "We know you just want to get started but please read this first."; /** Used in the preference pane (PrefsDialog) as well. */ public static final String BODY_TEXT = "By choosing to send certain usage statistics to Google, you can " + "help us improve the Android SDK. These usage statistics lets us " + "measure things like active usage of the SDK, and let us know things " + "like which versions of the SDK are in use and which tools are the " + "most popular with developers. This limited data is not associated " + "with personal information about you, and is examined on an aggregate " + "basis, and is maintained in accordance with the Google Privacy Policy."; /** Used in the ADT welcome wizard as well. */ public static final String PRIVACY_POLICY_LINK_TEXT = "Google " + "Privacy Policy"; /** Used in the preference pane (PrefsDialog) as well. */ public static final String CHECKBOX_TEXT = "Send usage statistics to Google."; /** Used in the ADT welcome wizard as well. */ public static final String FOOTER_TEXT = "If you later decide to change this setting, you can do so in the" + "\"ddms\" tool under \"File\" > \"Preferences\" > \"Usage Stats\"."; private static final String BUTTON_TEXT = "Proceed"; /** List of Linux browser commands to try, in order (see openUrl). */ private static final String[] LINUX_BROWSERS = new String[] { "firefox -remote openurl(%URL%,new-window)", //$NON-NLS-1$ running FF "mozilla -remote openurl(%URL%,new-window)", //$NON-NLS-1$ running Moz "firefox %URL%", //$NON-NLS-1$ new FF "mozilla %URL%", //$NON-NLS-1$ new Moz "kfmclient openURL %URL%", //$NON-NLS-1$ Konqueror "opera -newwindow %URL%", //$NON-NLS-1$ Opera }; private static final boolean ALLOW_PING_DEFAULT = true; private boolean mAllowPing = ALLOW_PING_DEFAULT; public SdkStatsPermissionDialog(Shell parentShell) { super(parentShell); setBlockOnOpen(true); } @Override protected void createButtonsForButtonBar(Composite parent) { createButton(parent, Window.OK, BUTTON_TEXT, true); } @Override protected Control createDialogArea(Composite parent) { Composite composite = (Composite) super.createDialogArea(parent); composite.setLayout(new GridLayout(1, false)); final Label title = new Label(composite, SWT.CENTER | SWT.WRAP); final FontData[] fontdata = title.getFont().getFontData(); for (int i = 0; i < fontdata.length; i++) { fontdata[i].setHeight(fontdata[i].getHeight() * 4 / 3); } title.setFont(new Font(getShell().getDisplay(), fontdata)); title.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); title.setText(HEADER_TEXT); final Label notice = new Label(composite, SWT.WRAP); notice.setFont(title.getFont()); notice.setForeground(new Color(getShell().getDisplay(), 255, 0, 0)); notice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); notice.setText(NOTICE_TEXT); notice.pack(); final Label bodyText = new Label(composite, SWT.WRAP); GridData gd = new GridData(); gd.widthHint = notice.getSize().x; // do not extend beyond the NOTICE text's width gd.grabExcessHorizontalSpace = true; bodyText.setLayoutData(gd); bodyText.setText(BODY_TEXT); final Link privacyLink = new Link(composite, SWT.NO_FOCUS); privacyLink.setText(PRIVACY_POLICY_LINK_TEXT); privacyLink.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { openUrl(event.text); } }); final Button checkbox = new Button(composite, SWT.CHECK); checkbox.setSelection(ALLOW_PING_DEFAULT); checkbox.setText(CHECKBOX_TEXT); checkbox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { mAllowPing = checkbox.getSelection(); } }); checkbox.setFocus(); final Label footer = new Label(composite, SWT.WRAP); gd = new GridData(); gd.widthHint = notice.getSize().x; gd.grabExcessHorizontalSpace = true; footer.setLayoutData(gd); footer.setText(FOOTER_TEXT); return composite; } /** * Open a URL in an external browser. * @param url to open - MUST be sanitized and properly formed! */ public static void openUrl(final String url) { // TODO: consider using something like BrowserLauncher2 // (http://browserlaunch2.sourceforge.net/) instead of these hacks. // SWT's Program.launch() should work on Mac, Windows, and GNOME // (because the OS shell knows how to launch a default browser). if (!Program.launch(url)) { // Must be Linux non-GNOME (or something else broke). // Try a few Linux browser commands in the background. new Thread() { @Override public void run() { for (String cmd : LINUX_BROWSERS) { cmd = cmd.replaceAll("%URL%", url); //$NON-NLS-1$ try { Process proc = Runtime.getRuntime().exec(cmd); if (proc.waitFor() == 0) break; // Success! } catch (InterruptedException e) { // Should never happen! throw new RuntimeException(e); } catch (IOException e) { // Swallow the exception and try the next browser. } } // TODO: Pop up some sort of error here? // (We're in a new thread; can't use the existing Display.) } }.start(); } } public boolean getPingUserPreference() { return mAllowPing; } } sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java0100644 0000000 0000000 00000053146 12747325007 024103 0ustar000000000 0000000 /* * 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.sdkstats; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.Arrays; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Utility class to send "ping" usage reports to the server. */ public class SdkStatsService { protected static final String SYS_PROP_OS_ARCH = "os.arch"; //$NON-NLS-1$ protected static final String SYS_PROP_JAVA_VERSION = "java.version"; //$NON-NLS-1$ protected static final String SYS_PROP_OS_VERSION = "os.version"; //$NON-NLS-1$ protected static final String SYS_PROP_OS_NAME = "os.name"; //$NON-NLS-1$ /** Minimum interval between ping, in milliseconds. */ private static final long PING_INTERVAL_MSEC = 86400 * 1000; // 1 day private static final boolean DEBUG = System.getenv("ANDROID_DEBUG_PING") != null; //$NON-NLS-1$ private DdmsPreferenceStore mStore = new DdmsPreferenceStore(); public SdkStatsService() { } /** * Send a "ping" to the Google toolbar server, if enough time has * elapsed since the last ping, and if the user has not opted out. *

* This is a simplified version of {@link #ping(String[])} that only * sends an "application" name and a "version" string. See the explanation * there for details. * * @param app The application name that reports the ping (e.g. "emulator" or "ddms".) * Valid characters are a-zA-Z0-9 only. * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.) * @see #ping(String[]) */ public void ping(String app, String version) { doPing(app, version, null); } /** * Send a "ping" to the Google toolbar server, if enough time has * elapsed since the last ping, and if the user has not opted out. *

* The ping will not be sent if the user opt out dialog has not been shown yet. * Use {@link #checkUserPermissionForPing(Shell)} to display the dialog requesting * user permissions. *

* Note: The actual ping (if any) is sent in a non-daemon background thread. *

* The arguments are defined as follow: *

    *
  • Argument 0 is the "ping" command and is ignored.
  • *
  • Argument 1 is the application name that reports the ping (e.g. "emulator" or "ddms".) * Valid characters are a-zA-Z0-9 only.
  • *
  • Argument 2 is the version string (e.g. "12" or "1.2.3.4", 4 groups max.)
  • *
  • Arguments 3+ are optional and depend on the application name.
  • *
  • "emulator" application currently has 3 optional arguments: *
      *
    • Arugment 3: android_gl_vendor
    • *
    • Arugment 4: android_gl_renderer
    • *
    • Arugment 5: android_gl_version
    • *
    *
  • *
* * @param arguments A non-empty non-null array of arguments to the ping as described above. */ public void ping(String[] arguments) { if (arguments == null || arguments.length < 3) { throw new IllegalArgumentException( "Invalid ping arguments: expected ['ping', app, version] but got " + (arguments == null ? "null" : Arrays.toString(arguments))); } int len = arguments.length; String app = arguments[1]; String version = arguments[2]; Map extras = new HashMap(); if ("emulator".equals(app)) { //$NON-NLS-1$ if (len > 3) { extras.put("glm", sanitizeGlArg(arguments[3])); //$NON-NLS-1$ vendor } if (len > 4) { extras.put("glr", sanitizeGlArg(arguments[4])); //$NON-NLS-1$ renderer } if (len > 5) { extras.put("glv", sanitizeGlArg(arguments[5])); //$NON-NLS-1$ version } } doPing(app, version, extras); } private String sanitizeGlArg(String arg) { if (arg == null) { arg = ""; //$NON-NLS-1$ } else { try { arg = arg.trim(); arg = arg.replaceAll("[^A-Za-z0-9\\s_()./-]", " "); //$NON-NLS-1$ //$NON-NLS-2$ arg = arg.replaceAll("\\s\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ // Guard from arbitrarily long parameters if (arg.length() > 128) { arg = arg.substring(0, 128); } arg = URLEncoder.encode(arg, "UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { arg = ""; //$NON-NLS-1$ } } return arg; } /** * Display a dialog to the user providing information about the ping service, * and whether they'd like to opt-out of it. * * Once the dialog has been shown, it sets a preference internally indicating * that the user has viewed this dialog. */ public void checkUserPermissionForPing(Shell parent) { if (!mStore.hasPingId()) { askUserPermissionForPing(parent); mStore.generateNewPingId(); } } /** * Prompt the user for whether they want to opt out of reporting, and save the user * input in preferences. */ private void askUserPermissionForPing(final Shell parent) { final Display display = parent.getDisplay(); display.syncExec(new Runnable() { @Override public void run() { SdkStatsPermissionDialog dialog = new SdkStatsPermissionDialog(parent); dialog.open(); mStore.setPingOptIn(dialog.getPingUserPreference()); } }); } // ------- /** * Pings the usage stats server, as long as the prefs contain the opt-in boolean * * @param app The application name that reports the ping (e.g. "emulator" or "ddms".) * Will be normalized. Valid characters are a-zA-Z0-9 only. * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.) * @param extras Extra key/value parameters to send. They are send as-is and must * already be well suited and escaped using {@link URLEncoder#encode(String, String)}. */ protected void doPing(String app, String version, final Map extras) { // Note: if you change the implementation here, you also need to change // the overloaded SdkStatsServiceTest.doPing() used for testing. // Validate the application and version input. final String nApp = normalizeAppName(app); final String nVersion = normalizeVersion(version); // If the user has not opted in, do nothing and quietly return. if (!mStore.isPingOptIn()) { // user opted out. return; } // If the last ping *for this app* was too recent, do nothing. long now = System.currentTimeMillis(); long then = mStore.getPingTime(app); if (now - then < PING_INTERVAL_MSEC) { // too soon after a ping. return; } // Record the time of the attempt, whether or not it succeeds. mStore.setPingTime(app, now); // Send the ping itself in the background (don't block if the // network is down or slow or confused). final long id = mStore.getPingId(); new Thread() { @Override public void run() { try { URL url = createPingUrl(nApp, nVersion, id, extras); actuallySendPing(url); } catch (IOException e) { e.printStackTrace(); } } }.start(); } /** * Unconditionally send a "ping" request to the server. * * @param url The URL to send to the server. * * @throws IOException if the ping failed */ private void actuallySendPing(URL url) throws IOException { assert url != null; if (DEBUG) { System.err.println("Ping: " + url.toString()); //$NON-NLS-1$ } // Discard the actual response, but make sure it reads OK HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // Believe it or not, a 404 response indicates success: // the ping was logged, but no update is configured. if (conn.getResponseCode() != HttpURLConnection.HTTP_OK && conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) { throw new IOException( conn.getResponseMessage() + ": " + url); //$NON-NLS-1$ } } /** * Compute the ping URL to send the data to the server. * * @param app The application name that reports the ping (e.g. "emulator" or "ddms".) * Valid characters are a-zA-Z0-9 only. * @param version The version string already formatted as a 4 dotted group (e.g. "1.2.3.4".) * @param id of the local installation * @param extras Extra key/value parameters to send. They are send as-is and must * already be well suited and escaped using {@link URLEncoder#encode(String, String)}. */ protected URL createPingUrl(String app, String version, long id, Map extras) throws UnsupportedEncodingException, MalformedURLException { String osName = URLEncoder.encode(getOsName(), "UTF-8"); //$NON-NLS-1$ String osArch = URLEncoder.encode(getOsArch(), "UTF-8"); //$NON-NLS-1$ String jvmArch = URLEncoder.encode(getJvmInfo(), "UTF-8"); //$NON-NLS-1$ // Include the application's name as part of the as= value. // Share the user ID for all apps, to allow unified activity reports. String extraStr = ""; //$NON-NLS-1$ if (extras != null && !extras.isEmpty()) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : extras.entrySet()) { sb.append('&').append(entry.getKey()).append('=').append(entry.getValue()); } extraStr = sb.toString(); } URL url = new URL( "http", //$NON-NLS-1$ "tools.google.com", //$NON-NLS-1$ "/service/update?as=androidsdk_" + app + //$NON-NLS-1$ "&id=" + Long.toHexString(id) + //$NON-NLS-1$ "&version=" + version + //$NON-NLS-1$ "&os=" + osName + //$NON-NLS-1$ "&osa=" + osArch + //$NON-NLS-1$ "&vma=" + jvmArch + //$NON-NLS-1$ extraStr); return url; } /** * Detects and reports the host OS: "linux", "win" or "mac". * For Windows and Mac also append the version, so for example * Win XP will return win-5.1. */ protected String getOsName() { // made protected for testing String os = getSystemProperty(SYS_PROP_OS_NAME); if (os == null || os.length() == 0) { return "unknown"; //$NON-NLS-1$ } String os2 = os.toLowerCase(Locale.US); if (os2.startsWith("mac")) { //$NON-NLS-1$ os = "mac"; //$NON-NLS-1$ String osVers = getOsVersion(); if (osVers != null) { os = os + '-' + osVers; } } else if (os2.startsWith("win")) { //$NON-NLS-1$ os = "win"; //$NON-NLS-1$ String osVers = getOsVersion(); if (osVers != null) { os = os + '-' + osVers; } } else if (os2.startsWith("linux")) { //$NON-NLS-1$ os = "linux"; //$NON-NLS-1$ } else if (os.length() > 32) { // Unknown -- send it verbatim so we can see it // but protect against arbitrarily long values os = os.substring(0, 32); } return os; } /** * Detects and returns the OS architecture: x86, x86_64, ppc. * This may differ or be equal to the JVM architecture in the sense that * a 64-bit OS can run a 32-bit JVM. */ protected String getOsArch() { // made protected for testing String arch = getJvmArch(); if ("x86_64".equals(arch)) { //$NON-NLS-1$ // This is a simple case: the JVM runs in 64-bit so the // OS must be a 64-bit one. return arch; } else if ("x86".equals(arch)) { //$NON-NLS-1$ // This is the misleading case: the JVM is 32-bit but the OS // might be either 32 or 64. We can't tell just from this // property. // Macs are always on 64-bit, so we just need to figure it // out for Windows and Linux. String os = getOsName(); if (os.startsWith("win")) { //$NON-NLS-1$ // When WOW64 emulates a 32-bit environment under a 64-bit OS, // it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly. // Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432"); //$NON-NLS-1$ if (w6432 != null && w6432.indexOf("64") != -1) { //$NON-NLS-1$ return "x86_64"; //$NON-NLS-1$ } } else if (os.startsWith("linux")) { //$NON-NLS-1$ // Let's try the obvious. This works in Ubuntu and Debian String s = getSystemEnv("HOSTTYPE"); //$NON-NLS-1$ s = sanitizeOsArch(s); if (s.indexOf("86") != -1) { //$NON-NLS-1$ arch = s; } } } return arch; } /** * Returns the version of the OS version if it is defined as X.Y, or null otherwise. *

* Example of returned versions can be found at http://lopica.sourceforge.net/os.html *

* This method removes any exiting micro versions. * Returns null if the version doesn't match X.Y.Z. */ protected String getOsVersion() { // made protected for testing Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$ String osVers = getSystemProperty(SYS_PROP_OS_VERSION); if (osVers != null && osVers.length() > 0) { Matcher m = p.matcher(osVers); if (m.matches()) { return m.group(1) + '.' + m.group(2); } } return null; } /** * Detects and returns the JVM info: version + architecture. * Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64 */ protected String getJvmInfo() { // made protected for testing return getJvmVersion() + '-' + getJvmArch(); } /** * Returns the major.minor Java version. *

* The "java.version" property returns something like "1.6.0_20" * of which we want to return "1.6". */ protected String getJvmVersion() { // made protected for testing String version = getSystemProperty(SYS_PROP_JAVA_VERSION); if (version == null || version.length() == 0) { return "unknown"; //$NON-NLS-1$ } Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$ Matcher m = p.matcher(version); if (m.matches()) { return m.group(1) + '.' + m.group(2); } // Unknown version. Send it as-is within a reasonable size limit. if (version.length() > 8) { version = version.substring(0, 8); } return version; } /** * Detects and returns the JVM architecture. *

* The HotSpot JVM has a private property for this, "sun.arch.data.model", * which returns either "32" or "64". However it's not in any kind of spec. *

* What we want is to know whether the JVM is running in 32-bit or 64-bit and * the best indicator is to use the "os.arch" property. * - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.
* - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs * to masquerade as a 32-bit OS for backward compatibility.
* - On a 64-bit system, a 64-bit JVM will properly return x86_64. *

     * JVM:       Java 32-bit   Java 64-bit
     * Windows:   x86           x86_64
     * Linux:     x86           x86_64
     * Mac        untested      x86_64
     * 
*/ protected String getJvmArch() { // made protected for testing String arch = getSystemProperty(SYS_PROP_OS_ARCH); return sanitizeOsArch(arch); } private String sanitizeOsArch(String arch) { if (arch == null || arch.length() == 0) { return "unknown"; //$NON-NLS-1$ } if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$ arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$ arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$ return "x86_64"; //$NON-NLS-1$ } if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$ // Any variation of iX86 counts as x86 (i386, i486, i686). return "x86"; //$NON-NLS-1$ } if (arch.equalsIgnoreCase("PowerPC")) { //$NON-NLS-1$ return "ppc"; //$NON-NLS-1$ } // Unknown arch. Send it as-is but protect against arbitrarily long values. if (arch.length() > 32) { arch = arch.substring(0, 32); } return arch; } /** * Normalize the supplied application name. * * @param app to report */ protected String normalizeAppName(String app) { // Filter out \W , non-word character: [^a-zA-Z_0-9] String app2 = app.replaceAll("\\W", ""); //$NON-NLS-1$ //$NON-NLS-2$ if (app.length() == 0) { throw new IllegalArgumentException("Bad app name: " + app); //$NON-NLS-1$ } return app2; } /** * Validate the supplied application version, and normalize the version. * * @param version supplied by caller * @return normalized dotted quad version */ protected String normalizeVersion(String version) { Pattern regex = Pattern.compile( //1=major 2=minor 3=micro 4=build | 5=rc "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+)| +rc(\\d+))?"); //$NON-NLS-1$ Matcher m = regex.matcher(version); if (m != null && m.lookingAt()) { StringBuilder normal = new StringBuilder(); for (int i = 1; i <= 4; i++) { int v = 0; // If build is null but we have an rc, take that number instead as the 4th part. if (i == 4 && i < m.groupCount() && m.group(i) == null && m.group(i+1) != null) { i++; } if (m.group(i) != null) { try { v = Integer.parseInt(m.group(i)); } catch (Exception ignore) { } } if (i > 1) { normal.append('.'); } normal.append(v); } return normal.toString(); } throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$ } /** * Calls {@link System#getProperty(String)}. * Allows unit-test to override the return value. * @see System#getProperty(String) */ protected String getSystemProperty(String name) { return System.getProperty(name); } /** * Calls {@link System#getenv(String)}. * Allows unit-test to override the return value. * @see System#getenv(String) */ protected String getSystemEnv(String name) { return System.getenv(name); } } sdkstats/src/test/0040755 0000000 0000000 00000000000 12747325007 013225 5ustar000000000 0000000 sdkstats/src/test/java/0040755 0000000 0000000 00000000000 12747325007 014146 5ustar000000000 0000000 sdkstats/src/test/java/com/0040755 0000000 0000000 00000000000 12747325007 014724 5ustar000000000 0000000 sdkstats/src/test/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016344 5ustar000000000 0000000 sdkstats/src/test/java/com/android/sdkstats/0040755 0000000 0000000 00000000000 12747325007 020204 5ustar000000000 0000000 sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java0100755 0000000 0000000 00000052273 12747325007 025001 0ustar000000000 0000000 /* * 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.sdkstats; import java.net.URL; import java.util.HashMap; import java.util.Map; import junit.framework.TestCase; public class SdkStatsServiceTest extends TestCase { private static class MockSdkStatsService extends SdkStatsService { private final String mOsName; private final String mOsVersion; private final String mOsArch; private final String mJavaVersion; private final Map mEnvVars = new HashMap(); private URL mPingUrlResult; public MockSdkStatsService(String osName, String osVersion, String osArch, String javaVersion) { mOsName = osName; mOsVersion = osVersion; mOsArch = osArch; mJavaVersion = javaVersion; } public URL getPingUrlResult() { return mPingUrlResult; } public void setSystemEnv(String varName, String value) { mEnvVars.put(varName, value); } @Override protected String getSystemProperty(String name) { if (SdkStatsService.SYS_PROP_OS_NAME.equals(name)) { return mOsName; } else if (SdkStatsService.SYS_PROP_OS_VERSION.equals(name)) { return mOsVersion; } else if (SdkStatsService.SYS_PROP_OS_ARCH.equals(name)) { return mOsArch; } else if (SdkStatsService.SYS_PROP_JAVA_VERSION.equals(name)) { return mJavaVersion; } // Don't use current properties values, we don't want the tests to be flaky fail("SdkStatsServiceTest doesn't define a system.property for " + name); return null; } @Override protected String getSystemEnv(String name) { if (mEnvVars.containsKey(name)) { return mEnvVars.get(name); } // Don't use current env vars, we don't want the tests to be flaky fail("SdkStatsServiceTest doesn't define a system.getenv for " + name); return null; } @Override protected void doPing(String app, String version, Map extras) { // The super.doPing() does: // 1- normalize input, // 2- check the ping time, // 3- check/create the pind id, // 4- create the ping URL // 5- and send the network ping in a thread. // In this mock version we just do steps 1 and 4 and record the URL; // obvious we don't check the ping time in the prefs nor send the actual ping. // Validate the application and version input. final String nApp = normalizeAppName(app); final String nVersion = normalizeVersion(version); long id = 0x42; try { mPingUrlResult = createPingUrl(nApp, nVersion, id, extras); } catch (Exception e) { throw new RuntimeException(e); } } } @Override protected void setUp() throws Exception { super.setUp(); } @Override protected void tearDown() throws Exception { super.tearDown(); } public void testSdkStatsService_getJvmArch() { MockSdkStatsService m; m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7"); assertEquals("x86", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "i386", "1.7"); assertEquals("x86", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "i486", "1.7"); assertEquals("x86", m.getJvmArch()); m = new MockSdkStatsService("Linux", "4.0", "i486-linux", "1.7"); assertEquals("x86", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "i586", "1.7"); assertEquals("x86", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "i686", "1.7"); assertEquals("x86", m.getJvmArch()); m = new MockSdkStatsService("Mac OS", "10.0", "x86_64", "1.7"); assertEquals("x86_64", m.getJvmArch()); m = new MockSdkStatsService("Mac OS", "8.0", "PowerPC", "1.7"); assertEquals("ppc", m.getJvmArch()); m = new MockSdkStatsService("Mac OS", "4.0", "x86_64", "1.7"); assertEquals("x86_64", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "ia64", "1.7"); assertEquals("x86_64", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "amd64", "1.7"); assertEquals("x86_64", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "atom", "1.7"); assertEquals("atom", m.getJvmArch()); // 32 chars max m = new MockSdkStatsService("Windows", "4.0", "one3456789ten3456789twenty6789thirty6789", "1.7"); assertEquals("one3456789ten3456789twenty6789th", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", "", "1.7"); assertEquals("unknown", m.getJvmArch()); m = new MockSdkStatsService("Windows", "4.0", null, "1.7"); assertEquals("unknown", m.getJvmArch()); } public void testSdkStatsService_getJvmVersion() { MockSdkStatsService m; m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); assertEquals("1.7", m.getJvmVersion()); m = new MockSdkStatsService("Windows", "4.0", "x86", ""); assertEquals("unknown", m.getJvmVersion()); m = new MockSdkStatsService("Windows", "4.0", "x86", null); assertEquals("unknown", m.getJvmVersion()); // 8 chars max m = new MockSdkStatsService("Windows", "4.0", "x86", "one3456789ten3456789twenty6789thirty6789"); assertEquals("one34567", m.getJvmVersion()); } public void testSdkStatsService_getJvmInfo() { MockSdkStatsService m; m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); assertEquals("1.7-x86", m.getJvmInfo()); m = new MockSdkStatsService("Windows", "4.0", "amd64", "1.7.8_09"); assertEquals("1.7-x86_64", m.getJvmInfo()); m = new MockSdkStatsService("Windows", "4.0", "", ""); assertEquals("unknown-unknown", m.getJvmInfo()); m = new MockSdkStatsService("Windows", "4.0", null, null); assertEquals("unknown-unknown", m.getJvmInfo()); // 8+32 chars max m = new MockSdkStatsService("Windows", "4.0", "one3456789ten3456789twenty6789thirty6789", "one3456789ten3456789twenty6789thirty6789"); assertEquals("one34567-one3456789ten3456789twenty6789th", m.getJvmInfo()); } public void testSdkStatsService_getOsVersion() { MockSdkStatsService m; m = new MockSdkStatsService("Windows", "4.0.32", "x86", "1.7.8_09"); assertEquals("4.0", m.getOsVersion()); m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); assertEquals("4.0", m.getOsVersion()); m = new MockSdkStatsService("Windows", "4", "x86", "1.7.8_09"); assertEquals(null, m.getOsVersion()); m = new MockSdkStatsService("Windows", "4.0;extrainfo", "x86", "1.7.8_09"); assertEquals("4.0", m.getOsVersion()); m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); assertEquals("10.8", m.getOsVersion()); m = new MockSdkStatsService("Mac OS", "10.8", "x86_64", "1.7.8_09"); assertEquals("10.8", m.getOsVersion()); m = new MockSdkStatsService("Other", "", "x86_64", "1.7.8_09"); assertEquals(null, m.getOsVersion()); m = new MockSdkStatsService("Other", null, "x86_64", "1.7.8_09"); assertEquals(null, m.getOsVersion()); } public void testSdkStatsService_getOsArch() { MockSdkStatsService m; // 64 bit jvm m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); assertEquals("x86_64", m.getOsArch()); m = new MockSdkStatsService("Windows", "8.32", "x86_64", "1.7.8_09"); assertEquals("x86_64", m.getOsArch()); m = new MockSdkStatsService("Linux", "8.32", "x86_64", "1.7.8_09"); assertEquals("x86_64", m.getOsArch()); // 32 bit jvm with 32 vs 64 bit os m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("PROCESSOR_ARCHITEW6432", null); assertEquals("x86", m.getOsArch()); m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("PROCESSOR_ARCHITEW6432", "AMD64"); assertEquals("x86_64", m.getOsArch()); m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("PROCESSOR_ARCHITEW6432", "IA64"); assertEquals("x86_64", m.getOsArch()); // 32 bit jvm with 32 vs 64 bit os m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("HOSTTYPE", null); assertEquals("x86", m.getOsArch()); m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("HOSTTYPE", "i686-linux"); assertEquals("x86", m.getOsArch()); m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("HOSTTYPE", "AMD64"); assertEquals("x86_64", m.getOsArch()); m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); m.setSystemEnv("HOSTTYPE", "x86_64"); assertEquals("x86_64", m.getOsArch()); } public void testSdkStatsService_getOsName() { MockSdkStatsService m; m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); assertEquals("mac-10.8", m.getOsName()); m = new MockSdkStatsService("mac", "10", "x86", "1.7.8_09"); assertEquals("mac", m.getOsName()); m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); assertEquals("win-6.2", m.getOsName()); m = new MockSdkStatsService("win", "6.2", "x86", "1.7.8_09"); assertEquals("win-6.2", m.getOsName()); m = new MockSdkStatsService("win", "6", "x86_64", "1.7.8_09"); assertEquals("win", m.getOsName()); m = new MockSdkStatsService("Linux", "foobuntu-32", "x86", "1.7.8_09"); assertEquals("linux", m.getOsName()); m = new MockSdkStatsService("linux", "1", "x86_64", "1.7.8_09"); assertEquals("linux", m.getOsName()); m = new MockSdkStatsService("PowerPC", "32", "ppc", "1.7.8_09"); assertEquals("PowerPC", m.getOsName()); m = new MockSdkStatsService("freebsd", "42", "x86_64", "1.7.8_09"); assertEquals("freebsd", m.getOsName()); m = new MockSdkStatsService("openbsd", "43", "x86_64", "1.7.8_09"); assertEquals("openbsd", m.getOsName()); // 32 chars max m = new MockSdkStatsService("one3456789ten3456789twenty6789thirty6789", "42", "x86_64", "1.7.8_09"); assertEquals("one3456789ten3456789twenty6789th", m.getOsName()); } public void testSdkStatsService_parseVersion() { // Tests that the version parses supports the new "major.minor.micro rcPreview" format // as well as "x.y.z.t" formats as well as Eclipse's "x.y.z.v2012somedate" formats. MockSdkStatsService m; m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); m.ping("monitor", "21"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); m.ping("monitor", "21.1"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.1.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); m.ping("monitor", "21.2.03"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.2.3.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); m.ping("monitor", "21.2.3.4"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.2.3.4&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // More than 4 parts or extra stuff that is not an "rc" preview are ignored. m.ping("monitor", "21.2.3.4.5.6.7.8"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.2.3.4&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); m.ping("monitor", "21.2.3.4.v20120101 the rest is ignored"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.2.3.4&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // If the "rc" preview integer is present, it's equivalent to a 4th number. m.ping("monitor", "21 rc4"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.0.0.4&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); m.ping("monitor", "21.01 rc5"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.1.0.5&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); m.ping("monitor", "21.02.03 rc6"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.2.3.6&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // If there's a 4-part version number, the rc preview number isn't used. m.ping("monitor", "21.2.3.4 rc7"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_monitor&" + "id=42&" + "version=21.2.3.4&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // For Eclipse plugins, the 4th part might be a date. It is ignored. m.ping("eclipse", "21.2.3.v20120102235958"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_eclipse&" + "id=42&" + "version=21.2.3.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); } public void testSdkStatsService_glPing() { MockSdkStatsService m; m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); // Send emulator ping with just emulator version, no GL stuff m.ping("emulator", "12"); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_emulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // Send emulator ping with just emulator version, no GL stuff. // This is the same request but using the variable string list API, arg 0 is the "ping" app. m.ping(new String[] { "ping", "emulator", "12" }); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_emulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // Send a ping for a non-emulator app with extra parameters, no GL stuff m.ping(new String[] { "ping", "not-emulator", "12", "arg1", "arg2", "arg3" }); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_notemulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64", m.getPingUrlResult().toString()); // Send a ping for the emulator app with extra parameters, GL stuff is added, 3 parameters m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc.", "Some cool_GPU!!! (fast one!)", "1.2.3.4_preview" }); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_emulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64&" + "glm=Vendor+Inc.&" + "glr=Some+cool_GPU+%28fast+one+%29&" + "glv=1.2.3.4_preview", m.getPingUrlResult().toString()); // Send a ping for the emulator app with extra parameters, GL stuff is added, 2 parameters m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc.", "Some cool_GPU!!! (fast one!)" }); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_emulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64&" + "glm=Vendor+Inc.&" + "glr=Some+cool_GPU+%28fast+one+%29", m.getPingUrlResult().toString()); // Send a ping for the emulator app with extra parameters, GL stuff is added, 1 parameter m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc." }); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_emulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64&" + "glm=Vendor+Inc.", m.getPingUrlResult().toString()); // Parameters that are more than 128 chars are cut short. m.ping(new String[] { "ping", "emulator", "12", // 130 chars each "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" }); assertEquals( "http://tools.google.com/service/update?" + "as=androidsdk_emulator&" + "id=42&" + "version=12.0.0.0&" + "os=win-6.2&" + "osa=x86_64&" + "vma=1.7-x86_64&" + "glm=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567&" + "glr=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567&" + "glv=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567", m.getPingUrlResult().toString()); } } swtmenubar/0040755 0000000 0000000 00000000000 12747325007 012006 5ustar000000000 0000000 swtmenubar/.classpath0100644 0000000 0000000 00000001612 12747325007 013766 0ustar000000000 0000000 swtmenubar/.project0100644 0000000 0000000 00000000561 12747325007 013454 0ustar000000000 0000000 swtmenubar org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature swtmenubar/.settings/0040755 0000000 0000000 00000000000 12747325007 013724 5ustar000000000 0000000 swtmenubar/.settings/README.txt0100644 0000000 0000000 00000000203 12747325007 015412 0ustar000000000 0000000 Copy this in eclipse project as a .settings folder at the root. This ensure proper compilation compliance and warning/error levels.swtmenubar/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 020711 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 swtmenubar/MODULE_LICENSE_EPL0100644 0000000 0000000 00000000000 12747325007 014543 0ustar000000000 0000000 swtmenubar/NOTICE0100644 0000000 0000000 00000026005 12747325007 012712 0ustar000000000 0000000 *Eclipse Public License - v 1.0* THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. *1. DEFINITIONS* "Contribution" means: a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor: i) changes to the Program, and ii) additions to the Program; where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. "Contributor" means any person or entity that distributes the Program. "Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. "Program" means the Contributions distributed in accordance with this Agreement. "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. *2. GRANT OF RIGHTS* a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. *3. REQUIREMENTS* A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: a) it complies with the terms and conditions of this Agreement; and b) its license agreement: i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. When the Program is made available in source code form: a) it must be made available under this Agreement; and b) a copy of this Agreement must be included with each copy of the Program. Contributors may not remove or alter any copyright notices contained within the Program. Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. *4. COMMERCIAL DISTRIBUTION* Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. *5. NO WARRANTY* EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. *6. DISCLAIMER OF LIABILITY* EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. *7. GENERAL* If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. swtmenubar/README0100755 0000000 0000000 00000005040 12747325007 012665 0ustar000000000 0000000 Using the Eclipse project SwtMenuBar ------------------------------------ This project provides a platform-specific way to hook into the default OS menu bar. On MacOS, it allows an SWT app to have an About menu item and to hook into the default Preferences menu item. On Windows and Linux, an SWT Menu should be provided (typically named "Tools") into which the About and Options menu items will be added. Consequently the implementation contains platform-specific source folders for the Java files that rely on a platform-specific version of SWT.jar. Right now we have the following source folders: - src/ - Generic implementation for all platforms. - src-darwin/ - Implementation for MacOS Carbon. *Only* the default "src/" folder is declared in the project .classpath so that the project can be opened in Eclipse on any platform and still work. However that means that on MacOS the custom src-darwin folder is not used by default. 1- To build the library: Do not use Eclipse to build the library. Instead use the makefile: $ cd $TOP_OF_ANDROID_TREE $ . build/envsetup.sh && lunch sdk-eng $ make swtmenubar This will create a Jar in /out/host//framework/ that can then be included in the target application. 2- To use the library in a target application: Build the swtmenubar library as explained in step 1. In the target application, define a classpath variable in Eclipse: - Open Preferences > Java > Build Path > Classpath Variables - Create a new classpath variable named ANDROID_OUT_FRAMEWORK - Set its folder value to /out/host//framework Then add a variable to the Build Path of the target project: - Open Project > Properties > Java Build Path - Select the "Libraries" tab - Use "Add Variable" - Select ANDROID_OUT_FRAMEWORK - Select "Extend..." - Select swtmenubar.jar (which you previously built at step 1) 3- Tip for developing this library: Keep in mind that src-darwin folder must not be added to the source folder list, otherwise the library would not compile on Windows or Linux. If you change anything to IMenuBarCallback, make sure to test on a Mac to be sure you're not breaking the API. To work on this on a Mac, you can either: a- simply temporarily add src-darwin as a source folder to the build path and remove it before submitting. b- or directly edit the java files and rebuild the library using 'make swtmenubar' from a shell. To test the library, use 'make swtmenubar'. This will build the library in out/... and the sdkmanager project is already setup to find it there. -- EOF swtmenubar/build.gradle0100644 0000000 0000000 00000000534 12747325007 014264 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'swtmenubar' dependencies { compile project(':base:sdklib') testCompile 'junit:junit:3.8.1' } sourceSets { main.resources.srcDir 'src/main/java' main.java.srcDir 'src/main/java' // Also add the MacOS specific sources for SWT Cocoa main.java.srcDir 'src/main-darwin/java' } swtmenubar/src/0040755 0000000 0000000 00000000000 12747325007 012575 5ustar000000000 0000000 swtmenubar/src/main-darwin/0040755 0000000 0000000 00000000000 12747325007 015003 5ustar000000000 0000000 swtmenubar/src/main-darwin/java/0040755 0000000 0000000 00000000000 12747325007 015724 5ustar000000000 0000000 swtmenubar/src/main-darwin/java/com/0040755 0000000 0000000 00000000000 12747325007 016502 5ustar000000000 0000000 swtmenubar/src/main-darwin/java/com/android/0040755 0000000 0000000 00000000000 12747325007 020122 5ustar000000000 0000000 swtmenubar/src/main-darwin/java/com/android/menubar/0040755 0000000 0000000 00000000000 12747325007 021553 5ustar000000000 0000000 swtmenubar/src/main-darwin/java/com/android/menubar/internal/0040755 0000000 0000000 00000000000 12747325007 023367 5ustar000000000 0000000 swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java0100644 0000000 0000000 00000031340 12747325007 030172 0ustar000000000 0000000 /* * 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. * * History: * Original code by the CarbonUIEnhancer from Agynami * with the implementation being modified from the org.eclipse.ui.internal.cocoa.CocoaUIEnhancer, * then modified by http://www.transparentech.com/opensource/cocoauienhancer to use reflection * rather than 'link' to SWT cocoa, and finally modified to be usable by the SwtMenuBar project. */ package com.android.menubar.internal; import com.android.menubar.IMenuBarCallback; import com.android.menubar.IMenuBarEnhancer; import org.eclipse.swt.SWT; import org.eclipse.swt.internal.C; import org.eclipse.swt.internal.Callback; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class MenuBarEnhancerCocoa implements IMenuBarEnhancer { private static final long kAboutMenuItem = 0; private static final long kPreferencesMenuItem = 2; // private static final long kServicesMenuItem = 4; // private static final long kHideApplicationMenuItem = 6; private static final long kQuitMenuItem = 10; static long mSelPreferencesMenuItemSelected; static long mSelAboutMenuItemSelected; static Callback mProc3Args; private String mAppName; /** * Class invoked via the Callback object to run the about and preferences * actions. *

* If you don't use JFace in your application (SWT only), change the * {@link org.eclipse.jface.action.IAction}s to * {@link org.eclipse.swt.widgets.Listener}s. *

*/ private static class ActionProctarget { private final IMenuBarCallback mCallbacks; public ActionProctarget(IMenuBarCallback callbacks) { mCallbacks = callbacks; } /** * Will be called on 32bit SWT. */ @SuppressWarnings("unused") public int actionProc(int id, int sel, int arg0) { return (int) actionProc((long) id, (long) sel, (long) arg0); } /** * Will be called on 64bit SWT. */ public long actionProc(long id, long sel, long arg0) { if (sel == mSelAboutMenuItemSelected) { mCallbacks.onAboutMenuSelected(); } else if (sel == mSelPreferencesMenuItemSelected) { mCallbacks.onPreferencesMenuSelected(); } else { // Unknown selection! } // Return value is not used. return 0; } } /** * Construct a new CocoaUIEnhancer. * * @param mAppName The name of the application. It will be used to customize * the About and Quit menu items. If you do not wish to customize * the About and Quit menu items, just pass null here. */ public MenuBarEnhancerCocoa() { } public MenuBarMode getMenuBarMode() { return MenuBarMode.MAC_OS; } /** * Setup the About and Preferences native menut items with the * given application name and links them to the callback. * * @param appName The application name. * @param display The SWT display. Must not be null. * @param callbacks The callbacks invoked by the menus. */ public void setupMenu( String appName, Display display, IMenuBarCallback callbacks) { mAppName = appName; // This is our callback object whose 'actionProc' method will be called // when the About or Preferences menuItem is invoked. ActionProctarget target = new ActionProctarget(callbacks); try { // Initialize the menuItems. initialize(target); } catch (Exception e) { throw new IllegalStateException(e); } // Schedule disposal of callback object display.disposeExec(new Runnable() { public void run() { invoke(mProc3Args, "dispose"); } }); } private void initialize(Object callbackObject) throws Exception { Class osCls = classForName("org.eclipse.swt.internal.cocoa.OS"); // Register names in objective-c. if (mSelAboutMenuItemSelected == 0) { mSelPreferencesMenuItemSelected = registerName(osCls, "preferencesMenuItemSelected:"); //$NON-NLS-1$ mSelAboutMenuItemSelected = registerName(osCls, "aboutMenuItemSelected:"); //$NON-NLS-1$ } // Create an SWT Callback object that will invoke the actionProc method // of our internal callback Object. mProc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$ Method getAddress = Callback.class.getMethod("getAddress", new Class[0]); Object object = getAddress.invoke(mProc3Args, (Object[]) null); long proc3 = convertToLong(object); if (proc3 == 0) { SWT.error(SWT.ERROR_NO_MORE_CALLBACKS); } Class nsMenuCls = classForName("org.eclipse.swt.internal.cocoa.NSMenu"); Class nsMenuitemCls = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"); Class nsStringCls = classForName("org.eclipse.swt.internal.cocoa.NSString"); Class nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication"); // Instead of creating a new delegate class in objective-c, // just use the current SWTApplicationDelegate. An instance of this // is a field of the Cocoa Display object and is already the target // for the menuItems. So just get this class and add the new methods // to it. object = invoke(osCls, "objc_lookUpClass", new Object[] { "SWTApplicationDelegate" }); long cls = convertToLong(object); // Add the action callbacks for Preferences and About menu items. invoke(osCls, "class_addMethod", new Object[] { wrapPointer(cls), wrapPointer(mSelPreferencesMenuItemSelected), wrapPointer(proc3), "@:@"}); //$NON-NLS-1$ invoke(osCls, "class_addMethod", new Object[] { wrapPointer(cls), wrapPointer(mSelAboutMenuItemSelected), wrapPointer(proc3), "@:@"}); //$NON-NLS-1$ // Get the Mac OS X Application menu. Object sharedApplication = invoke(nsApplicationCls, "sharedApplication"); Object mainMenu = invoke(sharedApplication, "mainMenu"); Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] { wrapPointer(0) }); Object appMenu = invoke(mainMenuItem, "submenu"); // Create the About menu command Object aboutMenuItem = invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { wrapPointer(kAboutMenuItem) }); if (mAppName != null) { Object nsStr = invoke(nsStringCls, "stringWith", new Object[] { "About " + mAppName }); invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] { nsStr }); } // Rename the quit action. if (mAppName != null) { Object quitMenuItem = invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { wrapPointer(kQuitMenuItem) }); Object nsStr = invoke(nsStringCls, "stringWith", new Object[] { "Quit " + mAppName }); invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] { nsStr }); } // Enable the Preferences menuItem. Object prefMenuItem = invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { wrapPointer(kPreferencesMenuItem) }); invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] { true }); // Set the action to execute when the About or Preferences menuItem is // invoked. // // We don't need to set the target here as the current target is the // SWTApplicationDelegate and we have registered the new selectors on // it. So just set the new action to invoke the selector. invoke(nsMenuitemCls, prefMenuItem, "setAction", new Object[] { wrapPointer(mSelPreferencesMenuItemSelected) }); invoke(nsMenuitemCls, aboutMenuItem, "setAction", new Object[] { wrapPointer(mSelAboutMenuItemSelected) }); } private long registerName(Class osCls, String name) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { Object object = invoke(osCls, "sel_registerName", new Object[] { name }); return convertToLong(object); } private long convertToLong(Object object) { if (object instanceof Integer) { Integer i = (Integer) object; return i.longValue(); } if (object instanceof Long) { Long l = (Long) object; return l.longValue(); } return 0; } private static Object wrapPointer(long value) { Class PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class; if (PTR_CLASS == long.class) { return new Long(value); } else { return new Integer((int) value); } } private static Object invoke(Class clazz, String methodName, Object[] args) { return invoke(clazz, null, methodName, args); } private static Object invoke(Class clazz, Object target, String methodName, Object[] args) { try { Class[] signature = new Class[args.length]; for (int i = 0; i < args.length; i++) { Class thisClass = args[i].getClass(); if (thisClass == Integer.class) signature[i] = int.class; else if (thisClass == Long.class) signature[i] = long.class; else if (thisClass == Byte.class) signature[i] = byte.class; else if (thisClass == Boolean.class) signature[i] = boolean.class; else signature[i] = thisClass; } Method method = clazz.getMethod(methodName, signature); return method.invoke(target, args); } catch (Exception e) { throw new IllegalStateException(e); } } private Class classForName(String classname) { try { Class cls = Class.forName(classname); return cls; } catch (ClassNotFoundException e) { throw new IllegalStateException(e); } } private Object invoke(Class cls, String methodName) { return invoke(cls, methodName, (Class[]) null, (Object[]) null); } private Object invoke(Class cls, String methodName, Class[] paramTypes, Object... arguments) { try { Method m = cls.getDeclaredMethod(methodName, paramTypes); return m.invoke(null, arguments); } catch (Exception e) { throw new IllegalStateException(e); } } private Object invoke(Object obj, String methodName) { return invoke(obj, methodName, (Class[]) null, (Object[]) null); } private Object invoke(Object obj, String methodName, Class[] paramTypes, Object... arguments) { try { Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes); return m.invoke(obj, arguments); } catch (Exception e) { throw new IllegalStateException(e); } } } swtmenubar/src/main/0040755 0000000 0000000 00000000000 12747325007 013521 5ustar000000000 0000000 swtmenubar/src/main/java/0040755 0000000 0000000 00000000000 12747325007 014442 5ustar000000000 0000000 swtmenubar/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 015220 5ustar000000000 0000000 swtmenubar/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016640 5ustar000000000 0000000 swtmenubar/src/main/java/com/android/menubar/0040755 0000000 0000000 00000000000 12747325007 020271 5ustar000000000 0000000 swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java0100644 0000000 0000000 00000002442 12747325007 024212 0ustar000000000 0000000 /* * 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.menubar; /** * Callbacks used by {@link IMenuBarEnhancer}. */ public interface IMenuBarCallback { /** * Invoked when the About menu item is selected by the user. */ abstract public void onAboutMenuSelected(); /** * Invoked when the Preferences or Options menu item is selected by the user. */ abstract public void onPreferencesMenuSelected(); /** * Used by the enhancer implementations to report errors. * * @param format A printf-like format string. * @param args The parameters for the printf-like format string. */ abstract public void printError(String format, Object...args); } swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java0100644 0000000 0000000 00000005167 12747325007 024250 0ustar000000000 0000000 /* * 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.menubar; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; /** * Interface to the platform-specific MenuBarEnhancer implementation returned by * {@link MenuBarEnhancer#setupMenu}. */ public interface IMenuBarEnhancer { /** Values that indicate how the menu bar is being handlded. */ public enum MenuBarMode { /** * The Mac-specific About and Preferences are being used. * No File > Exit menu should be provided by the application. */ MAC_OS, /** * The provided SWT {@link Menu} is being used for About and Options. * The application should provide a File > Exit menu. */ GENERIC } /** * Returns a {@link MenuBarMode} enum that indicates how the menu bar is going to * or has been modified. This is implementation specific and can be called before or * after {@link #setupMenu}. *

* Callers would typically call that to know if they need to hide or display * menu items. For example when {@link MenuBarMode#MAC_OS} is used, an app * would typically not need to provide any "File > Exit" menu item. * * @return One of the {@link MenuBarMode} values. */ public MenuBarMode getMenuBarMode(); /** * Updates the menu bar to provide an About menu item and a Preferences menu item. * Depending on the platform, the menu items might be decorated with the * given {@code appName}. *

* Users should not call this directly. * {@link MenuBarEnhancer#setupMenu} should be used instead. * * @param appName Name used for the About menu item and similar. Must not be null. * @param display The SWT display. Must not be null. * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked. * Must not be null. */ public void setupMenu( String appName, Display display, IMenuBarCallback callbacks); } swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java0100644 0000000 0000000 00000022134 12747325007 024130 0ustar000000000 0000000 /* * 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.menubar; import com.android.menubar.IMenuBarEnhancer.MenuBarMode; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; /** * On Mac, {@link MenuBarEnhancer#setupMenu} plugs a listener on the About and the * Preferences menu items of the standard "application" menu in the menu bar. * On Windows or Linux, it adds relevant items to a given {@link Menu} linked to * the same listeners. */ public final class MenuBarEnhancer { private MenuBarEnhancer() { } /** * Creates an instance of {@link IMenuBarEnhancer} specific to the current platform * and invoke its {@link IMenuBarEnhancer#setupMenu} to updates the menu bar. *

* Depending on the platform, this will either hook into the existing About menu item * and a Preferences or Options menu item or add new ones to the given {@code swtMenu}. * Depending on the platform, the menu items might be decorated with the * given {@code appName}. *

* Potential errors are reported through {@link IMenuBarCallback}. * * @param appName Name used for the About menu item and similar. Must not be null. * @param swtMenu For non-mac platform this is the menu where the "About" and * the "Options" menu items are created. Typically the menu might be * called "Tools". Must not be null. * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked. * Must not be null. * @return An actual {@link IMenuBarEnhancer} implementation. Can be null on failure. * This is currently not of any use for the caller but is left in case * we want to expand the functionality later. */ public static IMenuBarEnhancer setupMenu( String appName, final Menu swtMenu, IMenuBarCallback callbacks) { IMenuBarEnhancer enhancer = getEnhancer(callbacks, swtMenu.getDisplay()); // Default implementation for generic platforms if (enhancer == null) { enhancer = getGenericEnhancer(swtMenu); } try { enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks); } catch (Exception e) { // If the enhancer failed, try to fall back on the generic one if (enhancer.getMenuBarMode() != MenuBarMode.GENERIC) { enhancer = getGenericEnhancer(swtMenu); try { enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks); } catch (Exception e2) { callbacks.printError("SWTMenuBar failed: %s", e2.toString()); return null; } } } return enhancer; } private static IMenuBarEnhancer getGenericEnhancer(final Menu swtMenu) { IMenuBarEnhancer enhancer; enhancer = new IMenuBarEnhancer() { @Override public MenuBarMode getMenuBarMode() { return MenuBarMode.GENERIC; } @Override public void setupMenu( String appName, Display display, final IMenuBarCallback callbacks) { if (swtMenu.getItemCount() > 0) { new MenuItem(swtMenu, SWT.SEPARATOR); } // Note: we use "Preferences" on Mac and "Options" on Windows/Linux. final MenuItem pref = new MenuItem(swtMenu, SWT.NONE); pref.setText("&Options..."); final MenuItem about = new MenuItem(swtMenu, SWT.NONE); about.setText("&About..."); pref.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { try { pref.setEnabled(false); callbacks.onPreferencesMenuSelected(); super.widgetSelected(e); } finally { pref.setEnabled(true); } } }); about.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { try { about.setEnabled(false); callbacks.onAboutMenuSelected(); super.widgetSelected(e); } finally { about.setEnabled(true); } } }); } }; return enhancer; } public static IMenuBarEnhancer setupMenuManager( String appName, Display display, final IMenuManager menuManager, final IAction aboutAction, final IAction preferencesAction, final IAction quitAction) { IMenuBarCallback callbacks = new IMenuBarCallback() { @Override public void printError(String format, Object... args) { System.err.println(String.format(format, args)); } @Override public void onPreferencesMenuSelected() { if (preferencesAction != null) { preferencesAction.run(); } } @Override public void onAboutMenuSelected() { if (aboutAction != null) { aboutAction.run(); } } }; IMenuBarEnhancer enhancer = getEnhancer(callbacks, display); // Default implementation for generic platforms if (enhancer == null) { enhancer = new IMenuBarEnhancer() { @Override public MenuBarMode getMenuBarMode() { return MenuBarMode.GENERIC; } @Override public void setupMenu( String appName, Display display, final IMenuBarCallback callbacks) { if (!menuManager.isEmpty()) { menuManager.add(new Separator()); } if (aboutAction != null) { menuManager.add(aboutAction); } if (preferencesAction != null) { menuManager.add(preferencesAction); } if (quitAction != null) { if (aboutAction != null || preferencesAction != null) { menuManager.add(new Separator()); } menuManager.add(quitAction); } } }; } enhancer.setupMenu(appName, display, callbacks); return enhancer; } private static IMenuBarEnhancer getEnhancer(IMenuBarCallback callbacks, Display display) { IMenuBarEnhancer enhancer = null; String p = SWT.getPlatform(); String className = null; if ("cocoa".equals(p)) { //$NON-NLS-1$ className = "com.android.menubar.internal.MenuBarEnhancerCocoa"; //$NON-NLS-1$ if (SWT.getVersion() >= 3700 && MenuBarEnhancer37.isSupported(display)) { className = MenuBarEnhancer37.class.getName(); } } if (System.getenv("DEBUG_SWTMENUBAR") != null) { callbacks.printError("DEBUG SwtMenuBar: SWT=%1$s, class=%2$s", p, className); } if (className != null) { try { Class clazz = Class.forName(className); enhancer = (IMenuBarEnhancer) clazz.newInstance(); } catch (Exception e) { // Log an error and fallback on the default implementation. callbacks.printError( "Failed to instantiate %1$s: %2$s", //$NON-NLS-1$ className, e.toString()); } } return enhancer; } } swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java0100644 0000000 0000000 00000012411 12747325007 024277 0ustar000000000 0000000 /* * Copyright (C) 2012 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. * * References: * Based on the SWT snippet example at * http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet354.java?view=co */ package com.android.menubar; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import java.lang.reflect.Method; public class MenuBarEnhancer37 implements IMenuBarEnhancer { private static final int kAboutMenuItem = -1; // SWT.ID_ABOUT in SWT 3.7 private static final int kPreferencesMenuItem = -2; // SWT.ID_PREFERENCES in SWT 3.7 private static final int kQuitMenuItem = -6; // SWT.ID_QUIT in SWT 3.7 public MenuBarEnhancer37() { } @Override public MenuBarMode getMenuBarMode() { return MenuBarMode.MAC_OS; } /** * Setup the About and Preferences native menut items with the * given application name and links them to the callback. * * @param appName The application name. * @param display The SWT display. Must not be null. * @param callbacks The callbacks invoked by the menus. */ @Override public void setupMenu( String appName, Display display, IMenuBarCallback callbacks) { try { // Initialize the menuItems. initialize(display, appName, callbacks); } catch (Exception e) { throw new IllegalStateException(e); } // Schedule disposal of callback object display.disposeExec(new Runnable() { @Override public void run() { } }); } /** * Checks whether the required SWT 3.7 APIs are available. *
* Calling this will load the class, which is OK since this class doesn't * directly use any SWT 3.7 API -- instead it uses reflection so that the * code can be loaded under SWT 3.6. * * @param display The current SWT display. * @return True if the SWT 3.7 API are available and this enhancer can be used. */ public static boolean isSupported(Display display) { try { Object sysMenu = call0(display, "getSystemMenu"); if (sysMenu instanceof Menu) { return findMenuById((Menu)sysMenu, kPreferencesMenuItem) != null && findMenuById((Menu)sysMenu, kAboutMenuItem) != null; } } catch (Exception ignore) {} return false; } private void initialize( Display display, String appName, final IMenuBarCallback callbacks) throws Exception { Object sysMenu = call0(display, "getSystemMenu"); if (sysMenu instanceof Menu) { MenuItem menu = findMenuById((Menu)sysMenu, kPreferencesMenuItem); if (menu != null) { menu.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { callbacks.onPreferencesMenuSelected(); } }); } menu = findMenuById((Menu)sysMenu, kAboutMenuItem); if (menu != null) { menu.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { callbacks.onAboutMenuSelected(); } }); menu.setText("About " + appName); } menu = findMenuById((Menu)sysMenu, kQuitMenuItem); if (menu != null) { // We already support the "quit" operation, no need for an extra handler here. menu.setText("Quit " + appName); } } } private static Object call0(Object obj, String method) { try { Method m = obj.getClass().getMethod(method, (Class[])null); if (m != null) { return m.invoke(obj, (Object[])null); } } catch (Exception ignore) {} return null; } private static MenuItem findMenuById(Menu menu, int id) { MenuItem[] items = menu.getItems(); for (int i = items.length - 1; i >= 0; i--) { MenuItem item = items[i]; Object menuId = call0(item, "getID"); if (menuId instanceof Integer) { if (((Integer) menuId).intValue() == id) { return item; } } } return null; } } swtmenubar/swtmenubar.iml0100644 0000000 0000000 00000001517 12747325007 014701 0ustar000000000 0000000 traceview/0040755 0000000 0000000 00000000000 12747325007 011610 5ustar000000000 0000000 traceview/.classpath0100644 0000000 0000000 00000001100 12747325007 013560 0ustar000000000 0000000 traceview/.project0100644 0000000 0000000 00000000560 12747325007 013255 0ustar000000000 0000000 traceview org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature traceview/.settings/0040755 0000000 0000000 00000000000 12747325007 013526 5ustar000000000 0000000 traceview/.settings/README.txt0100644 0000000 0000000 00000000203 12747325007 015214 0ustar000000000 0000000 Copy this in eclipse project as a .settings folder at the root. This ensure proper compilation compliance and warning/error levels.traceview/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 020513 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 traceview/NOTICE0100644 0000000 0000000 00000024707 12747325007 012523 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS traceview/README0100644 0000000 0000000 00000000701 12747325007 012463 0ustar000000000 0000000 Using the Eclipse projects for traceview. traceview requires SWT to compile. SWT is available in the depot under //device/prebuild//swt Because the build path cannot contain relative path that are not inside the project directory, the .classpath file references a user library called ANDROID_SWT. In order to compile the project, make a user library called ANDROID_SWT containing the jar available at //device/prebuild//swt. traceview/build.gradle0100644 0000000 0000000 00000000744 12747325007 014071 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'traceview' dependencies { compile project(':base:common') compile project(':swt:sdkstats') } sdk { linux { item('etc/traceview') { executable true } } mac { item('etc/traceview') { executable true } } windows { item 'etc/traceview.bat' } } // configure the manifest of the buildDistributionJar task. sdkJar.manifest.attributes("Main-Class": "com.android.traceview.MainWindow") traceview/etc/0040755 0000000 0000000 00000000000 12747325007 012363 5ustar000000000 0000000 traceview/etc/traceview0100755 0000000 0000000 00000006223 12747325007 014302 0ustar000000000 0000000 #!/bin/bash # # Copyright 2005-2006, 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. # Set up prog to be the path of this script, including following symlinks, # and set up progdir to be the fully-qualified pathname of its directory. prog="$0" while [ -h "${prog}" ]; do newProg=`/bin/ls -ld "${prog}"` newProg=`expr "${newProg}" : ".* -> \(.*\)$"` if expr "x${newProg}" : 'x/' >/dev/null; then prog="${newProg}" else progdir=`dirname "${prog}"` prog="${progdir}/${newProg}" fi done oldwd=`pwd` progdir=`dirname "${prog}"` progname=`basename "${prog}"` cd "${progdir}" progdir=`pwd` prog="${progdir}"/"${progname}" cd "${oldwd}" jarfile=traceview.jar frameworkdir="$progdir" libdir="$progdir" if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/tools/lib libdir=`dirname "$progdir"`/tools/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/framework libdir=`dirname "$progdir"`/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then echo "${progname}: can't find $jarfile" exit 1 fi javaCmd="java" os=`uname` if [ $os == 'Darwin' ]; then javaOpts="-Xmx1600M -XstartOnFirstThread" else javaOpts="-Xmx1600M" fi if [ `uname` = "Linux" ]; then export GDK_NATIVE_WINDOWS=true fi while expr "x$1" : 'x-J' >/dev/null; do opt=`expr "x$1" : 'x-J\(.*\)'` javaOpts="${javaOpts} -${opt}" shift done jarpath="$frameworkdir/$jarfile" # Figure out the path to the swt.jar for the current architecture. # if ANDROID_SWT is defined, then just use this. # else, if running in the Android source tree, then look for the correct swt folder in prebuilt # else, look for the correct swt folder in the SDK under tools/lib/ swtpath="" if [ -n "$ANDROID_SWT" ]; then swtpath="$ANDROID_SWT" else vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` if [ -n "$ANDROID_BUILD_TOP" ]; then osname=`uname -s | tr A-Z a-z` swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" else swtpath="${frameworkdir}/${vmarch}" fi fi # Combine the swtpath and the framework dir path. if [ -d "$swtpath" ]; then frameworkdir="${swtpath}:${frameworkdir}" else echo "SWT folder '${swtpath}' does not exist." echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." exit 1 fi if [ -x $progdir/monitor ]; then echo "The standalone version of traceview is deprecated." echo "Please use Android Device Monitor (tools/monitor) instead." fi exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.traceview.toolsdir="$progdir" -jar "$jarpath" "$@" traceview/etc/traceview.bat0100755 0000000 0000000 00000004015 12747325007 015044 0ustar000000000 0000000 @echo off rem Copyright (C) 2007 The Android Open Source Project rem rem Licensed under the Apache License, Version 2.0 (the "License"); rem you may not use this file except in compliance with the License. rem You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem don't modify the caller's environment setlocal rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 rem Change current directory and drive to where the script is, to avoid rem issues with directories containing whitespaces. cd /d %~dp0 rem Check we have a valid Java.exe in the path. set java_exe= call lib\find_java.bat if not defined java_exe goto :EOF set jarfile=traceview.jar set frameworkdir=. if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=lib if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=..\framework :JarFileOk set jarpath=%frameworkdir%\%jarfile% if not defined ANDROID_SWT goto QueryArch set swt_path=%ANDROID_SWT% goto SwtDone :QueryArch for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a :SwtDone if exist "%swt_path%" goto SetPath echo SWT folder '%swt_path%' does not exist. echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. exit /B :SetPath set javaextdirs=%swt_path%;%frameworkdir% echo The standalone version of traceview is deprecated. echo Please use Android Device Monitor (tools/monitor) instead. call "%java_exe%" "-Djava.ext.dirs=%javaextdirs%" -Dcom.android.traceview.toolsdir= -jar %jarpath% %* traceview/src/0040755 0000000 0000000 00000000000 12747325007 012377 5ustar000000000 0000000 traceview/src/main/0040755 0000000 0000000 00000000000 12747325007 013323 5ustar000000000 0000000 traceview/src/main/java/0040755 0000000 0000000 00000000000 12747325007 014244 5ustar000000000 0000000 traceview/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 015022 5ustar000000000 0000000 traceview/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 016442 5ustar000000000 0000000 traceview/src/main/java/com/android/traceview/0040755 0000000 0000000 00000000000 12747325007 020433 5ustar000000000 0000000 traceview/src/main/java/com/android/traceview/Call.java0100644 0000000 0000000 00000010716 12747325007 022153 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import org.eclipse.swt.graphics.Color; class Call implements TimeLineView.Block { final private ThreadData mThreadData; final private MethodData mMethodData; final Call mCaller; // the caller, or null if this is the root private String mName; private boolean mIsRecursive; long mGlobalStartTime; long mGlobalEndTime; long mThreadStartTime; long mThreadEndTime; long mInclusiveRealTime; // real time spent in this call including its children long mExclusiveRealTime; // real time spent in this call including its children long mInclusiveCpuTime; // cpu time spent in this call including its children long mExclusiveCpuTime; // cpu time spent in this call excluding its children Call(ThreadData threadData, MethodData methodData, Call caller) { mThreadData = threadData; mMethodData = methodData; mName = methodData.getProfileName(); mCaller = caller; } public void updateName() { mName = mMethodData.getProfileName(); } @Override public double addWeight(int x, int y, double weight) { return mMethodData.addWeight(x, y, weight); } @Override public void clearWeight() { mMethodData.clearWeight(); } @Override public long getStartTime() { return mGlobalStartTime; } @Override public long getEndTime() { return mGlobalEndTime; } @Override public long getExclusiveCpuTime() { return mExclusiveCpuTime; } @Override public long getInclusiveCpuTime() { return mInclusiveCpuTime; } @Override public long getExclusiveRealTime() { return mExclusiveRealTime; } @Override public long getInclusiveRealTime() { return mInclusiveRealTime; } @Override public Color getColor() { return mMethodData.getColor(); } @Override public String getName() { return mName; } public void setName(String name) { mName = name; } public ThreadData getThreadData() { return mThreadData; } public int getThreadId() { return mThreadData.getId(); } @Override public MethodData getMethodData() { return mMethodData; } @Override public boolean isContextSwitch() { return mMethodData.getId() == -1; } @Override public boolean isIgnoredBlock() { // Ignore the top-level call or context switches within the top-level call. return mCaller == null || isContextSwitch() && mCaller.mCaller == null; } @Override public TimeLineView.Block getParentBlock() { return mCaller; } public boolean isRecursive() { return mIsRecursive; } void setRecursive(boolean isRecursive) { mIsRecursive = isRecursive; } void addCpuTime(long elapsedCpuTime) { mExclusiveCpuTime += elapsedCpuTime; mInclusiveCpuTime += elapsedCpuTime; } /** * Record time spent in the method call. */ void finish() { if (mCaller != null) { mCaller.mInclusiveCpuTime += mInclusiveCpuTime; mCaller.mInclusiveRealTime += mInclusiveRealTime; } mMethodData.addElapsedExclusive(mExclusiveCpuTime, mExclusiveRealTime); if (!mIsRecursive) { mMethodData.addTopExclusive(mExclusiveCpuTime, mExclusiveRealTime); } mMethodData.addElapsedInclusive(mInclusiveCpuTime, mInclusiveRealTime, mIsRecursive, mCaller); } public static final class TraceAction { public static final int ACTION_ENTER = 0; public static final int ACTION_EXIT = 1; public final int mAction; public final Call mCall; public TraceAction(int action, Call call) { mAction = action; mCall = call; } } } traceview/src/main/java/com/android/traceview/ColorController.java0100644 0000000 0000000 00000010361 12747325007 024416 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import java.util.HashMap; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Display; public class ColorController { private static final int[] systemColors = { SWT.COLOR_BLUE, SWT.COLOR_RED, SWT.COLOR_GREEN, SWT.COLOR_CYAN, SWT.COLOR_MAGENTA, SWT.COLOR_DARK_BLUE, SWT.COLOR_DARK_RED, SWT.COLOR_DARK_GREEN, SWT.COLOR_DARK_YELLOW, SWT.COLOR_DARK_CYAN, SWT.COLOR_DARK_MAGENTA, SWT.COLOR_BLACK }; private static RGB[] rgbColors = { new RGB(90, 90, 255), // blue new RGB(0, 240, 0), // green new RGB(255, 0, 0), // red new RGB(0, 255, 255), // cyan new RGB(255, 80, 255), // magenta new RGB(200, 200, 0), // yellow new RGB(40, 0, 200), // dark blue new RGB(150, 255, 150), // light green new RGB(150, 0, 0), // dark red new RGB(30, 150, 150), // dark cyan new RGB(200, 200, 255), // light blue new RGB(0, 120, 0), // dark green new RGB(255, 150, 150), // light red new RGB(140, 80, 140), // dark magenta new RGB(150, 100, 50), // brown new RGB(70, 70, 70), // dark grey }; private static HashMap colorCache = new HashMap(); private static HashMap imageCache = new HashMap(); public ColorController() { } public static Color requestColor(Display display, RGB rgb) { return requestColor(display, rgb.red, rgb.green, rgb.blue); } public static Image requestColorSquare(Display display, RGB rgb) { return requestColorSquare(display, rgb.red, rgb.green, rgb.blue); } public static Color requestColor(Display display, int red, int green, int blue) { int key = (red << 16) | (green << 8) | blue; Color color = colorCache.get(key); if (color == null) { color = new Color(display, red, green, blue); colorCache.put(key, color); } return color; } public static Image requestColorSquare(Display display, int red, int green, int blue) { int key = (red << 16) | (green << 8) | blue; Image image = imageCache.get(key); if (image == null) { image = new Image(display, 8, 14); GC gc = new GC(image); Color color = requestColor(display, red, green, blue); gc.setBackground(color); gc.fillRectangle(image.getBounds()); gc.dispose(); imageCache.put(key, image); } return image; } public static void assignMethodColors(Display display, MethodData[] methods) { int nextColorIndex = 0; for (MethodData md : methods) { RGB rgb = rgbColors[nextColorIndex]; if (++nextColorIndex == rgbColors.length) nextColorIndex = 0; Color color = requestColor(display, rgb); Image image = requestColorSquare(display, rgb); md.setColor(color); md.setImage(image); // Compute and set a faded color int fadedRed = 150 + rgb.red / 4; int fadedGreen = 150 + rgb.green / 4; int fadedBlue = 150 + rgb.blue / 4; RGB faded = new RGB(fadedRed, fadedGreen, fadedBlue); color = requestColor(display, faded); image = requestColorSquare(display, faded); md.setFadedColor(color); md.setFadedImage(image); } } } traceview/src/main/java/com/android/traceview/DmTraceReader.java0100644 0000000 0000000 00000067356 12747325007 023756 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.nio.BufferUnderflowException; import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DmTraceReader extends TraceReader { private static final int TRACE_MAGIC = 0x574f4c53; private static final int METHOD_TRACE_ENTER = 0x00; // method entry private static final int METHOD_TRACE_EXIT = 0x01; // method exit private static final int METHOD_TRACE_UNROLL = 0x02; // method exited by exception unrolling // When in dual clock mode, we report that a context switch has occurred // when skew between the real time and thread cpu clocks is more than this // many microseconds. private static final long MIN_CONTEXT_SWITCH_TIME_USEC = 100; private enum ClockSource { THREAD_CPU, WALL, DUAL, }; private int mVersionNumber; private boolean mRegression; private ProfileProvider mProfileProvider; private String mTraceFileName; private MethodData mTopLevel; private ArrayList mCallList; private HashMap mPropertiesMap; private HashMap mMethodMap; private HashMap mThreadMap; private ThreadData[] mSortedThreads; private MethodData[] mSortedMethods; private long mTotalCpuTime; private long mTotalRealTime; private MethodData mContextSwitch; private int mRecordSize; private ClockSource mClockSource; // A regex for matching the thread "id name" lines in the .key file private static final Pattern mIdNamePattern = Pattern.compile("(\\d+)\t(.*)"); //$NON-NLS-1$ public DmTraceReader(String traceFileName, boolean regression) throws IOException { mTraceFileName = traceFileName; mRegression = regression; mPropertiesMap = new HashMap(); mMethodMap = new HashMap(); mThreadMap = new HashMap(); mCallList = new ArrayList(); // Create a single top-level MethodData object to hold the profile data // for time spent in the unknown caller. mTopLevel = new MethodData(0, "(toplevel)"); mContextSwitch = new MethodData(-1, "(context switch)"); mMethodMap.put(0, mTopLevel); mMethodMap.put(-1, mContextSwitch); generateTrees(); } void generateTrees() throws IOException { long offset = parseKeys(); parseData(offset); analyzeData(); } @Override public ProfileProvider getProfileProvider() { if (mProfileProvider == null) mProfileProvider = new ProfileProvider(this); return mProfileProvider; } private MappedByteBuffer mapFile(String filename, long offset) throws IOException { MappedByteBuffer buffer = null; FileInputStream dataFile = new FileInputStream(filename); try { File file = new File(filename); FileChannel fc = dataFile.getChannel(); buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset); buffer.order(ByteOrder.LITTLE_ENDIAN); return buffer; } finally { dataFile.close(); // this *also* closes the associated channel, fc } } private void readDataFileHeader(MappedByteBuffer buffer) { int magic = buffer.getInt(); if (magic != TRACE_MAGIC) { System.err.printf( "Error: magic number mismatch; got 0x%x, expected 0x%x\n", magic, TRACE_MAGIC); throw new RuntimeException(); } // read version int version = buffer.getShort(); if (version != mVersionNumber) { System.err.printf( "Error: version number mismatch; got %d in data header but %d in options\n", version, mVersionNumber); throw new RuntimeException(); } if (version < 1 || version > 3) { System.err.printf( "Error: unsupported trace version number %d. " + "Please use a newer version of TraceView to read this file.", version); throw new RuntimeException(); } // read offset int offsetToData = buffer.getShort() - 16; // read startWhen buffer.getLong(); // read record size if (version == 1) { mRecordSize = 9; } else if (version == 2) { mRecordSize = 10; } else { mRecordSize = buffer.getShort(); offsetToData -= 2; } // Skip over offsetToData bytes while (offsetToData-- > 0) { buffer.get(); } } private void parseData(long offset) throws IOException { MappedByteBuffer buffer = mapFile(mTraceFileName, offset); readDataFileHeader(buffer); ArrayList trace = null; if (mClockSource == ClockSource.THREAD_CPU) { trace = new ArrayList(); } final boolean haveThreadClock = mClockSource != ClockSource.WALL; final boolean haveGlobalClock = mClockSource != ClockSource.THREAD_CPU; // Parse all call records to obtain elapsed time information. ThreadData prevThreadData = null; for (;;) { int threadId; int methodId; long threadTime, globalTime; try { int recordSize = mRecordSize; if (mVersionNumber == 1) { threadId = buffer.get(); recordSize -= 1; } else { threadId = buffer.getShort(); recordSize -= 2; } methodId = buffer.getInt(); recordSize -= 4; switch (mClockSource) { case WALL: threadTime = 0; globalTime = buffer.getInt(); recordSize -= 4; break; case DUAL: threadTime = buffer.getInt(); globalTime = buffer.getInt(); recordSize -= 8; break; default: case THREAD_CPU: threadTime = buffer.getInt(); globalTime = 0; recordSize -= 4; break; } while (recordSize-- > 0) { buffer.get(); } } catch (BufferUnderflowException ex) { break; } int methodAction = methodId & 0x03; methodId = methodId & ~0x03; MethodData methodData = mMethodMap.get(methodId); if (methodData == null) { String name = String.format("(0x%1$x)", methodId); //$NON-NLS-1$ methodData = new MethodData(methodId, name); mMethodMap.put(methodId, methodData); } ThreadData threadData = mThreadMap.get(threadId); if (threadData == null) { String name = String.format("[%1$d]", threadId); //$NON-NLS-1$ threadData = new ThreadData(threadId, name, mTopLevel); mThreadMap.put(threadId, threadData); } long elapsedGlobalTime = 0; if (haveGlobalClock) { if (!threadData.mHaveGlobalTime) { threadData.mGlobalStartTime = globalTime; threadData.mHaveGlobalTime = true; } else { elapsedGlobalTime = globalTime - threadData.mGlobalEndTime; } threadData.mGlobalEndTime = globalTime; } if (haveThreadClock) { long elapsedThreadTime = 0; if (!threadData.mHaveThreadTime) { threadData.mThreadStartTime = threadTime; threadData.mThreadCurrentTime = threadTime; threadData.mHaveThreadTime = true; } else { elapsedThreadTime = threadTime - threadData.mThreadEndTime; } threadData.mThreadEndTime = threadTime; if (!haveGlobalClock) { // Detect context switches whenever execution appears to switch from one // thread to another. This assumption is only valid on uniprocessor // systems (which is why we now have a dual clock mode). // We represent context switches in the trace by pushing a call record // with MethodData mContextSwitch onto the stack of the previous // thread. We arbitrarily set the start and end time of the context // switch such that the context switch occurs in the middle of the thread // time and itself accounts for zero thread time. if (prevThreadData != null && prevThreadData != threadData) { // Begin context switch from previous thread. Call switchCall = prevThreadData.enter(mContextSwitch, trace); switchCall.mThreadStartTime = prevThreadData.mThreadEndTime; mCallList.add(switchCall); // Return from context switch to current thread. Call top = threadData.top(); if (top.getMethodData() == mContextSwitch) { threadData.exit(mContextSwitch, trace); long beforeSwitch = elapsedThreadTime / 2; top.mThreadStartTime += beforeSwitch; top.mThreadEndTime = top.mThreadStartTime; } } prevThreadData = threadData; } else { // If we have a global clock, then we can detect context switches (or blocking // calls or cpu suspensions or clock anomalies) by comparing global time to // thread time for successive calls that occur on the same thread. // As above, we represent the context switch using a special method call. long sleepTime = elapsedGlobalTime - elapsedThreadTime; if (sleepTime > MIN_CONTEXT_SWITCH_TIME_USEC) { Call switchCall = threadData.enter(mContextSwitch, trace); long beforeSwitch = elapsedThreadTime / 2; long afterSwitch = elapsedThreadTime - beforeSwitch; switchCall.mGlobalStartTime = globalTime - elapsedGlobalTime + beforeSwitch; switchCall.mGlobalEndTime = globalTime - afterSwitch; switchCall.mThreadStartTime = threadTime - afterSwitch; switchCall.mThreadEndTime = switchCall.mThreadStartTime; threadData.exit(mContextSwitch, trace); mCallList.add(switchCall); } } // Add thread CPU time. Call top = threadData.top(); top.addCpuTime(elapsedThreadTime); } switch (methodAction) { case METHOD_TRACE_ENTER: { Call call = threadData.enter(methodData, trace); if (haveGlobalClock) { call.mGlobalStartTime = globalTime; } if (haveThreadClock) { call.mThreadStartTime = threadTime; } mCallList.add(call); break; } case METHOD_TRACE_EXIT: case METHOD_TRACE_UNROLL: { Call call = threadData.exit(methodData, trace); if (call != null) { if (haveGlobalClock) { call.mGlobalEndTime = globalTime; } if (haveThreadClock) { call.mThreadEndTime = threadTime; } } break; } default: throw new RuntimeException("Unrecognized method action: " + methodAction); } } // Exit any pending open-ended calls. for (ThreadData threadData : mThreadMap.values()) { threadData.endTrace(trace); } // Recreate the global timeline from thread times, if needed. if (!haveGlobalClock) { long globalTime = 0; prevThreadData = null; for (TraceAction traceAction : trace) { Call call = traceAction.mCall; ThreadData threadData = call.getThreadData(); if (traceAction.mAction == TraceAction.ACTION_ENTER) { long threadTime = call.mThreadStartTime; globalTime += call.mThreadStartTime - threadData.mThreadCurrentTime; call.mGlobalStartTime = globalTime; if (!threadData.mHaveGlobalTime) { threadData.mHaveGlobalTime = true; threadData.mGlobalStartTime = globalTime; } threadData.mThreadCurrentTime = threadTime; } else if (traceAction.mAction == TraceAction.ACTION_EXIT) { long threadTime = call.mThreadEndTime; globalTime += call.mThreadEndTime - threadData.mThreadCurrentTime; call.mGlobalEndTime = globalTime; threadData.mGlobalEndTime = globalTime; threadData.mThreadCurrentTime = threadTime; } // else, ignore ACTION_INCOMPLETE calls, nothing to do prevThreadData = threadData; } } // Finish updating all calls and calculate the total time spent. for (int i = mCallList.size() - 1; i >= 0; i--) { Call call = mCallList.get(i); // Calculate exclusive real-time by subtracting inclusive real time // accumulated by children from the total span. long realTime = call.mGlobalEndTime - call.mGlobalStartTime; call.mExclusiveRealTime = Math.max(realTime - call.mInclusiveRealTime, 0); call.mInclusiveRealTime = realTime; call.finish(); } mTotalCpuTime = 0; mTotalRealTime = 0; for (ThreadData threadData : mThreadMap.values()) { Call rootCall = threadData.getRootCall(); threadData.updateRootCallTimeBounds(); rootCall.finish(); mTotalCpuTime += rootCall.mInclusiveCpuTime; mTotalRealTime += rootCall.mInclusiveRealTime; } if (mRegression) { System.out.format("totalCpuTime %dus\n", mTotalCpuTime); System.out.format("totalRealTime %dus\n", mTotalRealTime); dumpThreadTimes(); dumpCallTimes(); } } static final int PARSE_VERSION = 0; static final int PARSE_THREADS = 1; static final int PARSE_METHODS = 2; static final int PARSE_OPTIONS = 4; long parseKeys() throws IOException { long offset = 0; BufferedReader in = null; try { in = new BufferedReader(new InputStreamReader( new FileInputStream(mTraceFileName), "US-ASCII")); int mode = PARSE_VERSION; String line = null; while (true) { line = in.readLine(); if (line == null) { throw new IOException("Key section does not have an *end marker"); } // Calculate how much we have read from the file so far. The // extra byte is for the line ending not included by readLine(). offset += line.length() + 1; if (line.startsWith("*")) { if (line.equals("*version")) { mode = PARSE_VERSION; continue; } if (line.equals("*threads")) { mode = PARSE_THREADS; continue; } if (line.equals("*methods")) { mode = PARSE_METHODS; continue; } if (line.equals("*end")) { break; } } switch (mode) { case PARSE_VERSION: mVersionNumber = Integer.decode(line); mode = PARSE_OPTIONS; break; case PARSE_THREADS: parseThread(line); break; case PARSE_METHODS: parseMethod(line); break; case PARSE_OPTIONS: parseOption(line); break; } } } catch (FileNotFoundException ex) { System.err.println(ex.getMessage()); } finally { if (in != null) { in.close(); } } if (mClockSource == null) { mClockSource = ClockSource.THREAD_CPU; } return offset; } void parseOption(String line) { String[] tokens = line.split("="); if (tokens.length == 2) { String key = tokens[0]; String value = tokens[1]; mPropertiesMap.put(key, value); if (key.equals("clock")) { if (value.equals("thread-cpu")) { mClockSource = ClockSource.THREAD_CPU; } else if (value.equals("wall")) { mClockSource = ClockSource.WALL; } else if (value.equals("dual")) { mClockSource = ClockSource.DUAL; } } } } void parseThread(String line) { String idStr = null; String name = null; Matcher matcher = mIdNamePattern.matcher(line); if (matcher.find()) { idStr = matcher.group(1); name = matcher.group(2); } if (idStr == null) return; if (name == null) name = "(unknown)"; int id = Integer.decode(idStr); mThreadMap.put(id, new ThreadData(id, name, mTopLevel)); } void parseMethod(String line) { String[] tokens = line.split("\t"); int id = Long.decode(tokens[0]).intValue(); String className = tokens[1]; String methodName = null; String signature = null; String pathname = null; int lineNumber = -1; if (tokens.length == 6) { methodName = tokens[2]; signature = tokens[3]; pathname = tokens[4]; lineNumber = Integer.decode(tokens[5]); pathname = constructPathname(className, pathname); } else if (tokens.length > 2) { if (tokens[3].startsWith("(")) { methodName = tokens[2]; signature = tokens[3]; } else { pathname = tokens[2]; lineNumber = Integer.decode(tokens[3]); } } mMethodMap.put(id, new MethodData(id, className, methodName, signature, pathname, lineNumber)); } private String constructPathname(String className, String pathname) { int index = className.lastIndexOf('/'); if (index > 0 && index < className.length() - 1 && pathname.endsWith(".java")) pathname = className.substring(0, index + 1) + pathname; return pathname; } private void analyzeData() { final TimeBase timeBase = getPreferredTimeBase(); // Sort the threads into decreasing cpu time Collection tv = mThreadMap.values(); mSortedThreads = tv.toArray(new ThreadData[tv.size()]); Arrays.sort(mSortedThreads, new Comparator() { @Override public int compare(ThreadData td1, ThreadData td2) { if (timeBase.getTime(td2) > timeBase.getTime(td1)) return 1; if (timeBase.getTime(td2) < timeBase.getTime(td1)) return -1; return td2.getName().compareTo(td1.getName()); } }); // Sort the methods into decreasing inclusive time Collection mv = mMethodMap.values(); MethodData[] methods; methods = mv.toArray(new MethodData[mv.size()]); Arrays.sort(methods, new Comparator() { @Override public int compare(MethodData md1, MethodData md2) { if (timeBase.getElapsedInclusiveTime(md2) > timeBase.getElapsedInclusiveTime(md1)) return 1; if (timeBase.getElapsedInclusiveTime(md2) < timeBase.getElapsedInclusiveTime(md1)) return -1; return md1.getName().compareTo(md2.getName()); } }); // Count the number of methods with non-zero inclusive time int nonZero = 0; for (MethodData md : methods) { if (timeBase.getElapsedInclusiveTime(md) == 0) break; nonZero += 1; } // Copy the methods with non-zero time mSortedMethods = new MethodData[nonZero]; int ii = 0; for (MethodData md : methods) { if (timeBase.getElapsedInclusiveTime(md) == 0) break; md.setRank(ii); mSortedMethods[ii++] = md; } // Let each method analyze its profile data for (MethodData md : mSortedMethods) { md.analyzeData(timeBase); } // Update all the calls to include the method rank in // their name. for (Call call : mCallList) { call.updateName(); } if (mRegression) { dumpMethodStats(); } } /* * This method computes a list of records that describe the the execution * timeline for each thread. Each record is a pair: (row, block) where: row: * is the ThreadData object block: is the call (containing the start and end * times) */ @Override public ArrayList getThreadTimeRecords() { TimeLineView.Record record; ArrayList timeRecs; timeRecs = new ArrayList(); // For each thread, push a "toplevel" call that encompasses the // entire execution of the thread. for (ThreadData threadData : mSortedThreads) { if (!threadData.isEmpty() && threadData.getId() != 0) { record = new TimeLineView.Record(threadData, threadData.getRootCall()); timeRecs.add(record); } } for (Call call : mCallList) { record = new TimeLineView.Record(call.getThreadData(), call); timeRecs.add(record); } if (mRegression) { dumpTimeRecs(timeRecs); System.exit(0); } return timeRecs; } private void dumpThreadTimes() { System.out.print("\nThread Times\n"); System.out.print("id t-start t-end g-start g-end name\n"); for (ThreadData threadData : mThreadMap.values()) { System.out.format("%2d %8d %8d %8d %8d %s\n", threadData.getId(), threadData.mThreadStartTime, threadData.mThreadEndTime, threadData.mGlobalStartTime, threadData.mGlobalEndTime, threadData.getName()); } } private void dumpCallTimes() { System.out.print("\nCall Times\n"); System.out.print("id t-start t-end g-start g-end excl. incl. method\n"); for (Call call : mCallList) { System.out.format("%2d %8d %8d %8d %8d %8d %8d %s\n", call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime, call.mGlobalStartTime, call.mGlobalEndTime, call.mExclusiveCpuTime, call.mInclusiveCpuTime, call.getMethodData().getName()); } } private void dumpMethodStats() { System.out.print("\nMethod Stats\n"); System.out.print("Excl Cpu Incl Cpu Excl Real Incl Real Calls Method\n"); for (MethodData md : mSortedMethods) { System.out.format("%9d %9d %9d %9d %9s %s\n", md.getElapsedExclusiveCpuTime(), md.getElapsedInclusiveCpuTime(), md.getElapsedExclusiveRealTime(), md.getElapsedInclusiveRealTime(), md.getCalls(), md.getProfileName()); } } private void dumpTimeRecs(ArrayList timeRecs) { System.out.print("\nTime Records\n"); System.out.print("id t-start t-end g-start g-end method\n"); for (TimeLineView.Record record : timeRecs) { Call call = (Call) record.block; System.out.format("%2d %8d %8d %8d %8d %s\n", call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime, call.mGlobalStartTime, call.mGlobalEndTime, call.getMethodData().getName()); } } @Override public HashMap getThreadLabels() { HashMap labels = new HashMap(); for (ThreadData t : mThreadMap.values()) { labels.put(t.getId(), t.getName()); } return labels; } @Override public MethodData[] getMethods() { return mSortedMethods; } @Override public ThreadData[] getThreads() { return mSortedThreads; } @Override public long getTotalCpuTime() { return mTotalCpuTime; } @Override public long getTotalRealTime() { return mTotalRealTime; } @Override public boolean haveCpuTime() { return mClockSource != ClockSource.WALL; } @Override public boolean haveRealTime() { return mClockSource != ClockSource.THREAD_CPU; } @Override public HashMap getProperties() { return mPropertiesMap; } @Override public TimeBase getPreferredTimeBase() { if (mClockSource == ClockSource.WALL) { return TimeBase.REAL_TIME; } return TimeBase.CPU_TIME; } @Override public String getClockSource() { switch (mClockSource) { case THREAD_CPU: return "cpu time"; case WALL: return "real time"; case DUAL: return "real time, dual clock"; } return null; } } traceview/src/main/java/com/android/traceview/MainWindow.java0100644 0000000 0000000 00000023475 12747325007 023362 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import com.android.sdkstats.SdkStatsService; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.window.ApplicationWindow; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.channels.FileChannel; import java.util.HashMap; import java.util.Properties; public class MainWindow extends ApplicationWindow { private final static String PING_NAME = "Traceview"; private TraceReader mReader; private String mTraceName; // A global cache of string names. public static HashMap sStringCache = new HashMap(); public MainWindow(String traceName, TraceReader reader) { super(null); mReader = reader; mTraceName = traceName; addMenuBar(); } public void run() { setBlockOnOpen(true); open(); } @Override protected void configureShell(Shell shell) { super.configureShell(shell); shell.setText("Traceview: " + mTraceName); InputStream in = getClass().getClassLoader().getResourceAsStream( "icons/traceview-128.png"); if (in != null) { shell.setImage(new Image(shell.getDisplay(), in)); } shell.setBounds(100, 10, 1282, 900); } @Override protected Control createContents(Composite parent) { ColorController.assignMethodColors(parent.getDisplay(), mReader.getMethods()); SelectionController selectionController = new SelectionController(); GridLayout gridLayout = new GridLayout(1, false); gridLayout.marginWidth = 0; gridLayout.marginHeight = 0; gridLayout.horizontalSpacing = 0; gridLayout.verticalSpacing = 0; parent.setLayout(gridLayout); Display display = parent.getDisplay(); Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); // Create a sash form to separate the timeline view (on top) // and the profile view (on bottom) SashForm sashForm1 = new SashForm(parent, SWT.VERTICAL); sashForm1.setBackground(darkGray); sashForm1.SASH_WIDTH = 3; GridData data = new GridData(GridData.FILL_BOTH); sashForm1.setLayoutData(data); // Create the timeline view new TimeLineView(sashForm1, mReader, selectionController); // Create the profile view new ProfileView(sashForm1, mReader, selectionController); return sashForm1; } @Override protected MenuManager createMenuManager() { MenuManager manager = super.createMenuManager(); MenuManager viewMenu = new MenuManager("View"); manager.add(viewMenu); Action showPropertiesAction = new Action("Show Properties...") { @Override public void run() { showProperties(); } }; viewMenu.add(showPropertiesAction); return manager; } private void showProperties() { PropertiesDialog dialog = new PropertiesDialog(getShell()); dialog.setProperties(mReader.getProperties()); dialog.open(); } /** * Convert the old two-file format into the current concatenated one. * * @param base Base path of the two files, i.e. base.key and base.data * @return Path to a temporary file that will be deleted on exit. * @throws IOException */ private static String makeTempTraceFile(String base) throws IOException { // Make a temporary file that will go away on exit and prepare to // write into it. File temp = File.createTempFile(base, ".trace"); temp.deleteOnExit(); FileOutputStream dstStream = null; FileInputStream keyStream = null; FileInputStream dataStream = null; try { dstStream = new FileOutputStream(temp); FileChannel dstChannel = dstStream.getChannel(); // First copy the contents of the key file into our temp file. keyStream = new FileInputStream(base + ".key"); FileChannel srcChannel = keyStream.getChannel(); long size = dstChannel.transferFrom(srcChannel, 0, srcChannel.size()); srcChannel.close(); // Then concatenate the data file. dataStream = new FileInputStream(base + ".data"); srcChannel = dataStream.getChannel(); dstChannel.transferFrom(srcChannel, size, srcChannel.size()); } finally { if (dstStream != null) { dstStream.close(); // also closes dstChannel } if (keyStream != null) { keyStream.close(); // also closes srcChannel } if (dataStream != null) { dataStream.close(); } } // Return the path of the temp file. return temp.getPath(); } /** * Returns the tools revision number. */ private static String getRevision() { Properties p = new Properties(); try{ String toolsdir = System.getProperty("com.android.traceview.toolsdir"); //$NON-NLS-1$ File sourceProp; if (toolsdir == null || toolsdir.length() == 0) { sourceProp = new File("source.properties"); //$NON-NLS-1$ } else { sourceProp = new File(toolsdir, "source.properties"); //$NON-NLS-1$ } FileInputStream fis = null; try { fis = new FileInputStream(sourceProp); p.load(fis); } finally { if (fis != null) { try { fis.close(); } catch (IOException ignore) { } } } String revision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ if (revision != null && revision.length() > 0) { return revision; } } catch (FileNotFoundException e) { // couldn't find the file? don't ping. } catch (IOException e) { // couldn't find the file? don't ping. } return null; } public static void main(String[] args) { TraceReader reader = null; boolean regression = false; // ping the usage server String revision = getRevision(); if (revision != null) { new SdkStatsService().ping(PING_NAME, revision); } // Process command line arguments int argc = 0; int len = args.length; while (argc < len) { String arg = args[argc]; if (arg.charAt(0) != '-') { break; } if (arg.equals("-r")) { regression = true; } else { break; } argc++; } if (argc != len - 1) { System.out.printf("Usage: java %s [-r] trace%n", MainWindow.class.getName()); System.out.printf(" -r regression only%n"); return; } String traceName = args[len - 1]; File file = new File(traceName); if (file.exists() && file.isDirectory()) { System.out.printf("Qemu trace files not supported yet.\n"); System.exit(1); // reader = new QtraceReader(traceName); } else { // If the filename as given doesn't exist... if (!file.exists()) { // Try appending .trace. if (new File(traceName + ".trace").exists()) { traceName = traceName + ".trace"; // Next, see if it is the old two-file trace. } else if (new File(traceName + ".data").exists() && new File(traceName + ".key").exists()) { try { traceName = makeTempTraceFile(traceName); } catch (IOException e) { System.err.printf("cannot convert old trace file '%s'\n", traceName); System.exit(1); } // Otherwise, give up. } else { System.err.printf("trace file '%s' not found\n", traceName); System.exit(1); } } try { reader = new DmTraceReader(traceName, regression); } catch (IOException e) { System.err.printf("Failed to read the trace file"); e.printStackTrace(); System.exit(1); return; } } reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds); Display.setAppName("Traceview"); new MainWindow(traceName, reader).run(); } } traceview/src/main/java/com/android/traceview/MethodData.java0100644 0000000 0000000 00000041151 12747325007 023307 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; public class MethodData { private int mId; private int mRank = -1; private String mClassName; private String mMethodName; private String mSignature; private String mName; private String mProfileName; private String mPathname; private int mLineNumber; private long mElapsedExclusiveCpuTime; private long mElapsedInclusiveCpuTime; private long mTopExclusiveCpuTime; private long mElapsedExclusiveRealTime; private long mElapsedInclusiveRealTime; private long mTopExclusiveRealTime; private int[] mNumCalls = new int[2]; // index 0=normal, 1=recursive private Color mColor; private Color mFadedColor; private Image mImage; private Image mFadedImage; private HashMap mParents; private HashMap mChildren; // The parents of this method when this method was in a recursive call private HashMap mRecursiveParents; // The children of this method when this method was in a recursive call private HashMap mRecursiveChildren; private ProfileNode[] mProfileNodes; private int mX; private int mY; private double mWeight; public MethodData(int id, String className) { mId = id; mClassName = className; mMethodName = null; mSignature = null; mPathname = null; mLineNumber = -1; computeName(); computeProfileName(); } public MethodData(int id, String className, String methodName, String signature, String pathname, int lineNumber) { mId = id; mClassName = className; mMethodName = methodName; mSignature = signature; mPathname = pathname; mLineNumber = lineNumber; computeName(); computeProfileName(); } public double addWeight(int x, int y, double weight) { if (mX == x && mY == y) mWeight += weight; else { mX = x; mY = y; mWeight = weight; } return mWeight; } public void clearWeight() { mWeight = 0; } public int getRank() { return mRank; } public void setRank(int rank) { mRank = rank; computeProfileName(); } public void addElapsedExclusive(long cpuTime, long realTime) { mElapsedExclusiveCpuTime += cpuTime; mElapsedExclusiveRealTime += realTime; } public void addElapsedInclusive(long cpuTime, long realTime, boolean isRecursive, Call parent) { if (isRecursive == false) { mElapsedInclusiveCpuTime += cpuTime; mElapsedInclusiveRealTime += realTime; mNumCalls[0] += 1; } else { mNumCalls[1] += 1; } if (parent == null) return; // Find the child method in the parent MethodData parentMethod = parent.getMethodData(); if (parent.isRecursive()) { parentMethod.mRecursiveChildren = updateInclusive(cpuTime, realTime, parentMethod, this, false, parentMethod.mRecursiveChildren); } else { parentMethod.mChildren = updateInclusive(cpuTime, realTime, parentMethod, this, false, parentMethod.mChildren); } // Find the parent method in the child if (isRecursive) { mRecursiveParents = updateInclusive(cpuTime, realTime, this, parentMethod, true, mRecursiveParents); } else { mParents = updateInclusive(cpuTime, realTime, this, parentMethod, true, mParents); } } private HashMap updateInclusive(long cpuTime, long realTime, MethodData contextMethod, MethodData elementMethod, boolean elementIsParent, HashMap map) { if (map == null) { map = new HashMap(4); } else { ProfileData profileData = map.get(elementMethod.mId); if (profileData != null) { profileData.addElapsedInclusive(cpuTime, realTime); return map; } } ProfileData elementData = new ProfileData(contextMethod, elementMethod, elementIsParent); elementData.setElapsedInclusive(cpuTime, realTime); elementData.setNumCalls(1); map.put(elementMethod.mId, elementData); return map; } public void analyzeData(TimeBase timeBase) { // Sort the parents and children into decreasing inclusive time ProfileData[] sortedParents; ProfileData[] sortedChildren; ProfileData[] sortedRecursiveParents; ProfileData[] sortedRecursiveChildren; sortedParents = sortProfileData(mParents, timeBase); sortedChildren = sortProfileData(mChildren, timeBase); sortedRecursiveParents = sortProfileData(mRecursiveParents, timeBase); sortedRecursiveChildren = sortProfileData(mRecursiveChildren, timeBase); // Add "self" time to the top of the sorted children sortedChildren = addSelf(sortedChildren); // Create the ProfileNode objects that we need ArrayList nodes = new ArrayList(); ProfileNode profileNode; if (mParents != null) { profileNode = new ProfileNode("Parents", this, sortedParents, true, false); nodes.add(profileNode); } if (mChildren != null) { profileNode = new ProfileNode("Children", this, sortedChildren, false, false); nodes.add(profileNode); } if (mRecursiveParents!= null) { profileNode = new ProfileNode("Parents while recursive", this, sortedRecursiveParents, true, true); nodes.add(profileNode); } if (mRecursiveChildren != null) { profileNode = new ProfileNode("Children while recursive", this, sortedRecursiveChildren, false, true); nodes.add(profileNode); } mProfileNodes = nodes.toArray(new ProfileNode[nodes.size()]); } // Create and return a ProfileData[] array that is a sorted copy // of the given HashMap values. private ProfileData[] sortProfileData(HashMap map, final TimeBase timeBase) { if (map == null) return null; // Convert the hash values to an array of ProfileData Collection values = map.values(); ProfileData[] sorted = values.toArray(new ProfileData[values.size()]); // Sort the array by elapsed inclusive time Arrays.sort(sorted, new Comparator() { @Override public int compare(ProfileData pd1, ProfileData pd2) { if (timeBase.getElapsedInclusiveTime(pd2) > timeBase.getElapsedInclusiveTime(pd1)) return 1; if (timeBase.getElapsedInclusiveTime(pd2) < timeBase.getElapsedInclusiveTime(pd1)) return -1; return 0; } }); return sorted; } private ProfileData[] addSelf(ProfileData[] children) { ProfileData[] pdata; if (children == null) { pdata = new ProfileData[1]; } else { pdata = new ProfileData[children.length + 1]; System.arraycopy(children, 0, pdata, 1, children.length); } pdata[0] = new ProfileSelf(this); return pdata; } public void addTopExclusive(long cpuTime, long realTime) { mTopExclusiveCpuTime += cpuTime; mTopExclusiveRealTime += realTime; } public long getTopExclusiveCpuTime() { return mTopExclusiveCpuTime; } public long getTopExclusiveRealTime() { return mTopExclusiveRealTime; } public int getId() { return mId; } private void computeName() { if (mMethodName == null) { mName = mClassName; return; } StringBuilder sb = new StringBuilder(); sb.append(mClassName); sb.append("."); //$NON-NLS-1$ sb.append(mMethodName); sb.append(" "); //$NON-NLS-1$ sb.append(mSignature); mName = sb.toString(); } public String getName() { return mName; } public String getClassName() { return mClassName; } public String getMethodName() { return mMethodName; } public String getProfileName() { return mProfileName; } public String getSignature() { return mSignature; } public void computeProfileName() { if (mRank == -1) { mProfileName = mName; return; } StringBuilder sb = new StringBuilder(); sb.append(mRank); sb.append(" "); //$NON-NLS-1$ sb.append(getName()); mProfileName = sb.toString(); } public String getCalls() { return String.format("%d+%d", mNumCalls[0], mNumCalls[1]); } public int getTotalCalls() { return mNumCalls[0] + mNumCalls[1]; } public Color getColor() { return mColor; } public void setColor(Color color) { mColor = color; } public void setImage(Image image) { mImage = image; } public Image getImage() { return mImage; } @Override public String toString() { return getName(); } public long getElapsedExclusiveCpuTime() { return mElapsedExclusiveCpuTime; } public long getElapsedExclusiveRealTime() { return mElapsedExclusiveRealTime; } public long getElapsedInclusiveCpuTime() { return mElapsedInclusiveCpuTime; } public long getElapsedInclusiveRealTime() { return mElapsedInclusiveRealTime; } public void setFadedColor(Color fadedColor) { mFadedColor = fadedColor; } public Color getFadedColor() { return mFadedColor; } public void setFadedImage(Image fadedImage) { mFadedImage = fadedImage; } public Image getFadedImage() { return mFadedImage; } public void setPathname(String pathname) { mPathname = pathname; } public String getPathname() { return mPathname; } public void setLineNumber(int lineNumber) { mLineNumber = lineNumber; } public int getLineNumber() { return mLineNumber; } public ProfileNode[] getProfileNodes() { return mProfileNodes; } public static class Sorter implements Comparator { @Override public int compare(MethodData md1, MethodData md2) { if (mColumn == Column.BY_NAME) { int result = md1.getName().compareTo(md2.getName()); return (mDirection == Direction.INCREASING) ? result : -result; } if (mColumn == Column.BY_INCLUSIVE_CPU_TIME) { if (md2.getElapsedInclusiveCpuTime() > md1.getElapsedInclusiveCpuTime()) return (mDirection == Direction.INCREASING) ? -1 : 1; if (md2.getElapsedInclusiveCpuTime() < md1.getElapsedInclusiveCpuTime()) return (mDirection == Direction.INCREASING) ? 1 : -1; return md1.getName().compareTo(md2.getName()); } if (mColumn == Column.BY_EXCLUSIVE_CPU_TIME) { if (md2.getElapsedExclusiveCpuTime() > md1.getElapsedExclusiveCpuTime()) return (mDirection == Direction.INCREASING) ? -1 : 1; if (md2.getElapsedExclusiveCpuTime() < md1.getElapsedExclusiveCpuTime()) return (mDirection == Direction.INCREASING) ? 1 : -1; return md1.getName().compareTo(md2.getName()); } if (mColumn == Column.BY_INCLUSIVE_REAL_TIME) { if (md2.getElapsedInclusiveRealTime() > md1.getElapsedInclusiveRealTime()) return (mDirection == Direction.INCREASING) ? -1 : 1; if (md2.getElapsedInclusiveRealTime() < md1.getElapsedInclusiveRealTime()) return (mDirection == Direction.INCREASING) ? 1 : -1; return md1.getName().compareTo(md2.getName()); } if (mColumn == Column.BY_EXCLUSIVE_REAL_TIME) { if (md2.getElapsedExclusiveRealTime() > md1.getElapsedExclusiveRealTime()) return (mDirection == Direction.INCREASING) ? -1 : 1; if (md2.getElapsedExclusiveRealTime() < md1.getElapsedExclusiveRealTime()) return (mDirection == Direction.INCREASING) ? 1 : -1; return md1.getName().compareTo(md2.getName()); } if (mColumn == Column.BY_CALLS) { int result = md1.getTotalCalls() - md2.getTotalCalls(); if (result == 0) return md1.getName().compareTo(md2.getName()); return (mDirection == Direction.INCREASING) ? result : -result; } if (mColumn == Column.BY_CPU_TIME_PER_CALL) { double time1 = md1.getElapsedInclusiveCpuTime(); time1 = time1 / md1.getTotalCalls(); double time2 = md2.getElapsedInclusiveCpuTime(); time2 = time2 / md2.getTotalCalls(); double diff = time1 - time2; int result = 0; if (diff < 0) result = -1; else if (diff > 0) result = 1; if (result == 0) return md1.getName().compareTo(md2.getName()); return (mDirection == Direction.INCREASING) ? result : -result; } if (mColumn == Column.BY_REAL_TIME_PER_CALL) { double time1 = md1.getElapsedInclusiveRealTime(); time1 = time1 / md1.getTotalCalls(); double time2 = md2.getElapsedInclusiveRealTime(); time2 = time2 / md2.getTotalCalls(); double diff = time1 - time2; int result = 0; if (diff < 0) result = -1; else if (diff > 0) result = 1; if (result == 0) return md1.getName().compareTo(md2.getName()); return (mDirection == Direction.INCREASING) ? result : -result; } return 0; } public void setColumn(Column column) { // If the sort column specified is the same as last time, // then reverse the sort order. if (mColumn == column) { // Reverse the sort order if (mDirection == Direction.INCREASING) mDirection = Direction.DECREASING; else mDirection = Direction.INCREASING; } else { // Sort names into increasing order, data into decreasing order. if (column == Column.BY_NAME) mDirection = Direction.INCREASING; else mDirection = Direction.DECREASING; } mColumn = column; } public Column getColumn() { return mColumn; } public void setDirection(Direction direction) { mDirection = direction; } public Direction getDirection() { return mDirection; } public static enum Column { BY_NAME, BY_EXCLUSIVE_CPU_TIME, BY_EXCLUSIVE_REAL_TIME, BY_INCLUSIVE_CPU_TIME, BY_INCLUSIVE_REAL_TIME, BY_CALLS, BY_REAL_TIME_PER_CALL, BY_CPU_TIME_PER_CALL, }; public static enum Direction { INCREASING, DECREASING }; private Column mColumn; private Direction mDirection; } } traceview/src/main/java/com/android/traceview/ProfileData.java0100644 0000000 0000000 00000004566 12747325007 023500 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; public class ProfileData { protected MethodData mElement; /** mContext is either the parent or child of mElement */ protected MethodData mContext; protected boolean mElementIsParent; protected long mElapsedInclusiveCpuTime; protected long mElapsedInclusiveRealTime; protected int mNumCalls; public ProfileData() { } public ProfileData(MethodData context, MethodData element, boolean elementIsParent) { mContext = context; mElement = element; mElementIsParent = elementIsParent; } public String getProfileName() { return mElement.getProfileName(); } public MethodData getMethodData() { return mElement; } public void addElapsedInclusive(long cpuTime, long realTime) { mElapsedInclusiveCpuTime += cpuTime; mElapsedInclusiveRealTime += realTime; mNumCalls += 1; } public void setElapsedInclusive(long cpuTime, long realTime) { mElapsedInclusiveCpuTime = cpuTime; mElapsedInclusiveRealTime = realTime; } public long getElapsedInclusiveCpuTime() { return mElapsedInclusiveCpuTime; } public long getElapsedInclusiveRealTime() { return mElapsedInclusiveRealTime; } public void setNumCalls(int numCalls) { mNumCalls = numCalls; } public String getNumCalls() { int totalCalls; if (mElementIsParent) totalCalls = mContext.getTotalCalls(); else totalCalls = mElement.getTotalCalls(); return String.format("%d/%d", mNumCalls, totalCalls); } public boolean isParent() { return mElementIsParent; } public MethodData getContext() { return mContext; } } traceview/src/main/java/com/android/traceview/ProfileNode.java0100644 0000000 0000000 00000002615 12747325007 023505 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; public class ProfileNode { private String mLabel; private MethodData mMethodData; private ProfileData[] mChildren; private boolean mIsParent; private boolean mIsRecursive; public ProfileNode(String label, MethodData methodData, ProfileData[] children, boolean isParent, boolean isRecursive) { mLabel = label; mMethodData = methodData; mChildren = children; mIsParent = isParent; mIsRecursive = isRecursive; } public String getLabel() { return mLabel; } public ProfileData[] getChildren() { return mChildren; } public boolean isParent() { return mIsParent; } public boolean isRecursive() { return mIsRecursive; } } traceview/src/main/java/com/android/traceview/ProfileProvider.java0100644 0000000 0000000 00000045100 12747325007 024406 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import com.android.utils.SdkUtils; import org.eclipse.jface.viewers.IColorProvider; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import java.io.InputStream; import java.util.Arrays; class ProfileProvider implements ITreeContentProvider { private MethodData[] mRoots; private SelectionAdapter mListener; private TreeViewer mTreeViewer; private TraceReader mReader; private Image mSortUp; private Image mSortDown; private String mColumnNames[] = { "Name", "Incl Cpu Time %", "Incl Cpu Time", "Excl Cpu Time %", "Excl Cpu Time", "Incl Real Time %", "Incl Real Time", "Excl Real Time %", "Excl Real Time", "Calls+Recur\nCalls/Total", "Cpu Time/Call", "Real Time/Call" }; private int mColumnWidths[] = { 370, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100 }; private int mColumnAlignments[] = { SWT.LEFT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.CENTER, SWT.RIGHT, SWT.RIGHT }; private static final int COL_NAME = 0; private static final int COL_INCLUSIVE_CPU_TIME_PER = 1; private static final int COL_INCLUSIVE_CPU_TIME = 2; private static final int COL_EXCLUSIVE_CPU_TIME_PER = 3; private static final int COL_EXCLUSIVE_CPU_TIME = 4; private static final int COL_INCLUSIVE_REAL_TIME_PER = 5; private static final int COL_INCLUSIVE_REAL_TIME = 6; private static final int COL_EXCLUSIVE_REAL_TIME_PER = 7; private static final int COL_EXCLUSIVE_REAL_TIME = 8; private static final int COL_CALLS = 9; private static final int COL_CPU_TIME_PER_CALL = 10; private static final int COL_REAL_TIME_PER_CALL = 11; private long mTotalCpuTime; private long mTotalRealTime; private int mPrevMatchIndex = -1; public ProfileProvider(TraceReader reader) { mRoots = reader.getMethods(); mReader = reader; mTotalCpuTime = reader.getTotalCpuTime(); mTotalRealTime = reader.getTotalRealTime(); Display display = Display.getCurrent(); InputStream in = getClass().getClassLoader().getResourceAsStream( "icons/sort_up.png"); mSortUp = new Image(display, in); in = getClass().getClassLoader().getResourceAsStream( "icons/sort_down.png"); mSortDown = new Image(display, in); } private MethodData doMatchName(String name, int startIndex) { // Check if the given "name" has any uppercase letters boolean hasUpper = SdkUtils.hasUpperCaseCharacter(name); for (int ii = startIndex; ii < mRoots.length; ++ii) { MethodData md = mRoots[ii]; String fullName = md.getName(); // If there were no upper case letters in the given name, // then ignore case when matching. if (!hasUpper) fullName = fullName.toLowerCase(); if (fullName.indexOf(name) != -1) { mPrevMatchIndex = ii; return md; } } mPrevMatchIndex = -1; return null; } public MethodData findMatchingName(String name) { return doMatchName(name, 0); } public MethodData findNextMatchingName(String name) { return doMatchName(name, mPrevMatchIndex + 1); } public MethodData findMatchingTreeItem(TreeItem item) { if (item == null) return null; String text = item.getText(); if (Character.isDigit(text.charAt(0)) == false) return null; int spaceIndex = text.indexOf(' '); String numstr = text.substring(0, spaceIndex); int rank = Integer.valueOf(numstr); for (MethodData md : mRoots) { if (md.getRank() == rank) return md; } return null; } public void setTreeViewer(TreeViewer treeViewer) { mTreeViewer = treeViewer; } public String[] getColumnNames() { return mColumnNames; } public int[] getColumnWidths() { int[] widths = Arrays.copyOf(mColumnWidths, mColumnWidths.length); if (!mReader.haveCpuTime()) { widths[COL_EXCLUSIVE_CPU_TIME] = 0; widths[COL_EXCLUSIVE_CPU_TIME_PER] = 0; widths[COL_INCLUSIVE_CPU_TIME] = 0; widths[COL_INCLUSIVE_CPU_TIME_PER] = 0; widths[COL_CPU_TIME_PER_CALL] = 0; } if (!mReader.haveRealTime()) { widths[COL_EXCLUSIVE_REAL_TIME] = 0; widths[COL_EXCLUSIVE_REAL_TIME_PER] = 0; widths[COL_INCLUSIVE_REAL_TIME] = 0; widths[COL_INCLUSIVE_REAL_TIME_PER] = 0; widths[COL_REAL_TIME_PER_CALL] = 0; } return widths; } public int[] getColumnAlignments() { return mColumnAlignments; } @Override public Object[] getChildren(Object element) { if (element instanceof MethodData) { MethodData md = (MethodData) element; return md.getProfileNodes(); } if (element instanceof ProfileNode) { ProfileNode pn = (ProfileNode) element; return pn.getChildren(); } return new Object[0]; } @Override public Object getParent(Object element) { return null; } @Override public boolean hasChildren(Object element) { if (element instanceof MethodData) return true; if (element instanceof ProfileNode) return true; return false; } @Override public Object[] getElements(Object element) { return mRoots; } @Override public void dispose() { } @Override public void inputChanged(Viewer arg0, Object arg1, Object arg2) { } public Object getRoot() { return "root"; } public SelectionAdapter getColumnListener() { if (mListener == null) mListener = new ColumnListener(); return mListener; } public LabelProvider getLabelProvider() { return new ProfileLabelProvider(); } class ProfileLabelProvider extends LabelProvider implements ITableLabelProvider, IColorProvider { Color colorRed; Color colorParentsBack; Color colorChildrenBack; TraceUnits traceUnits; public ProfileLabelProvider() { Display display = Display.getCurrent(); colorRed = display.getSystemColor(SWT.COLOR_RED); colorParentsBack = new Color(display, 230, 230, 255); // blue colorChildrenBack = new Color(display, 255, 255, 210); // yellow traceUnits = mReader.getTraceUnits(); } @Override public String getColumnText(Object element, int col) { if (element instanceof MethodData) { MethodData md = (MethodData) element; if (col == COL_NAME) return md.getProfileName(); if (col == COL_EXCLUSIVE_CPU_TIME) { double val = md.getElapsedExclusiveCpuTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_EXCLUSIVE_CPU_TIME_PER) { double val = md.getElapsedExclusiveCpuTime(); double per = val * 100.0 / mTotalCpuTime; return String.format("%.1f%%", per); } if (col == COL_INCLUSIVE_CPU_TIME) { double val = md.getElapsedInclusiveCpuTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_INCLUSIVE_CPU_TIME_PER) { double val = md.getElapsedInclusiveCpuTime(); double per = val * 100.0 / mTotalCpuTime; return String.format("%.1f%%", per); } if (col == COL_EXCLUSIVE_REAL_TIME) { double val = md.getElapsedExclusiveRealTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_EXCLUSIVE_REAL_TIME_PER) { double val = md.getElapsedExclusiveRealTime(); double per = val * 100.0 / mTotalRealTime; return String.format("%.1f%%", per); } if (col == COL_INCLUSIVE_REAL_TIME) { double val = md.getElapsedInclusiveRealTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_INCLUSIVE_REAL_TIME_PER) { double val = md.getElapsedInclusiveRealTime(); double per = val * 100.0 / mTotalRealTime; return String.format("%.1f%%", per); } if (col == COL_CALLS) return md.getCalls(); if (col == COL_CPU_TIME_PER_CALL) { int numCalls = md.getTotalCalls(); double val = md.getElapsedInclusiveCpuTime(); val = val / numCalls; val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_REAL_TIME_PER_CALL) { int numCalls = md.getTotalCalls(); double val = md.getElapsedInclusiveRealTime(); val = val / numCalls; val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } } else if (element instanceof ProfileSelf) { ProfileSelf ps = (ProfileSelf) element; if (col == COL_NAME) return ps.getProfileName(); if (col == COL_INCLUSIVE_CPU_TIME) { double val = ps.getElapsedInclusiveCpuTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_INCLUSIVE_CPU_TIME_PER) { double total; double val = ps.getElapsedInclusiveCpuTime(); MethodData context = ps.getContext(); total = context.getElapsedInclusiveCpuTime(); double per = val * 100.0 / total; return String.format("%.1f%%", per); } if (col == COL_INCLUSIVE_REAL_TIME) { double val = ps.getElapsedInclusiveRealTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_INCLUSIVE_REAL_TIME_PER) { double total; double val = ps.getElapsedInclusiveRealTime(); MethodData context = ps.getContext(); total = context.getElapsedInclusiveRealTime(); double per = val * 100.0 / total; return String.format("%.1f%%", per); } return ""; } else if (element instanceof ProfileData) { ProfileData pd = (ProfileData) element; if (col == COL_NAME) return pd.getProfileName(); if (col == COL_INCLUSIVE_CPU_TIME) { double val = pd.getElapsedInclusiveCpuTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_INCLUSIVE_CPU_TIME_PER) { double total; double val = pd.getElapsedInclusiveCpuTime(); MethodData context = pd.getContext(); total = context.getElapsedInclusiveCpuTime(); double per = val * 100.0 / total; return String.format("%.1f%%", per); } if (col == COL_INCLUSIVE_REAL_TIME) { double val = pd.getElapsedInclusiveRealTime(); val = traceUnits.getScaledValue(val); return String.format("%.3f", val); } if (col == COL_INCLUSIVE_REAL_TIME_PER) { double total; double val = pd.getElapsedInclusiveRealTime(); MethodData context = pd.getContext(); total = context.getElapsedInclusiveRealTime(); double per = val * 100.0 / total; return String.format("%.1f%%", per); } if (col == COL_CALLS) return pd.getNumCalls(); return ""; } else if (element instanceof ProfileNode) { ProfileNode pn = (ProfileNode) element; if (col == COL_NAME) return pn.getLabel(); return ""; } return "col" + col; } @Override public Image getColumnImage(Object element, int col) { if (col != COL_NAME) return null; if (element instanceof MethodData) { MethodData md = (MethodData) element; return md.getImage(); } if (element instanceof ProfileData) { ProfileData pd = (ProfileData) element; MethodData md = pd.getMethodData(); return md.getImage(); } return null; } @Override public Color getForeground(Object element) { return null; } @Override public Color getBackground(Object element) { if (element instanceof ProfileData) { ProfileData pd = (ProfileData) element; if (pd.isParent()) return colorParentsBack; return colorChildrenBack; } if (element instanceof ProfileNode) { ProfileNode pn = (ProfileNode) element; if (pn.isParent()) return colorParentsBack; return colorChildrenBack; } return null; } } class ColumnListener extends SelectionAdapter { MethodData.Sorter sorter = new MethodData.Sorter(); @Override public void widgetSelected(SelectionEvent event) { TreeColumn column = (TreeColumn) event.widget; String name = column.getText(); Tree tree = column.getParent(); tree.setRedraw(false); TreeColumn[] columns = tree.getColumns(); for (TreeColumn col : columns) { col.setImage(null); } if (name == mColumnNames[COL_NAME]) { // Sort names alphabetically sorter.setColumn(MethodData.Sorter.Column.BY_NAME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME]) { sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME_PER]) { sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME]) { sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME_PER]) { sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME]) { sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME_PER]) { sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME]) { sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME_PER]) { sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_CALLS]) { sorter.setColumn(MethodData.Sorter.Column.BY_CALLS); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_CPU_TIME_PER_CALL]) { sorter.setColumn(MethodData.Sorter.Column.BY_CPU_TIME_PER_CALL); Arrays.sort(mRoots, sorter); } else if (name == mColumnNames[COL_REAL_TIME_PER_CALL]) { sorter.setColumn(MethodData.Sorter.Column.BY_REAL_TIME_PER_CALL); Arrays.sort(mRoots, sorter); } MethodData.Sorter.Direction direction = sorter.getDirection(); if (direction == MethodData.Sorter.Direction.INCREASING) column.setImage(mSortDown); else column.setImage(mSortUp); tree.setRedraw(true); mTreeViewer.refresh(); } } } traceview/src/main/java/com/android/traceview/ProfileSelf.java0100644 0000000 0000000 00000002161 12747325007 023505 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; public class ProfileSelf extends ProfileData { public ProfileSelf(MethodData methodData) { mElement = methodData; mContext = methodData; } @Override public String getProfileName() { return "self"; } @Override public long getElapsedInclusiveCpuTime() { return mElement.getTopExclusiveCpuTime(); } @Override public long getElapsedInclusiveRealTime() { return mElement.getTopExclusiveRealTime(); } } traceview/src/main/java/com/android/traceview/ProfileView.java0100644 0000000 0000000 00000031436 12747325007 023535 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeViewerListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeExpansionEvent; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import java.util.ArrayList; import java.util.Observable; import java.util.Observer; public class ProfileView extends Composite implements Observer { private TreeViewer mTreeViewer; private Text mSearchBox; private SelectionController mSelectionController; private ProfileProvider mProfileProvider; private Color mColorNoMatch; private Color mColorMatch; private MethodData mCurrentHighlightedMethod; private MethodHandler mMethodHandler; public interface MethodHandler { void handleMethod(MethodData method); } public ProfileView(Composite parent, TraceReader reader, SelectionController selectController) { super(parent, SWT.NONE); setLayout(new GridLayout(1, false)); this.mSelectionController = selectController; mSelectionController.addObserver(this); // Add a tree viewer at the top mTreeViewer = new TreeViewer(this, SWT.MULTI | SWT.NONE); mTreeViewer.setUseHashlookup(true); mProfileProvider = reader.getProfileProvider(); mProfileProvider.setTreeViewer(mTreeViewer); SelectionAdapter listener = mProfileProvider.getColumnListener(); final Tree tree = mTreeViewer.getTree(); tree.setHeaderVisible(true); tree.setLayoutData(new GridData(GridData.FILL_BOTH)); // Get the column names from the ProfileProvider String[] columnNames = mProfileProvider.getColumnNames(); int[] columnWidths = mProfileProvider.getColumnWidths(); int[] columnAlignments = mProfileProvider.getColumnAlignments(); for (int ii = 0; ii < columnWidths.length; ++ii) { TreeColumn column = new TreeColumn(tree, SWT.LEFT); column.setText(columnNames[ii]); column.setWidth(columnWidths[ii]); column.setMoveable(true); column.addSelectionListener(listener); column.setAlignment(columnAlignments[ii]); } // Add a listener to the tree so that we can make the row // height smaller. tree.addListener(SWT.MeasureItem, new Listener() { @Override public void handleEvent(Event event) { int fontHeight = event.gc.getFontMetrics().getHeight(); event.height = fontHeight; } }); mTreeViewer.setContentProvider(mProfileProvider); mTreeViewer.setLabelProvider(mProfileProvider.getLabelProvider()); mTreeViewer.setInput(mProfileProvider.getRoot()); // Create another composite to hold the label and text box Composite composite = new Composite(this, SWT.NONE); composite.setLayout(new GridLayout(2, false)); composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // Add a label for the search box Label label = new Label(composite, SWT.NONE); label.setText("Find:"); // Add a text box for searching for method names mSearchBox = new Text(composite, SWT.BORDER); mSearchBox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Display display = getDisplay(); mColorNoMatch = new Color(display, 255, 200, 200); mColorMatch = mSearchBox.getBackground(); mSearchBox.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent ev) { String query = mSearchBox.getText(); if (query.length() == 0) return; findName(query); } }); // Add a key listener to the text box so that we can clear // the text box if the user presses . mSearchBox.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { if (event.keyCode == SWT.ESC) { mSearchBox.setText(""); } else if (event.keyCode == SWT.CR) { String query = mSearchBox.getText(); if (query.length() == 0) return; findNextName(query); } } }); // Also add a key listener to the tree viewer so that the // user can just start typing anywhere in the tree view. tree.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent event) { if (event.keyCode == SWT.ESC) { mSearchBox.setText(""); } else if (event.keyCode == SWT.BS) { // Erase the last character from the search box String text = mSearchBox.getText(); int len = text.length(); String chopped; if (len <= 1) chopped = ""; else chopped = text.substring(0, len - 1); mSearchBox.setText(chopped); } else if (event.keyCode == SWT.CR) { String query = mSearchBox.getText(); if (query.length() == 0) return; findNextName(query); } else { // Append the given character to the search box String str = String.valueOf(event.character); mSearchBox.append(str); } event.doit = false; } }); // Add a selection listener to the tree so that the user can click // on a method that is a child or parent and jump to that method. mTreeViewer .addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent ev) { ISelection sel = ev.getSelection(); if (sel.isEmpty()) return; if (sel instanceof IStructuredSelection) { IStructuredSelection selection = (IStructuredSelection) sel; Object element = selection.getFirstElement(); if (element == null) return; if (element instanceof MethodData) { MethodData md = (MethodData) element; highlightMethod(md, true); } if (element instanceof ProfileData) { MethodData md = ((ProfileData) element) .getMethodData(); highlightMethod(md, true); } } } }); // Add a tree listener so that we can expand the parents and children // of a method when a method is expanded. mTreeViewer.addTreeListener(new ITreeViewerListener() { @Override public void treeExpanded(TreeExpansionEvent event) { Object element = event.getElement(); if (element instanceof MethodData) { MethodData md = (MethodData) element; expandNode(md); } } @Override public void treeCollapsed(TreeExpansionEvent event) { } }); tree.addListener(SWT.MouseDown, new Listener() { @Override public void handleEvent(Event event) { Point point = new Point(event.x, event.y); TreeItem treeItem = tree.getItem(point); MethodData md = mProfileProvider.findMatchingTreeItem(treeItem); if (md == null) return; ArrayList selections = new ArrayList(); selections.add(Selection.highlight("MethodData", md)); mSelectionController.change(selections, "ProfileView"); if (mMethodHandler != null && (event.stateMask & SWT.MOD1) != 0) { mMethodHandler.handleMethod(md); } } }); } public void setMethodHandler(MethodHandler handler) { mMethodHandler = handler; } private void findName(String query) { MethodData md = mProfileProvider.findMatchingName(query); selectMethod(md); } private void findNextName(String query) { MethodData md = mProfileProvider.findNextMatchingName(query); selectMethod(md); } private void selectMethod(MethodData md) { if (md == null) { mSearchBox.setBackground(mColorNoMatch); return; } mSearchBox.setBackground(mColorMatch); highlightMethod(md, false); } @Override public void update(Observable objservable, Object arg) { // Ignore updates from myself if (arg == "ProfileView") return; // System.out.printf("profileview update from %s\n", arg); ArrayList selections; selections = mSelectionController.getSelections(); for (Selection selection : selections) { Selection.Action action = selection.getAction(); if (action != Selection.Action.Highlight) continue; String name = selection.getName(); if (name == "MethodData") { MethodData md = (MethodData) selection.getValue(); highlightMethod(md, true); return; } if (name == "Call") { Call call = (Call) selection.getValue(); MethodData md = call.getMethodData(); highlightMethod(md, true); return; } } } private void highlightMethod(MethodData md, boolean clearSearch) { if (md == null) return; // Avoid an infinite recursion if (md == mCurrentHighlightedMethod) return; if (clearSearch) { mSearchBox.setText(""); mSearchBox.setBackground(mColorMatch); } mCurrentHighlightedMethod = md; mTreeViewer.collapseAll(); // Expand this node and its children expandNode(md); StructuredSelection sel = new StructuredSelection(md); mTreeViewer.setSelection(sel, true); Tree tree = mTreeViewer.getTree(); TreeItem[] items = tree.getSelection(); if (items.length != 0) { tree.setTopItem(items[0]); // workaround a Mac bug by adding showItem(). tree.showItem(items[0]); } } private void expandNode(MethodData md) { ProfileNode[] nodes = md.getProfileNodes(); mTreeViewer.setExpandedState(md, true); // Also expand the "Parents" and "Children" nodes. if (nodes != null) { for (ProfileNode node : nodes) { if (node.isRecursive() == false) mTreeViewer.setExpandedState(node, true); } } } } traceview/src/main/java/com/android/traceview/PropertiesDialog.java0100644 0000000 0000000 00000007470 12747325007 024557 0ustar000000000 0000000 /* * 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.traceview; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import java.util.HashMap; import java.util.Map.Entry; public class PropertiesDialog extends Dialog { private HashMap mProperties; public PropertiesDialog(Shell parent) { super(parent); setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE); } public void setProperties(HashMap properties) { mProperties = properties; } @Override protected void createButtonsForButtonBar(Composite parent) { createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); } @Override protected Control createDialogArea(Composite parent) { Composite container = (Composite) super.createDialogArea(parent); GridLayout gridLayout = new GridLayout(1, false); gridLayout.marginWidth = 0; gridLayout.marginHeight = 0; gridLayout.horizontalSpacing = 0; gridLayout.verticalSpacing = 0; container.setLayout(gridLayout); TableViewer tableViewer = new TableViewer(container, SWT.HIDE_SELECTION | SWT.V_SCROLL | SWT.BORDER); tableViewer.getTable().setLinesVisible(true); tableViewer.getTable().setHeaderVisible(true); TableViewerColumn propertyColumn = new TableViewerColumn(tableViewer, SWT.NONE); propertyColumn.getColumn().setText("Property"); propertyColumn.setLabelProvider(new ColumnLabelProvider() { @Override @SuppressWarnings("unchecked") public String getText(Object element) { Entry entry = (Entry) element; return entry.getKey(); } }); propertyColumn.getColumn().setWidth(400); TableViewerColumn valueColumn = new TableViewerColumn(tableViewer, SWT.NONE); valueColumn.getColumn().setText("Value"); valueColumn.setLabelProvider(new ColumnLabelProvider() { @Override @SuppressWarnings("unchecked") public String getText(Object element) { Entry entry = (Entry) element; return entry.getValue(); } }); valueColumn.getColumn().setWidth(200); tableViewer.setContentProvider(new ArrayContentProvider()); tableViewer.setInput(mProperties.entrySet().toArray()); GridData gridData = new GridData(); gridData.verticalAlignment = GridData.FILL; gridData.horizontalAlignment = GridData.FILL; gridData.grabExcessHorizontalSpace = true; gridData.grabExcessVerticalSpace = true; tableViewer.getControl().setLayoutData(gridData); return container; } } traceview/src/main/java/com/android/traceview/Selection.java0100644 0000000 0000000 00000003375 12747325007 023230 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; public class Selection { private Action mAction; private String mName; private Object mValue; public Selection(Action action, String name, Object value) { mAction = action; mName = name; mValue = value; } public static Selection highlight(String name, Object value) { return new Selection(Action.Highlight, name, value); } public static Selection include(String name, Object value) { return new Selection(Action.Include, name, value); } public static Selection exclude(String name, Object value) { return new Selection(Action.Exclude, name, value); } public void setName(String name) { mName = name; } public String getName() { return mName; } public void setValue(Object value) { mValue = value; } public Object getValue() { return mValue; } public void setAction(Action action) { mAction = action; } public Action getAction() { return mAction; } public static enum Action { Highlight, Include, Exclude, Aggregate }; } traceview/src/main/java/com/android/traceview/SelectionController.java0100644 0000000 0000000 00000002053 12747325007 025264 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import java.util.ArrayList; import java.util.Observable; public class SelectionController extends Observable { private ArrayList mSelections; public void change(ArrayList selections, Object arg) { this.mSelections = selections; setChanged(); notifyObservers(arg); } public ArrayList getSelections() { return mSelections; } } traceview/src/main/java/com/android/traceview/ThreadData.java0100644 0000000 0000000 00000011026 12747325007 023274 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import java.util.ArrayList; import java.util.HashMap; class ThreadData implements TimeLineView.Row { private int mId; private String mName; private boolean mIsEmpty; private Call mRootCall; private ArrayList mStack = new ArrayList(); // This is a hash of all the methods that are currently on the stack. private HashMap mStackMethods = new HashMap(); boolean mHaveGlobalTime; long mGlobalStartTime; long mGlobalEndTime; boolean mHaveThreadTime; long mThreadStartTime; long mThreadEndTime; long mThreadCurrentTime; // only used while parsing thread-cpu clock ThreadData(int id, String name, MethodData topLevel) { mId = id; mName = String.format("[%d] %s", id, name); mIsEmpty = true; mRootCall = new Call(this, topLevel, null); mRootCall.setName(mName); mStack.add(mRootCall); } @Override public String getName() { return mName; } public Call getRootCall() { return mRootCall; } /** * Returns true if no calls have ever been recorded for this thread. */ public boolean isEmpty() { return mIsEmpty; } Call enter(MethodData method, ArrayList trace) { if (mIsEmpty) { mIsEmpty = false; if (trace != null) { trace.add(new TraceAction(TraceAction.ACTION_ENTER, mRootCall)); } } Call caller = top(); Call call = new Call(this, method, caller); mStack.add(call); if (trace != null) { trace.add(new TraceAction(TraceAction.ACTION_ENTER, call)); } Integer num = mStackMethods.get(method); if (num == null) { num = 0; } else if (num > 0) { call.setRecursive(true); } mStackMethods.put(method, num + 1); return call; } Call exit(MethodData method, ArrayList trace) { Call call = top(); if (call.mCaller == null) { return null; } if (call.getMethodData() != method) { String error = "Method exit (" + method.getName() + ") does not match current method (" + call.getMethodData().getName() + ")"; throw new RuntimeException(error); } mStack.remove(mStack.size() - 1); if (trace != null) { trace.add(new TraceAction(TraceAction.ACTION_EXIT, call)); } Integer num = mStackMethods.get(method); if (num != null) { if (num == 1) { mStackMethods.remove(method); } else { mStackMethods.put(method, num - 1); } } return call; } Call top() { return mStack.get(mStack.size() - 1); } void endTrace(ArrayList trace) { for (int i = mStack.size() - 1; i >= 1; i--) { Call call = mStack.get(i); call.mGlobalEndTime = mGlobalEndTime; call.mThreadEndTime = mThreadEndTime; if (trace != null) { trace.add(new TraceAction(TraceAction.ACTION_INCOMPLETE, call)); } } mStack.clear(); mStackMethods.clear(); } void updateRootCallTimeBounds() { if (!mIsEmpty) { mRootCall.mGlobalStartTime = mGlobalStartTime; mRootCall.mGlobalEndTime = mGlobalEndTime; mRootCall.mThreadStartTime = mThreadStartTime; mRootCall.mThreadEndTime = mThreadEndTime; } } @Override public String toString() { return mName; } @Override public int getId() { return mId; } public long getCpuTime() { return mRootCall.mInclusiveCpuTime; } public long getRealTime() { return mRootCall.mInclusiveRealTime; } } traceview/src/main/java/com/android/traceview/TickScaler.java0100644 0000000 0000000 00000010606 12747325007 023322 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; class TickScaler { private double mMinVal; // required input private double mMaxVal; // required input private double mRangeVal; private int mNumPixels; // required input private int mPixelsPerTick; // required input private double mPixelsPerRange; private double mTickIncrement; private double mMinMajorTick; TickScaler(double minVal, double maxVal, int numPixels, int pixelsPerTick) { mMinVal = minVal; mMaxVal = maxVal; mNumPixels = numPixels; mPixelsPerTick = pixelsPerTick; } public void setMinVal(double minVal) { mMinVal = minVal; } public double getMinVal() { return mMinVal; } public void setMaxVal(double maxVal) { mMaxVal = maxVal; } public double getMaxVal() { return mMaxVal; } public void setNumPixels(int numPixels) { mNumPixels = numPixels; } public int getNumPixels() { return mNumPixels; } public void setPixelsPerTick(int pixelsPerTick) { mPixelsPerTick = pixelsPerTick; } public int getPixelsPerTick() { return mPixelsPerTick; } public void setPixelsPerRange(double pixelsPerRange) { mPixelsPerRange = pixelsPerRange; } public double getPixelsPerRange() { return mPixelsPerRange; } public void setTickIncrement(double tickIncrement) { mTickIncrement = tickIncrement; } public double getTickIncrement() { return mTickIncrement; } public void setMinMajorTick(double minMajorTick) { mMinMajorTick = minMajorTick; } public double getMinMajorTick() { return mMinMajorTick; } // Convert a time value to a 0-based pixel value public int valueToPixel(double value) { return (int) Math.ceil(mPixelsPerRange * (value - mMinVal) - 0.5); } // Convert a time value to a 0-based fractional pixel public double valueToPixelFraction(double value) { return mPixelsPerRange * (value - mMinVal); } // Convert a 0-based pixel value to a time value public double pixelToValue(int pixel) { return mMinVal + (pixel / mPixelsPerRange); } public void computeTicks(boolean useGivenEndPoints) { int numTicks = mNumPixels / mPixelsPerTick; mRangeVal = mMaxVal - mMinVal; mTickIncrement = mRangeVal / numTicks; double dlogTickIncrement = Math.log10(mTickIncrement); int logTickIncrement = (int) Math.floor(dlogTickIncrement); double scale = Math.pow(10, logTickIncrement); double scaledTickIncr = mTickIncrement / scale; if (scaledTickIncr > 5.0) scaledTickIncr = 10; else if (scaledTickIncr > 2) scaledTickIncr = 5; else if (scaledTickIncr > 1) scaledTickIncr = 2; else scaledTickIncr = 1; mTickIncrement = scaledTickIncr * scale; if (!useGivenEndPoints) { // Round up the max val to the next minor tick double minorTickIncrement = mTickIncrement / 5; double dval = mMaxVal / minorTickIncrement; int ival = (int) dval; if (ival != dval) mMaxVal = (ival + 1) * minorTickIncrement; // Round down the min val to a multiple of tickIncrement ival = (int) (mMinVal / mTickIncrement); mMinVal = ival * mTickIncrement; mMinMajorTick = mMinVal; } else { int ival = (int) (mMinVal / mTickIncrement); mMinMajorTick = ival * mTickIncrement; if (mMinMajorTick < mMinVal) mMinMajorTick = mMinMajorTick + mTickIncrement; } mRangeVal = mMaxVal - mMinVal; mPixelsPerRange = (double) mNumPixels / mRangeVal; } } traceview/src/main/java/com/android/traceview/TimeBase.java0100644 0000000 0000000 00000004536 12747325007 022774 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; interface TimeBase { public static final TimeBase CPU_TIME = new CpuTimeBase(); public static final TimeBase REAL_TIME = new RealTimeBase(); public long getTime(ThreadData threadData); public long getElapsedInclusiveTime(MethodData methodData); public long getElapsedExclusiveTime(MethodData methodData); public long getElapsedInclusiveTime(ProfileData profileData); public static final class CpuTimeBase implements TimeBase { @Override public long getTime(ThreadData threadData) { return threadData.getCpuTime(); } @Override public long getElapsedInclusiveTime(MethodData methodData) { return methodData.getElapsedInclusiveCpuTime(); } @Override public long getElapsedExclusiveTime(MethodData methodData) { return methodData.getElapsedExclusiveCpuTime(); } @Override public long getElapsedInclusiveTime(ProfileData profileData) { return profileData.getElapsedInclusiveCpuTime(); } } public static final class RealTimeBase implements TimeBase { @Override public long getTime(ThreadData threadData) { return threadData.getRealTime(); } @Override public long getElapsedInclusiveTime(MethodData methodData) { return methodData.getElapsedInclusiveRealTime(); } @Override public long getElapsedExclusiveTime(MethodData methodData) { return methodData.getElapsedExclusiveRealTime(); } @Override public long getElapsedInclusiveTime(ProfileData profileData) { return profileData.getElapsedInclusiveRealTime(); } } } traceview/src/main/java/com/android/traceview/TimeLineView.java0100644 0000000 0000000 00000246270 12747325007 023647 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import org.eclipse.jface.resource.FontRegistry; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.MouseWheelListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ScrollBar; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Observable; import java.util.Observer; public class TimeLineView extends Composite implements Observer { private HashMap mRowByName; private RowData[] mRows; private Segment[] mSegments; private HashMap mThreadLabels; private Timescale mTimescale; private Surface mSurface; private RowLabels mLabels; private SashForm mSashForm; private int mScrollOffsetY; public static final int PixelsPerTick = 50; private TickScaler mScaleInfo = new TickScaler(0, 0, 0, PixelsPerTick); private static final int LeftMargin = 10; // blank space on left private static final int RightMargin = 60; // blank space on right private Color mColorBlack; private Color mColorGray; private Color mColorDarkGray; private Color mColorForeground; private Color mColorRowBack; private Color mColorZoomSelection; private FontRegistry mFontRegistry; /** vertical height of drawn blocks in each row */ private static final int rowHeight = 20; /** the blank space between rows */ private static final int rowYMargin = 12; private static final int rowYMarginHalf = rowYMargin / 2; /** total vertical space for row */ private static final int rowYSpace = rowHeight + rowYMargin; private static final int majorTickLength = 8; private static final int minorTickLength = 4; private static final int timeLineOffsetY = 58; private static final int tickToFontSpacing = 2; /** start of first row */ private static final int topMargin = 90; private int mMouseRow = -1; private int mNumRows; private int mStartRow; private int mEndRow; private TraceUnits mUnits; private String mClockSource; private boolean mHaveCpuTime; private boolean mHaveRealTime; private int mSmallFontWidth; private int mSmallFontHeight; private SelectionController mSelectionController; private MethodData mHighlightMethodData; private Call mHighlightCall; private static final int MinInclusiveRange = 3; /** Setting the fonts looks good on Linux but bad on Macs */ private boolean mSetFonts = false; public static interface Block { public String getName(); public MethodData getMethodData(); public long getStartTime(); public long getEndTime(); public Color getColor(); public double addWeight(int x, int y, double weight); public void clearWeight(); public long getExclusiveCpuTime(); public long getInclusiveCpuTime(); public long getExclusiveRealTime(); public long getInclusiveRealTime(); public boolean isContextSwitch(); public boolean isIgnoredBlock(); public Block getParentBlock(); } public static interface Row { public int getId(); public String getName(); } public static class Record { Row row; Block block; public Record(Row row, Block block) { this.row = row; this.block = block; } } public TimeLineView(Composite parent, TraceReader reader, SelectionController selectionController) { super(parent, SWT.NONE); mRowByName = new HashMap(); this.mSelectionController = selectionController; selectionController.addObserver(this); mUnits = reader.getTraceUnits(); mClockSource = reader.getClockSource(); mHaveCpuTime = reader.haveCpuTime(); mHaveRealTime = reader.haveRealTime(); mThreadLabels = reader.getThreadLabels(); Display display = getDisplay(); mColorGray = display.getSystemColor(SWT.COLOR_GRAY); mColorDarkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); mColorBlack = display.getSystemColor(SWT.COLOR_BLACK); // mColorBackground = display.getSystemColor(SWT.COLOR_WHITE); mColorForeground = display.getSystemColor(SWT.COLOR_BLACK); mColorRowBack = new Color(display, 240, 240, 255); mColorZoomSelection = new Color(display, 230, 230, 230); mFontRegistry = new FontRegistry(display); mFontRegistry.put("small", //$NON-NLS-1$ new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$ mFontRegistry.put("courier8", //$NON-NLS-1$ new FontData[] { new FontData("Courier New", 8, SWT.BOLD) }); //$NON-NLS-1$ mFontRegistry.put("medium", //$NON-NLS-1$ new FontData[] { new FontData("Courier New", 10, SWT.NORMAL) }); //$NON-NLS-1$ Image image = new Image(display, new Rectangle(100, 100, 100, 100)); GC gc = new GC(image); if (mSetFonts) { gc.setFont(mFontRegistry.get("small")); //$NON-NLS-1$ } mSmallFontWidth = gc.getFontMetrics().getAverageCharWidth(); mSmallFontHeight = gc.getFontMetrics().getHeight(); image.dispose(); gc.dispose(); setLayout(new FillLayout()); // Create a sash form for holding two canvas views, one for the // thread labels and one for the thread timeline. mSashForm = new SashForm(this, SWT.HORIZONTAL); mSashForm.setBackground(mColorGray); mSashForm.SASH_WIDTH = 3; // Create a composite for the left side of the sash Composite composite = new Composite(mSashForm, SWT.NONE); GridLayout layout = new GridLayout(1, true /* make columns equal width */); layout.marginHeight = 0; layout.marginWidth = 0; layout.verticalSpacing = 1; composite.setLayout(layout); // Create a blank corner space in the upper left corner BlankCorner corner = new BlankCorner(composite); GridData gridData = new GridData(GridData.FILL_HORIZONTAL); gridData.heightHint = topMargin; corner.setLayoutData(gridData); // Add the thread labels below the blank corner. mLabels = new RowLabels(composite); gridData = new GridData(GridData.FILL_BOTH); mLabels.setLayoutData(gridData); // Create another composite for the right side of the sash composite = new Composite(mSashForm, SWT.NONE); layout = new GridLayout(1, true /* make columns equal width */); layout.marginHeight = 0; layout.marginWidth = 0; layout.verticalSpacing = 1; composite.setLayout(layout); mTimescale = new Timescale(composite); gridData = new GridData(GridData.FILL_HORIZONTAL); gridData.heightHint = topMargin; mTimescale.setLayoutData(gridData); mSurface = new Surface(composite); gridData = new GridData(GridData.FILL_BOTH); mSurface.setLayoutData(gridData); mSashForm.setWeights(new int[] { 1, 5 }); final ScrollBar vBar = mSurface.getVerticalBar(); vBar.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { mScrollOffsetY = vBar.getSelection(); Point dim = mSurface.getSize(); int newScrollOffsetY = computeVisibleRows(dim.y); if (newScrollOffsetY != mScrollOffsetY) { mScrollOffsetY = newScrollOffsetY; vBar.setSelection(newScrollOffsetY); } mLabels.redraw(); mSurface.redraw(); } }); final ScrollBar hBar = mSurface.getHorizontalBar(); hBar.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { mSurface.setScaleFromHorizontalScrollBar(hBar.getSelection()); mSurface.redraw(); } }); mSurface.addListener(SWT.Resize, new Listener() { @Override public void handleEvent(Event e) { Point dim = mSurface.getSize(); // If we don't need the scroll bar then don't display it. if (dim.y >= mNumRows * rowYSpace) { vBar.setVisible(false); } else { vBar.setVisible(true); } int newScrollOffsetY = computeVisibleRows(dim.y); if (newScrollOffsetY != mScrollOffsetY) { mScrollOffsetY = newScrollOffsetY; vBar.setSelection(newScrollOffsetY); } int spaceNeeded = mNumRows * rowYSpace; vBar.setMaximum(spaceNeeded); vBar.setThumb(dim.y); mLabels.redraw(); mSurface.redraw(); } }); mSurface.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent me) { mSurface.mouseUp(me); } @Override public void mouseDown(MouseEvent me) { mSurface.mouseDown(me); } @Override public void mouseDoubleClick(MouseEvent me) { mSurface.mouseDoubleClick(me); } }); mSurface.addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent me) { mSurface.mouseMove(me); } }); mSurface.addMouseWheelListener(new MouseWheelListener() { @Override public void mouseScrolled(MouseEvent me) { mSurface.mouseScrolled(me); } }); mTimescale.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent me) { mTimescale.mouseUp(me); } @Override public void mouseDown(MouseEvent me) { mTimescale.mouseDown(me); } @Override public void mouseDoubleClick(MouseEvent me) { mTimescale.mouseDoubleClick(me); } }); mTimescale.addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent me) { mTimescale.mouseMove(me); } }); mLabels.addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent me) { mLabels.mouseMove(me); } }); setData(reader.getThreadTimeRecords()); } @Override public void update(Observable objservable, Object arg) { // Ignore updates from myself if (arg == "TimeLineView") //$NON-NLS-1$ return; // System.out.printf("timeline update from %s\n", arg); boolean foundHighlight = false; ArrayList selections; selections = mSelectionController.getSelections(); for (Selection selection : selections) { Selection.Action action = selection.getAction(); if (action != Selection.Action.Highlight) continue; String name = selection.getName(); // System.out.printf(" timeline highlight %s from %s\n", name, arg); if (name == "MethodData") { //$NON-NLS-1$ foundHighlight = true; mHighlightMethodData = (MethodData) selection.getValue(); // System.out.printf(" method %s\n", // highlightMethodData.getName()); mHighlightCall = null; startHighlighting(); } else if (name == "Call") { //$NON-NLS-1$ foundHighlight = true; mHighlightCall = (Call) selection.getValue(); // System.out.printf(" call %s\n", highlightCall.getName()); mHighlightMethodData = null; startHighlighting(); } } if (foundHighlight == false) mSurface.clearHighlights(); } public void setData(ArrayList records) { if (records == null) records = new ArrayList(); if (false) { System.out.println("TimelineView() list of records:"); //$NON-NLS-1$ for (Record r : records) { System.out.printf("row '%s' block '%s' [%d, %d]\n", r.row //$NON-NLS-1$ .getName(), r.block.getName(), r.block.getStartTime(), r.block.getEndTime()); if (r.block.getStartTime() > r.block.getEndTime()) { System.err.printf("Error: block startTime > endTime\n"); //$NON-NLS-1$ System.exit(1); } } } // Sort the records into increasing start time, and decreasing end time Collections.sort(records, new Comparator() { @Override public int compare(Record r1, Record r2) { long start1 = r1.block.getStartTime(); long start2 = r2.block.getStartTime(); if (start1 > start2) return 1; if (start1 < start2) return -1; // The start times are the same, so compare the end times long end1 = r1.block.getEndTime(); long end2 = r2.block.getEndTime(); if (end1 > end2) return -1; if (end1 < end2) return 1; return 0; } }); ArrayList segmentList = new ArrayList(); // The records are sorted into increasing start time, // so the minimum start time is the start time of the first record. double minVal = 0; if (records.size() > 0) minVal = records.get(0).block.getStartTime(); // Sum the time spent in each row and block, and // keep track of the maximum end time. double maxVal = 0; for (Record rec : records) { Row row = rec.row; Block block = rec.block; if (block.isIgnoredBlock()) { continue; } String rowName = row.getName(); RowData rd = mRowByName.get(rowName); if (rd == null) { rd = new RowData(row); mRowByName.put(rowName, rd); } long blockStartTime = block.getStartTime(); long blockEndTime = block.getEndTime(); if (blockEndTime > rd.mEndTime) { long start = Math.max(blockStartTime, rd.mEndTime); rd.mElapsed += blockEndTime - start; rd.mEndTime = blockEndTime; } if (blockEndTime > maxVal) maxVal = blockEndTime; // Keep track of nested blocks by using a stack (for each row). // Create a Segment object for each visible part of a block. Block top = rd.top(); if (top == null) { rd.push(block); continue; } long topStartTime = top.getStartTime(); long topEndTime = top.getEndTime(); if (topEndTime >= blockStartTime) { // Add this segment if it has a non-zero elapsed time. if (topStartTime < blockStartTime) { Segment segment = new Segment(rd, top, topStartTime, blockStartTime); segmentList.add(segment); } // If this block starts where the previous (top) block ends, // then pop off the top block. if (topEndTime == blockStartTime) rd.pop(); rd.push(block); } else { // We may have to pop several frames here. popFrames(rd, top, blockStartTime, segmentList); rd.push(block); } } // Clean up the stack of each row for (RowData rd : mRowByName.values()) { Block top = rd.top(); popFrames(rd, top, Integer.MAX_VALUE, segmentList); } mSurface.setRange(minVal, maxVal); mSurface.setLimitRange(minVal, maxVal); // Sort the rows into decreasing elapsed time Collection rv = mRowByName.values(); mRows = rv.toArray(new RowData[rv.size()]); Arrays.sort(mRows, new Comparator() { @Override public int compare(RowData rd1, RowData rd2) { return (int) (rd2.mElapsed - rd1.mElapsed); } }); // Assign ranks to the sorted rows for (int ii = 0; ii < mRows.length; ++ii) { mRows[ii].mRank = ii; } // Compute the number of rows with data mNumRows = 0; for (int ii = 0; ii < mRows.length; ++ii) { if (mRows[ii].mElapsed == 0) break; mNumRows += 1; } // Sort the blocks into increasing rows, and within rows into // increasing start values. mSegments = segmentList.toArray(new Segment[segmentList.size()]); Arrays.sort(mSegments, new Comparator() { @Override public int compare(Segment bd1, Segment bd2) { RowData rd1 = bd1.mRowData; RowData rd2 = bd2.mRowData; int diff = rd1.mRank - rd2.mRank; if (diff == 0) { long timeDiff = bd1.mStartTime - bd2.mStartTime; if (timeDiff == 0) timeDiff = bd1.mEndTime - bd2.mEndTime; return (int) timeDiff; } return diff; } }); if (false) { for (Segment segment : mSegments) { System.out.printf("seg '%s' [%6d, %6d] %s\n", segment.mRowData.mName, segment.mStartTime, segment.mEndTime, segment.mBlock.getName()); if (segment.mStartTime > segment.mEndTime) { System.err.printf("Error: segment startTime > endTime\n"); System.exit(1); } } } } private static void popFrames(RowData rd, Block top, long startTime, ArrayList segmentList) { long topEndTime = top.getEndTime(); long lastEndTime = top.getStartTime(); while (topEndTime <= startTime) { if (topEndTime > lastEndTime) { Segment segment = new Segment(rd, top, lastEndTime, topEndTime); segmentList.add(segment); lastEndTime = topEndTime; } rd.pop(); top = rd.top(); if (top == null) return; topEndTime = top.getEndTime(); } // If we get here, then topEndTime > startTime if (lastEndTime < startTime) { Segment bd = new Segment(rd, top, lastEndTime, startTime); segmentList.add(bd); } } private class RowLabels extends Canvas { /** The space between the row label and the sash line */ private static final int labelMarginX = 2; public RowLabels(Composite parent) { super(parent, SWT.NO_BACKGROUND); addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent pe) { draw(pe.display, pe.gc); } }); } private void mouseMove(MouseEvent me) { int rownum = (me.y + mScrollOffsetY) / rowYSpace; if (mMouseRow != rownum) { mMouseRow = rownum; redraw(); mSurface.redraw(); } } private void draw(Display display, GC gc) { if (mSegments.length == 0) { // gc.setBackground(colorBackground); // gc.fillRectangle(getBounds()); return; } Point dim = getSize(); // Create an image for double-buffering Image image = new Image(display, getBounds()); // Set up the off-screen gc GC gcImage = new GC(image); if (mSetFonts) gcImage.setFont(mFontRegistry.get("medium")); //$NON-NLS-1$ if (mNumRows > 2) { // Draw the row background stripes gcImage.setBackground(mColorRowBack); for (int ii = 1; ii < mNumRows; ii += 2) { RowData rd = mRows[ii]; int y1 = rd.mRank * rowYSpace - mScrollOffsetY; gcImage.fillRectangle(0, y1, dim.x, rowYSpace); } } // Draw the row labels int offsetY = rowYMarginHalf - mScrollOffsetY; for (int ii = mStartRow; ii <= mEndRow; ++ii) { RowData rd = mRows[ii]; int y1 = rd.mRank * rowYSpace + offsetY; Point extent = gcImage.stringExtent(rd.mName); int x1 = dim.x - extent.x - labelMarginX; gcImage.drawString(rd.mName, x1, y1, true); } // Draw a highlight box on the row where the mouse is. if (mMouseRow >= mStartRow && mMouseRow <= mEndRow) { gcImage.setForeground(mColorGray); int y1 = mMouseRow * rowYSpace - mScrollOffsetY; gcImage.drawRectangle(0, y1, dim.x, rowYSpace); } // Draw the off-screen buffer to the screen gc.drawImage(image, 0, 0); // Clean up image.dispose(); gcImage.dispose(); } } private class BlankCorner extends Canvas { public BlankCorner(Composite parent) { //super(parent, SWT.NO_BACKGROUND); super(parent, SWT.NONE); addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent pe) { draw(pe.display, pe.gc); } }); } private void draw(Display display, GC gc) { // Create a blank image and draw it to the canvas Image image = new Image(display, getBounds()); gc.drawImage(image, 0, 0); // Clean up image.dispose(); } } private class Timescale extends Canvas { private Point mMouse = new Point(LeftMargin, 0); private Cursor mZoomCursor; private String mMethodName = null; private Color mMethodColor = null; private String mDetails; private int mMethodStartY; private int mDetailsStartY; private int mMarkStartX; private int mMarkEndX; /** The space between the colored block and the method name */ private static final int METHOD_BLOCK_MARGIN = 10; public Timescale(Composite parent) { //super(parent, SWT.NO_BACKGROUND); super(parent, SWT.NONE); Display display = getDisplay(); mZoomCursor = new Cursor(display, SWT.CURSOR_SIZEWE); setCursor(mZoomCursor); mMethodStartY = mSmallFontHeight + 1; mDetailsStartY = mMethodStartY + mSmallFontHeight + 1; addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent pe) { draw(pe.display, pe.gc); } }); } public void setVbarPosition(int x) { mMouse.x = x; } public void setMarkStart(int x) { mMarkStartX = x; } public void setMarkEnd(int x) { mMarkEndX = x; } public void setMethodName(String name) { mMethodName = name; } public void setMethodColor(Color color) { mMethodColor = color; } public void setDetails(String details) { mDetails = details; } private void mouseMove(MouseEvent me) { me.y = -1; mSurface.mouseMove(me); } private void mouseDown(MouseEvent me) { mSurface.startScaling(me.x); mSurface.redraw(); } private void mouseUp(MouseEvent me) { mSurface.stopScaling(me.x); } private void mouseDoubleClick(MouseEvent me) { mSurface.resetScale(); mSurface.redraw(); } private void draw(Display display, GC gc) { Point dim = getSize(); // Create an image for double-buffering Image image = new Image(display, getBounds()); // Set up the off-screen gc GC gcImage = new GC(image); if (mSetFonts) gcImage.setFont(mFontRegistry.get("medium")); //$NON-NLS-1$ if (mSurface.drawingSelection()) { drawSelection(display, gcImage); } drawTicks(display, gcImage); // Draw the vertical bar where the mouse is gcImage.setForeground(mColorDarkGray); gcImage.drawLine(mMouse.x, timeLineOffsetY, mMouse.x, dim.y); // Draw the current millseconds drawTickLegend(display, gcImage); // Draw the method name and color, if needed drawMethod(display, gcImage); // Draw the details, if needed drawDetails(display, gcImage); // Draw the off-screen buffer to the screen gc.drawImage(image, 0, 0); // Clean up image.dispose(); gcImage.dispose(); } private void drawSelection(Display display, GC gc) { Point dim = getSize(); gc.setForeground(mColorGray); gc.drawLine(mMarkStartX, timeLineOffsetY, mMarkStartX, dim.y); gc.setBackground(mColorZoomSelection); int x, width; if (mMarkStartX < mMarkEndX) { x = mMarkStartX; width = mMarkEndX - mMarkStartX; } else { x = mMarkEndX; width = mMarkStartX - mMarkEndX; } if (width > 1) { gc.fillRectangle(x, timeLineOffsetY, width, dim.y); } } private void drawTickLegend(Display display, GC gc) { int mouseX = mMouse.x - LeftMargin; double mouseXval = mScaleInfo.pixelToValue(mouseX); String info = mUnits.labelledString(mouseXval); gc.setForeground(mColorForeground); gc.drawString(info, LeftMargin + 2, 1, true); // Display the maximum data value double maxVal = mScaleInfo.getMaxVal(); info = mUnits.labelledString(maxVal); if (mClockSource != null) { info = String.format(" max %s (%s)", info, mClockSource); //$NON-NLS-1$ } else { info = String.format(" max %s ", info); //$NON-NLS-1$ } Point extent = gc.stringExtent(info); Point dim = getSize(); int x1 = dim.x - RightMargin - extent.x; gc.drawString(info, x1, 1, true); } private void drawMethod(Display display, GC gc) { if (mMethodName == null) { return; } int x1 = LeftMargin; int y1 = mMethodStartY; gc.setBackground(mMethodColor); int width = 2 * mSmallFontWidth; gc.fillRectangle(x1, y1, width, mSmallFontHeight); x1 += width + METHOD_BLOCK_MARGIN; gc.drawString(mMethodName, x1, y1, true); } private void drawDetails(Display display, GC gc) { if (mDetails == null) { return; } int x1 = LeftMargin + 2 * mSmallFontWidth + METHOD_BLOCK_MARGIN; int y1 = mDetailsStartY; gc.drawString(mDetails, x1, y1, true); } private void drawTicks(Display display, GC gc) { Point dim = getSize(); int y2 = majorTickLength + timeLineOffsetY; int y3 = minorTickLength + timeLineOffsetY; int y4 = y2 + tickToFontSpacing; gc.setForeground(mColorForeground); gc.drawLine(LeftMargin, timeLineOffsetY, dim.x - RightMargin, timeLineOffsetY); double minVal = mScaleInfo.getMinVal(); double maxVal = mScaleInfo.getMaxVal(); double minMajorTick = mScaleInfo.getMinMajorTick(); double tickIncrement = mScaleInfo.getTickIncrement(); double minorTickIncrement = tickIncrement / 5; double pixelsPerRange = mScaleInfo.getPixelsPerRange(); // Draw the initial minor ticks, if any if (minVal < minMajorTick) { gc.setForeground(mColorGray); double xMinor = minMajorTick; for (int ii = 1; ii <= 4; ++ii) { xMinor -= minorTickIncrement; if (xMinor < minVal) break; int x1 = LeftMargin + (int) (0.5 + (xMinor - minVal) * pixelsPerRange); gc.drawLine(x1, timeLineOffsetY, x1, y3); } } if (tickIncrement <= 10) { // TODO avoid rendering the loop when tickIncrement is invalid. It can be zero // or too small. // System.out.println(String.format("Timescale.drawTicks error: tickIncrement=%1f", tickIncrement)); return; } for (double x = minMajorTick; x <= maxVal; x += tickIncrement) { int x1 = LeftMargin + (int) (0.5 + (x - minVal) * pixelsPerRange); // Draw a major tick gc.setForeground(mColorForeground); gc.drawLine(x1, timeLineOffsetY, x1, y2); if (x > maxVal) break; // Draw the tick text String tickString = mUnits.valueOf(x); gc.drawString(tickString, x1, y4, true); // Draw 4 minor ticks between major ticks gc.setForeground(mColorGray); double xMinor = x; for (int ii = 1; ii <= 4; ii++) { xMinor += minorTickIncrement; if (xMinor > maxVal) break; x1 = LeftMargin + (int) (0.5 + (xMinor - minVal) * pixelsPerRange); gc.drawLine(x1, timeLineOffsetY, x1, y3); } } } } private static enum GraphicsState { Normal, Marking, Scaling, Animating, Scrolling }; private class Surface extends Canvas { public Surface(Composite parent) { super(parent, SWT.NO_BACKGROUND | SWT.V_SCROLL | SWT.H_SCROLL); Display display = getDisplay(); mNormalCursor = new Cursor(display, SWT.CURSOR_CROSS); mIncreasingCursor = new Cursor(display, SWT.CURSOR_SIZEE); mDecreasingCursor = new Cursor(display, SWT.CURSOR_SIZEW); initZoomFractionsWithExp(); addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent pe) { draw(pe.display, pe.gc); } }); mZoomAnimator = new Runnable() { @Override public void run() { animateZoom(); } }; mHighlightAnimator = new Runnable() { @Override public void run() { animateHighlight(); } }; } private void initZoomFractionsWithExp() { mZoomFractions = new double[ZOOM_STEPS]; int next = 0; for (int ii = 0; ii < ZOOM_STEPS / 2; ++ii, ++next) { mZoomFractions[next] = (double) (1 << ii) / (double) (1 << (ZOOM_STEPS / 2)); // System.out.printf("%d %f\n", next, zoomFractions[next]); } for (int ii = 2; ii < 2 + ZOOM_STEPS / 2; ++ii, ++next) { mZoomFractions[next] = (double) ((1 << ii) - 1) / (double) (1 << ii); // System.out.printf("%d %f\n", next, zoomFractions[next]); } } @SuppressWarnings("unused") private void initZoomFractionsWithSinWave() { mZoomFractions = new double[ZOOM_STEPS]; for (int ii = 0; ii < ZOOM_STEPS; ++ii) { double offset = Math.PI * ii / ZOOM_STEPS; mZoomFractions[ii] = (Math.sin((1.5 * Math.PI + offset)) + 1.0) / 2.0; // System.out.printf("%d %f\n", ii, zoomFractions[ii]); } } public void setRange(double minVal, double maxVal) { mMinDataVal = minVal; mMaxDataVal = maxVal; mScaleInfo.setMinVal(minVal); mScaleInfo.setMaxVal(maxVal); } public void setLimitRange(double minVal, double maxVal) { mLimitMinVal = minVal; mLimitMaxVal = maxVal; } public void resetScale() { mScaleInfo.setMinVal(mLimitMinVal); mScaleInfo.setMaxVal(mLimitMaxVal); } public void setScaleFromHorizontalScrollBar(int selection) { double minVal = mScaleInfo.getMinVal(); double maxVal = mScaleInfo.getMaxVal(); double visibleRange = maxVal - minVal; minVal = mLimitMinVal + selection; maxVal = minVal + visibleRange; if (maxVal > mLimitMaxVal) { maxVal = mLimitMaxVal; minVal = maxVal - visibleRange; } mScaleInfo.setMinVal(minVal); mScaleInfo.setMaxVal(maxVal); mGraphicsState = GraphicsState.Scrolling; } private void updateHorizontalScrollBar() { double minVal = mScaleInfo.getMinVal(); double maxVal = mScaleInfo.getMaxVal(); double visibleRange = maxVal - minVal; double fullRange = mLimitMaxVal - mLimitMinVal; ScrollBar hBar = getHorizontalBar(); if (fullRange > visibleRange) { hBar.setVisible(true); hBar.setMinimum(0); hBar.setMaximum((int)Math.ceil(fullRange)); hBar.setThumb((int)Math.ceil(visibleRange)); hBar.setSelection((int)Math.floor(minVal - mLimitMinVal)); } else { hBar.setVisible(false); } } private void draw(Display display, GC gc) { if (mSegments.length == 0) { // gc.setBackground(colorBackground); // gc.fillRectangle(getBounds()); return; } // Create an image for double-buffering Image image = new Image(display, getBounds()); // Set up the off-screen gc GC gcImage = new GC(image); if (mSetFonts) gcImage.setFont(mFontRegistry.get("small")); //$NON-NLS-1$ // Draw the background // gcImage.setBackground(colorBackground); // gcImage.fillRectangle(image.getBounds()); if (mGraphicsState == GraphicsState.Scaling) { double diff = mMouse.x - mMouseMarkStartX; if (diff > 0) { double newMinVal = mScaleMinVal - diff / mScalePixelsPerRange; if (newMinVal < mLimitMinVal) newMinVal = mLimitMinVal; mScaleInfo.setMinVal(newMinVal); // System.out.printf("diff %f scaleMin %f newMin %f\n", // diff, scaleMinVal, newMinVal); } else if (diff < 0) { double newMaxVal = mScaleMaxVal - diff / mScalePixelsPerRange; if (newMaxVal > mLimitMaxVal) newMaxVal = mLimitMaxVal; mScaleInfo.setMaxVal(newMaxVal); // System.out.printf("diff %f scaleMax %f newMax %f\n", // diff, scaleMaxVal, newMaxVal); } } // Recompute the ticks and strips only if the size has changed, // or we scrolled so that a new row is visible. Point dim = getSize(); if (mStartRow != mCachedStartRow || mEndRow != mCachedEndRow || mScaleInfo.getMinVal() != mCachedMinVal || mScaleInfo.getMaxVal() != mCachedMaxVal) { mCachedStartRow = mStartRow; mCachedEndRow = mEndRow; int xdim = dim.x - TotalXMargin; mScaleInfo.setNumPixels(xdim); boolean forceEndPoints = (mGraphicsState == GraphicsState.Scaling || mGraphicsState == GraphicsState.Animating || mGraphicsState == GraphicsState.Scrolling); mScaleInfo.computeTicks(forceEndPoints); mCachedMinVal = mScaleInfo.getMinVal(); mCachedMaxVal = mScaleInfo.getMaxVal(); if (mLimitMinVal > mScaleInfo.getMinVal()) mLimitMinVal = mScaleInfo.getMinVal(); if (mLimitMaxVal < mScaleInfo.getMaxVal()) mLimitMaxVal = mScaleInfo.getMaxVal(); // Compute the strips computeStrips(); // Update the horizontal scrollbar. updateHorizontalScrollBar(); } if (mNumRows > 2) { // Draw the row background stripes gcImage.setBackground(mColorRowBack); for (int ii = 1; ii < mNumRows; ii += 2) { RowData rd = mRows[ii]; int y1 = rd.mRank * rowYSpace - mScrollOffsetY; gcImage.fillRectangle(0, y1, dim.x, rowYSpace); } } if (drawingSelection()) { drawSelection(display, gcImage); } String blockName = null; Color blockColor = null; String blockDetails = null; if (mDebug) { double pixelsPerRange = mScaleInfo.getPixelsPerRange(); System.out .printf( "dim.x %d pixels %d minVal %f, maxVal %f ppr %f rpp %f\n", dim.x, dim.x - TotalXMargin, mScaleInfo .getMinVal(), mScaleInfo.getMaxVal(), pixelsPerRange, 1.0 / pixelsPerRange); } // Draw the strips Block selectBlock = null; for (Strip strip : mStripList) { if (strip.mColor == null) { // System.out.printf("strip.color is null\n"); continue; } gcImage.setBackground(strip.mColor); gcImage.fillRectangle(strip.mX, strip.mY - mScrollOffsetY, strip.mWidth, strip.mHeight); if (mMouseRow == strip.mRowData.mRank) { if (mMouse.x >= strip.mX && mMouse.x < strip.mX + strip.mWidth) { Block block = strip.mSegment.mBlock; blockName = block.getName(); blockColor = strip.mColor; if (mHaveCpuTime) { if (mHaveRealTime) { blockDetails = String.format( "excl cpu %s, incl cpu %s, " + "excl real %s, incl real %s", mUnits.labelledString(block.getExclusiveCpuTime()), mUnits.labelledString(block.getInclusiveCpuTime()), mUnits.labelledString(block.getExclusiveRealTime()), mUnits.labelledString(block.getInclusiveRealTime())); } else { blockDetails = String.format( "excl cpu %s, incl cpu %s", mUnits.labelledString(block.getExclusiveCpuTime()), mUnits.labelledString(block.getInclusiveCpuTime())); } } else { blockDetails = String.format( "excl real %s, incl real %s", mUnits.labelledString(block.getExclusiveRealTime()), mUnits.labelledString(block.getInclusiveRealTime())); } } if (mMouseSelect.x >= strip.mX && mMouseSelect.x < strip.mX + strip.mWidth) { selectBlock = strip.mSegment.mBlock; } } } mMouseSelect.x = 0; mMouseSelect.y = 0; if (selectBlock != null) { ArrayList selections = new ArrayList(); // Get the row label RowData rd = mRows[mMouseRow]; selections.add(Selection.highlight("Thread", rd.mName)); //$NON-NLS-1$ selections.add(Selection.highlight("Call", selectBlock)); //$NON-NLS-1$ int mouseX = mMouse.x - LeftMargin; double mouseXval = mScaleInfo.pixelToValue(mouseX); selections.add(Selection.highlight("Time", mouseXval)); //$NON-NLS-1$ mSelectionController.change(selections, "TimeLineView"); //$NON-NLS-1$ mHighlightMethodData = null; mHighlightCall = (Call) selectBlock; startHighlighting(); } // Draw a highlight box on the row where the mouse is. // Except don't draw the box if we are animating the // highlighing of a call or method because the inclusive // highlight bar passes through the highlight box and // causes an annoying flashing artifact. if (mMouseRow >= 0 && mMouseRow < mNumRows && mHighlightStep == 0) { gcImage.setForeground(mColorGray); int y1 = mMouseRow * rowYSpace - mScrollOffsetY; gcImage.drawLine(0, y1, dim.x, y1); gcImage.drawLine(0, y1 + rowYSpace, dim.x, y1 + rowYSpace); } // Highlight a selected method, if any drawHighlights(gcImage, dim); // Draw a vertical line where the mouse is. gcImage.setForeground(mColorDarkGray); int lineEnd = Math.min(dim.y, mNumRows * rowYSpace); gcImage.drawLine(mMouse.x, 0, mMouse.x, lineEnd); if (blockName != null) { mTimescale.setMethodName(blockName); mTimescale.setMethodColor(blockColor); mTimescale.setDetails(blockDetails); mShowHighlightName = false; } else if (mShowHighlightName) { // Draw the highlighted method name MethodData md = mHighlightMethodData; if (md == null && mHighlightCall != null) md = mHighlightCall.getMethodData(); if (md == null) System.out.printf("null highlight?\n"); //$NON-NLS-1$ if (md != null) { mTimescale.setMethodName(md.getProfileName()); mTimescale.setMethodColor(md.getColor()); mTimescale.setDetails(null); } } else { mTimescale.setMethodName(null); mTimescale.setMethodColor(null); mTimescale.setDetails(null); } mTimescale.redraw(); // Draw the off-screen buffer to the screen gc.drawImage(image, 0, 0); // Clean up image.dispose(); gcImage.dispose(); } private void drawHighlights(GC gc, Point dim) { int height = mHighlightHeight; if (height <= 0) return; for (Range range : mHighlightExclusive) { gc.setBackground(range.mColor); int xStart = range.mXdim.x; int width = range.mXdim.y; gc.fillRectangle(xStart, range.mY - height - mScrollOffsetY, width, height); } // Draw the inclusive lines a bit shorter height -= 1; if (height <= 0) height = 1; // Highlight the inclusive ranges gc.setForeground(mColorDarkGray); gc.setBackground(mColorDarkGray); for (Range range : mHighlightInclusive) { int x1 = range.mXdim.x; int x2 = range.mXdim.y; boolean drawLeftEnd = false; boolean drawRightEnd = false; if (x1 >= LeftMargin) drawLeftEnd = true; else x1 = LeftMargin; if (x2 >= LeftMargin) drawRightEnd = true; else x2 = dim.x - RightMargin; int y1 = range.mY + rowHeight + 2 - mScrollOffsetY; // If the range is very narrow, then just draw a small // rectangle. if (x2 - x1 < MinInclusiveRange) { int width = x2 - x1; if (width < 2) width = 2; gc.fillRectangle(x1, y1, width, height); continue; } if (drawLeftEnd) { if (drawRightEnd) { // Draw both ends int[] points = { x1, y1, x1, y1 + height, x2, y1 + height, x2, y1 }; gc.drawPolyline(points); } else { // Draw the left end int[] points = { x1, y1, x1, y1 + height, x2, y1 + height }; gc.drawPolyline(points); } } else { if (drawRightEnd) { // Draw the right end int[] points = { x1, y1 + height, x2, y1 + height, x2, y1 }; gc.drawPolyline(points); } else { // Draw neither end, just the line int[] points = { x1, y1 + height, x2, y1 + height }; gc.drawPolyline(points); } } // Draw the arrowheads, if necessary if (drawLeftEnd == false) { int[] points = { x1 + 7, y1 + height - 4, x1, y1 + height, x1 + 7, y1 + height + 4 }; gc.fillPolygon(points); } if (drawRightEnd == false) { int[] points = { x2 - 7, y1 + height - 4, x2, y1 + height, x2 - 7, y1 + height + 4 }; gc.fillPolygon(points); } } } private boolean drawingSelection() { return mGraphicsState == GraphicsState.Marking || mGraphicsState == GraphicsState.Animating; } private void drawSelection(Display display, GC gc) { Point dim = getSize(); gc.setForeground(mColorGray); gc.drawLine(mMouseMarkStartX, 0, mMouseMarkStartX, dim.y); gc.setBackground(mColorZoomSelection); int width; int mouseX = (mGraphicsState == GraphicsState.Animating) ? mMouseMarkEndX : mMouse.x; int x; if (mMouseMarkStartX < mouseX) { x = mMouseMarkStartX; width = mouseX - mMouseMarkStartX; } else { x = mouseX; width = mMouseMarkStartX - mouseX; } gc.fillRectangle(x, 0, width, dim.y); } private void computeStrips() { double minVal = mScaleInfo.getMinVal(); double maxVal = mScaleInfo.getMaxVal(); // Allocate space for the pixel data Pixel[] pixels = new Pixel[mNumRows]; for (int ii = 0; ii < mNumRows; ++ii) pixels[ii] = new Pixel(); // Clear the per-block pixel data for (int ii = 0; ii < mSegments.length; ++ii) { mSegments[ii].mBlock.clearWeight(); } mStripList.clear(); mHighlightExclusive.clear(); mHighlightInclusive.clear(); MethodData callMethod = null; long callStart = 0; long callEnd = -1; RowData callRowData = null; int prevMethodStart = -1; int prevMethodEnd = -1; int prevCallStart = -1; int prevCallEnd = -1; if (mHighlightCall != null) { int callPixelStart = -1; int callPixelEnd = -1; callStart = mHighlightCall.getStartTime(); callEnd = mHighlightCall.getEndTime(); callMethod = mHighlightCall.getMethodData(); if (callStart >= minVal) callPixelStart = mScaleInfo.valueToPixel(callStart); if (callEnd <= maxVal) callPixelEnd = mScaleInfo.valueToPixel(callEnd); // System.out.printf("callStart,End %d,%d minVal,maxVal %f,%f // callPixelStart,End %d,%d\n", // callStart, callEnd, minVal, maxVal, callPixelStart, // callPixelEnd); int threadId = mHighlightCall.getThreadId(); String threadName = mThreadLabels.get(threadId); callRowData = mRowByName.get(threadName); int y1 = callRowData.mRank * rowYSpace + rowYMarginHalf; Color color = callMethod.getColor(); mHighlightInclusive.add(new Range(callPixelStart + LeftMargin, callPixelEnd + LeftMargin, y1, color)); } for (Segment segment : mSegments) { if (segment.mEndTime <= minVal) continue; if (segment.mStartTime >= maxVal) continue; Block block = segment.mBlock; // Skip over blocks that were not assigned a color, including the // top level block and others that have zero inclusive time. Color color = block.getColor(); if (color == null) continue; double recordStart = Math.max(segment.mStartTime, minVal); double recordEnd = Math.min(segment.mEndTime, maxVal); if (recordStart == recordEnd) continue; int pixelStart = mScaleInfo.valueToPixel(recordStart); int pixelEnd = mScaleInfo.valueToPixel(recordEnd); int width = pixelEnd - pixelStart; boolean isContextSwitch = segment.mIsContextSwitch; RowData rd = segment.mRowData; MethodData md = block.getMethodData(); // We will add the scroll offset later when we draw the strips int y1 = rd.mRank * rowYSpace + rowYMarginHalf; // If we can't display any more rows, then quit if (rd.mRank > mEndRow) break; // System.out.printf("segment %s val: [%.1f, %.1f] frac [%f, %f] // pixel: [%d, %d] pix.start %d weight %.2f %s\n", // block.getName(), recordStart, recordEnd, // scaleInfo.valueToPixelFraction(recordStart), // scaleInfo.valueToPixelFraction(recordEnd), // pixelStart, pixelEnd, pixels[rd.rank].start, // pixels[rd.rank].maxWeight, // pixels[rd.rank].segment != null // ? pixels[rd.rank].segment.block.getName() // : "null"); if (mHighlightMethodData != null) { if (mHighlightMethodData == md) { if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) { prevMethodStart = pixelStart; prevMethodEnd = pixelEnd; int rangeWidth = width; if (rangeWidth == 0) rangeWidth = 1; mHighlightExclusive.add(new Range(pixelStart + LeftMargin, rangeWidth, y1, color)); callStart = block.getStartTime(); int callPixelStart = -1; if (callStart >= minVal) callPixelStart = mScaleInfo.valueToPixel(callStart); int callPixelEnd = -1; callEnd = block.getEndTime(); if (callEnd <= maxVal) callPixelEnd = mScaleInfo.valueToPixel(callEnd); if (prevCallStart != callPixelStart || prevCallEnd != callPixelEnd) { prevCallStart = callPixelStart; prevCallEnd = callPixelEnd; mHighlightInclusive.add(new Range( callPixelStart + LeftMargin, callPixelEnd + LeftMargin, y1, color)); } } } else if (mFadeColors) { color = md.getFadedColor(); } } else if (mHighlightCall != null) { if (segment.mStartTime >= callStart && segment.mEndTime <= callEnd && callMethod == md && callRowData == rd) { if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) { prevMethodStart = pixelStart; prevMethodEnd = pixelEnd; int rangeWidth = width; if (rangeWidth == 0) rangeWidth = 1; mHighlightExclusive.add(new Range(pixelStart + LeftMargin, rangeWidth, y1, color)); } } else if (mFadeColors) { color = md.getFadedColor(); } } // Cases: // 1. This segment starts on a different pixel than the // previous segment started on. In this case, emit // the pixel strip, if any, and: // A. If the width is 0, then add this segment's // weight to the Pixel. // B. If the width > 0, then emit a strip for this // segment (no partial Pixel data). // // 2. Otherwise (the new segment starts on the same // pixel as the previous segment): add its "weight" // to the current pixel, and: // A. If the new segment has width 1, // then emit the pixel strip and then // add the segment's weight to the pixel. // B. If the new segment has width > 1, // then emit the pixel strip, and emit the rest // of the strip for this segment (no partial Pixel // data). Pixel pix = pixels[rd.mRank]; if (pix.mStart != pixelStart) { if (pix.mSegment != null) { // Emit the pixel strip. This also clears the pixel. emitPixelStrip(rd, y1, pix); } if (width == 0) { // Compute the "weight" of this segment for the first // pixel. For a pixel N, the "weight" of a segment is // how much of the region [N - 0.5, N + 0.5] is covered // by the segment. double weight = computeWeight(recordStart, recordEnd, isContextSwitch, pixelStart); weight = block.addWeight(pixelStart, rd.mRank, weight); if (weight > pix.mMaxWeight) { pix.setFields(pixelStart, weight, segment, color, rd); } } else { int x1 = pixelStart + LeftMargin; Strip strip = new Strip( x1, isContextSwitch ? y1 + rowHeight - 1 : y1, width, isContextSwitch ? 1 : rowHeight, rd, segment, color); mStripList.add(strip); } } else { double weight = computeWeight(recordStart, recordEnd, isContextSwitch, pixelStart); weight = block.addWeight(pixelStart, rd.mRank, weight); if (weight > pix.mMaxWeight) { pix.setFields(pixelStart, weight, segment, color, rd); } if (width == 1) { // Emit the pixel strip. This also clears the pixel. emitPixelStrip(rd, y1, pix); // Compute the weight for the next pixel pixelStart += 1; weight = computeWeight(recordStart, recordEnd, isContextSwitch, pixelStart); weight = block.addWeight(pixelStart, rd.mRank, weight); pix.setFields(pixelStart, weight, segment, color, rd); } else if (width > 1) { // Emit the pixel strip. This also clears the pixel. emitPixelStrip(rd, y1, pix); // Emit a strip for the rest of the segment. pixelStart += 1; width -= 1; int x1 = pixelStart + LeftMargin; Strip strip = new Strip( x1, isContextSwitch ? y1 + rowHeight - 1 : y1, width, isContextSwitch ? 1 : rowHeight, rd,segment, color); mStripList.add(strip); } } } // Emit the last pixels of each row, if any for (int ii = 0; ii < mNumRows; ++ii) { Pixel pix = pixels[ii]; if (pix.mSegment != null) { RowData rd = pix.mRowData; int y1 = rd.mRank * rowYSpace + rowYMarginHalf; // Emit the pixel strip. This also clears the pixel. emitPixelStrip(rd, y1, pix); } } if (false) { System.out.printf("computeStrips()\n"); for (Strip strip : mStripList) { System.out.printf("%3d, %3d width %3d height %d %s\n", strip.mX, strip.mY, strip.mWidth, strip.mHeight, strip.mSegment.mBlock.getName()); } } } private double computeWeight(double start, double end, boolean isContextSwitch, int pixel) { if (isContextSwitch) { return 0; } double pixelStartFraction = mScaleInfo.valueToPixelFraction(start); double pixelEndFraction = mScaleInfo.valueToPixelFraction(end); double leftEndPoint = Math.max(pixelStartFraction, pixel - 0.5); double rightEndPoint = Math.min(pixelEndFraction, pixel + 0.5); double weight = rightEndPoint - leftEndPoint; return weight; } private void emitPixelStrip(RowData rd, int y, Pixel pixel) { Strip strip; if (pixel.mSegment == null) return; int x = pixel.mStart + LeftMargin; // Compute the percentage of the row height proportional to // the weight of this pixel. But don't let the proportion // exceed 3/4 of the row height so that we can easily see // if a given time range includes more than one method. int height = (int) (pixel.mMaxWeight * rowHeight * 0.75); if (height < mMinStripHeight) height = mMinStripHeight; int remainder = rowHeight - height; if (remainder > 0) { strip = new Strip(x, y, 1, remainder, rd, pixel.mSegment, mFadeColors ? mColorGray : mColorBlack); mStripList.add(strip); // System.out.printf("emitPixel (%d, %d) height %d black\n", // x, y, remainder); } strip = new Strip(x, y + remainder, 1, height, rd, pixel.mSegment, pixel.mColor); mStripList.add(strip); // System.out.printf("emitPixel (%d, %d) height %d %s\n", // x, y + remainder, height, pixel.segment.block.getName()); pixel.mSegment = null; pixel.mMaxWeight = 0.0; } private void mouseMove(MouseEvent me) { if (false) { if (mHighlightMethodData != null) { mHighlightMethodData = null; // Force a recomputation of the strip colors mCachedEndRow = -1; } } Point dim = mSurface.getSize(); int x = me.x; if (x < LeftMargin) x = LeftMargin; if (x > dim.x - RightMargin) x = dim.x - RightMargin; mMouse.x = x; mMouse.y = me.y; mTimescale.setVbarPosition(x); if (mGraphicsState == GraphicsState.Marking) { mTimescale.setMarkEnd(x); } if (mGraphicsState == GraphicsState.Normal) { // Set the cursor to the normal state. mSurface.setCursor(mNormalCursor); } else if (mGraphicsState == GraphicsState.Marking) { // Make the cursor point in the direction of the sweep if (mMouse.x >= mMouseMarkStartX) mSurface.setCursor(mIncreasingCursor); else mSurface.setCursor(mDecreasingCursor); } int rownum = (mMouse.y + mScrollOffsetY) / rowYSpace; if (me.y < 0 || me.y >= dim.y) { rownum = -1; } if (mMouseRow != rownum) { mMouseRow = rownum; mLabels.redraw(); } redraw(); } private void mouseDown(MouseEvent me) { Point dim = mSurface.getSize(); int x = me.x; if (x < LeftMargin) x = LeftMargin; if (x > dim.x - RightMargin) x = dim.x - RightMargin; mMouseMarkStartX = x; mGraphicsState = GraphicsState.Marking; mSurface.setCursor(mIncreasingCursor); mTimescale.setMarkStart(mMouseMarkStartX); mTimescale.setMarkEnd(mMouseMarkStartX); redraw(); } private void mouseUp(MouseEvent me) { mSurface.setCursor(mNormalCursor); if (mGraphicsState != GraphicsState.Marking) { mGraphicsState = GraphicsState.Normal; return; } mGraphicsState = GraphicsState.Animating; Point dim = mSurface.getSize(); // If the user released the mouse outside the drawing area then // cancel the zoom. if (me.y <= 0 || me.y >= dim.y) { mGraphicsState = GraphicsState.Normal; redraw(); return; } int x = me.x; if (x < LeftMargin) x = LeftMargin; if (x > dim.x - RightMargin) x = dim.x - RightMargin; mMouseMarkEndX = x; // If the user clicked and released the mouse at the same point // (+/- a pixel or two) then cancel the zoom (but select the // method). int dist = mMouseMarkEndX - mMouseMarkStartX; if (dist < 0) dist = -dist; if (dist <= 2) { mGraphicsState = GraphicsState.Normal; // Select the method underneath the mouse mMouseSelect.x = mMouseMarkStartX; mMouseSelect.y = me.y; redraw(); return; } // Make mouseEndX be the higher end point if (mMouseMarkEndX < mMouseMarkStartX) { int temp = mMouseMarkEndX; mMouseMarkEndX = mMouseMarkStartX; mMouseMarkStartX = temp; } // If the zoom area is the whole window (or nearly the whole // window) then cancel the zoom. if (mMouseMarkStartX <= LeftMargin + MinZoomPixelMargin && mMouseMarkEndX >= dim.x - RightMargin - MinZoomPixelMargin) { mGraphicsState = GraphicsState.Normal; redraw(); return; } // Compute some variables needed for zooming. // It's probably easiest to explain by an example. There // are two scales (or dimensions) involved: one for the pixels // and one for the values (microseconds). To keep the example // simple, suppose we have pixels in the range [0,16] and // values in the range [100, 260], and suppose the user // selects a zoom window from pixel 4 to pixel 8. // // usec: 100 140 180 260 // |-------|ZZZZZZZ|---------------| // pixel: 0 4 8 16 // // I've drawn the pixels starting at zero for simplicity, but // in fact the drawable area is offset from the left margin // by the value of "LeftMargin". // // The "pixels-per-range" (ppr) in this case is 0.1 (a tenth of // a pixel per usec). What we want is to redraw the screen in // several steps, each time increasing the zoom window until the // zoom window fills the screen. For simplicity, assume that // we want to zoom in four equal steps. Then the snapshots // of the screen at each step would look something like this: // // usec: 100 140 180 260 // |-------|ZZZZZZZ|---------------| // pixel: 0 4 8 16 // // usec: ? 140 180 ? // |-----|ZZZZZZZZZZZZZ|-----------| // pixel: 0 3 10 16 // // usec: ? 140 180 ? // |---|ZZZZZZZZZZZZZZZZZZZ|-------| // pixel: 0 2 12 16 // // usec: ?140 180 ? // |-|ZZZZZZZZZZZZZZZZZZZZZZZZZ|---| // pixel: 0 1 14 16 // // usec: 140 180 // |ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ| // pixel: 0 16 // // The problem is how to compute the endpoints (denoted by ?) // for each step. This is a little tricky. We first need to // compute the "fixed point": this is the point in the selection // that doesn't move left or right. Then we can recompute the // "ppr" (pixels per range) at each step and then find the // endpoints. The computation of the end points is done // in animateZoom(). This method computes the fixed point // and some other variables needed in animateZoom(). double minVal = mScaleInfo.getMinVal(); double maxVal = mScaleInfo.getMaxVal(); double ppr = mScaleInfo.getPixelsPerRange(); mZoomMin = minVal + ((mMouseMarkStartX - LeftMargin) / ppr); mZoomMax = minVal + ((mMouseMarkEndX - LeftMargin) / ppr); // Clamp the min and max values to the actual data min and max if (mZoomMin < mMinDataVal) mZoomMin = mMinDataVal; if (mZoomMax > mMaxDataVal) mZoomMax = mMaxDataVal; // Snap the min and max points to the grid determined by the // TickScaler // before we zoom. int xdim = dim.x - TotalXMargin; TickScaler scaler = new TickScaler(mZoomMin, mZoomMax, xdim, PixelsPerTick); scaler.computeTicks(false); mZoomMin = scaler.getMinVal(); mZoomMax = scaler.getMaxVal(); // Also snap the mouse points (in pixel space) to be consistent with // zoomMin and zoomMax (in value space). mMouseMarkStartX = (int) ((mZoomMin - minVal) * ppr + LeftMargin); mMouseMarkEndX = (int) ((mZoomMax - minVal) * ppr + LeftMargin); mTimescale.setMarkStart(mMouseMarkStartX); mTimescale.setMarkEnd(mMouseMarkEndX); // Compute the mouse selection end point distances mMouseEndDistance = dim.x - RightMargin - mMouseMarkEndX; mMouseStartDistance = mMouseMarkStartX - LeftMargin; mZoomMouseStart = mMouseMarkStartX; mZoomMouseEnd = mMouseMarkEndX; mZoomStep = 0; // Compute the fixed point in both value space and pixel space. mMin2ZoomMin = mZoomMin - minVal; mZoomMax2Max = maxVal - mZoomMax; mZoomFixed = mZoomMin + (mZoomMax - mZoomMin) * mMin2ZoomMin / (mMin2ZoomMin + mZoomMax2Max); mZoomFixedPixel = (mZoomFixed - minVal) * ppr + LeftMargin; mFixedPixelStartDistance = mZoomFixedPixel - LeftMargin; mFixedPixelEndDistance = dim.x - RightMargin - mZoomFixedPixel; mZoomMin2Fixed = mZoomFixed - mZoomMin; mFixed2ZoomMax = mZoomMax - mZoomFixed; getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); redraw(); update(); } private void mouseScrolled(MouseEvent me) { mGraphicsState = GraphicsState.Scrolling; double tMin = mScaleInfo.getMinVal(); double tMax = mScaleInfo.getMaxVal(); double zoomFactor = 2; double tMinRef = mLimitMinVal; double tMaxRef = mLimitMaxVal; double t; // the fixed point double tMinNew; double tMaxNew; if (me.count > 0) { // we zoom in Point dim = mSurface.getSize(); int x = me.x; if (x < LeftMargin) x = LeftMargin; if (x > dim.x - RightMargin) x = dim.x - RightMargin; double ppr = mScaleInfo.getPixelsPerRange(); t = tMin + ((x - LeftMargin) / ppr); tMinNew = Math.max(tMinRef, t - (t - tMin) / zoomFactor); tMaxNew = Math.min(tMaxRef, t + (tMax - t) / zoomFactor); } else { // we zoom out double factor = (tMax - tMin) / (tMaxRef - tMinRef); if (factor < 1) { t = (factor * tMinRef - tMin) / (factor - 1); tMinNew = Math.max(tMinRef, t - zoomFactor * (t - tMin)); tMaxNew = Math.min(tMaxRef, t + zoomFactor * (tMax - t)); } else { return; } } mScaleInfo.setMinVal(tMinNew); mScaleInfo.setMaxVal(tMaxNew); mSurface.redraw(); } // No defined behavior yet for double-click. private void mouseDoubleClick(MouseEvent me) { } public void startScaling(int mouseX) { Point dim = mSurface.getSize(); int x = mouseX; if (x < LeftMargin) x = LeftMargin; if (x > dim.x - RightMargin) x = dim.x - RightMargin; mMouseMarkStartX = x; mGraphicsState = GraphicsState.Scaling; mScalePixelsPerRange = mScaleInfo.getPixelsPerRange(); mScaleMinVal = mScaleInfo.getMinVal(); mScaleMaxVal = mScaleInfo.getMaxVal(); } public void stopScaling(int mouseX) { mGraphicsState = GraphicsState.Normal; } private void animateHighlight() { mHighlightStep += 1; if (mHighlightStep >= HIGHLIGHT_STEPS) { mFadeColors = false; mHighlightStep = 0; // Force a recomputation of the strip colors mCachedEndRow = -1; } else { mFadeColors = true; mShowHighlightName = true; mHighlightHeight = highlightHeights[mHighlightStep]; getDisplay().timerExec(HIGHLIGHT_TIMER_INTERVAL, mHighlightAnimator); } redraw(); } private void clearHighlights() { // System.out.printf("clearHighlights()\n"); mShowHighlightName = false; mHighlightHeight = 0; mHighlightMethodData = null; mHighlightCall = null; mFadeColors = false; mHighlightStep = 0; // Force a recomputation of the strip colors mCachedEndRow = -1; redraw(); } private void animateZoom() { mZoomStep += 1; if (mZoomStep > ZOOM_STEPS) { mGraphicsState = GraphicsState.Normal; // Force a normal recomputation mCachedMinVal = mScaleInfo.getMinVal() + 1; } else if (mZoomStep == ZOOM_STEPS) { mScaleInfo.setMinVal(mZoomMin); mScaleInfo.setMaxVal(mZoomMax); mMouseMarkStartX = LeftMargin; Point dim = getSize(); mMouseMarkEndX = dim.x - RightMargin; mTimescale.setMarkStart(mMouseMarkStartX); mTimescale.setMarkEnd(mMouseMarkEndX); getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); } else { // Zoom in slowly at first, then speed up, then slow down. // The zoom fractions are precomputed to save time. double fraction = mZoomFractions[mZoomStep]; mMouseMarkStartX = (int) (mZoomMouseStart - fraction * mMouseStartDistance); mMouseMarkEndX = (int) (mZoomMouseEnd + fraction * mMouseEndDistance); mTimescale.setMarkStart(mMouseMarkStartX); mTimescale.setMarkEnd(mMouseMarkEndX); // Compute the new pixels-per-range. Avoid division by zero. double ppr; if (mZoomMin2Fixed >= mFixed2ZoomMax) ppr = (mZoomFixedPixel - mMouseMarkStartX) / mZoomMin2Fixed; else ppr = (mMouseMarkEndX - mZoomFixedPixel) / mFixed2ZoomMax; double newMin = mZoomFixed - mFixedPixelStartDistance / ppr; double newMax = mZoomFixed + mFixedPixelEndDistance / ppr; mScaleInfo.setMinVal(newMin); mScaleInfo.setMaxVal(newMax); getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); } redraw(); } private static final int TotalXMargin = LeftMargin + RightMargin; private static final int yMargin = 1; // blank space on top // The minimum margin on each side of the zoom window, in pixels. private static final int MinZoomPixelMargin = 10; private GraphicsState mGraphicsState = GraphicsState.Normal; private Point mMouse = new Point(LeftMargin, 0); private int mMouseMarkStartX; private int mMouseMarkEndX; private boolean mDebug = false; private ArrayList mStripList = new ArrayList(); private ArrayList mHighlightExclusive = new ArrayList(); private ArrayList mHighlightInclusive = new ArrayList(); private int mMinStripHeight = 2; private double mCachedMinVal; private double mCachedMaxVal; private int mCachedStartRow; private int mCachedEndRow; private double mScalePixelsPerRange; private double mScaleMinVal; private double mScaleMaxVal; private double mLimitMinVal; private double mLimitMaxVal; private double mMinDataVal; private double mMaxDataVal; private Cursor mNormalCursor; private Cursor mIncreasingCursor; private Cursor mDecreasingCursor; private static final int ZOOM_TIMER_INTERVAL = 10; private static final int HIGHLIGHT_TIMER_INTERVAL = 50; private static final int ZOOM_STEPS = 8; // must be even private int mHighlightHeight = 4; private final int[] highlightHeights = { 0, 2, 4, 5, 6, 5, 4, 2, 4, 5, 6 }; private final int HIGHLIGHT_STEPS = highlightHeights.length; private boolean mFadeColors; private boolean mShowHighlightName; private double[] mZoomFractions; private int mZoomStep; private int mZoomMouseStart; private int mZoomMouseEnd; private int mMouseStartDistance; private int mMouseEndDistance; private Point mMouseSelect = new Point(0, 0); private double mZoomFixed; private double mZoomFixedPixel; private double mFixedPixelStartDistance; private double mFixedPixelEndDistance; private double mZoomMin2Fixed; private double mMin2ZoomMin; private double mFixed2ZoomMax; private double mZoomMax2Max; private double mZoomMin; private double mZoomMax; private Runnable mZoomAnimator; private Runnable mHighlightAnimator; private int mHighlightStep; } private int computeVisibleRows(int ydim) { // If we resize, then move the bottom row down. Don't allow the scroll // to waste space at the bottom. int offsetY = mScrollOffsetY; int spaceNeeded = mNumRows * rowYSpace; if (offsetY + ydim > spaceNeeded) { offsetY = spaceNeeded - ydim; if (offsetY < 0) { offsetY = 0; } } mStartRow = offsetY / rowYSpace; mEndRow = (offsetY + ydim) / rowYSpace; if (mEndRow >= mNumRows) { mEndRow = mNumRows - 1; } return offsetY; } private void startHighlighting() { // System.out.printf("startHighlighting()\n"); mSurface.mHighlightStep = 0; mSurface.mFadeColors = true; // Force a recomputation of the color strips mSurface.mCachedEndRow = -1; getDisplay().timerExec(0, mSurface.mHighlightAnimator); } private static class RowData { RowData(Row row) { mName = row.getName(); mStack = new ArrayList(); } public void push(Block block) { mStack.add(block); } public Block top() { if (mStack.size() == 0) return null; return mStack.get(mStack.size() - 1); } public void pop() { if (mStack.size() == 0) return; mStack.remove(mStack.size() - 1); } private String mName; private int mRank; private long mElapsed; private long mEndTime; private ArrayList mStack; } private static class Segment { Segment(RowData rowData, Block block, long startTime, long endTime) { mRowData = rowData; if (block.isContextSwitch()) { mBlock = block.getParentBlock(); mIsContextSwitch = true; } else { mBlock = block; } mStartTime = startTime; mEndTime = endTime; } private RowData mRowData; private Block mBlock; private long mStartTime; private long mEndTime; private boolean mIsContextSwitch; } private static class Strip { Strip(int x, int y, int width, int height, RowData rowData, Segment segment, Color color) { mX = x; mY = y; mWidth = width; mHeight = height; mRowData = rowData; mSegment = segment; mColor = color; } int mX; int mY; int mWidth; int mHeight; RowData mRowData; Segment mSegment; Color mColor; } private static class Pixel { public void setFields(int start, double weight, Segment segment, Color color, RowData rowData) { mStart = start; mMaxWeight = weight; mSegment = segment; mColor = color; mRowData = rowData; } int mStart = -2; // some value that won't match another pixel double mMaxWeight; Segment mSegment; Color mColor; // we need the color here because it may be faded RowData mRowData; } private static class Range { Range(int xStart, int width, int y, Color color) { mXdim.x = xStart; mXdim.y = width; mY = y; mColor = color; } Point mXdim = new Point(0, 0); int mY; Color mColor; } } traceview/src/main/java/com/android/traceview/TraceAction.java0100644 0000000 0000000 00000001732 12747325007 023472 0ustar000000000 0000000 /* * 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.traceview; final class TraceAction { public static final int ACTION_ENTER = 0; public static final int ACTION_EXIT = 1; public static final int ACTION_INCOMPLETE = 2; public final int mAction; public final Call mCall; public TraceAction(int action, Call call) { mAction = action; mCall = call; } } traceview/src/main/java/com/android/traceview/TraceReader.java0100644 0000000 0000000 00000003422 12747325007 023455 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import java.util.ArrayList; import java.util.HashMap; public abstract class TraceReader { private TraceUnits mTraceUnits; public TraceUnits getTraceUnits() { if (mTraceUnits == null) mTraceUnits = new TraceUnits(); return mTraceUnits; } public ArrayList getThreadTimeRecords() { return null; } public HashMap getThreadLabels() { return null; } public MethodData[] getMethods() { return null; } public ThreadData[] getThreads() { return null; } public long getTotalCpuTime() { return 0; } public long getTotalRealTime() { return 0; } public boolean haveCpuTime() { return false; } public boolean haveRealTime() { return false; } public HashMap getProperties() { return null; } public ProfileProvider getProfileProvider() { return null; } public TimeBase getPreferredTimeBase() { return TimeBase.CPU_TIME; } public String getClockSource() { return null; } } traceview/src/main/java/com/android/traceview/TraceUnits.java0100644 0000000 0000000 00000004621 12747325007 023357 0ustar000000000 0000000 /* * Copyright (C) 2006 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.traceview; import java.text.DecimalFormat; // This should be a singleton. public class TraceUnits { private TimeScale mTimeScale = TimeScale.MicroSeconds; private double mScale = 1.0; DecimalFormat mFormatter = new DecimalFormat(); public double getScaledValue(long value) { return value * mScale; } public double getScaledValue(double value) { return value * mScale; } public String valueOf(long value) { return valueOf((double) value); } public String valueOf(double value) { String pattern; double scaled = value * mScale; if ((int) scaled == scaled) pattern = "###,###"; else pattern = "###,###.###"; mFormatter.applyPattern(pattern); return mFormatter.format(scaled); } public String labelledString(double value) { String units = label(); String num = valueOf(value); return String.format("%s: %s", units, num); } public String labelledString(long value) { return labelledString((double) value); } public String label() { if (mScale == 1.0) return "usec"; if (mScale == 0.001) return "msec"; if (mScale == 0.000001) return "sec"; return null; } public void setTimeScale(TimeScale val) { mTimeScale = val; switch (val) { case Seconds: mScale = 0.000001; break; case MilliSeconds: mScale = 0.001; break; case MicroSeconds: mScale = 1.0; break; } } public TimeScale getTimeScale() { return mTimeScale; } public enum TimeScale { Seconds, MilliSeconds, MicroSeconds }; } traceview/src/main/resources/0040755 0000000 0000000 00000000000 12747325007 015335 5ustar000000000 0000000 traceview/src/main/resources/icons/0040755 0000000 0000000 00000000000 12747325007 016450 5ustar000000000 0000000 traceview/src/main/resources/icons/sort_down.png0100644 0000000 0000000 00000000146 12747325007 021172 0ustar000000000 0000000 PNG  IHDR 2Ͻ-IDATc`0?0R VbtyA&2E!8ȨIENDB`traceview/src/main/resources/icons/sort_up.png0100644 0000000 0000000 00000000151 12747325007 020643 0ustar000000000 0000000 PNG  IHDR 2Ͻ0IDATc`T?5 Qx pP)PCj` q)YSӓVjIENDB`traceview/src/main/resources/icons/traceview-128.png0100644 0000000 0000000 00000041353 12747325007 021462 0ustar000000000 0000000 PNG  IHDR>atEXtSoftwareAdobe ImageReadyqe<fiTXtXML:com.adobe.xmp 'dVU+[ q^/49t?NLn.lʰ.s{qŵC[Yxǿ|g1i[ut"0;=vD CobfONYWԦtF?#KmL4IWtKȐx6"kh:Zkk<2q6{͖O b|1C/{^\'@;=-×ix;pڶ?7yt?]!sAh]ͥJ[,L1tqz!5O) Kömm6[VQ4[w C!Pu=W#"?hT&V>fo|W_,r& FeW9H-#c4Ed,vx4L&(` iҟ#V @OR85fƜDh-aPAmPrڇ?lUGxBbmNG^NߖLF ?4t.ӈrV"vG`;(v˃f^A/3?ܔ 5**r-t[ fn [Р߀ r} )Ři=HT>qyv?#lνOM\%"xTq@%~:6֡}Uu֛.]_-)-˦i4L9=GC:j8ЬT+: נDfP'NČg-D&AB YrEG.ds5$ Ri$$ ͔umDXEqFb֘88OlV ApOneW3.CA>]7{>4\zI>3٬o@0ZM5&[ppv î'Wݫ_<&"c9`tl.|Zh'3dP4Rx@Á]=Z*!H߅oxզKƽ߫<%"뤍|zcib$7x * nU>Ղ'7|ԃܯ4lB_)#[xt&+KdEX)?xhc؛HoO:l>6Zđ,G\ȃw:To` "Gbp:9m$5ι?oUH$ "x t G}58@x~uQ)lT7 CH뒮HJ}h .Ep08fzl?BM  !*nB$,`dII/N͍iWl}; $jsͷe8犰6oJ? $ <Ø`p&m 9q܂C{\?p5د!PSP㱒pR8}}0И/C\A\g>)}3i>4vhJ( y1w|@ͲL>ZvMp`,04 4_.*~pUZIz~ οTt/p=|Q&g>/⥇Z!G_ }}AU䳬-_˖A~`@z \'ql SG-t!W^n0WIaf 6B!s\iT=<`Ý7џJ,e!g2<`K7`3PA0B#`1rU%4_"++q#PiCn04 ’ ڄHWtlyF ~CIz#ß~f` p"ڼ lTڐj!ͳIFa:GuU]ܱLdг<7{٫eG0)`޾|]>(џt.ڷ֟rV׊H'l{W~ـoV/<[E#Qd~f,Tb#&huF>`MpE|Jy嘮 !쟹Je ipl jFC0%^<|qނ4?VxòwŚ|eFpzF(@ NP`Y~LyAkI@4ԦXĎ=YlMw,r?YBs3?{$Cu^Z)Sܚ؃aioցWe>gm8{G_FC x{ _D?˳?𩣍4fwRn2!& GE8S @RL@S3Sz D$>|^S=W@X!;B.~&UB`pĒ$hr4=od-?Ǐ#@מ8kRj=X=FDfGDp\Hʈ>QKqŭ-xy?qZOt2} 6;0PaI!E9 bҎXox_%!,|PZЙb,NYrl. ;z$܏-&A4-10XGU|"`x˜`W]X1 IҼ%iYd Th5\nXy#uSO;N<8,E9QRZZ\B>QīR6xܲ榚Ь;輆5|`8sկOR#k7קSCG"d\E Yre =KQyհh98!{7G 4Lk2v܀L0 ݁?'g…Zxo lyР=yHoaO:M T$YX})?ȕb<kH~ݩoHyY}cFA,iz c||X&&hܝ@J j5±bȿBL<Ʀs9ljxt55&8NA4F!AyUăDPb7v= lB&G#'`N2O7| k#1wJvqvʆ폮CtD7rk')^9@mf1>LFU:?nnYILšAEJ;\BjS[ZsRK:@p.q]h6n08'kg[L]d>D{sAI˿ Q!xfklvˑ@p,Bi]^'x] 2a";6@"ɿ.}e{- aާpBQy@%׼dbtpfȲ[s"d̼QCem?4dpJPDyS`k"r>#fI #`KPs dirE 'I`?.$U* vdڇF=wwZ̃㦅^AkDet<Ͼg{a3ψuk H^ҡo|+rb|\B R'w?8PDJιSu_6h [%_'_5 CKYk|βIylкAxg`~}f-9OA!baYs࡮)tV#j/yS֜* Ȭ;3잍bC|F qQ 1ϖkҎ+OJL+C.FWv}oG&URih`UP1:3xh/:U g 08U|7U9.Nʔ6z*ޅ݇XZ2iWpW Id! ez2's03z- 苘ޱԕ.M, ]w7V;OY,zL҂ُL~Qo<1]5H,ͥ|z-ɦ,bf6w==XQ_`-ab@${^!D% Q!HCGu'PqdOfbo-`>I\,Jt9X{Żwy+մT#qoQ'O3vEܟkW`ZC}(fc!wc``9t kjG?2Yx)GE<^jy[{Fr黎wp] {wA)YTMDPn©O/. g?g Ͳnc:@WB]!.!l-:939J=Y5FNHq.^,[9%eX'nT3N$$i/X] U&!% bF ?Py?בC L 4Ր rsd;[M WB?9Ջ}F(peo?Y /O[TQbݔbuEr%U* THFr>v!|9Ă#r^|#H%T7 f'Iz@7?w.ʝE8V*jI2r!}χ# ԕ@7九ARSB e/ ^푬@@Ձay7=@&KK̑LZfvCB`/JB8U 'Dm 骹CnHMwZ0I1n([$ aAPlAƴMD@*놞]H^\ c&@}n5u|fdN9T Uk_Igf/ F,AfJ[)UKS4Y}+\,tK !ODII*\FូDX.i} ,Hg\}^^= 1BV@$:yTo^f %bdQbPxQ"[^wL)*yrlJY!"V O>EC8j#(ͷG=nWBf6cB( HaߴO*р`zχD:`6aL)R Vkd3/r Ŗۂ*4kUq1?HD"iF!FjfrM#8Z8T3 $_up<ֻ:4>1Tّ/r:Laʶ"Dttܪ:|n@6-x||x{gvR)C-'X ;M8_ Z筐a ûT#4 UL}Hejd `tOl23DDlCޘ"4`Ϯx&Õ\n y R(z(EJerbE(Bۘ #^+ Ku |nxD{9UU =s6?*'oS鈌Ei#g*LQkuVe BAXէ&ωO;wj!"ӄrEeҢ~Ͼl>@g4C|~e0=N^>M[SYO_<0&<USϫ6nFCs|1$' ,xƅ9?O ܭxf<Ќr+ v*,*#P5<]+L!DfF5ϤJ@A6qg)64]Tߖ%djݚ :gU>%EhJU7dtr\*%f[zU}.7{TՆ U1gbl8hԠA jn _Oz,E9}ĕgʦ/(Dl LDXtEH~6؋?%DqJ^,PVg*AeR[]UT- A{UWg!i9s'#'OӰ#(d"\gM0,|88=f"'ـU (4.Eeְ&.b-L* RYaRJA -hvg4 *$HZc*\ju gP]f]S8@fn ߅fZxk yi%E&rJ,mѨۄC&Mx)TYOYجZ]5%:-* 1 6, /[ %sQ=9c1U`Nz=D\3v-Y2.E+4Mv%! ~tEgix !t ⭂'/K*s %@~D8;(GpPZ.){' zй }t<(hl4dE<B8\,A.Kb.|âj0C-곥ʙ4]UqI.y^4 <2ʞUO\\G+~o:vs'ϸ7iV?Gs\3$Njc]+$t.wuC^1l {Ang;b PuyojJ2&ܦצHeO_-/K<-|#D@~]^".w G'I apG8.ţ*dMc'^-2@ \ 0n@\)|^WYdԄz#EM A&]B&>~!iRo\ ZX@GNGpu3mvVd)֍JA KyN8eыaG,_T'8 EGXURX!s70W#8FZGDA$lqޜg"Ppad NEtMⴕ,;℉jjߣ=cK   ܒsRrcb#pDR;\rg3/jޛWFVlp)x~TY:Aȕ-#Ɲ4*&ڬ" v|7\4 F3QX𺦽%i@JtMG,mLEϙʸcs:<׼p=V N` &ঁ @fÉ[}Orqnޓ8!H]kP8~kU߹Lߩ8,= \,tzLt1,Ig"X pnp qI8^D߅kv6 UN@~ 9rσkִgFRkn,>N IJHfO;k%; 30JqzTU94~.`LS7#X0@  C7=EH҂S1a)hx8Fp tU@Z/ePr$1`el{=\ʻK=Mɕ;v J *kUa)gq,X qxyȶ#ؐ[p MlKE$#=1Yb 75Հܵ3EְcI+€LOrt}Q td[M;BBM%|hQ0DjI?VP/_`ߘ5«j Upo] $Ѩ_P(/@ !yYm3O]?E@]pR1йɣuۖixw׬߽T!k6 ',@7uv?8x DA.ѻ\RHXl TID 7`Cv)gpҙ&T[Uz1C 4u|ypJPqV˄g7CIZ/B4E37`_%ߝ%*tyD6Yª7Cm˭A S/HDuS7ZٌLؠ=C68MBRg?#:bcGdWv}0o^I<J߯o` Y".x|5nm\!+dg2 ],!ati)hd(yWF5)?ÈL{9%йŒpyߙ$J]dj rm;R'fTnŊ]fjV5xε*gM!#XU86ٱF Hu'Zj"Qv(BYU GǙAkG`S9 N:=~s=@\qϷcOĪfp 2pdt'+-k`D$-t8oO'Ӎ.I]Z Z?O5`O8Pf_C^DԿ)x6&$rW&ϸ@۶"CCLŭ5&ƥ -߯Z=Epe `D"E#a};ܟ-8чDEA'"w_F|Qv_0}sYxyAh?Ux$ˢfO3GJ Sm h ߩ!R'%xͣ<8Rb':jμ -`SWYT;C H Jͷwn8E@fŐ`47[EVWy 9 RTƓqH&WQ -1p6 EQ(b85M\[DjyxTBهRw᢫&M.`Ѩ]kZkVOD/{w؟Yqxae©9:p P)w?ft)Tv\׺6 _:»%vm>x㻖 l&`&Fz@O+G ;伳c*뵆Hh<%źP9YLE2J>6$(1$503~f VƾIk=NS7QRv`JpًK q3`I0{8Kμ6]pRýU$w,si2_'ZOMyrT1eljuF@9/N-D@pCsi/a`āe`aȿU>:O˭BA5hېNR޺hqc͎Hu:>]kmM:D+MEV^?LdW 'S!PM(> MDu>j]82) $1k(*1&r4R_j A>?2NTL>1icb@l+ڹn]y&vOI E!-%ۀ>D-Cy%b 4C?#ϥe2󓓁b6tYuO K @d" ̵D^6|,[mMh~??oƯ" . n /VYnrS6#ǁ.,™Eю'`~ !4zA郇`b>R([i6;8C(~_4zmPC Ad#DG{+yyy3LGWP:w2Sh]٭\DBJd>xVIt>s3 HY0:856QVSԚWh6], gPt $Z6]7\~KD3 Ŝg~R>zLX< lgy!Y$:aD3QKY W@{d#,_.t s(fC9+xP<REjֱ| Zjv?:,Yie/{ZY$}hwhs=[7W {C+ޡ>G6^6Bl=r3嬏3:a&Y@]!eErqg%ݵCEnD6ls7lȖ@3s@VJ`Y%8 =;;ݤ(dWn#%]XoC- h9f77=98_ñy1F]Nj(KC/ Ө,j+ppOKCpp5sG$UzJH["f]H `tUFV8,XlP"dEOj \uab| 1@ሙx">!0>P%k6_TC8jEL6|eZ oiyH`ɮ2j $dj*= K3ڲ17]lWDyE/Sp''t ^:ޔ˛k9SqM 5Cpܗ1=@\ FIأ'·*αۿ 7U¥PsŰbQ'kSX:>0 ^t%&_0NZa; Y?OtVTw zpQQE|(U|~O#ĉ@B?.|1\1+seF9BBcVez ~pLЪE>wC1Gq@7nCL )^ Vo,+rycZh65&FjX:A윞[wov0PYd򣟏:/M^AXBPB= شt5\QY8 9yTD$)t*oZho >|+l^݌Pn쾗Y~w'H, A,XW`~J3.u`Ʀ|NCai}hZV/xv-22[ћvGh]RݐOc^􄈲ru{k6 NHga%H(#dXwQš uiautomatorviewer/0040755 0000000 0000000 00000000000 12747325007 013412 5ustar000000000 0000000 uiautomatorviewer/.classpath0100644 0000000 0000000 00000002031 12747325007 015366 0ustar000000000 0000000 uiautomatorviewer/.project0100644 0000000 0000000 00000000570 12747325007 015060 0ustar000000000 0000000 uiautomatorviewer org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature uiautomatorviewer/.settings/0040755 0000000 0000000 00000000000 12747325007 015330 5ustar000000000 0000000 uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs0100644 0000000 0000000 00000015226 12747325007 022315 0ustar000000000 0000000 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled org.eclipse.jdt.core.compiler.problem.fieldHiding=warning org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 uiautomatorviewer/MODULE_LICENSE_APACHE20100644 0000000 0000000 00000000000 12747325007 016532 0ustar000000000 0000000 uiautomatorviewer/NOTICE0100644 0000000 0000000 00000024707 12747325007 014325 0ustar000000000 0000000 Copyright (c) 2005-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. 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. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS uiautomatorviewer/build.gradle0100644 0000000 0000000 00000001042 12747325007 015663 0ustar000000000 0000000 group = 'com.android.tools' archivesBaseName = 'uiautomatorviewer' dependencies { compile project(':base:ddmlib') } sourceSets { main.resources.srcDir 'src/main/java' } sdk { linux { item('etc/uiautomatorviewer') { executable true } } mac { item('etc/uiautomatorviewer') { executable true } } windows { item 'etc/uiautomatorviewer.bat' } } // configure the manifest of the buildDistributionJar task. sdkJar.manifest.attributes("Main-Class": "com.android.uiautomator.UiAutomatorViewer") uiautomatorviewer/etc/0040755 0000000 0000000 00000000000 12747325007 014165 5ustar000000000 0000000 uiautomatorviewer/etc/uiautomatorviewer0100755 0000000 0000000 00000005756 12747325007 017720 0ustar000000000 0000000 #!/bin/bash # # Copyright 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. # Set up prog to be the path of this script, including following symlinks, # and set up progdir to be the fully-qualified pathname of its directory. prog="$0" while [ -h "${prog}" ]; do newProg=`/bin/ls -ld "${prog}"` newProg=`expr "${newProg}" : ".* -> \(.*\)$"` if expr "x${newProg}" : 'x/' >/dev/null; then prog="${newProg}" else progdir=`dirname "${prog}"` prog="${progdir}/${newProg}" fi done oldwd=`pwd` progdir=`dirname "${prog}"` progname=`basename "${prog}"` cd "${progdir}" progdir=`pwd` prog="${progdir}"/"${progname}" cd "${oldwd}" jarfile=uiautomatorviewer.jar frameworkdir="$progdir" libdir="$progdir" if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/tools/lib libdir=`dirname "$progdir"`/tools/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then frameworkdir=`dirname "$progdir"`/framework libdir=`dirname "$progdir"`/lib fi if [ ! -r "$frameworkdir/$jarfile" ] then echo "${progname}: can't find $jarfile" exit 1 fi javaCmd="java" os=`uname` if [ $os == 'Darwin' ]; then javaOpts="-Xmx1600M -XstartOnFirstThread" else javaOpts="-Xmx1600M" fi if [ `uname` = "Linux" ]; then export GDK_NATIVE_WINDOWS=true fi while expr "x$1" : 'x-J' >/dev/null; do opt=`expr "x$1" : 'x-J\(.*\)'` javaOpts="${javaOpts} -${opt}" shift done jarpath="$frameworkdir/$jarfile" # Figure out the path to the swt.jar for the current architecture. # if ANDROID_SWT is defined, then just use this. # else, if running in the Android source tree, then look for the correct swt folder in prebuilt # else, look for the correct swt folder in the SDK under tools/lib/ swtpath="" if [ -n "$ANDROID_SWT" ]; then swtpath="$ANDROID_SWT" else vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` if [ -n "$ANDROID_BUILD_TOP" ]; then osname=`uname -s | tr A-Z a-z` swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" else swtpath="${frameworkdir}/${vmarch}" fi fi # Combine the swtpath and the framework dir path. if [ -d "$swtpath" ]; then frameworkdir="${swtpath}:${frameworkdir}" else echo "SWT folder '${swtpath}' does not exist." echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." exit 1 fi exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.uiautomator.bindir="$progdir" -jar "$jarpath" "$@" uiautomatorviewer/etc/uiautomatorviewer.bat0100755 0000000 0000000 00000004033 12747325007 020450 0ustar000000000 0000000 @echo off rem Copyright (C) 2012 The Android Open Source Project rem rem Licensed under the Apache License, Version 2.0 (the "License"); rem you may not use this file except in compliance with the License. rem You may obtain a copy of the License at rem rem http://www.apache.org/licenses/LICENSE-2.0 rem rem Unless required by applicable law or agreed to in writing, software rem distributed under the License is distributed on an "AS IS" BASIS, rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. rem See the License for the specific language governing permissions and rem limitations under the License. rem don't modify the caller's environment setlocal rem Set up prog to be the path of this script, including following symlinks, rem and set up progdir to be the fully-qualified pathname of its directory. set prog=%~f0 rem Change current directory and drive to where the script is, to avoid rem issues with directories containing whitespaces. cd /d %~dp0 rem Get the CWD as a full path with short names only (without spaces) for %%i in ("%cd%") do set prog_dir=%%~fsi rem Check we have a valid Java.exe in the path. set java_exe= call lib\find_java.bat if not defined java_exe goto :EOF set jarfile=uiautomatorviewer.jar set frameworkdir=. if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=lib if exist %frameworkdir%\%jarfile% goto JarFileOk set frameworkdir=..\framework :JarFileOk set jarpath=%frameworkdir%\%jarfile% if not defined ANDROID_SWT goto QueryArch set swt_path=%ANDROID_SWT% goto SwtDone :QueryArch for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a :SwtDone if exist "%swt_path%" goto SetPath echo SWT folder '%swt_path%' does not exist. echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. exit /B :SetPath set javaextdirs=%swt_path%;%frameworkdir% call "%java_exe%" "-Djava.ext.dirs=%javaextdirs%" "-Dcom.android.uiautomator.bindir=%prog_dir%" -jar %jarpath% %* uiautomatorviewer/src/0040755 0000000 0000000 00000000000 12747325007 014201 5ustar000000000 0000000 uiautomatorviewer/src/main/0040755 0000000 0000000 00000000000 12747325007 015125 5ustar000000000 0000000 uiautomatorviewer/src/main/java/0040755 0000000 0000000 00000000000 12747325007 016046 5ustar000000000 0000000 uiautomatorviewer/src/main/java/com/0040755 0000000 0000000 00000000000 12747325007 016624 5ustar000000000 0000000 uiautomatorviewer/src/main/java/com/android/0040755 0000000 0000000 00000000000 12747325007 020244 5ustar000000000 0000000 uiautomatorviewer/src/main/java/com/android/uiautomator/0040755 0000000 0000000 00000000000 12747325007 022615 5ustar000000000 0000000 uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java0100644 0000000 0000000 00000005267 12747325007 025632 0ustar000000000 0000000 /* * 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.uiautomator; import com.android.SdkConstants; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; import java.io.File; import java.util.Arrays; import java.util.List; public class DebugBridge { private static AndroidDebugBridge sDebugBridge; private static String getAdbLocation() { String toolsDir = System.getProperty("com.android.uiautomator.bindir"); //$NON-NLS-1$ if (toolsDir == null) { return null; } File sdk = new File(toolsDir).getParentFile(); // check if adb is present in platform-tools File platformTools = new File(sdk, "platform-tools"); File adb = new File(platformTools, SdkConstants.FN_ADB); if (adb.exists()) { return adb.getAbsolutePath(); } // check if adb is present in the tools directory adb = new File(toolsDir, SdkConstants.FN_ADB); if (adb.exists()) { return adb.getAbsolutePath(); } // check if we're in the Android source tree where adb is in $ANDROID_HOST_OUT/bin/adb String androidOut = System.getenv("ANDROID_HOST_OUT"); if (androidOut != null) { String adbLocation = androidOut + File.separator + "bin" + File.separator + SdkConstants.FN_ADB; if (new File(adbLocation).exists()) { return adbLocation; } } return null; } public static void init() { String adbLocation = getAdbLocation(); if (adbLocation != null) { AndroidDebugBridge.init(false /* debugger support */); sDebugBridge = AndroidDebugBridge.createBridge(adbLocation, false); } } public static void terminate() { if (sDebugBridge != null) { sDebugBridge = null; AndroidDebugBridge.terminate(); } } public static boolean isInitialized() { return sDebugBridge != null; } public static List getDevices() { return Arrays.asList(sDebugBridge.getDevices()); } } uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java0100644 0000000 0000000 00000020157 12747325007 025503 0ustar000000000 0000000 /* * 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.uiautomator; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import java.io.File; /** * Implements a file selection dialog for both screen shot and xml dump file * * "OK" button won't be enabled unless both files are selected * It also has a convenience feature such that if one file has been picked, and the other * file path is empty, then selection for the other file will start from the same base folder * */ public class OpenDialog extends Dialog { private static final int FIXED_TEXT_FIELD_WIDTH = 300; private static final int DEFAULT_LAYOUT_SPACING = 10; private Text mScreenshotText; private Text mXmlText; private boolean mFileChanged = false; private Button mOkButton; private static File sScreenshotFile; private static File sXmlDumpFile; /** * Create the dialog. * @param parentShell */ public OpenDialog(Shell parentShell) { super(parentShell); setShellStyle(SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); } /** * Create contents of the dialog. * @param parent */ @Override protected Control createDialogArea(Composite parent) { Composite container = (Composite) super.createDialogArea(parent); GridLayout gl_container = new GridLayout(1, false); gl_container.verticalSpacing = DEFAULT_LAYOUT_SPACING; gl_container.horizontalSpacing = DEFAULT_LAYOUT_SPACING; gl_container.marginWidth = DEFAULT_LAYOUT_SPACING; gl_container.marginHeight = DEFAULT_LAYOUT_SPACING; container.setLayout(gl_container); Group openScreenshotGroup = new Group(container, SWT.NONE); openScreenshotGroup.setLayout(new GridLayout(2, false)); openScreenshotGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); openScreenshotGroup.setText("Screenshot"); mScreenshotText = new Text(openScreenshotGroup, SWT.BORDER | SWT.READ_ONLY); if (sScreenshotFile != null) { mScreenshotText.setText(sScreenshotFile.getAbsolutePath()); } GridData gd_screenShotText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); gd_screenShotText.minimumWidth = FIXED_TEXT_FIELD_WIDTH; gd_screenShotText.widthHint = FIXED_TEXT_FIELD_WIDTH; mScreenshotText.setLayoutData(gd_screenShotText); Button openScreenshotButton = new Button(openScreenshotGroup, SWT.NONE); openScreenshotButton.setText("..."); openScreenshotButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { handleOpenScreenshotFile(); } }); Group openXmlGroup = new Group(container, SWT.NONE); openXmlGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); openXmlGroup.setText("UI XML Dump"); openXmlGroup.setLayout(new GridLayout(2, false)); mXmlText = new Text(openXmlGroup, SWT.BORDER | SWT.READ_ONLY); mXmlText.setEditable(false); if (sXmlDumpFile != null) { mXmlText.setText(sXmlDumpFile.getAbsolutePath()); } GridData gd_xmlText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); gd_xmlText.minimumWidth = FIXED_TEXT_FIELD_WIDTH; gd_xmlText.widthHint = FIXED_TEXT_FIELD_WIDTH; mXmlText.setLayoutData(gd_xmlText); Button openXmlButton = new Button(openXmlGroup, SWT.NONE); openXmlButton.setText("..."); openXmlButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { handleOpenXmlDumpFile(); } }); return container; } /** * Create contents of the button bar. * @param parent */ @Override protected void createButtonsForButtonBar(Composite parent) { mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); updateButtonState(); } /** * Return the initial size of the dialog. */ @Override protected Point getInitialSize() { return new Point(368, 233); } @Override protected void configureShell(Shell newShell) { super.configureShell(newShell); newShell.setText("Open UI Dump Files"); } private void handleOpenScreenshotFile() { FileDialog fd = new FileDialog(getShell(), SWT.OPEN); fd.setText("Open Screenshot File"); File initialFile = sScreenshotFile; // if file has never been selected before, try to base initial path on the mXmlDumpFile if (initialFile == null && sXmlDumpFile != null && sXmlDumpFile.isFile()) { initialFile = sXmlDumpFile.getParentFile(); } if (initialFile != null) { if (initialFile.isFile()) { fd.setFileName(initialFile.getAbsolutePath()); } else if (initialFile.isDirectory()) { fd.setFilterPath(initialFile.getAbsolutePath()); } } String[] filter = {"*.png"}; fd.setFilterExtensions(filter); String selected = fd.open(); if (selected != null) { sScreenshotFile = new File(selected); mScreenshotText.setText(selected); mFileChanged = true; } updateButtonState(); } private void handleOpenXmlDumpFile() { FileDialog fd = new FileDialog(getShell(), SWT.OPEN); fd.setText("Open UI Dump XML File"); File initialFile = sXmlDumpFile; // if file has never been selected before, try to base initial path on the mScreenshotFile if (initialFile == null && sScreenshotFile != null && sScreenshotFile.isFile()) { initialFile = sScreenshotFile.getParentFile(); } if (initialFile != null) { if (initialFile.isFile()) { fd.setFileName(initialFile.getAbsolutePath()); } else if (initialFile.isDirectory()) { fd.setFilterPath(initialFile.getAbsolutePath()); } } String initialPath = mXmlText.getText(); if (initialPath.isEmpty() && sScreenshotFile != null && sScreenshotFile.isFile()) { initialPath = sScreenshotFile.getParentFile().getAbsolutePath(); } String[] filter = {"*.uix"}; fd.setFilterExtensions(filter); String selected = fd.open(); if (selected != null) { sXmlDumpFile = new File(selected); mXmlText.setText(selected); mFileChanged = true; } updateButtonState(); } private void updateButtonState() { mOkButton.setEnabled(sXmlDumpFile != null && sXmlDumpFile.isFile()); } public boolean hasFileChanged() { return mFileChanged; } public File getScreenshotFile() { return sScreenshotFile; } public File getXmlDumpFile() { return sXmlDumpFile; } } uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java0100644 0000000 0000000 00000017703 12747325007 027076 0ustar000000000 0000000 /* * 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.uiautomator; import com.android.ddmlib.CollectingOutputReceiver; import com.android.ddmlib.IDevice; import com.android.ddmlib.RawImage; import com.android.ddmlib.SyncService; import com.android.uiautomator.tree.BasicTreeNode; import com.android.uiautomator.tree.RootWindowNode; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.widgets.Display; import java.io.File; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class UiAutomatorHelper { public static final int UIAUTOMATOR_MIN_API_LEVEL = 16; private static final String UIAUTOMATOR = "/system/bin/uiautomator"; //$NON-NLS-1$ private static final String UIAUTOMATOR_DUMP_COMMAND = "dump"; //$NON-NLS-1$ private static final String UIDUMP_DEVICE_PATH = "/data/local/tmp/uidump.xml"; //$NON-NLS-1$ private static final int XML_CAPTURE_TIMEOUT_SEC = 40; private static boolean supportsUiAutomator(IDevice device) { String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL); int apiLevel; try { apiLevel = Integer.parseInt(apiLevelString); } catch (NumberFormatException e) { apiLevel = UIAUTOMATOR_MIN_API_LEVEL; } return apiLevel >= UIAUTOMATOR_MIN_API_LEVEL; } private static void getUiHierarchyFile(IDevice device, File dst, IProgressMonitor monitor, boolean compressed) { if (monitor == null) { monitor = new NullProgressMonitor(); } monitor.subTask("Deleting old UI XML snapshot ..."); String command = "rm " + UIDUMP_DEVICE_PATH; try { CountDownLatch commandCompleteLatch = new CountDownLatch(1); device.executeShellCommand(command, new CollectingOutputReceiver(commandCompleteLatch)); commandCompleteLatch.await(5, TimeUnit.SECONDS); } catch (Exception e1) { // ignore exceptions while deleting stale files } monitor.subTask("Taking UI XML snapshot..."); if (compressed){ command = String.format("%s %s --compressed %s", UIAUTOMATOR, UIAUTOMATOR_DUMP_COMMAND, UIDUMP_DEVICE_PATH); } else { command = String.format("%s %s %s", UIAUTOMATOR, UIAUTOMATOR_DUMP_COMMAND, UIDUMP_DEVICE_PATH); } CountDownLatch commandCompleteLatch = new CountDownLatch(1); try { device.executeShellCommand( command, new CollectingOutputReceiver(commandCompleteLatch), XML_CAPTURE_TIMEOUT_SEC * 1000); commandCompleteLatch.await(XML_CAPTURE_TIMEOUT_SEC, TimeUnit.SECONDS); monitor.subTask("Pull UI XML snapshot from device..."); device.getSyncService().pullFile(UIDUMP_DEVICE_PATH, dst.getAbsolutePath(), SyncService.getNullProgressMonitor()); } catch (Exception e) { throw new RuntimeException(e); } } //to maintain a backward compatible api, use non-compressed as default snapshot type public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor) throws UiAutomatorException { return takeSnapshot(device, monitor,false); } public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor, boolean compressed) throws UiAutomatorException { if (monitor == null) { monitor = new NullProgressMonitor(); } monitor.subTask("Checking if device support UI Automator"); if (!supportsUiAutomator(device)) { String msg = "UI Automator requires a device with API Level " + UIAUTOMATOR_MIN_API_LEVEL; throw new UiAutomatorException(msg, null); } monitor.subTask("Creating temporary files for uiautomator results."); File tmpDir = null; File xmlDumpFile = null; File screenshotFile = null; try { tmpDir = File.createTempFile("uiautomatorviewer_", ""); tmpDir.delete(); if (!tmpDir.mkdirs()) throw new IOException("Failed to mkdir"); xmlDumpFile = File.createTempFile("dump_", ".uix", tmpDir); screenshotFile = File.createTempFile("screenshot_", ".png", tmpDir); } catch (Exception e) { String msg = "Error while creating temporary file to save snapshot: " + e.getMessage(); throw new UiAutomatorException(msg, e); } tmpDir.deleteOnExit(); xmlDumpFile.deleteOnExit(); screenshotFile.deleteOnExit(); monitor.subTask("Obtaining UI hierarchy"); try { UiAutomatorHelper.getUiHierarchyFile(device, xmlDumpFile, monitor, compressed); } catch (Exception e) { String msg = "Error while obtaining UI hierarchy XML file: " + e.getMessage(); throw new UiAutomatorException(msg, e); } UiAutomatorModel model; try { model = new UiAutomatorModel(xmlDumpFile); } catch (Exception e) { String msg = "Error while parsing UI hierarchy XML file: " + e.getMessage(); throw new UiAutomatorException(msg, e); } monitor.subTask("Obtaining device screenshot"); RawImage rawImage; try { rawImage = device.getScreenshot(); } catch (Exception e) { String msg = "Error taking device screenshot: " + e.getMessage(); throw new UiAutomatorException(msg, e); } // rotate the screen shot per device rotation BasicTreeNode root = model.getXmlRootNode(); if (root instanceof RootWindowNode) { for (int i = 0; i < ((RootWindowNode)root).getRotation(); i++) { rawImage = rawImage.getRotated(); } } PaletteData palette = new PaletteData( rawImage.getRedMask(), rawImage.getGreenMask(), rawImage.getBlueMask()); ImageData imageData = new ImageData(rawImage.width, rawImage.height, rawImage.bpp, palette, 1, rawImage.data); ImageLoader loader = new ImageLoader(); loader.data = new ImageData[] { imageData }; loader.save(screenshotFile.getAbsolutePath(), SWT.IMAGE_PNG); Image screenshot = new Image(Display.getDefault(), imageData); return new UiAutomatorResult(xmlDumpFile, model, screenshot); } @SuppressWarnings("serial") public static class UiAutomatorException extends Exception { public UiAutomatorException(String msg, Throwable t) { super(msg, t); } } public static class UiAutomatorResult { public final File uiHierarchy; public final UiAutomatorModel model; public final Image screenshot; public UiAutomatorResult(File uiXml, UiAutomatorModel m, Image s) { uiHierarchy = uiXml; model = m; screenshot = s; } } } uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java0100644 0000000 0000000 00000012160 12747325007 026707 0ustar000000000 0000000 /* * 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.uiautomator; import com.android.uiautomator.tree.AttributePair; import com.android.uiautomator.tree.BasicTreeNode; import com.android.uiautomator.tree.BasicTreeNode.IFindNodeListener; import com.android.uiautomator.tree.UiHierarchyXmlLoader; import com.android.uiautomator.tree.UiNode; import org.eclipse.swt.graphics.Rectangle; import java.io.File; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class UiAutomatorModel { private BasicTreeNode mRootNode; private BasicTreeNode mSelectedNode; private Rectangle mCurrentDrawingRect; private List mNafNodes; // determines whether we lookup the leaf UI node on mouse move of screenshot image private boolean mExploreMode = true; private boolean mShowNafNodes = false; private List mNodelist; private Set mSearchKeySet = new HashSet(); public UiAutomatorModel(File xmlDumpFile) { mSearchKeySet.add("text"); mSearchKeySet.add("content-desc"); UiHierarchyXmlLoader loader = new UiHierarchyXmlLoader(); BasicTreeNode rootNode = loader.parseXml(xmlDumpFile.getAbsolutePath()); if (rootNode == null) { System.err.println("null rootnode after parsing."); throw new IllegalArgumentException("Invalid ui automator hierarchy file."); } mNafNodes = loader.getNafNodes(); if (mRootNode != null) { mRootNode.clearAllChildren(); } mRootNode = rootNode; mExploreMode = true; mNodelist = loader.getAllNodes(); } public BasicTreeNode getXmlRootNode() { return mRootNode; } public BasicTreeNode getSelectedNode() { return mSelectedNode; } /** * change node selection in the Model recalculate the rect to highlight, * also notifies the View to refresh accordingly * * @param node */ public void setSelectedNode(BasicTreeNode node) { mSelectedNode = node; if (mSelectedNode instanceof UiNode) { UiNode uiNode = (UiNode) mSelectedNode; mCurrentDrawingRect = new Rectangle(uiNode.x, uiNode.y, uiNode.width, uiNode.height); } else { mCurrentDrawingRect = null; } } public Rectangle getCurrentDrawingRect() { return mCurrentDrawingRect; } /** * Do a search in tree to find a leaf node or deepest parent node containing the coordinate * * @param x * @param y * @return */ public BasicTreeNode updateSelectionForCoordinates(int x, int y) { BasicTreeNode node = null; if (mRootNode != null) { MinAreaFindNodeListener listener = new MinAreaFindNodeListener(); boolean found = mRootNode.findLeafMostNodesAtPoint(x, y, listener); if (found && listener.mNode != null && !listener.mNode.equals(mSelectedNode)) { node = listener.mNode; } } return node; } public boolean isExploreMode() { return mExploreMode; } public void toggleExploreMode() { mExploreMode = !mExploreMode; } public void setExploreMode(boolean exploreMode) { mExploreMode = exploreMode; } private static class MinAreaFindNodeListener implements IFindNodeListener { BasicTreeNode mNode = null; @Override public void onFoundNode(BasicTreeNode node) { if (mNode == null) { mNode = node; } else { if ((node.height * node.width) < (mNode.height * mNode.width)) { mNode = node; } } } } public List getNafNodes() { return mNafNodes; } public void toggleShowNaf() { mShowNafNodes = !mShowNafNodes; } public boolean shouldShowNafNodes() { return mShowNafNodes; } public List searchNode(String tofind) { List result = new LinkedList(); for (BasicTreeNode node : mNodelist) { Object[] attrs = node.getAttributesArray(); for (Object attr : attrs) { if (!mSearchKeySet.contains(((AttributePair) attr).key)) continue; if (((AttributePair) attr).value.toLowerCase().contains(tofind.toLowerCase())) { result.add(node); break; } } } return result; } } uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java0100644 0000000 0000000 00000061115 12747325007 026565 0ustar000000000 0000000 /* * 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.uiautomator; import com.android.uiautomator.actions.ExpandAllAction; import com.android.uiautomator.actions.ImageHelper; import com.android.uiautomator.actions.ToggleNafAction; import com.android.uiautomator.tree.AttributePair; import com.android.uiautomator.tree.BasicTreeNode; import com.android.uiautomator.tree.BasicTreeNodeContentProvider; import com.android.uiautomator.tree.UiNode; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.TextCellEditor; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Transform; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; import org.eclipse.swt.widgets.Tree; import java.io.File; import java.util.List; public class UiAutomatorView extends Composite { private static final int IMG_BORDER = 2; // The screenshot area is made of a stack layout of two components: screenshot canvas and // a "specify screenshot" button. If a screenshot is already available, then that is displayed // on the canvas. If it is not availble, then the "specify screenshot" button is displayed. private Composite mScreenshotComposite; private StackLayout mStackLayout; private Composite mSetScreenshotComposite; private Canvas mScreenshotCanvas; private TreeViewer mTreeViewer; private TableViewer mTableViewer; private float mScale = 1.0f; private int mDx, mDy; private UiAutomatorModel mModel; private File mModelFile; private Image mScreenshot; private List mSearchResult; private int mSearchResultIndex; private ToolItem itemDeleteAndInfo; private Text searchTextarea; private Cursor mOrginialCursor; private ToolItem itemPrev, itemNext; private ToolItem coordinateLabel; private String mLastSearchedTerm; private Cursor mCrossCursor; public UiAutomatorView(Composite parent, int style) { super(parent, SWT.NONE); setLayout(new FillLayout()); SashForm baseSash = new SashForm(this, SWT.HORIZONTAL); mOrginialCursor = getShell().getCursor(); mCrossCursor = new Cursor(getDisplay(), SWT.CURSOR_CROSS); mScreenshotComposite = new Composite(baseSash, SWT.BORDER); mStackLayout = new StackLayout(); mScreenshotComposite.setLayout(mStackLayout); // draw the canvas with border, so the divider area for sash form can be highlighted mScreenshotCanvas = new Canvas(mScreenshotComposite, SWT.BORDER); mStackLayout.topControl = mScreenshotCanvas; mScreenshotComposite.layout(); // set cursor when enter canvas mScreenshotCanvas.addListener(SWT.MouseEnter, new Listener() { @Override public void handleEvent(Event arg0) { getShell().setCursor(mCrossCursor); } }); mScreenshotCanvas.addListener(SWT.MouseExit, new Listener() { @Override public void handleEvent(Event arg0) { getShell().setCursor(mOrginialCursor); } }); mScreenshotCanvas.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent e) { if (mModel != null) { mModel.toggleExploreMode(); redrawScreenshot(); } } }); mScreenshotCanvas.setBackground( getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); mScreenshotCanvas.addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { if (mScreenshot != null) { updateScreenshotTransformation(); // shifting the image here, so that there's a border around screen shot // this makes highlighting red rectangles on the screen shot edges more visible Transform t = new Transform(e.gc.getDevice()); t.translate(mDx, mDy); t.scale(mScale, mScale); e.gc.setTransform(t); e.gc.drawImage(mScreenshot, 0, 0); // this resets the transformation to identity transform, i.e. no change // we don't use transformation here because it will cause the line pattern // and line width of highlight rect to be scaled, causing to appear to be blurry e.gc.setTransform(null); if (mModel.shouldShowNafNodes()) { // highlight the "Not Accessibility Friendly" nodes e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); for (Rectangle r : mModel.getNafNodes()) { e.gc.setAlpha(50); e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), getScaledSize(r.width), getScaledSize(r.height)); e.gc.setAlpha(255); e.gc.setLineStyle(SWT.LINE_SOLID); e.gc.setLineWidth(2); e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), getScaledSize(r.width), getScaledSize(r.height)); } } // draw the search result rects if (mSearchResult != null){ for (BasicTreeNode result : mSearchResult){ if (result instanceof UiNode) { UiNode uiNode = (UiNode) result; Rectangle rect = new Rectangle( uiNode.x, uiNode.y, uiNode.width, uiNode.height); e.gc.setForeground( e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); e.gc.setLineStyle(SWT.LINE_DASH); e.gc.setLineWidth(1); e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y), getScaledSize(rect.width), getScaledSize(rect.height)); } } } // draw the mouseover rects Rectangle rect = mModel.getCurrentDrawingRect(); if (rect != null) { e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED)); if (mModel.isExploreMode()) { // when we highlight nodes dynamically on mouse move, // use dashed borders e.gc.setLineStyle(SWT.LINE_DASH); e.gc.setLineWidth(1); } else { // when highlighting nodes on tree node selection, // use solid borders e.gc.setLineStyle(SWT.LINE_SOLID); e.gc.setLineWidth(2); } e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y), getScaledSize(rect.width), getScaledSize(rect.height)); } } } }); mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { if (mModel != null) { int x = getInverseScaledSize(e.x - mDx); int y = getInverseScaledSize(e.y - mDy); // show coordinate coordinateLabel.setText(String.format("(%d,%d)", x,y)); if (mModel.isExploreMode()) { BasicTreeNode node = mModel.updateSelectionForCoordinates(x, y); if (node != null) { updateTreeSelection(node); } } } } }); mSetScreenshotComposite = new Composite(mScreenshotComposite, SWT.NONE); mSetScreenshotComposite.setLayout(new GridLayout()); final Button setScreenshotButton = new Button(mSetScreenshotComposite, SWT.PUSH); setScreenshotButton.setText("Specify Screenshot..."); setScreenshotButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { FileDialog fd = new FileDialog(setScreenshotButton.getShell()); fd.setFilterExtensions(new String[] {"*.png" }); if (mModelFile != null) { fd.setFilterPath(mModelFile.getParent()); } String screenshotPath = fd.open(); if (screenshotPath == null) { return; } ImageData[] data; try { data = new ImageLoader().load(screenshotPath); } catch (Exception e) { return; } // "data" is an array, probably used to handle images that has multiple frames // i.e. gifs or icons, we just care if it has at least one here if (data.length < 1) { return; } mScreenshot = new Image(Display.getDefault(), data[0]); redrawScreenshot(); } }); // right sash is split into 2 parts: upper-right and lower-right // both are composites with borders, so that the horizontal divider can be highlighted by // the borders SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL); // upper-right base contains the toolbar and the tree Composite upperRightBase = new Composite(rightSash, SWT.BORDER); upperRightBase.setLayout(new GridLayout(1, false)); ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); toolBarManager.add(new ExpandAllAction(this)); toolBarManager.add(new ToggleNafAction(this)); ToolBar searchtoolbar = toolBarManager.createControl(upperRightBase); // add search box and navigation buttons for search results ToolItem itemSeparator = new ToolItem(searchtoolbar, SWT.SEPARATOR | SWT.RIGHT); searchTextarea = new Text(searchtoolbar, SWT.BORDER | SWT.SINGLE | SWT.SEARCH); searchTextarea.pack(); itemSeparator.setWidth(searchTextarea.getBounds().width); itemSeparator.setControl(searchTextarea); itemPrev = new ToolItem(searchtoolbar, SWT.SIMPLE); itemPrev.setImage(ImageHelper.loadImageDescriptorFromResource("images/prev.png") .createImage()); itemNext = new ToolItem(searchtoolbar, SWT.SIMPLE); itemNext.setImage(ImageHelper.loadImageDescriptorFromResource("images/next.png") .createImage()); itemDeleteAndInfo = new ToolItem(searchtoolbar, SWT.SIMPLE); itemDeleteAndInfo.setImage(ImageHelper.loadImageDescriptorFromResource("images/delete.png") .createImage()); itemDeleteAndInfo.setToolTipText("Clear search results"); coordinateLabel = new ToolItem(searchtoolbar, SWT.SIMPLE); coordinateLabel.setText(""); coordinateLabel.setEnabled(false); // add search function searchTextarea.addKeyListener(new KeyListener() { @Override public void keyReleased(KeyEvent event) { if (event.keyCode == SWT.CR) { String term = searchTextarea.getText(); if (!term.isEmpty()) { if (term.equals(mLastSearchedTerm)) { nextSearchResult(); return; } clearSearchResult(); mSearchResult = mModel.searchNode(term); if (!mSearchResult.isEmpty()) { mSearchResultIndex = 0; updateSearchResultSelection(); mLastSearchedTerm = term; } } } } @Override public void keyPressed(KeyEvent event) { } }); SelectionListener l = new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent se) { if (se.getSource() == itemPrev) { prevSearchResult(); } else if (se.getSource() == itemNext) { nextSearchResult(); } else if (se.getSource() == itemDeleteAndInfo) { searchTextarea.setText(""); clearSearchResult(); } } }; itemPrev.addSelectionListener(l); itemNext.addSelectionListener(l); itemDeleteAndInfo.addSelectionListener(l); searchtoolbar.pack(); searchtoolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE); mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider()); // default LabelProvider uses toString() to generate text to display mTreeViewer.setLabelProvider(new LabelProvider()); mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { BasicTreeNode selectedNode = null; if (event.getSelection() instanceof IStructuredSelection) { IStructuredSelection selection = (IStructuredSelection) event.getSelection(); Object o = selection.getFirstElement(); if (o instanceof BasicTreeNode) { selectedNode = (BasicTreeNode) o; } } mModel.setSelectedNode(selectedNode); redrawScreenshot(); if (selectedNode != null) { loadAttributeTable(); } } }); Tree tree = mTreeViewer.getTree(); tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); // move focus so that it's not on tool bar (looks weird) tree.setFocus(); // lower-right base contains the detail group Composite lowerRightBase = new Composite(rightSash, SWT.BORDER); lowerRightBase.setLayout(new FillLayout()); Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE); grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL)); grpNodeDetail.setText("Node Detail"); Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE); TableColumnLayout columnLayout = new TableColumnLayout(); tableContainer.setLayout(columnLayout); mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION); Table table = mTableViewer.getTable(); table.setLinesVisible(true); // use ArrayContentProvider here, it assumes the input to the TableViewer // is an array, where each element represents a row in the table mTableViewer.setContentProvider(new ArrayContentProvider()); TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE); TableColumn tblclmnKey = tableViewerColumnKey.getColumn(); tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { if (element instanceof AttributePair) { // first column, shows the attribute name return ((AttributePair) element).key; } return super.getText(element); } }); columnLayout.setColumnData(tblclmnKey, new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE); tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer)); TableColumn tblclmnValue = tableViewerColumnValue.getColumn(); columnLayout.setColumnData(tblclmnValue, new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true)); tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { if (element instanceof AttributePair) { // second column, shows the attribute value return ((AttributePair) element).value; } return super.getText(element); } }); // sets the ratio of the vertical split: left 5 vs right 3 baseSash.setWeights(new int[] {5, 3 }); } protected void prevSearchResult() { if (mSearchResult == null) return; if(mSearchResult.isEmpty()){ mSearchResult = null; return; } mSearchResultIndex = mSearchResultIndex - 1; if (mSearchResultIndex < 0){ mSearchResultIndex += mSearchResult.size(); } updateSearchResultSelection(); } protected void clearSearchResult() { itemDeleteAndInfo.setText(""); mSearchResult = null; mSearchResultIndex = 0; mLastSearchedTerm = ""; mScreenshotCanvas.redraw(); } protected void nextSearchResult() { if (mSearchResult == null) return; if(mSearchResult.isEmpty()){ mSearchResult = null; return; } mSearchResultIndex = (mSearchResultIndex + 1) % mSearchResult.size(); updateSearchResultSelection(); } private void updateSearchResultSelection() { updateTreeSelection(mSearchResult.get(mSearchResultIndex)); itemDeleteAndInfo.setText("" + (mSearchResultIndex + 1) + "/" + mSearchResult.size()); } private int getScaledSize(int size) { if (mScale == 1.0f) { return size; } else { return new Double(Math.floor((size * mScale))).intValue(); } } private int getInverseScaledSize(int size) { if (mScale == 1.0f) { return size; } else { return new Double(Math.floor((size / mScale))).intValue(); } } private void updateScreenshotTransformation() { Rectangle canvas = mScreenshotCanvas.getBounds(); Rectangle image = mScreenshot.getBounds(); float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float) image.width; float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float) image.height; // use the smaller scale here so that we can fit the entire screenshot mScale = Math.min(scaleX, scaleY); // calculate translation values to center the image on the canvas mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER; mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER; } private class AttributeTableEditingSupport extends EditingSupport { private TableViewer mViewer; public AttributeTableEditingSupport(TableViewer viewer) { super(viewer); mViewer = viewer; } @Override protected boolean canEdit(Object arg0) { return true; } @Override protected CellEditor getCellEditor(Object arg0) { return new TextCellEditor(mViewer.getTable()); } @Override protected Object getValue(Object o) { return ((AttributePair) o).value; } @Override protected void setValue(Object arg0, Object arg1) { } } /** * Causes a redraw of the canvas. * * The drawing code of canvas will handle highlighted nodes and etc based on data * retrieved from Model */ public void redrawScreenshot() { if (mScreenshot == null) { mStackLayout.topControl = mSetScreenshotComposite; } else { mStackLayout.topControl = mScreenshotCanvas; } mScreenshotComposite.layout(); mScreenshotCanvas.redraw(); } public void setInputHierarchy(Object input) { mTreeViewer.setInput(input); } public void loadAttributeTable() { // update the lower right corner table to show the attributes of the node mTableViewer.setInput(mModel.getSelectedNode().getAttributesArray()); } public void expandAll() { mTreeViewer.expandAll(); } public void updateTreeSelection(BasicTreeNode node) { mTreeViewer.setSelection(new StructuredSelection(node), true); } public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) { mModel = model; mModelFile = modelBackingFile; if (mScreenshot != null) { mScreenshot.dispose(); } mScreenshot = screenshot; clearSearchResult(); redrawScreenshot(); // load xml into tree BasicTreeNode wrapper = new BasicTreeNode(); // putting another root node on top of existing root node // because Tree seems to like to hide the root node wrapper.addChild(mModel.getXmlRootNode()); setInputHierarchy(wrapper); mTreeViewer.getTree().setFocus(); } public boolean shouldShowNafNodes() { return mModel != null ? mModel.shouldShowNafNodes() : false; } public void toggleShowNaf() { if (mModel != null) { mModel.toggleShowNaf(); } } public Image getScreenShot() { return mScreenshot; } public File getModelFile() { return mModelFile; } } uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java0100644 0000000 0000000 00000007545 12747325007 027123 0ustar000000000 0000000 /* * 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.uiautomator; import com.android.uiautomator.actions.OpenFilesAction; import com.android.uiautomator.actions.SaveScreenShotAction; import com.android.uiautomator.actions.ScreenshotAction; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.window.ApplicationWindow; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.ToolBar; import java.io.File; public class UiAutomatorViewer extends ApplicationWindow { private UiAutomatorView mUiAutomatorView; public UiAutomatorViewer() { super(null); } @Override protected Control createContents(Composite parent) { Composite c = new Composite(parent, SWT.BORDER); GridLayout gridLayout = new GridLayout(1, false); gridLayout.marginWidth = 0; gridLayout.marginHeight = 0; gridLayout.horizontalSpacing = 0; gridLayout.verticalSpacing = 0; c.setLayout(gridLayout); GridData gd = new GridData(GridData.FILL_HORIZONTAL); c.setLayoutData(gd); ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); toolBarManager.add(new OpenFilesAction(this)); toolBarManager.add(new ScreenshotAction(this,false)); toolBarManager.add(new ScreenshotAction(this,true)); toolBarManager.add(new SaveScreenShotAction(this)); ToolBar tb = toolBarManager.createControl(c); tb.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mUiAutomatorView = new UiAutomatorView(c, SWT.BORDER); mUiAutomatorView.setLayoutData(new GridData(GridData.FILL_BOTH)); return parent; } public static void main(String args[]) { DebugBridge.init(); try { UiAutomatorViewer window = new UiAutomatorViewer(); window.setBlockOnOpen(true); window.open(); } catch (Exception e) { e.printStackTrace(); } finally { DebugBridge.terminate(); } } @Override protected void configureShell(Shell newShell) { super.configureShell(newShell); newShell.setText("UI Automator Viewer"); } @Override protected Point getInitialSize() { return new Point(800, 600); } public void setModel(final UiAutomatorModel model, final File modelFile, final Image screenshot) { if (Display.getDefault().getThread() != Thread.currentThread()) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { mUiAutomatorView.setModel(model, modelFile, screenshot); } }); } else { mUiAutomatorView.setModel(model, modelFile, screenshot); } } public Image getScreenShot() { return mUiAutomatorView.getScreenShot(); } public File getModelFile(){ return mUiAutomatorView.getModelFile(); } } uiautomatorviewer/src/main/java/com/android/uiautomator/actions/0040755 0000000 0000000 00000000000 12747325007 024255 5ustar000000000 0000000 uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java0100644 0000000 0000000 00000002302 12747325007 030120 0ustar000000000 0000000 /* * 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.uiautomator.actions; import com.android.uiautomator.UiAutomatorView; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; public class ExpandAllAction extends Action { UiAutomatorView mView; public ExpandAllAction(UiAutomatorView view) { super("&Expand All"); mView = view;; } @Override public ImageDescriptor getImageDescriptor() { return ImageHelper.loadImageDescriptorFromResource("images/expandall.png"); } @Override public void run() { mView.expandAll(); } } uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java0100644 0000000 0000000 00000003065 12747325007 027303 0ustar000000000 0000000 /* * 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.uiautomator.actions; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import java.io.IOException; import java.io.InputStream; public class ImageHelper { public static ImageDescriptor loadImageDescriptorFromResource(String path) { InputStream is = ImageHelper.class.getClassLoader().getResourceAsStream(path); if (is != null) { ImageData[] data = null; try { data = new ImageLoader().load(is); } catch (SWTException e) { } finally { try { is.close(); } catch (IOException e) { } } if (data != null && data.length > 0) { return ImageDescriptor.createFromImageData(data[0]); } } return null; } } uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java0100644 0000000 0000000 00000005156 12747325007 030146 0ustar000000000 0000000 /* * 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.uiautomator.actions; import com.android.uiautomator.OpenDialog; import com.android.uiautomator.UiAutomatorModel; import com.android.uiautomator.UiAutomatorViewer; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.widgets.Display; import java.io.File; public class OpenFilesAction extends Action { private UiAutomatorViewer mViewer; public OpenFilesAction(UiAutomatorViewer viewer) { super("&Open"); mViewer = viewer; } @Override public ImageDescriptor getImageDescriptor() { return ImageHelper.loadImageDescriptorFromResource("images/open-folder.png"); } @Override public void run() { OpenDialog d = new OpenDialog(Display.getDefault().getActiveShell()); if (d.open() != OpenDialog.OK) { return; } UiAutomatorModel model; try { model = new UiAutomatorModel(d.getXmlDumpFile()); } catch (Exception e) { // FIXME: show error return; } Image img = null; File screenshot = d.getScreenshotFile(); if (screenshot != null) { try { ImageData[] data = new ImageLoader().load(screenshot.getAbsolutePath()); // "data" is an array, probably used to handle images that has multiple frames // i.e. gifs or icons, we just care if it has at least one here if (data.length < 1) { throw new RuntimeException("Unable to load image: " + screenshot.getAbsolutePath()); } img = new Image(Display.getDefault(), data[0]); } catch (Exception e) { // FIXME: show error return; } } mViewer.setModel(model, d.getXmlDumpFile(), img); } } uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java0100644 0000000 0000000 00000006710 12747325010 031145 0ustar000000000 0000000 /* * Copyright (C) 2013 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.uiautomator.actions; import com.android.uiautomator.UiAutomatorViewer; import com.google.common.io.Files; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.Action; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.widgets.DirectoryDialog; import org.eclipse.swt.widgets.Display; import java.io.File; public class SaveScreenShotAction extends Action { private static final String PNG_TYPE = ".png"; private static final String UIX_TYPE = ".uix"; private UiAutomatorViewer mViewer; public SaveScreenShotAction(UiAutomatorViewer viewer) { super("&Save"); mViewer = viewer; } @Override public ImageDescriptor getImageDescriptor() { return ImageHelper.loadImageDescriptorFromResource("images/save.png"); } @Override public void run() { final Image screenshot = mViewer.getScreenShot(); final File model = mViewer.getModelFile(); if (model == null || screenshot == null) { return; } DirectoryDialog dd = new DirectoryDialog(Display.getDefault().getActiveShell()); dd.setText("Save Screenshot and UiX Files"); final String path = dd.open(); if (path == null) { return; } // to prevent blocking the ui thread, we do the saving in the other thread. new Thread(){ String filepath; @Override public void run() { filepath = new File(path, model.getName()).toString(); filepath = filepath.substring(0,filepath.lastIndexOf(".")); ImageLoader imageLoader = new ImageLoader(); imageLoader.data = new ImageData[] { screenshot.getImageData() }; try { imageLoader.save(filepath + PNG_TYPE, SWT.IMAGE_PNG); Files.copy(model, new File(filepath + UIX_TYPE)); } catch (final Exception e) { Display.getDefault().syncExec(new Runnable() { @Override public void run() { Status status = new Status(IStatus.ERROR, "Error writing file", e.getLocalizedMessage()); ErrorDialog.openError(Display.getDefault().getActiveShell(), String.format("Error writing %s.uix", filepath), e.getLocalizedMessage(), status); } }); } }; }.start(); } } uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java0100644 0000000 0000000 00000015304 12747325010 030365 0ustar000000000 0000000 /* * 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.uiautomator.actions; import com.android.ddmlib.IDevice; import com.android.uiautomator.DebugBridge; import com.android.uiautomator.UiAutomatorHelper; import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; import com.android.uiautomator.UiAutomatorViewer; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.Action; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import java.lang.reflect.InvocationTargetException; import java.util.List; public class ScreenshotAction extends Action { UiAutomatorViewer mViewer; private boolean mCompressed; public ScreenshotAction(UiAutomatorViewer viewer, boolean compressed) { super("&Device Screenshot "+ (compressed ? "with Compressed Hierarchy" : "") +"(uiautomator dump" + (compressed ? " --compressed)" : ")")); mViewer = viewer; mCompressed = compressed; } @Override public ImageDescriptor getImageDescriptor() { if(mCompressed) return ImageHelper.loadImageDescriptorFromResource("images/screenshotcompressed.png"); else return ImageHelper.loadImageDescriptorFromResource("images/screenshot.png"); } @Override public void run() { if (!DebugBridge.isInitialized()) { MessageDialog.openError(mViewer.getShell(), "Error obtaining Device Screenshot", "Unable to connect to adb. Check if adb is installed correctly."); return; } final IDevice device = pickDevice(); if (device == null) { return; } ProgressMonitorDialog dialog = new ProgressMonitorDialog(mViewer.getShell()); try { dialog.run(true, false, new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { UiAutomatorResult result = null; try { result = UiAutomatorHelper.takeSnapshot(device, monitor, mCompressed); } catch (UiAutomatorException e) { monitor.done(); showError(e.getMessage(), e); return; } mViewer.setModel(result.model, result.uiHierarchy, result.screenshot); monitor.done(); } }); } catch (Exception e) { showError("Unexpected error while obtaining UI hierarchy", e); } } private void showError(final String msg, final Throwable t) { mViewer.getShell().getDisplay().syncExec(new Runnable() { @Override public void run() { Status s = new Status(IStatus.ERROR, "Screenshot", msg, t); ErrorDialog.openError( mViewer.getShell(), "Error", "Error obtaining UI hierarchy", s); } }); } private IDevice pickDevice() { List devices = DebugBridge.getDevices(); if (devices.size() == 0) { MessageDialog.openError(mViewer.getShell(), "Error obtaining Device Screenshot", "No Android devices were detected by adb."); return null; } else if (devices.size() == 1) { return devices.get(0); } else { DevicePickerDialog dlg = new DevicePickerDialog(mViewer.getShell(), devices); if (dlg.open() != Window.OK) { return null; } return dlg.getSelectedDevice(); } } private static class DevicePickerDialog extends Dialog { private final List mDevices; private final String[] mDeviceNames; private static int sSelectedDeviceIndex; public DevicePickerDialog(Shell parentShell, List devices) { super(parentShell); mDevices = devices; mDeviceNames = new String[mDevices.size()]; for (int i = 0; i < devices.size(); i++) { mDeviceNames[i] = devices.get(i).getName(); } } @Override protected Control createDialogArea(Composite parentShell) { Composite parent = (Composite) super.createDialogArea(parentShell); Composite c = new Composite(parent, SWT.NONE); c.setLayout(new GridLayout(2, false)); Label l = new Label(c, SWT.NONE); l.setText("Select device: "); final Combo combo = new Combo(c, SWT.BORDER | SWT.READ_ONLY); combo.setItems(mDeviceNames); int defaultSelection = sSelectedDeviceIndex < mDevices.size() ? sSelectedDeviceIndex : 0; combo.select(defaultSelection); sSelectedDeviceIndex = defaultSelection; combo.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent arg0) { sSelectedDeviceIndex = combo.getSelectionIndex(); } }); return parent; } public IDevice getSelectedDevice() { return mDevices.get(sSelectedDeviceIndex); } } } uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java0100644 0000000 0000000 00000002621 12747325010 030114 0ustar000000000 0000000 /* * 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.uiautomator.actions; import com.android.uiautomator.UiAutomatorView; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.resource.ImageDescriptor; public class ToggleNafAction extends Action { private UiAutomatorView mView; public ToggleNafAction(UiAutomatorView view) { super("&Toggle NAF Nodes", IAction.AS_CHECK_BOX); setChecked(view.shouldShowNafNodes()); mView = view; } @Override public ImageDescriptor getImageDescriptor() { return ImageHelper.loadImageDescriptorFromResource("images/warning.png"); } @Override public void run() { mView.toggleShowNaf(); mView.redrawScreenshot(); setChecked(mView.shouldShowNafNodes()); } } uiautomatorviewer/src/main/java/com/android/uiautomator/tree/0040755 0000000 0000000 00000000000 12747325010 023546 5ustar000000000 0000000 uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java0100644 0000000 0000000 00000001501 12747325010 027162 0ustar000000000 0000000 /* * 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.uiautomator.tree; public class AttributePair { public String key, value; public AttributePair(String key, String value) { this.key = key; this.value = value; } } uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java0100644 0000000 0000000 00000006662 12747325010 027067 0ustar000000000 0000000 /* * 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.uiautomator.tree; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class BasicTreeNode { private static final BasicTreeNode[] CHILDREN_TEMPLATE = new BasicTreeNode[] {}; protected BasicTreeNode mParent; protected final List mChildren = new ArrayList(); public int x, y, width, height; // whether the boundary fields are applicable for the node or not // RootWindowNode has no bounds, but UiNodes should protected boolean mHasBounds = false; public void addChild(BasicTreeNode child) { if (child == null) { throw new NullPointerException("Cannot add null child"); } if (mChildren.contains(child)) { throw new IllegalArgumentException("node already a child"); } mChildren.add(child); child.mParent = this; } public List getChildrenList() { return Collections.unmodifiableList(mChildren); } public BasicTreeNode[] getChildren() { return mChildren.toArray(CHILDREN_TEMPLATE); } public BasicTreeNode getParent() { return mParent; } public boolean hasChild() { return mChildren.size() != 0; } public int getChildCount() { return mChildren.size(); } public void clearAllChildren() { for (BasicTreeNode child : mChildren) { child.clearAllChildren(); } mChildren.clear(); } /** * * Find nodes in the tree containing the coordinate * * The found node should have bounds covering the coordinate, and none of its children's * bounds covers it. Depending on the layout, some app may have multiple nodes matching it, * the caller must provide a {@link IFindNodeListener} to receive all found nodes * * @param px * @param py * @return */ public boolean findLeafMostNodesAtPoint(int px, int py, IFindNodeListener listener) { boolean foundInChild = false; for (BasicTreeNode node : mChildren) { foundInChild |= node.findLeafMostNodesAtPoint(px, py, listener); } // checked all children, if at least one child covers the point, return directly if (foundInChild) return true; // check self if the node has no children, or no child nodes covers the point if (mHasBounds) { if (x <= px && px <= x + width && y <= py && py <= y + height) { listener.onFoundNode(this); return true; } else { return false; } } else { return false; } } public Object[] getAttributesArray () { return null; }; public static interface IFindNodeListener { void onFoundNode(BasicTreeNode node); } } uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java0100644 0000000 0000000 00000003411 12747325010 032122 0ustar000000000 0000000 /* * 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.uiautomator.tree; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; public class BasicTreeNodeContentProvider implements ITreeContentProvider { private static final Object[] EMPTY_ARRAY = {}; @Override public void dispose() { } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } @Override public Object[] getElements(Object inputElement) { return getChildren(inputElement); } @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof BasicTreeNode) { return ((BasicTreeNode)parentElement).getChildren(); } return EMPTY_ARRAY; } @Override public Object getParent(Object element) { if (element instanceof BasicTreeNode) { return ((BasicTreeNode)element).getParent(); } return null; } @Override public boolean hasChildren(Object element) { if (element instanceof BasicTreeNode) { return ((BasicTreeNode) element).hasChild(); } return false; } } uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java0100644 0000000 0000000 00000002643 12747325010 027334 0ustar000000000 0000000 /* * 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.uiautomator.tree; public class RootWindowNode extends BasicTreeNode { private final String mWindowName; private Object[] mCachedAttributesArray; private int mRotation; public RootWindowNode(String windowName) { this(windowName, 0); } public RootWindowNode(String windowName, int rotation) { mWindowName = windowName; mRotation = rotation; } @Override public String toString() { return mWindowName; } @Override public Object[] getAttributesArray() { if (mCachedAttributesArray == null) { mCachedAttributesArray = new Object[]{new AttributePair("window-name", mWindowName)}; } return mCachedAttributesArray; } public int getRotation() { return mRotation; } } uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java0100644 0000000 0000000 00000013436 12747325010 030441 0ustar000000000 0000000 /* * 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.uiautomator.tree; import org.eclipse.swt.graphics.Rectangle; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; public class UiHierarchyXmlLoader { private BasicTreeNode mRootNode; private List mNafNodes; private List mNodeList; public UiHierarchyXmlLoader() { } /** * Uses a SAX parser to process XML dump * @param xmlPath * @return */ public BasicTreeNode parseXml(String xmlPath) { mRootNode = null; mNafNodes = new ArrayList(); mNodeList = new ArrayList(); // standard boilerplate to get a SAX parser SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser parser = null; try { parser = factory.newSAXParser(); } catch (ParserConfigurationException e) { e.printStackTrace(); return null; } catch (SAXException e) { e.printStackTrace(); return null; } // handler class for SAX parser to receiver standard parsing events: // e.g. on reading "", startElement is called, on reading "", // endElement is called DefaultHandler handler = new DefaultHandler(){ BasicTreeNode mParentNode; BasicTreeNode mWorkingNode; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { boolean nodeCreated = false; // starting an element implies that the element that has not yet been closed // will be the parent of the element that is being started here mParentNode = mWorkingNode; if ("hierarchy".equals(qName)) { int rotation = 0; for (int i = 0; i < attributes.getLength(); i++) { if ("rotation".equals(attributes.getQName(i))) { try { rotation = Integer.parseInt(attributes.getValue(i)); } catch (NumberFormatException nfe) { // do nothing } } } mWorkingNode = new RootWindowNode(attributes.getValue("windowName"), rotation); nodeCreated = true; } else if ("node".equals(qName)) { UiNode tmpNode = new UiNode(); for (int i = 0; i < attributes.getLength(); i++) { tmpNode.addAtrribute(attributes.getQName(i), attributes.getValue(i)); } mWorkingNode = tmpNode; nodeCreated = true; // check if current node is NAF String naf = tmpNode.getAttribute("NAF"); if ("true".equals(naf)) { mNafNodes.add(new Rectangle(tmpNode.x, tmpNode.y, tmpNode.width, tmpNode.height)); } } // nodeCreated will be false if the element started is neither // "hierarchy" nor "node" if (nodeCreated) { if (mRootNode == null) { // this will only happen once mRootNode = mWorkingNode; } if (mParentNode != null) { mParentNode.addChild(mWorkingNode); mNodeList.add(mWorkingNode); } } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { //mParentNode should never be null here in a well formed XML if (mParentNode != null) { // closing an element implies that we are back to working on // the parent node of the element just closed, i.e. continue to // parse more child nodes mWorkingNode = mParentNode; mParentNode = mParentNode.getParent(); } } }; try { parser.parse(new File(xmlPath), handler); } catch (SAXException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } return mRootNode; } /** * Returns the list of "Not Accessibility Friendly" nodes found during parsing. * * Call this function after parsing * * @return */ public List getNafNodes() { return Collections.unmodifiableList(mNafNodes); } public List getAllNodes(){ return mNodeList; } } uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java0100644 0000000 0000000 00000010240 12747325010 025566 0ustar000000000 0000000 /* * 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.uiautomator.tree; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class UiNode extends BasicTreeNode { private static final Pattern BOUNDS_PATTERN = Pattern .compile("\\[-?(\\d+),-?(\\d+)\\]\\[-?(\\d+),-?(\\d+)\\]"); // use LinkedHashMap to preserve the order of the attributes private final Map mAttributes = new LinkedHashMap(); private String mDisplayName = "ShouldNotSeeMe"; private Object[] mCachedAttributesArray; public void addAtrribute(String key, String value) { mAttributes.put(key, value); updateDisplayName(); if ("bounds".equals(key)) { updateBounds(value); } } public Map getAttributes() { return Collections.unmodifiableMap(mAttributes); } /** * Builds the display name based on attributes of the node */ private void updateDisplayName() { String className = mAttributes.get("class"); if (className == null) return; String text = mAttributes.get("text"); if (text == null) return; String contentDescription = mAttributes.get("content-desc"); if (contentDescription == null) return; String index = mAttributes.get("index"); if (index == null) return; String bounds = mAttributes.get("bounds"); if (bounds == null) { return; } // shorten the standard class names, otherwise it takes up too much space on UI className = className.replace("android.widget.", ""); className = className.replace("android.view.", ""); StringBuilder builder = new StringBuilder(); builder.append('('); builder.append(index); builder.append(") "); builder.append(className); if (!text.isEmpty()) { builder.append(':'); builder.append(text); } if (!contentDescription.isEmpty()) { builder.append(" {"); builder.append(contentDescription); builder.append('}'); } builder.append(' '); builder.append(bounds); mDisplayName = builder.toString(); } private void updateBounds(String bounds) { Matcher m = BOUNDS_PATTERN.matcher(bounds); if (m.matches()) { x = Integer.parseInt(m.group(1)); y = Integer.parseInt(m.group(2)); width = Integer.parseInt(m.group(3)) - x; height = Integer.parseInt(m.group(4)) - y; mHasBounds = true; } else { throw new RuntimeException("Invalid bounds: " + bounds); } } @Override public String toString() { return mDisplayName; } public String getAttribute(String key) { return mAttributes.get(key); } @Override public Object[] getAttributesArray() { // this approach means we do not handle the situation where an attribute is added // after this function is first called. This is currently not a concern because the // tree is supposed to be readonly if (mCachedAttributesArray == null) { mCachedAttributesArray = new Object[mAttributes.size()]; int i = 0; for (String attr : mAttributes.keySet()) { mCachedAttributesArray[i++] = new AttributePair(attr, mAttributes.get(attr)); } } return mCachedAttributesArray; } } uiautomatorviewer/src/main/java/images/0040755 0000000 0000000 00000000000 12747325010 017305 5ustar000000000 0000000 uiautomatorviewer/src/main/java/images/delete.png0100644 0000000 0000000 00000002645 12747325010 021261 0ustar000000000 0000000 PNG  IHDR00WtEXtSoftwareAdobe ImageReadyqe<GIDATxYOWϝ ;,Ce1 K*!EUW&HQY4k$V^#1Y 3Bt1u{GQSġ꠺Μ@/ FTiF|iegNIO:q̃5+hy+?-*RgL~'s6b}'!jY5'J ?+Zm -5+C;U(J9dF_ϕ`w^|NwB Ki% ،|3_qndJ$a"6!_rDB7_c,Nk9dUb ^+CW7NIJj(fqʒ"ps,/d=/1ab tnpr_b"*t԰5xwpo=0,v;t^,$Z^ ܡ}獑7.Ϗ Z>P"#^][3/8nu~L  / $n:.&O'1%>`>ɛp_eoEx٫ܬF`ȩMX$4:}vtOs.08G\(ޫFe%09 ؐa&1sӦeĻR,B&= y=Q(.ǞO?6.We#9Pb06J^̻LNV9^rV֩ bKZG_X}{=ʛ*Ŧ:odʩLN7=- rW/^aM0OCY?,V`X3L@q5/6\Q®ԂLq2cNԃ)o } 15LLZ|k- a\FN]rWdzeɏC_%(.JTH{MF66]QpaF.m.|(~ }V(%#lM?F3>k.!x|%ֺV,kw: @A V +?D0yIENDB`uiautomatorviewer/src/main/java/images/expandall.png0100644 0000000 0000000 00000000414 12747325010 021757 0ustar000000000 0000000 PNG  IHDRRsRGB$PLTE8FztRNS@fbKGDH pHYs  tIME 8BҕTIDATc`J d!@(H(/./3:#:40 ̘ Ud(Z `ݽ;z73  B~IENDB`uiautomatorviewer/src/main/java/images/next.png0100644 0000000 0000000 00000001636 12747325010 020774 0ustar000000000 0000000 PNG  IHDR00 1 sRGBbKGD̿ pHYs  tIME..rBn"IDATXMh\esL6M'18Sb&MbZaR)*hB[IQ*F]UƅR⢠ PQ նwf87эP)qqBBivγ|9jԨQY|D,i~X^ψDef=,#Íߝ/c?6VǸK̹fLܞ1-W9_O&oq1hEp}c@0:`p9/sxêLMC83xx䝉I;+*Q@\_7D iJM~3%UJvbkcJxx$8j~Ƿ$lBjfk<%RxL2Eoi؅jz[I0/_ŤN "&7V;mSWuۨgN\mP Og cq]ͽ~wxx>?4`˟-}G%CR'Bw?N,{gMkE%f5ߙ g2BIoֽNIP%vkG>GiɐԥiQ]#0Y>Ti)"E4Yr[mÎ#.&1qixLmM6}ZY dq0w֬W:8t ovn57d`p000}٭!]NO8牎ۑf}/ {ID6Ve?FkS#5DNS-㇜s˦!!PC DEc|׫HuG,#)ᡠ oyEFb2=)1IJQ 8 vkCϪ+uIzl>D(3M^2MT"PA fmGj*q1SN5~˽傏\_WĀ_جdFºSAiw,Bp)jlXҫV`<m6ub^biS1J1.ߧ5N޺>jG,Y@4.ƭt`"Y$O<y]UCV$FfۙcWW+u/_1DY~~pkb:Bf# Fc>Qã<pdzP^ a[kj>&#Tݩ-&jn:8%=q) -;:1vi82$&T&s.1lm*Zo;Y9J3h[\K}ZyNcggSpjarufOaApb).65d(}ý Dt}WLw  ͯY_jҕX~ ^\hgO. ' +Ŝen1,< jy34 vc J?|IENDB`uiautomatorviewer/src/main/java/images/save.png0100644 0000000 0000000 00000001725 12747325010 020753 0ustar000000000 0000000 PNG  IHDR sbKGD̿ pHYsHHFk> vpAg IDATHŕkTW?&11&0l% bjh[h7lƅ[J B\J\E[hF mlt7s{8M2qQ{\{;~(*#6ƨ~jW^0Sq\);͵Ka<@E藗]iQzJQĺ'm-p\ ۳J\]@LE3TmOc$M&\gC-LBhNۺ~O? תkaG D2 ('S&KY)äuKllE}W#etK+˳l e@Ie- Me;)%dvoN*tT%tEXtdate:create2011-01-28T22:47:53-08:00Nr%tEXtdate:modify2011-01-28T22:47:53-08:00utEXtSoftwareAdobe ImageReadyqe<IENDB`uiautomatorviewer/src/main/java/images/screenshot.png0100644 0000000 0000000 00000002312 12747325010 022163 0ustar000000000 0000000 PNG  IHDRatEXtSoftwareAdobe ImageReadyqe<hiTXtXML:com.adobe.xmp bVIDATxb?Tπ0kRRRg8}#ÝV7`ĉ. Mwof`1"@,`ifpd`Q` 01Pho@]6b,4뼁yap7+"D ` mar`m^$D\k#XݻJ Cdb DF ߗ1qRspR% ]6 Y ϢfGJ3@(X+?IENDB`uiautomatorviewer/src/main/java/images/screenshotcompressed.png0100644 0000000 0000000 00000001116 12747325010 024251 0ustar000000000 0000000 PNG  IHDRasRGBbKGD pHYs  tIME ;,IDAT8˝KHTa53͋c;Ȉ6"B !Ahs36E-\D0-ڄT Th3C׌{υqK_Ǒ#!˱s9L06^t:GdE$bTϹfz:^NdL^PAm%)N"/(\gpdU)D;|o&5.R\.6@=++ւ?֜Bon%3RU~jv=v%>un Tz\xVSѳ= 0pP9pi| rlөx#ղ,<"CKbGC!tz~s!Z%;[ 偮(qm47Eoevw.̯KY %BheԣU32IENDB`uiautomatorviewer/src/main/java/images/warning.png0100644 0000000 0000000 00000000223 12747325010 021452 0ustar000000000 0000000 PNG  IHDRaZIDAT8c`1OH63ل]A  Q[-^@5 @_XP=0l' .e.0lGDlqK07EDdIENDB`