diff options
author | Raphael Moll <ralf@android.com> | 2013-03-07 13:40:07 -0800 |
---|---|---|
committer | Raphael Moll <ralf@android.com> | 2013-03-13 14:33:50 -0700 |
commit | 8029da5b58decfc7669ad6d849271fbbd82700b7 (patch) | |
tree | f2d44bb8472d0bfc5341c0653c48704acb269a70 /ddms | |
parent | 9198deb60d5a57da9c78c98f9ad8f14d83e28ee9 (diff) | |
download | swt-8029da5b58decfc7669ad6d849271fbbd82700b7.tar.gz |
Move ddms + ddmuilib from sdk.git to tools/swt.
Change-Id: I2093d1d780ff23368abbc18466510c6ad61b6e46
Diffstat (limited to 'ddms')
164 files changed, 33350 insertions, 0 deletions
diff --git a/ddms/app/.classpath b/ddms/app/.classpath new file mode 100644 index 0000000..50b7a68 --- /dev/null +++ b/ddms/app/.classpath @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src/main/java"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/> + <classpathentry combineaccessrules="false" kind="src" path="/ddmuilib"/> + <classpathentry combineaccessrules="false" kind="src" path="/sdkstats"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.core.commands_3.6.0.I20100512-1500.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.equinox.common_3.6.0.v20100503.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.jface_3.6.2.M20110210-1200.jar"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swtmenubar.jar" sourcepath="/ANDROID_SRC/sdk/swtmenubar/src"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/osgi/osgi.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/common"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/ddms/app/.project b/ddms/app/.project new file mode 100644 index 0000000..ffb19d7 --- /dev/null +++ b/ddms/app/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddms</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/ddms/app/.settings/org.eclipse.jdt.core.prefs b/ddms/app/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..9dbff07 --- /dev/null +++ b/ddms/app/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,98 @@ +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=error +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +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 diff --git a/ddms/app/NOTICE b/ddms/app/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/ddms/app/NOTICE @@ -0,0 +1,190 @@ + + 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 + diff --git a/ddms/app/README b/ddms/app/README new file mode 100644 index 0000000..0d9bbc4 --- /dev/null +++ b/ddms/app/README @@ -0,0 +1,75 @@ +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/<platform>/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/<platform>/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 <Android tree>/out/host/<platform>/framework +- Create a new classpath variable named ANDROID_SRC +- Set its folder value to <Android tree> + +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 diff --git a/ddms/app/build.gradle b/ddms/app/build.gradle new file mode 100644 index 0000000..a07e3bd --- /dev/null +++ b/ddms/app/build.gradle @@ -0,0 +1,21 @@ +group = 'com.android.tools.ddms' +archivesBaseName = 'ddms' + +dependencies { + compile project(':ddmuilib') + compile project(':sdkstats') + compile project(':swtmenubar') + compile "com.android.tools:common:$version" + compile "com.android.tools.ddms:ddmlib:$version" +} + +shipping { + launcherScripts = ['etc/ddms', 'etc/ddms.bat'] +} + +// include swt for compilation +sourceSets.main.compileClasspath += configurations.swt + +// configure the manifest of the buildDistributionJar task. +buildDistributionJar.manifest.attributes("Main-Class": "com.android.ddms.Main") + diff --git a/ddms/app/etc/ddms b/ddms/app/etc/ddms new file mode 100755 index 0000000..79b93f9 --- /dev/null +++ b/ddms/app/etc/ddms @@ -0,0 +1,111 @@ +#!/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 "$@" diff --git a/ddms/app/etc/ddms.bat b/ddms/app/etc/ddms.bat new file mode 100755 index 0000000..d710ea6 --- /dev/null +++ b/ddms/app/etc/ddms.bat @@ -0,0 +1,74 @@ +@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 %%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 %*
+
diff --git a/ddms/app/src/main/java/com/android/ddms/AboutDialog.java b/ddms/app/src/main/java/com/android/ddms/AboutDialog.java new file mode 100644 index 0000000..b3ddff7 --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/AboutDialog.java @@ -0,0 +1,158 @@ +/* //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); + } +} diff --git a/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java b/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java new file mode 100644 index 0000000..2dcd5d4 --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/DebugPortProvider.java @@ -0,0 +1,164 @@ +/* + * 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<String, Map<String, Integer>> 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<String, Integer> 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<String, Map<String, Integer>> getPortList() { + return mMap; + } + + /** + * Create the map member from the values contained in the Preference Store. + */ + private void computePortList() { + mMap = new HashMap<String, Map<String, Integer>>(); + + // 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<String, Integer> deviceMap = mMap.get(deviceName); + if (deviceMap == null) { + deviceMap = new HashMap<String, Integer>(); + 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<String, Map<String,Integer>> 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<String> deviceKeys = map.keySet(); + for (String deviceKey : deviceKeys) { + Map<String, Integer> deviceMap = map.get(deviceKey); + if (deviceMap != null) { + Set<String> 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); + } +} diff --git a/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java b/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java new file mode 100644 index 0000000..6775cbb --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/DeviceCommandDialog.java @@ -0,0 +1,441 @@ +/* //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; + } +} + diff --git a/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java b/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java new file mode 100644 index 0000000..04d921c --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/DropdownSelectionListener.java @@ -0,0 +1,80 @@ +/* //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"); + } + } +} + diff --git a/ddms/app/src/main/java/com/android/ddms/Main.java b/ddms/app/src/main/java/com/android/ddms/Main.java new file mode 100644 index 0000000..bfdb78b --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/Main.java @@ -0,0 +1,171 @@ +/* + * 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. + } + } +} diff --git a/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java b/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java new file mode 100644 index 0000000..acadeb8 --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/PrefsDialog.java @@ -0,0 +1,610 @@ +/* + * 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.PortFieldEditor; +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$ + + 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 PortFieldEditor(PREFS_DEBUG_PORT_BASE, + "Starting value for local port:", getFieldEditorParent()); + addField(ife); + + ife = new PortFieldEditor(PREFS_SELECTED_DEBUG_PORT, + "Port of Selected VM:", getFieldEditorParent()); + 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()); + } + } + } + + /** + * "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(); + } + } + +} + + diff --git a/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java b/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java new file mode 100644 index 0000000..9a8ada3 --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/StaticPortConfigDialog.java @@ -0,0 +1,395 @@ +/* + * 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<Integer> mPorts = new ArrayList<Integer>(); + + /** + * 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<String, Map<String, Integer>> map = provider.getPortList(); + + // we're going to loop on the keys and fill the table. + Set<String> deviceKeys = map.keySet(); + + for (String deviceKey : deviceKeys) { + Map<String, Integer> deviceMap = map.get(deviceKey); + if (deviceMap != null) { + Set<String> 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<String, Map<String, Integer>> map = new HashMap<String, Map<String, Integer>>(); + + int count = mPortTable.getItemCount(); + + for (int i = 0 ; i < count ; i++) { + TableItem item = mPortTable.getItem(i); + String deviceName = item.getText(COL_DEVICE); + + Map<String, Integer> deviceMap = map.get(deviceName); + if (deviceMap == null) { + deviceMap = new HashMap<String, Integer>(); + 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); + } +} diff --git a/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java b/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java new file mode 100644 index 0000000..c9cb044 --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/StaticPortEditDialog.java @@ -0,0 +1,334 @@ +/* + * 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<Integer> 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<Integer> 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<Integer> 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); + } +} diff --git a/ddms/app/src/main/java/com/android/ddms/UIThread.java b/ddms/app/src/main/java/com/android/ddms/UIThread.java new file mode 100644 index 0000000..8aaa806 --- /dev/null +++ b/ddms/app/src/main/java/com/android/ddms/UIThread.java @@ -0,0 +1,1803 @@ +/* + * 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.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.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.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<LogFilter> list = + new ArrayList<LogFilter>(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.setLogOutput(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 + "adb"; //$NON-NLS-1$ + } else { + adbLocation = ddmsParentLocation + File.separator + "adb"; //$NON-NLS-1$ + } + } else { + adbLocation = "adb"; //$NON-NLS-1$ + } + + 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); + screenShotItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mCurrentDevice != null) { + ScreenShotDialog dlg = new ScreenShotDialog(shell); + dlg.open(mCurrentDevice); + } + } + }); + + 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); + } + }); + + // 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.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("Start 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(); + } + }); + } + } + } +} diff --git a/ddms/app/src/main/java/images/ddms-128.png b/ddms/app/src/main/java/images/ddms-128.png Binary files differnew file mode 100644 index 0000000..392a8f3 --- /dev/null +++ b/ddms/app/src/main/java/images/ddms-128.png diff --git a/ddms/ddmuilib/.classpath b/ddms/ddmuilib/.classpath new file mode 100644 index 0000000..829ac1c --- /dev/null +++ b/ddms/ddmuilib/.classpath @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src/main/java"/> + <classpathentry kind="src" path="src/test/java"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.core.commands_3.6.0.I20100512-1500.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.equinox.common_3.6.0.v20100503.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.jface_3.6.2.M20110210-1200.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/jfreechart/jcommon-1.0.12.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/jfreechart/jfreechart-1.0.9-swt.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/jfreechart/jfreechart-1.0.9.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-13.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/ddms/ddmuilib/.project b/ddms/ddmuilib/.project new file mode 100644 index 0000000..29cb2f2 --- /dev/null +++ b/ddms/ddmuilib/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>ddmuilib</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs b/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..9dbff07 --- /dev/null +++ b/ddms/ddmuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,98 @@ +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=error +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +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 diff --git a/ddms/ddmuilib/NOTICE b/ddms/ddmuilib/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/ddms/ddmuilib/NOTICE @@ -0,0 +1,190 @@ + + 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 + diff --git a/ddms/ddmuilib/README b/ddms/ddmuilib/README new file mode 100644 index 0000000..971e211 --- /dev/null +++ b/ddms/ddmuilib/README @@ -0,0 +1,14 @@ +Using the Eclipse projects for ddmuilib. + +ddmuilib requires SWT to compile. + +SWT is available in the depot under prebuild/<platform>/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/<platform>/swt. + +You also need a user library called ANDROID_JFREECHART containing the jar files +available at prebuild/common/jfreechart. diff --git a/ddms/ddmuilib/build.gradle b/ddms/ddmuilib/build.gradle new file mode 100644 index 0000000..f565e49 --- /dev/null +++ b/ddms/ddmuilib/build.gradle @@ -0,0 +1,14 @@ +group = 'com.android.tools.ddms' +archivesBaseName = 'ddmuilib' + +dependencies { + compile "com.android.tools.ddms:ddmlib:$version" + compile 'jfree:jfreechart:1.0.9' + compile 'jfree:jfreechart-swt:1.0.9' + + testCompile 'junit:junit:3.8.1' +} + +// include swt for compilation +sourceSets.main.compileClasspath += configurations.swt + diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java new file mode 100644 index 0000000..13a787a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java @@ -0,0 +1,117 @@ +/* + * 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(); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java new file mode 100644 index 0000000..10799ec --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java @@ -0,0 +1,355 @@ +/* + * 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 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.<br> + * Each process can only handle one library, which should be provided when + * creating a new process.<br> + * <br> + * 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 <code>getProcess(String library)<code>.<br></br> + * 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<String, Addr2Line> sProcessCache = + new HashMap<String, Addr2Line>(); + + /** + * 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; + + /** 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<String> mLibrarySearchPaths = new ArrayList<String>(); + + /** + * 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 a Addr2Line process for the specified library. + * <br>The library should be in a format that makes<br> + * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file. + * + * @param library the library in which to look for addresses. + * @return a new Addr2Line object representing a started process, ready to + * be queried for addresses. If any error happened when launching a + * new process, <code>null</code> will be returned. + */ + public static Addr2Line getProcess(final NativeLibraryMapInfo library) { + 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); + + // 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. 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. + */ + private Addr2Line(final NativeLibraryMapInfo library) { + mLibrary = library; + } + + /** + * 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. + + String addr2Line = System.getenv("ANDROID_ADDR2LINE"); + if (addr2Line == null) { + addr2Line = DdmUiPreferences.getAddr2Line(); + } + + // build the command line + String[] command = new String[5]; + command[0] = addr2Line; + 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", + DdmUiPreferences.getAddr2Line(), 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<Addr2Line> 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java new file mode 100644 index 0000000..a48f73d --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java @@ -0,0 +1,651 @@ +/* + * 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. + * <p/> + * 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. + * <p/> + * This <b>must</b> 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 <code>null</code> 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 <code>null</code> if none is found. + * If a {@link ISelection} object is specified, the first {@link AllocationInfo} from this + * selection is returned, otherwise, the <code>ISelection</code> returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or <code>null</code> + */ + 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<AllocationInfo> results = new ArrayList<AllocationInfo>(); + // 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()]); + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java new file mode 100644 index 0000000..0ed4c95 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java @@ -0,0 +1,50 @@ +/* + * 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 <code>isQuitting()</code> 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; + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java new file mode 100644 index 0000000..3e66ea5 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java @@ -0,0 +1,193 @@ +/* + * 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<Integer, ArrayList<HeapSegmentElement>> mHeapMap; + + /** + * Serialize the heap data into an array. The resulting array is available through + * <code>getSerializedData()</code>. + * @param heapData The heap data to serialize + * @return true if the data changed. + */ + protected boolean serializeHeapData(HeapData heapData) { + Collection<HeapSegment> 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<Integer, ArrayList<HeapSegmentElement>> heapMap = + heapData.getProcessedHeapMap(); + mHeapMap = heapMap; + } + } + + return true; + } + + /** + * Returns the serialized heap data + */ + protected byte[] getSerializedData() { + return mProcessedHeapData; + } + + /** + * Processes and serialize the heapData. + * <p/> + * The resulting serialized array is {@link #mProcessedHeapData}. + * <p/> + * the resulting map is {@link #mHeapMap}. + * @param heapData the collection of {@link HeapSegment} that forms the heap data. + */ + private void doSerializeHeapData(Collection<HeapSegment> heapData) { + mHeapMap = new TreeMap<Integer, ArrayList<HeapSegmentElement>>(); + + Iterator<HeapSegment> 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<HeapSegmentElement> elementList = mHeapMap.get(v); + if (elementList == null) { + elementList = new ArrayList<HeapSegmentElement>(); + 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<ArrayList<HeapSegmentElement>> elementLists = mHeapMap.values(); + for (ArrayList<HeapSegmentElement> 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; + } + + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java new file mode 100644 index 0000000..a711933 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java @@ -0,0 +1,33 @@ +/* + * 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); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java new file mode 100644 index 0000000..db3642b --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java @@ -0,0 +1,79 @@ +/* + * 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 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 getTraceview() { + return sTraceviewLocation; + } + + public static void setTraceviewLocation(String location) { + sTraceviewLocation = location; + } + + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java new file mode 100644 index 0000000..a24b8a0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java @@ -0,0 +1,784 @@ +/* + * 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; +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 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.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.util.ArrayList; +import java.util.Locale; + +/** + * 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<IUiSelectionListener> mListeners = new ArrayList<IUiSelectionListener>(); + + private final ArrayList<IDevice> mDevicesToExpand = new ArrayList<IDevice>(); + + private boolean mAdvancedPortSupport; + + /** + * A Content provider for the {@link TreeViewer}. + * <p/> + * 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 loader + * @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) { + mCurrentClient.toggleMethodProfiling(); + } + } + + 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. + * <p/> + * This is sent from a non UI thread. + * @param bridge the new {@link AndroidDebugBridge} object. + * + * @see IDebugBridgeChangeListener#serverChanged(AndroidDebugBridge) + */ + @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}. + * <p/> + * 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}. + * <p/> + * 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. + * <p/> + * 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) + */ + @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. + * <p/> + * 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) { + } + } + } + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java new file mode 100644 index 0000000..82aed98 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java @@ -0,0 +1,1463 @@ +/* + * 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(); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java new file mode 100644 index 0000000..fe3f438 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java @@ -0,0 +1,142 @@ +/* + * 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(""); + } + } + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java new file mode 100644 index 0000000..d0af8b0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java @@ -0,0 +1,1310 @@ +/* + * 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. + * <p/> + * 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<Integer, ArrayList<HeapSegmentElement>> heapMap = + client.getClientData().getVmHeapData().getProcessedHeapMap(); + + ArrayList<HeapSegmentElement> 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<Integer> iter = cd.getVmHeapIds(); + + while (iter.hasNext()) { + numRows++; + Integer id = iter.next(); + Map<String, Long> heapInfo = cd.getVmHeapInfo(id); + if (heapInfo == null) { + continue; + } + long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES); + long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED); + long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED); + + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(0, id.toString()); + + item.setText(1, prettyByteCount(sizeInBytes)); + item.setText(2, prettyByteCount(bytesAllocated)); + item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated)); + item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes)); + item.setText(5, addCommasToNumber(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<Integer, ArrayList<HeapSegmentElement>> 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<HeapSegmentElement> selectedList = null; + + // get the keys + Set<Integer> 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<HeapSegmentElement> 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<ByteLong> { + 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<HeapSegmentElement> 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<<i)) != 0) { + if (i < 31) { + return 1<<(i + 1); + } else { + return 1<<31; + } + } + } + return 0; + } + + private int zOrderData(ImageData id, byte pixData[]) { + int maxX = 0; + for (int i = 0; i < pixData.length; i++) { + /* Tread the pixData index as a z-order curve index and + * decompose into Cartesian coordinates. + */ + int x = (i & 1) | + ((i >>> 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); + } +} + diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java new file mode 100644 index 0000000..9aa6943 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java @@ -0,0 +1,21 @@ +/* + * 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); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java new file mode 100644 index 0000000..37dd9a0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java @@ -0,0 +1,38 @@ +/* + * 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.<br> + * 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); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java new file mode 100644 index 0000000..fd480f6 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java @@ -0,0 +1,206 @@ +/* + * 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/<var>filename</var> + * + * 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<String, Image> mLoadedImages = new HashMap<String, Image>(); + private static final HashMap<Class<?>, ImageLoader> mInstances = + new HashMap<Class<?>, 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/<var>filename</var>, 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/<var>filename</var>. + * + * @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/<var>filename</var>. + * + * @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.<br> + * 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java new file mode 100644 index 0000000..60dc2c0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java @@ -0,0 +1,199 @@ +/* + * 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:", + }; + 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; + + /** + * 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. + * <p/> + * 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"); + } + } + + mCol2.pack(); + + //Log.i("ddms", "InfoPanel: changed " + client); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mTable); + } +} + diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java new file mode 100644 index 0000000..337bff2 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java @@ -0,0 +1,1648 @@ +/* + * 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<NativeAllocationInfo> mAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** list of the {@link NativeAllocationInfo} being displayed based on the selection + * of {@link #mAllocDisplayCombo}. + */ + private final ArrayList<NativeAllocationInfo> mDisplayedAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */ + private final ArrayList<NativeAllocationInfo> mBackUpAllocations = + new ArrayList<NativeAllocationInfo>(); + + /** 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<LibraryAllocations> mLibraryAllocations = + new ArrayList<LibraryAllocations>(); + + /* 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<Long, NativeStackCallInfo> mSourceCache = + new HashMap<Long, NativeStackCallInfo>(); + 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<NativeAllocationInfo> 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<Long> list = info.getStackCallAddresses(); + final int size = list.size(); + + ArrayList<NativeStackCallInfo> resolvedStackCall = + new ArrayList<NativeStackCallInfo>(); + + 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); + 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<NativeAllocationInfo> mLibAllocations = + new ArrayList<NativeAllocationInfo>(); + + 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<NativeAllocationInfo> 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 <code>addAllocation()</code>, 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<NativeAllocationInfo>() { + @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 <update btn> <status> + 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. + * <p/> + * 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<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>(); + + // 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<Long> addresses = mi.getStackCallAddresses(); + List<NativeStackCallInfo> 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<String, LibraryAllocations> libcache = + new HashMap<String, LibraryAllocations>(); + + // 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<LibraryAllocations>() { + @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); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java new file mode 100644 index 0000000..d910cc7 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/Panel.java @@ -0,0 +1,49 @@ +/* + * 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(); +} + diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java new file mode 100644 index 0000000..533372e --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/PortFieldEditor.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.swt.widgets.Composite; + +/** + * Edit an integer field, validating it as a port number. + */ +public class PortFieldEditor extends IntegerFieldEditor { + + public boolean mRecursiveCheck = false; + + public PortFieldEditor(String name, String label, Composite parent) { + super(name, label, parent); + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + } + + /* + * Get the current value of the field, as an integer. + */ + public int getCurrentValue() { + int val; + try { + val = Integer.parseInt(getStringValue()); + } + catch (NumberFormatException nfe) { + val = -1; + } + return val; + } + + /* + * Check the validity of the field. + */ + @Override + protected boolean checkState() { + if (super.checkState() == false) { + return false; + } + //Log.i("ddms", "check state " + getStringValue()); + boolean err = false; + int val = getCurrentValue(); + if (val < 1024 || val > 32767) { + setErrorMessage("Port must be between 1024 and 32767"); + err = true; + } else { + setErrorMessage(null); + err = false; + } + showErrorMessage(); + return !err; + } + + protected void updateCheckState(PortFieldEditor pfe) { + pfe.refreshValidState(); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java new file mode 100644 index 0000000..b0f885a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java @@ -0,0 +1,350 @@ +/* + * 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. + 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()); + } + } + } + +} + diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java new file mode 100644 index 0000000..e6d2211 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java @@ -0,0 +1,78 @@ +/* + * 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(); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java new file mode 100644 index 0000000..b00120b --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java @@ -0,0 +1,223 @@ +/* + * 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. + * <p/>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. + * <p/>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. + * <p/>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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java new file mode 100644 index 0000000..732de59 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java @@ -0,0 +1,100 @@ +/* + * 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]; + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java new file mode 100644 index 0000000..4254f67 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java @@ -0,0 +1,60 @@ +/* + * 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); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java new file mode 100644 index 0000000..8ba2171 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java @@ -0,0 +1,907 @@ +/* + * 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.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.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +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 implements IShellOutputReceiver { + + // 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; + + // The bugreport file to process + private File mDataFile; + + // To get output from adb commands + private FileOutputStream mTempStream; + + // Selects the current display: MODE_CPU, etc. + private int mMode = 0; + private String mGfxPackageName; + + 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 { + initShellOutputBuffer(); + if (mMode == MODE_MEMINFO) { + // Hack to add bugreport-style section header for meminfo + mTempStream.write("------ MEMORY INFO ------\n".getBytes()); + } + getCurrentDevice().executeShellCommand(command, SysinfoPanel.this); + } 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 + */ + void initShellOutputBuffer() throws IOException { + mDataFile = File.createTempFile("ddmsfile", ".txt"); + mDataFile.deleteOnExit(); + mTempStream = new FileOutputStream(mDataFile); + } + + /** + * Adds output to the temp file. IShellOutputReceiver method. Called by + * executeShellCommand(). + */ + @Override + public void addOutput(byte[] data, int offset, int length) { + try { + mTempStream.write(data, offset, length); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Processes output from shell command. IShellOutputReceiver method. The + * output is passed to generateDataset(). Called by executeShellCommand() on + * completion. + */ + @Override + public void flush() { + if (mTempStream != null) { + try { + mTempStream.close(); + generateDataset(mDataFile); + mTempStream = null; + mDataFile = null; + } catch (IOException e) { + Log.e("DDMS", e); + } + } + } + + /** + * IShellOutputReceiver method. + * + * @return false - don't cancel + */ + @Override + public boolean isCancelled() { + return false; + } + + /** + * 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 (mDataFile != null) { + generateDataset(mDataFile); + } else 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<GfxProfileData> 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<GfxProfileData> data = new ArrayList<BugReportParser.GfxProfileData>(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<DataValue> readWakelockDataset(BufferedReader br) throws IOException { + List<DataValue> results = new ArrayList<DataValue>(); + + 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<DataValue> readAlarmDataset(BufferedReader br) throws IOException { + List<DataValue> results = new ArrayList<DataValue>(); + 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<DataValue> readCpuDataset(BufferedReader br) throws IOException { + List<DataValue> results = new ArrayList<DataValue>(); + 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<DataValue> readMeminfoDataset(BufferedReader br) throws IOException { + List<DataValue> results = new ArrayList<DataValue>(); + 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<DataValue> 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<DataValue> readProcRankDataset(BufferedReader br, String header) + throws IOException { + List<DataValue> results = new ArrayList<DataValue>(); + + if (header == null || !header.contains("PID")) { + return results; + } + + Splitter PROCRANK_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults(); + List<String> 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<DataValue> readSyncDataset(BufferedReader br) throws IOException { + List<DataValue> results = new ArrayList<DataValue>(); + + 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<BugReportParser.DataValue> 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> 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); + } + } + }); + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java new file mode 100644 index 0000000..66dcc0a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java @@ -0,0 +1,209 @@ +/* + * 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 + * <code>PreferenceStore</code> 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 + * <code>PreferenceStore</code> 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 + * <code>PreferenceStore</code> 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); + } + }); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java new file mode 100644 index 0000000..c1eb7f6 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java @@ -0,0 +1,132 @@ +/* + * 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.<br> + * 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.<br> + * 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); + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java new file mode 100644 index 0000000..81e245d --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java @@ -0,0 +1,573 @@ +/* + * 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. + * <p/> + * 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. + * <p/> + * This <b>must</b> 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 <code>null</code> 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 <code>null</code> if none is found. + * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection + * is returned, otherwise, the <code>ISelection</code> returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or <code>null</code> + */ + 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; + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java new file mode 100644 index 0000000..856b874 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java @@ -0,0 +1,42 @@ +/* + * 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 <code>true</code> to enable, and + * <code>false</code> 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); +} + diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java new file mode 100644 index 0000000..c7fef32 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java @@ -0,0 +1,71 @@ +/* + * 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 <code>true</code> to enable, and + * <code>false</code> to disable + * @see ICommonAction#setChecked(boolean) + */ + @Override + public void setChecked(boolean checked) { + item.setSelection(checked); + } + + /** + * Sets the enabled state of this action. + * @param enabled <code>true</code> to enable, and + * <code>false</code> 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(); + } + }); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java new file mode 100644 index 0000000..8e9e11b --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java @@ -0,0 +1,31 @@ +/* + * 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 { +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java new file mode 100644 index 0000000..e767eda --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java @@ -0,0 +1,31 @@ +/* + * 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 { +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java new file mode 100644 index 0000000..4df4376 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java @@ -0,0 +1,91 @@ +/* + * 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java new file mode 100644 index 0000000..3679d41 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java @@ -0,0 +1,47 @@ +/* + * 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); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java new file mode 100644 index 0000000..062d4f0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java @@ -0,0 +1,177 @@ +/* + * 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; + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java new file mode 100644 index 0000000..b69d3b5 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -0,0 +1,922 @@ +/* + * 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<FileEntry> entries = new ArrayList<FileEntry>(); + 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); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java new file mode 100644 index 0000000..1240e59 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java @@ -0,0 +1,160 @@ +/* + * 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 + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java new file mode 100644 index 0000000..f50a94c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java @@ -0,0 +1,184 @@ +/* + * 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. + * <p/>This <strong>must</strong> 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. + * <p/>This <strong>must</strong> 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. + * <p/>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(); + } + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java new file mode 100644 index 0000000..ab1b5f7 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java @@ -0,0 +1,195 @@ +/* + * 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); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java new file mode 100644 index 0000000..88db5cc --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java @@ -0,0 +1,222 @@ +/* + * 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 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<NativeAllocationInfo> allocations = new ArrayList<NativeAllocationInfo>(); + 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); + + 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<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>(); + 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; + } + + 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java new file mode 100644 index 0000000..9eb6ddf --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java @@ -0,0 +1,65 @@ +/* + * 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.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<NativeAllocationInfo> commonAllocations = + new HashSet<NativeAllocationInfo>(oldSnapshot.getAllocations()); + commonAllocations.retainAll(newSnapshot.getAllocations()); + + // Memory common between the old and new snapshots + mCommonAllocationsTotalMemory = getTotalMemory(commonAllocations); + } + + private static List<NativeAllocationInfo> getNewAllocations(NativeHeapSnapshot newSnapshot, + NativeHeapSnapshot oldSnapshot) { + Set<NativeAllocationInfo> allocations = + new HashSet<NativeAllocationInfo>(newSnapshot.getAllocations()); + allocations.removeAll(oldSnapshot.getAllocations()); + return new ArrayList<NativeAllocationInfo>(allocations); + } + + @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)); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java new file mode 100644 index 0000000..b96fa02 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java @@ -0,0 +1,112 @@ +/* + * 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java new file mode 100644 index 0000000..f6631b7 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java @@ -0,0 +1,1150 @@ +/* + * 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"); + if (useOldResolver != null && useOldResolver.equalsIgnoreCase("true")) { + USE_OLD_RESOLVER = true; + } else { + USE_OLD_RESOLVER = false; + } + } + 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.: <android-src>/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<NativeHeapSnapshot> 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<NativeHeapSnapshot> mDiffSnapshots; + + private Map<Integer, List<NativeHeapSnapshot>> 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 ToolBar mDetailsToolBar; + 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<NativeHeapSnapshot>(); + mDiffSnapshots = new ArrayList<NativeHeapSnapshot>(); + mImportedSnapshotsPerPid = new HashMap<Integer, List<NativeHeapSnapshot>>(); + } + + /** {@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<NativeAllocationInfo> 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<NativeAllocationInfo> 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()); + + 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<Long> 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<String> 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<Long> 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<NativeAllocationInfo> shallowCloneList(List<NativeAllocationInfo> allocations) { + List<NativeAllocationInfo> clonedList = + new ArrayList<NativeAllocationInfo>(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<NativeHeapSnapshot>(); + mDiffSnapshots = new ArrayList<NativeHeapSnapshot>(); + + mSnapshotHeapButton.setEnabled(true); + mLoadHeapDataButton.setEnabled(true); + + List<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get( + c.getClientData().getPid()); + if (importedSnapshots != null) { + for (NativeHeapSnapshot n : importedSnapshots) { + addNativeHeapSnapshot(n); + } + } + + List<NativeAllocationInfo> 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<String> items = new ArrayList<String>(); + + 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[0])); + + 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<NativeHeapSnapshot> importedSnapshots = mImportedSnapshotsPerPid.get(pid); + if (importedSnapshots == null) { + importedSnapshots = new ArrayList<NativeHeapSnapshot>(); + } + + 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.NONE | 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)); + + mDetailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER); + initializeDetailsToolBar(mDetailsToolBar); + + 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); + mDetailsToolBar.setLayoutData(data); + + data = new FormData(); + data.top = new FormAttachment(mDetailsToolBar, 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<String> properties = Arrays.asList(new String[] { + "Library", + "Total", + "Percentage", + "Count", + "Size", + "Method", + }); + + List<String> sampleValues = Arrays.asList(new String[] { + "/path/in/device/to/system/library.so", + "123456789", + " 100%", + "123456789", + "123456789", + "PossiblyLongDemangledMethodName", + }); + + // right align numeric values + List<Integer> swtFlags = Arrays.asList(new Integer[] { + 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<String> properties = Arrays.asList(new String[] { + "Address", + "Library", + "Method", + "File", + "Line", + }); + + List<String> sampleValues = Arrays.asList(new String[] { + "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<NativeAllocationInfo> mCallSites; + private List<NativeLibraryMapInfo> mMappedLibraries; + private Map<Long, NativeStackCallInfo> mResolvedSymbolCache; + + public SymbolResolverTask(List<NativeAllocationInfo> callSites, + List<NativeLibraryMapInfo> mappedLibraries) { + mCallSites = callSites; + mMappedLibraries = mappedLibraries; + + mResolvedSymbolCache = new HashMap<Long, NativeStackCallInfo>(); + } + + @Override + public void run() { + for (NativeAllocationInfo callSite : mCallSites) { + if (callSite.isStackCallResolved()) { + continue; + } + + List<Long> addresses = callSite.getStackCallAddresses(); + List<NativeStackCallInfo> resolvedStackInfo = + new ArrayList<NativeStackCallInfo>(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) { + Addr2Line process = Addr2Line.getProcess(library); + 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; + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java new file mode 100644 index 0000000..c31716b --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java @@ -0,0 +1,90 @@ +/* + * 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<NativeAllocationInfo> getAllocations() { + if (mDisplayZygoteMemory) { + return mNativeHeapDump.getAllocations(); + } else { + return mNativeHeapDump.getNonZygoteAllocations(); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java new file mode 100644 index 0000000..b786bfa --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java @@ -0,0 +1,92 @@ +/* + * 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(); + } + + 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<NativeLibraryAllocationInfo> getLibraryAllocations(NativeHeapSnapshot snapshot) { + if (mDisplayZygoteMemory) { + return snapshot.getAllocationsByLibrary(); + } else { + return snapshot.getNonZygoteAllocationsByLibrary(); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java new file mode 100644 index 0000000..e2023d2 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java @@ -0,0 +1,133 @@ +/* + * 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<NativeAllocationInfo> mHeapAllocations; + private List<NativeLibraryAllocationInfo> mHeapAllocationsByLibrary; + + private List<NativeAllocationInfo> mNonZygoteHeapAllocations; + private List<NativeLibraryAllocationInfo> mNonZygoteHeapAllocationsByLibrary; + + private long mTotalSize; + + public NativeHeapSnapshot(List<NativeAllocationInfo> heapAllocations) { + mHeapAllocations = heapAllocations; + + // precompute the total size as this is always needed. + mTotalSize = getTotalMemory(heapAllocations); + } + + protected long getTotalMemory(Collection<NativeAllocationInfo> heapSnapshot) { + long total = 0; + + for (NativeAllocationInfo info : heapSnapshot) { + total += info.getAllocationCount() * info.getSize(); + } + + return total; + } + + public List<NativeAllocationInfo> getAllocations() { + return mHeapAllocations; + } + + public List<NativeLibraryAllocationInfo> getAllocationsByLibrary() { + if (mHeapAllocationsByLibrary != null) { + return mHeapAllocationsByLibrary; + } + + List<NativeLibraryAllocationInfo> 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<NativeLibraryAllocationInfo> 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<NativeAllocationInfo> getNonZygoteAllocations() { + if (mNonZygoteHeapAllocations != null) { + return mNonZygoteHeapAllocations; + } + + // filter out all zygote allocations + mNonZygoteHeapAllocations = new ArrayList<NativeAllocationInfo>(); + for (NativeAllocationInfo info : mHeapAllocations) { + if (info.isZygoteChild()) { + mNonZygoteHeapAllocations.add(info); + } + } + + return mNonZygoteHeapAllocations; + } + + public List<NativeLibraryAllocationInfo> getNonZygoteAllocationsByLibrary() { + if (mNonZygoteHeapAllocationsByLibrary != null) { + return mNonZygoteHeapAllocationsByLibrary; + } + + List<NativeLibraryAllocationInfo> heapAllocations = + NativeLibraryAllocationInfo.constructFrom(getNonZygoteAllocations()); + + // cache for future uses only if it is fully resolved. + if (isFullyResolved(heapAllocations)) { + mNonZygoteHeapAllocationsByLibrary = heapAllocations; + } + + return heapAllocations; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java new file mode 100644 index 0000000..1722cdb --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java @@ -0,0 +1,135 @@ +/* + * 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<NativeAllocationInfo> mHeapAllocations; + private int mTotalSize; + + private NativeLibraryAllocationInfo(String libraryName) { + mLibraryName = libraryName; + mHeapAllocations = new ArrayList<NativeAllocationInfo>(); + } + + 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<NativeAllocationInfo> 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<NativeLibraryAllocationInfo> constructFrom( + List<NativeAllocationInfo> allocations) { + if (allocations == null) { + return null; + } + + Map<String, NativeLibraryAllocationInfo> allocationsByLibrary = + new HashMap<String, NativeLibraryAllocationInfo>(); + + // 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<NativeLibraryAllocationInfo> libraryAllocations = + new ArrayList<NativeLibraryAllocationInfo>(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<NativeLibraryAllocationInfo>() { + @Override + public int compare(NativeLibraryAllocationInfo o1, + NativeLibraryAllocationInfo o2) { + return (int) (o2.getTotalSize() - o1.getTotalSize()); + } + }); + + return libraryAllocations; + } + + private static void addtoLibrary(Map<String, NativeLibraryAllocationInfo> libraryAllocations, + String libName, NativeAllocationInfo info) { + NativeLibraryAllocationInfo libAllocationInfo = libraryAllocations.get(libName); + if (libAllocationInfo == null) { + libAllocationInfo = new NativeLibraryAllocationInfo(libName); + libraryAllocations.put(libName, libAllocationInfo); + } + + libAllocationInfo.addAllocation(info); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java new file mode 100644 index 0000000..9a6ddb2 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java @@ -0,0 +1,56 @@ +/* + * 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java new file mode 100644 index 0000000..b7428b9 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java @@ -0,0 +1,71 @@ +/* + * 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java new file mode 100644 index 0000000..1a75c6e --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java @@ -0,0 +1,306 @@ +/* + * 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.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.IOException; +import java.io.InputStreamReader; +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 DEFAULT_SYMBOLS_FOLDER; + + static { + String addr2lineEnv = System.getenv("ANDROID_ADDR2LINE"); + ADDR2LINE = addr2lineEnv != null ? addr2lineEnv : DdmUiPreferences.getAddr2Line(); + + String symbols = System.getenv("ANDROID_SYMBOLS"); + DEFAULT_SYMBOLS_FOLDER = symbols != null ? symbols : DdmUiPreferences.getSymbolDirectory(); + } + + private List<NativeAllocationInfo> mCallSites; + private List<NativeLibraryMapInfo> mMappedLibraries; + private List<String> mSymbolSearchFolders; + + /** All unresolved addresses from all the callsites. */ + private SortedSet<Long> mUnresolvedAddresses; + + /** Set of all addresses that could were not resolved at the end of the resolution process. */ + private Set<Long> mUnresolvableAddresses; + + /** Map of library -> [unresolved addresses mapping to this library]. */ + private Map<NativeLibraryMapInfo, Set<Long>> mUnresolvedAddressesPerLibrary; + + /** Addresses that could not be mapped to a library, should be mostly empty. */ + private Set<Long> mUnmappedAddresses; + + /** Cache of the resolution for every unresolved address. */ + private Map<Long, NativeStackCallInfo> mAddressResolution; + + /** List of libraries that were not located on disk. */ + private Set<String> mNotFoundLibraries; + private String mAddr2LineErrorMessage = null; + + public NativeSymbolResolverTask(List<NativeAllocationInfo> callSites, + List<NativeLibraryMapInfo> mappedLibraries, + String symbolSearchPath) { + mCallSites = callSites; + mMappedLibraries = mappedLibraries; + mSymbolSearchFolders = new ArrayList<String>(); + mSymbolSearchFolders.add(DEFAULT_SYMBOLS_FOLDER); + mSymbolSearchFolders.addAll(Arrays.asList(symbolSearchPath.split(":"))); + + mUnresolvedAddresses = new TreeSet<Long>(); + mUnresolvableAddresses = new HashSet<Long>(); + mUnresolvedAddressesPerLibrary = new HashMap<NativeLibraryMapInfo, Set<Long>>(); + mUnmappedAddresses = new HashSet<Long>(); + mAddressResolution = new HashMap<Long, NativeStackCallInfo>(); + mNotFoundLibraries = new HashSet<String>(); + } + + @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<Long> mappedAddresses = new HashSet<Long>(); + + for (NativeLibraryMapInfo lib : mMappedLibraries) { + SortedSet<Long> 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<Long> 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 void resolveAddresses(NativeLibraryMapInfo lib, String libPath, + Set<Long> addressesToResolve) { + Process addr2line = null; + try { + addr2line = new ProcessBuilder(ADDR2LINE, + "-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(); + try { + for (Long addr : addressesToResolve) { + long offset = addr.longValue() - libStartAddress; + addressWriter.write(Long.toHexString(offset)); + addressWriter.newLine(); + addressWriter.flush(); + String method = resultReader.readLine(); + String sourceFile = resultReader.readLine(); + + mAddressResolution.put(addr, + new NativeStackCallInfo(addr.longValue(), + 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<Long> addressesToResolve, + NativeLibraryMapInfo lib) { + for (Long addr : addressesToResolve) { + markAddressNotResolvable(lib, addr); + } + } + + private void markAddressNotResolvable(NativeLibraryMapInfo lib, Long addr) { + mAddressResolution.put(addr, + new NativeStackCallInfo(addr.longValue(), + 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<NativeAllocationInfo> callSites) { + for (NativeAllocationInfo callSite : callSites) { + List<NativeStackCallInfo> stackInfo = new ArrayList<NativeStackCallInfo>(); + + 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<Long> getUnmappedAddresses() { + return mUnmappedAddresses; + } + + public Set<Long> getUnresolvableAddresses() { + return mUnresolvableAddresses; + } + + public Set<String> getNotFoundLibraries() { + return mNotFoundLibraries; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java new file mode 100644 index 0000000..2aef53c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java @@ -0,0 +1,249 @@ +/* + * 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. + * <p/>This handle the conversion between both modes automatically by using a {@link ModifyListener} + * on all the {@link Text} widgets. + * <p/>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. + * <p/>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. + * <p/>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. + * <p/>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. + * <p/>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). + * <p/>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--; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java new file mode 100644 index 0000000..a30337a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java @@ -0,0 +1,373 @@ +/* + * 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. + * <p/> + * 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<WayPoint> mWayPoints; + List<Track> 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<WayPoint>(); + } + + mWayPoints.add(mCurrentWayPoint = new WayPoint()); + handleLocation(mCurrentWayPoint, attributes); + } else if (NODE_TRACK.equals(localName)) { + if (mTrackList == null) { + mTrackList = new ArrayList<Track>(); + } + + 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. + * <p/>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<TrackPoint> mPoints = new ArrayList<TrackPoint>(); + + 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 <code>true</code> 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 <code>null</code> 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 <code>null</code> if none were found (or + * if the parsing failed. + */ + public Track[] getTracks() { + if (mHandler != null) { + return mHandler.getTracks(); + } + + return null; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java new file mode 100644 index 0000000..af485ac --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java @@ -0,0 +1,210 @@ +/* + * 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. + * <p/> + * 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<WayPoint> 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<WayPoint>(); + } + + 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 <code>true</code> 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 <code>null</code> if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java new file mode 100644 index 0000000..dbb8f41 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java new file mode 100644 index 0000000..da21920 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java @@ -0,0 +1,48 @@ +/* + * 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. + * <p/>The expected type for the input is {@link Track}<code>[]</code>. + */ +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 + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java new file mode 100644 index 0000000..50acb53 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java @@ -0,0 +1,87 @@ +/* + * 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 + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java new file mode 100644 index 0000000..527f4bf --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java @@ -0,0 +1,34 @@ +/* + * 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. + * <p/>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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java new file mode 100644 index 0000000..32880bd --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java @@ -0,0 +1,42 @@ +/* + * 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. + * <p/>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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java new file mode 100644 index 0000000..1b7fe15 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java @@ -0,0 +1,46 @@ +/* + * 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. + * <p/>The expected type for the input is {@link WayPoint}<code>[]</code>. + */ +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 + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java new file mode 100644 index 0000000..9f642f1 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java @@ -0,0 +1,79 @@ +/* + * 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 + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java new file mode 100644 index 0000000..da41e70 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java @@ -0,0 +1,96 @@ +/* + * 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<String> content = new ArrayList<String>(); + 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<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith(HEADER_TAG) == false) { + content.add(line); + } else { + break; + } + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 0000000..473387a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java @@ -0,0 +1,55 @@ +/* + * 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<ValueDisplayDescriptor> valueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 0000000..0cffd7e --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java @@ -0,0 +1,422 @@ +/* + * 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<TimeSeriesCollection> 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<ValueDisplayDescriptor> valueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + 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. + * <p/>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<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> occurrenceDescriptors) { + Map<Integer, String> 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<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap<Integer, TimeSeries>(); + 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<Integer, TimeSeries> map = mOcurrenceDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap<Integer, TimeSeries>(); + 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<HashMap<Integer, TimeSeries>> pidMapValues = + mValueDescriptorSeriesMap.values(); + + for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) { + Collection<TimeSeries> seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + + pidMapValues = mOcurrenceDescriptorSeriesMap.values(); + for (HashMap<Integer, TimeSeries> pidMapValue : pidMapValues) { + Collection<TimeSeries> 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; + } +}
\ No newline at end of file diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 0000000..8e7c1ac --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java @@ -0,0 +1,381 @@ +/* + * 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). + * <p/>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<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> 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 <code>index</code>-th column of the log {@link Table} (if applicable). + * <p/> + * This does nothing if the <code>Table</code> object is <code>null</code> (because the display + * type does not use a column) or if the <code>index</code>-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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java new file mode 100644 index 0000000..6122513 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java @@ -0,0 +1,304 @@ +/* + * 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<String> 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<String> 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<String>(); + 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)"); + } + 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java new file mode 100644 index 0000000..5bfc039 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java @@ -0,0 +1,181 @@ +/* + * 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<SimpleTimePeriod, Integer> 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<SimpleTimePeriod, Integer> 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<SimpleTimePeriod, Integer>(); + + } + } + + /** + * 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java new file mode 100644 index 0000000..10176e3 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java @@ -0,0 +1,227 @@ +/* + * 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<String> 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<String>[] 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<String>(); + 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java new file mode 100644 index 0000000..d0d2789 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java @@ -0,0 +1,975 @@ +/* + * 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 = "<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<Integer> mPidFilterList = null; + + protected final ArrayList<ValueDisplayDescriptor> mValueDescriptors = + new ArrayList<ValueDisplayDescriptor>(); + private final ArrayList<OccurrenceDisplayDescriptor> mOccurrenceDescriptors = + new ArrayList<OccurrenceDisplayDescriptor>(); + + /* ================== + * 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<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>> mValueDescriptorSeriesMap = + new HashMap<ValueDisplayDescriptor, HashMap<Integer, TimeSeries>>(); + /** + * This is a map of (descriptor, map2) where map2 is a map of (pid, chart-series) + */ + protected final HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>> mOcurrenceDescriptorSeriesMap = + new HashMap<OccurrenceDisplayDescriptor, HashMap<Integer, TimeSeries>>(); + + /** + * This is a map of (ValueType, dataset) + */ + protected final HashMap<ValueType, TimeSeriesCollection> mValueTypeDataSetMap = + new HashMap<ValueType, TimeSeriesCollection>(); + + 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<Integer>(); + 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<Integer> pids) { + if (mPidFiltering == false) { + throw new InvalidParameterException(); + } + + mPidFilterList = pids; + } + + ArrayList<Integer> getPidFilterList() { + return mPidFilterList; + } + + void addPidFiler(int pid) { + if (mPidFiltering == false) { + throw new InvalidParameterException(); + } + + if (mPidFilterList == null) { + mPidFilterList = new ArrayList<Integer>(); + } + + mPidFilterList.add(pid); + } + + /** + * Returns an iterator to the list of {@link ValueDisplayDescriptor}. + */ + Iterator<ValueDisplayDescriptor> 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<OccurrenceDisplayDescriptor> 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 <code>null</code> if <code>descriptorClass</code> is another class. + */ + OccurrenceDisplayDescriptor getDescriptor( + Class<? extends OccurrenceDisplayDescriptor> 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<? extends OccurrenceDisplayDescriptor> 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<ValueDisplayDescriptor> descKeys = mValueDescriptorSeriesMap.keySet(); + + for (ValueDisplayDescriptor descKey : descKeys) { + HashMap<Integer, TimeSeries> map = mValueDescriptorSeriesMap.get(descKey); + + Set<Integer> 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 <code>index</code>-th column of the log {@link Table} (if applicable). + * Subclasses can override if necessary. + * <p/> + * This does nothing if the <code>Table</code> object is <code>null</code> (because the display + * type does not use a column) or if the <code>index</code>-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 <code>null</code>. + */ + 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<Integer>(); + } + mPidFilterList.add(Integer.parseInt(value)); + } + } + } + + private String getDescriptorStorageString( + ArrayList<? extends OccurrenceDisplayDescriptor> 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<? extends OccurrenceDisplayDescriptor> 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<ValueDisplayDescriptor> valueDescriptors, + ArrayList<OccurrenceDisplayDescriptor> 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java new file mode 100644 index 0000000..b13f3f4 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -0,0 +1,961 @@ +/* + * 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<EventDisplay> mDisplayList = new ArrayList<EventDisplay>(); + + /* 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<Integer, String> mEventTagMap; + + /** Map with (event-tag, array of value info for the event) */ + private Map<Integer, EventValueDescription[]> mEventDescriptionMap; + + /** list of current pids */ + private ArrayList<Integer> 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<EventDisplay> displayList, + ArrayList<EventContainer> 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<EventDisplay> 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<Integer> list = new ArrayList<Integer>(); + 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<? extends OccurrenceDisplayDescriptor> 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<EventDisplay> displayList) { + for (EventDisplay eventDisplay : displayList) { + mDisplayList.add(EventDisplay.clone(eventDisplay)); + } + } + + private void buildPidList(ArrayList<EventContainer> eventList) { + mPidList = new ArrayList<Integer>(); + 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<Integer> 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<ValueDisplayDescriptor> 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<OccurrenceDisplayDescriptor> 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$ + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java new file mode 100644 index 0000000..011bcf1 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java @@ -0,0 +1,95 @@ +/* + * 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<String> content = new ArrayList<String>(); + 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<String> content = new ArrayList<String>(); + while ((line = reader.readLine()) != null) { + content.add(line); + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java new file mode 100644 index 0000000..937ee40 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java @@ -0,0 +1,938 @@ +/* + * 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<EventContainer> mEvents = new ArrayList<EventContainer>(); + + /** list of all the new events, that have yet to be displayed by the ui */ + private final ArrayList<EventContainer> mNewEvents = new ArrayList<EventContainer>(); + /** indicates a pending ui thread display */ + private boolean mPendingDisplay = false; + + /** list of all the custom event displays */ + private final ArrayList<EventDisplay> mEventDisplays = new ArrayList<EventDisplay>(); + + 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. + * <p/>This method sets up the {@link ICommonAction} objects to execute the proper code + * when triggered by using {@link ICommonAction#setRunnable(Runnable)}. + * <p/>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. + * </p> + * <b>This must be called from the UI thread</b> + */ + @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. + * <p/> + * <b>This must be called from the UI thread</b> + */ + 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}. + * <p/> + * 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); + } + } + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java new file mode 100644 index 0000000..e7c5196 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java @@ -0,0 +1,630 @@ +/* + * 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<Integer> mSeriesIndices = new ArrayList<Integer>(); + + 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<? extends OccurrenceDisplayDescriptor> 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<Integer, String> eventTagMap = mLogParser.getTagMap(); + Map<Integer, EventValueDescription[]> eventInfoMap = mLogParser.getEventInfoMap(); + Set<Integer> keys = eventTagMap.keySet(); + ArrayList<Integer> list = new ArrayList<Integer>(); + 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)); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java new file mode 100644 index 0000000..3af1447 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java @@ -0,0 +1,90 @@ +/* + * 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); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 0000000..0fa6f28 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java @@ -0,0 +1,173 @@ +/* + * 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); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java new file mode 100644 index 0000000..0e302ce --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -0,0 +1,397 @@ +/* + * 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("<none>"); + 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); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java new file mode 100644 index 0000000..2804629 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java @@ -0,0 +1,33 @@ +/* + * 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<LogCatMessage> addedMessages, List<LogCatMessage> deletedMessages); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java new file mode 100644 index 0000000..728b518 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.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); +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java new file mode 100644 index 0000000..629b0e0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java @@ -0,0 +1,46 @@ +/* + * 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(); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java new file mode 100644 index 0000000..dbc34d8 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java @@ -0,0 +1,81 @@ +/* + * 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<LogCatMessage> 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java new file mode 100644 index 0000000..fe24ddd --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java @@ -0,0 +1,63 @@ +/* + * 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<LogCatFilter, LogCatFilterData> mFilterData; + + public LogCatFilterLabelProvider(Map<LogCatFilter, LogCatFilterData> 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(); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java new file mode 100644 index 0000000..39b3fa9 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java @@ -0,0 +1,327 @@ +/* + * 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<String> getLogLevels() { + List<String> logLevels = new ArrayList<String>(); + + for (LogLevel l : LogLevel.values()) { + logLevels.add(l.getStringValue()); + } + + return logLevels; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java new file mode 100644 index 0000000..de35162 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java @@ -0,0 +1,211 @@ +/* + * 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<LogCatFilter> filters, + Map<LogCatFilter, LogCatFilterData> 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<LogCatFilter> decodeFromPreferenceString(String pref) { + List<LogCatFilter> fs = new ArrayList<LogCatFilter>(); + + /* first split the string into a list of key, value pairs */ + List<String> 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<String> getKeyValues(String pref) { + List<String> kv = new ArrayList<String>(); + 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(); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java new file mode 100644 index 0000000..c5cd548 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java @@ -0,0 +1,116 @@ +/* + * 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<LogCatMessage> mQ; + + /** + * Construct an empty message list. + * @param maxMessages capacity of the circular buffer + */ + public LogCatMessageList(int maxMessages) { + mFifoSize = maxMessages; + + mQ = new ArrayBlockingQueue<LogCatMessage>(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<LogCatMessage>(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<LogCatMessage>(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<LogCatMessage> 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<LogCatMessage> ensureSpace(int messageCount) { + List<LogCatMessage> l = new ArrayList<LogCatMessage>(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<LogCatMessage> getAllMessages() { + return new ArrayList<LogCatMessage>(mQ); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java new file mode 100644 index 0000000..bda742c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java @@ -0,0 +1,1607 @@ +/* + * 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<LogCatFilter> mLogCatFilters; + private Map<LogCatFilter, LogCatFilterData> mLogCatFilterData; + private int mCurrentSelectedFilterIndex; + + private ToolItem mNewFilterToolItem; + private ToolItem mDeleteFilterToolItem; + private ToolItem mEditFilterToolItem; + private TableViewer mFiltersTableViewer; + + private Combo mLiveFilterLevelCombo; + private Text mLiveFilterText; + + private List<LogCatFilter> 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<LogCatMessage> 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<LogCatMessage>(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<LogCatFilter>(); + mLogCatFilterData = new ConcurrentHashMap<LogCatFilter, LogCatFilterData>(); + + /* 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<LogCatFilter> 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<LogCatFilter> 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<LogCatMessage> 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<LogCatMessage> getSelectedLogCatMessages() { + int[] indices = mTable.getSelectionIndices(); + Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */ + + List<LogCatMessage> selectedMessages = new ArrayList<LogCatMessage>(indices.length); + for (int i : indices) { + Object data = mTable.getItem(i).getData(); + if (data instanceof LogCatMessage) { + selectedMessages.add((LogCatMessage) data); + } + } + + return selectedMessages; + } + + private List<LogCatMessage> applyCurrentFilters(List<LogCatMessage> msgList) { + List<LogCatMessage> filteredItems = new ArrayList<LogCatMessage>(msgList.size()); + + for (LogCatMessage msg: msgList) { + if (isMessageAccepted(msg, mCurrentFilters)) { + filteredItems.add(msg); + } + } + + return filteredItems; + } + + private boolean isMessageAccepted(LogCatMessage msg, List<LogCatFilter> 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<LogCatMessage> selectedMessages = getSelectedLogCatMessages(); + if (selectedMessages.size() == 0) { + addNewFilter(); + } else { + LogCatMessage m = selectedMessages.get(0); + addNewFilter(m.getTag(), m.getMessage(), 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<LogCatFilter> getFiltersToApply() { + /* list of filters to apply = saved filter + live filters */ + List<LogCatFilter> filters = new ArrayList<LogCatFilter>(); + + if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) { + filters.add(getSelectedSavedFilter()); + } + + filters.addAll(getCurrentLiveFilters()); + return filters; + } + + private List<LogCatFilter> 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<LogCatMessage> addedMessages, + List<LogCatMessage> 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<LogCatMessage> addedMessages = mReceiver.getMessages().getAllMessages(); + List<LogCatMessage> 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<LogCatMessage> 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<LogCatMessage> newMessages; + int deletedMessageCount; + synchronized (mLogBuffer) { + newMessages = new ArrayList<LogCatMessage>(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<String> 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.getTime(), + m.getPid(), + 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<String> wrapMessage(String msg, int wrapWidth) { + if (msg.length() < wrapWidth) { + return Collections.singletonList(msg); + } + + List<String> wrappedMessages = new ArrayList<String>(); + + 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<ILogCatMessageSelectionListener> mMessageSelectionListeners; + + private void initDoubleClickListener() { + mMessageSelectionListeners = new ArrayList<ILogCatMessageSelectionListener>(1); + + mTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent arg0) { + List<LogCatMessage> 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java new file mode 100644 index 0000000..a85cd03 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java @@ -0,0 +1,151 @@ +/* + * 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<ILogCatBufferChangeListener> 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<ILogCatBufferChangeListener>(); + 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<LogCatMessage> newMessages) { + List<LogCatMessage> 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<LogCatMessage> addedMessages, + List<LogCatMessage> 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); + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java new file mode 100644 index 0000000..5b25e17 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java @@ -0,0 +1,95 @@ +/* + * 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<String, LogCatReceiver> mReceiverCache = new HashMap<String, LogCatReceiver>(); + + /** 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: <ul> + * <li> {@link #removeReceiverFor(IDevice)} waiting to lock {@link LogCatReceiverFactory}, + * while holding a DDMS monitor internal lock. </li> + * <li> {@link #newReceiver(IDevice, IPreferenceStore)} holding {@link LogCatReceiverFactory} + * while attempting to obtain a DDMS monitor lock. </li> + * </ul> + */ + 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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java new file mode 100644 index 0000000..3da9fd0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java @@ -0,0 +1,81 @@ +/* + * 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; + } + } + +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java new file mode 100644 index 0000000..9cff656 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java @@ -0,0 +1,27 @@ +/* + * 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; +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java new file mode 100644 index 0000000..74a5e37 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java @@ -0,0 +1,556 @@ +/* + * 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<LogMessage> mMessages = new ArrayList<LogMessage>(); + private final ArrayList<LogMessage> mNewMessages = new ArrayList<LogMessage>(); + + 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. + * <p/>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); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java new file mode 100644 index 0000000..a347155 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java @@ -0,0 +1,1626 @@ +/* + * 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).<br> + * This first line looks something like<br> + * <code>"[ 00-00 00:00:00.000 <pid>:0x<???> <severity>/<tag>]"</code> + * <br> + * Note: severity is one of V, D, I, W, or EM<br> + * 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 <code>mBufferStart</code> to + * <code>mBufferEnd - 1</code>. Therefore its number of item is + * <code>mBufferEnd - mBufferStart</code>. + */ + 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.<br> + * <b>MUST</b> 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<LogMessage> newMessages = new ArrayList<LogMessage>(); + + 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 = "<unknown>"; //$NON-NLS1$ + mLastMessageInfo.pid = 0; + mLastMessageInfo.logLevel = LogLevel.INFO; + mLastMessageInfo.tag = "<unknown>"; //$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. + * <p/>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<String> keywords = new ArrayList<String>(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; + } +} diff --git a/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java new file mode 100644 index 0000000..15b8b56 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java @@ -0,0 +1,1125 @@ +/* + * 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<TrackedItem> mTrackedItems = new ArrayList<TrackedItem>(); + + 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<NetworkSnapshot.Entry> { + private ArrayList<Entry> mStats = new ArrayList<Entry>(); + + 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<Entry> 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); + } + } +} diff --git a/ddms/ddmuilib/src/main/java/images/add.png b/ddms/ddmuilib/src/main/java/images/add.png Binary files differnew file mode 100644 index 0000000..eefc2ca --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/add.png diff --git a/ddms/ddmuilib/src/main/java/images/android.png b/ddms/ddmuilib/src/main/java/images/android.png Binary files differnew file mode 100644 index 0000000..3779d4d --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/android.png diff --git a/ddms/ddmuilib/src/main/java/images/backward.png b/ddms/ddmuilib/src/main/java/images/backward.png Binary files differnew file mode 100644 index 0000000..90a9713 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/backward.png diff --git a/ddms/ddmuilib/src/main/java/images/capture.png b/ddms/ddmuilib/src/main/java/images/capture.png Binary files differnew file mode 100644 index 0000000..da5c10b --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/capture.png diff --git a/ddms/ddmuilib/src/main/java/images/clear.png b/ddms/ddmuilib/src/main/java/images/clear.png Binary files differnew file mode 100644 index 0000000..0009cf6 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/clear.png diff --git a/ddms/ddmuilib/src/main/java/images/d.png b/ddms/ddmuilib/src/main/java/images/d.png Binary files differnew file mode 100644 index 0000000..d45506e --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/d.png diff --git a/ddms/ddmuilib/src/main/java/images/debug-attach.png b/ddms/ddmuilib/src/main/java/images/debug-attach.png Binary files differnew file mode 100644 index 0000000..9b8a11c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/debug-attach.png diff --git a/ddms/ddmuilib/src/main/java/images/debug-error.png b/ddms/ddmuilib/src/main/java/images/debug-error.png Binary files differnew file mode 100644 index 0000000..f22da1f --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/debug-error.png diff --git a/ddms/ddmuilib/src/main/java/images/debug-wait.png b/ddms/ddmuilib/src/main/java/images/debug-wait.png Binary files differnew file mode 100644 index 0000000..322be63 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/debug-wait.png diff --git a/ddms/ddmuilib/src/main/java/images/delete.png b/ddms/ddmuilib/src/main/java/images/delete.png Binary files differnew file mode 100644 index 0000000..db5fab8 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/delete.png diff --git a/ddms/ddmuilib/src/main/java/images/device.png b/ddms/ddmuilib/src/main/java/images/device.png Binary files differnew file mode 100644 index 0000000..7dbbbb6 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/device.png diff --git a/ddms/ddmuilib/src/main/java/images/diff.png b/ddms/ddmuilib/src/main/java/images/diff.png Binary files differnew file mode 100644 index 0000000..bdd9e5c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/diff.png diff --git a/ddms/ddmuilib/src/main/java/images/displayfilters.png b/ddms/ddmuilib/src/main/java/images/displayfilters.png Binary files differnew file mode 100644 index 0000000..d110c2c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/displayfilters.png diff --git a/ddms/ddmuilib/src/main/java/images/down.png b/ddms/ddmuilib/src/main/java/images/down.png Binary files differnew file mode 100644 index 0000000..f9426cb --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/down.png diff --git a/ddms/ddmuilib/src/main/java/images/e.png b/ddms/ddmuilib/src/main/java/images/e.png Binary files differnew file mode 100644 index 0000000..dee7c97 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/e.png diff --git a/ddms/ddmuilib/src/main/java/images/edit.png b/ddms/ddmuilib/src/main/java/images/edit.png Binary files differnew file mode 100644 index 0000000..b8f65bc --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/edit.png diff --git a/ddms/ddmuilib/src/main/java/images/empty.png b/ddms/ddmuilib/src/main/java/images/empty.png Binary files differnew file mode 100644 index 0000000..f021542 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/empty.png diff --git a/ddms/ddmuilib/src/main/java/images/emulator.png b/ddms/ddmuilib/src/main/java/images/emulator.png Binary files differnew file mode 100644 index 0000000..a718042 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/emulator.png diff --git a/ddms/ddmuilib/src/main/java/images/file.png b/ddms/ddmuilib/src/main/java/images/file.png Binary files differnew file mode 100644 index 0000000..043a814 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/file.png diff --git a/ddms/ddmuilib/src/main/java/images/folder.png b/ddms/ddmuilib/src/main/java/images/folder.png Binary files differnew file mode 100644 index 0000000..7e29b1a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/folder.png diff --git a/ddms/ddmuilib/src/main/java/images/forward.png b/ddms/ddmuilib/src/main/java/images/forward.png Binary files differnew file mode 100644 index 0000000..a97a605 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/forward.png diff --git a/ddms/ddmuilib/src/main/java/images/gc.png b/ddms/ddmuilib/src/main/java/images/gc.png Binary files differnew file mode 100644 index 0000000..5194806 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/gc.png diff --git a/ddms/ddmuilib/src/main/java/images/groupby.png b/ddms/ddmuilib/src/main/java/images/groupby.png Binary files differnew file mode 100644 index 0000000..250b982 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/groupby.png diff --git a/ddms/ddmuilib/src/main/java/images/halt.png b/ddms/ddmuilib/src/main/java/images/halt.png Binary files differnew file mode 100644 index 0000000..10e3720 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/halt.png diff --git a/ddms/ddmuilib/src/main/java/images/heap.png b/ddms/ddmuilib/src/main/java/images/heap.png Binary files differnew file mode 100644 index 0000000..e3aa3f0 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/heap.png diff --git a/ddms/ddmuilib/src/main/java/images/hprof.png b/ddms/ddmuilib/src/main/java/images/hprof.png Binary files differnew file mode 100644 index 0000000..123d062 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/hprof.png diff --git a/ddms/ddmuilib/src/main/java/images/i.png b/ddms/ddmuilib/src/main/java/images/i.png Binary files differnew file mode 100644 index 0000000..98385c5 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/i.png diff --git a/ddms/ddmuilib/src/main/java/images/importBug.png b/ddms/ddmuilib/src/main/java/images/importBug.png Binary files differnew file mode 100644 index 0000000..f5da179 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/importBug.png diff --git a/ddms/ddmuilib/src/main/java/images/load.png b/ddms/ddmuilib/src/main/java/images/load.png Binary files differnew file mode 100644 index 0000000..9e7bf6e --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/load.png diff --git a/ddms/ddmuilib/src/main/java/images/pause.png b/ddms/ddmuilib/src/main/java/images/pause.png Binary files differnew file mode 100644 index 0000000..19d286d --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/pause.png diff --git a/ddms/ddmuilib/src/main/java/images/play.png b/ddms/ddmuilib/src/main/java/images/play.png Binary files differnew file mode 100644 index 0000000..d54f013 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/play.png diff --git a/ddms/ddmuilib/src/main/java/images/pull.png b/ddms/ddmuilib/src/main/java/images/pull.png Binary files differnew file mode 100644 index 0000000..f48f1b1 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/pull.png diff --git a/ddms/ddmuilib/src/main/java/images/push.png b/ddms/ddmuilib/src/main/java/images/push.png Binary files differnew file mode 100644 index 0000000..6222864 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/push.png diff --git a/ddms/ddmuilib/src/main/java/images/save.png b/ddms/ddmuilib/src/main/java/images/save.png Binary files differnew file mode 100644 index 0000000..040ebda --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/save.png diff --git a/ddms/ddmuilib/src/main/java/images/scroll_lock.png b/ddms/ddmuilib/src/main/java/images/scroll_lock.png Binary files differnew file mode 100644 index 0000000..5d26689 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/scroll_lock.png diff --git a/ddms/ddmuilib/src/main/java/images/sort_down.png b/ddms/ddmuilib/src/main/java/images/sort_down.png Binary files differnew file mode 100644 index 0000000..2d4ccc1 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/sort_down.png diff --git a/ddms/ddmuilib/src/main/java/images/sort_up.png b/ddms/ddmuilib/src/main/java/images/sort_up.png Binary files differnew file mode 100644 index 0000000..3a0bc3c --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/sort_up.png diff --git a/ddms/ddmuilib/src/main/java/images/thread.png b/ddms/ddmuilib/src/main/java/images/thread.png Binary files differnew file mode 100644 index 0000000..ac839e8 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/thread.png diff --git a/ddms/ddmuilib/src/main/java/images/tracing_start.png b/ddms/ddmuilib/src/main/java/images/tracing_start.png Binary files differnew file mode 100644 index 0000000..88771cc --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/tracing_start.png diff --git a/ddms/ddmuilib/src/main/java/images/tracing_stop.png b/ddms/ddmuilib/src/main/java/images/tracing_stop.png Binary files differnew file mode 100644 index 0000000..71bd215 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/tracing_stop.png diff --git a/ddms/ddmuilib/src/main/java/images/up.png b/ddms/ddmuilib/src/main/java/images/up.png Binary files differnew file mode 100644 index 0000000..92edf5a --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/up.png diff --git a/ddms/ddmuilib/src/main/java/images/v.png b/ddms/ddmuilib/src/main/java/images/v.png Binary files differnew file mode 100644 index 0000000..8044051 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/v.png diff --git a/ddms/ddmuilib/src/main/java/images/w.png b/ddms/ddmuilib/src/main/java/images/w.png Binary files differnew file mode 100644 index 0000000..129d0f9 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/w.png diff --git a/ddms/ddmuilib/src/main/java/images/warning.png b/ddms/ddmuilib/src/main/java/images/warning.png Binary files differnew file mode 100644 index 0000000..ca3b6ed --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/warning.png diff --git a/ddms/ddmuilib/src/main/java/images/zygote.png b/ddms/ddmuilib/src/main/java/images/zygote.png Binary files differnew file mode 100644 index 0000000..5cbb1d2 --- /dev/null +++ b/ddms/ddmuilib/src/main/java/images/zygote.png diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java new file mode 100644 index 0000000..7894965 --- /dev/null +++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/BugReportParserTest.java @@ -0,0 +1,188 @@ +/* + * 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 com.android.ddmuilib.SysinfoPanel.BugReportParser; +import com.android.ddmuilib.SysinfoPanel.BugReportParser.DataValue; +import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData; + +import junit.framework.TestCase; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +public class BugReportParserTest extends TestCase { + public void testParseEclairCpuDataSet() throws IOException { + String cpuInfo = + "Currently running services:\n" + + " cpuinfo\n" + + " ----------------------------------------------------------------------------\n" + + " DUMP OF SERVICE cpuinfo:\n" + + " Load: 0.53 / 0.11 / 0.04\n" + + " CPU usage from 33406ms to 28224ms ago:\n" + + " system_server: 56% = 42% user + 13% kernel / faults: 6724 minor 9 major\n" + + " bootanimation: 1% = 0% user + 0% kernel\n" + + " zygote: 0% = 0% user + 0% kernel / faults: 146 minor\n" + + " TOTAL: 98% = 67% user + 30% kernel;\n"; + BufferedReader br = new BufferedReader(new StringReader(cpuInfo)); + List<DataValue> 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<DataValue> 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<DataValue> 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<DataValue> 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<DataValue> 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<DataValue> 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" + + " <snip>\n" + + " RestoreToCount\n" + + "\n" + + "Caches:\n" + + "Current memory usage / total memory usage (bytes):\n" + + " TextureCache 4663920 / 25165824\n" + + " <snip>\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<GfxProfileData> gfxProfile = BugReportParser.parseGfxInfo(br); + + assertEquals(4, gfxProfile.size()); + assertEquals(0.85, gfxProfile.get(0).draw); + assertEquals(1.01, gfxProfile.get(3).execute); + } +} diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java new file mode 100644 index 0000000..4487454 --- /dev/null +++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java @@ -0,0 +1,74 @@ +/* + * 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<NativeStackCallInfo> stack = info.getResolvedStackCall(); + assertEquals(4, stack.size()); + } + + private NativeHeapDataImporter createImporter(String contentsToParse) { + StringReader r = new StringReader(contentsToParse); + return new NativeHeapDataImporter(r); + } +} diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java new file mode 100644 index 0000000..e6c0e76 --- /dev/null +++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java @@ -0,0 +1,75 @@ +/* + * 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<LogCatFilter, LogCatFilterData>()); + List<LogCatFilter> 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<LogCatFilter, LogCatFilterData> fdMap = + new HashMap<LogCatFilter, LogCatFilterData>(); + fdMap.put(fs, fd); + + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String s = serializer.encodeToPreferenceString(Arrays.asList(fs), fdMap); + List<LogCatFilter> decodedFiltersList = serializer.decodeFromPreferenceString(s); + + assertEquals(0, decodedFiltersList.size()); + } +} diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java new file mode 100644 index 0000000..7d9869a --- /dev/null +++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java @@ -0,0 +1,54 @@ +/* + * 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)); + } +} diff --git a/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java new file mode 100644 index 0000000..32a36c4 --- /dev/null +++ b/ddms/ddmuilib/src/test/java/com/android/ddmuilib/logcat/RollingBufferFindTest.java @@ -0,0 +1,108 @@ +/* + * 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<String> 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); + } +} |